mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-22 15:14:11 -04:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7991c0acbc | ||
|
|
8fac89f448 |
No files matched your search
@@ -31,7 +31,7 @@ log = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rfd = { workspace = true }
|
||||
arboard = { version = "3", features = ["wayland-data-control"] }
|
||||
arboard = { version = "3.6", features = ["wayland-data-control"] }
|
||||
open = "5"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
|
||||
@@ -71,6 +71,29 @@ fn write_atomic(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||
photocraft_format::atomic_write(path, bytes).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// The pixels to paste when the clipboard holds copied files rather than an image (Copy in
|
||||
/// Files, Finder or Explorer puts paths on the clipboard, #338): the first file that decodes,
|
||||
/// upright, as RGBA8. Files that aren't images are skipped after reading only their header.
|
||||
fn image_from_files(paths: &[PathBuf]) -> Option<(u32, u32, Vec<u8>)> {
|
||||
use std::io::Read;
|
||||
paths.iter().find_map(|path| {
|
||||
// text/uri-list lines end in CRLF (RFC 2483, and GTK writes them so), but arboard splits
|
||||
// on LF only, so a path copied in GNOME Files arrives with a trailing '\r'.
|
||||
let path = path.to_str().map_or_else(|| path.clone(), |s| PathBuf::from(s.trim_end_matches('\r')));
|
||||
let mut head = Vec::with_capacity(256);
|
||||
std::fs::File::open(&path).ok()?.take(256).read_to_end(&mut head).ok()?;
|
||||
// TGA has no magic number and is only guessed from the header, so trust the extension
|
||||
// too before reading what may be a large non-image file.
|
||||
let ext = path.extension().and_then(|e| e.to_str()).unwrap_or("");
|
||||
match photocraft_codecs::detect(&head)? {
|
||||
photocraft_codecs::Format::Tga if photocraft_codecs::from_extension(ext) != Some(photocraft_codecs::Format::Tga) => return None,
|
||||
_ => {}
|
||||
}
|
||||
let img = photocraft_codecs::decode(&std::fs::read(&path).ok()?).ok()?;
|
||||
Some((img.width(), img.height(), img.to_rgba8()))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) -> Services {
|
||||
let savers: Rc<RefCell<HashMap<u64, Autosaver>>> = Rc::default();
|
||||
let savers2 = savers.clone();
|
||||
@@ -146,6 +169,11 @@ pub fn native(automation: Option<photocraft_automation::AuthorizedWorkspace>) ->
|
||||
Some(c) => c,
|
||||
None => slot.insert(arboard::Clipboard::new().ok()?),
|
||||
};
|
||||
// Copied files first: Finder also puts the file's icon on the clipboard as an
|
||||
// image, which would otherwise paste instead of the file.
|
||||
if let Some(img) = cb.get().file_list().ok().and_then(|paths| image_from_files(&paths)) {
|
||||
return Some(img);
|
||||
}
|
||||
let img = cb.get_image().ok()?;
|
||||
Some((img.width as u32, img.height as u32, img.bytes.into_owned()))
|
||||
})
|
||||
@@ -250,3 +278,79 @@ pub fn export_flat(doc: &Document, path: &str) -> Result<Vec<u8>, String> {
|
||||
};
|
||||
photocraft_codecs::encode(&img, format, &EncodeOptions::default()).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
|
||||
fn temp(tag: &str) -> PathBuf {
|
||||
static N: AtomicUsize = AtomicUsize::new(0);
|
||||
let d = std::env::temp_dir().join(format!("photocraft-services-{tag}-{}-{}", std::process::id(), N.fetch_add(1, Ordering::Relaxed)));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn red_png(w: u32, h: u32) -> Vec<u8> {
|
||||
let img = Image::from_u8(w, h, ChannelLayout::Rgba, [255u8, 0, 0, 255].repeat((w * h) as usize)).unwrap();
|
||||
photocraft_codecs::encode(&img, photocraft_codecs::Format::Png, &EncodeOptions::default()).unwrap()
|
||||
}
|
||||
|
||||
/// A 1×1 uncompressed 32-bit TGA (TGA has no magic number).
|
||||
fn tga_1x1() -> Vec<u8> {
|
||||
let mut b = vec![0u8, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 32, 8];
|
||||
b.extend_from_slice(&[0, 0, 255, 255]); // BGRA red
|
||||
b
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copied_image_file_pastes_its_pixels() {
|
||||
let dir = temp("png");
|
||||
let png = dir.join("red.png");
|
||||
std::fs::write(&png, red_png(3, 2)).unwrap();
|
||||
let (w, h, px) = image_from_files(&[png]).unwrap();
|
||||
assert_eq!((w, h), (3, 2));
|
||||
assert_eq!(px, [255u8, 0, 0, 255].repeat(6));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn crlf_from_a_uri_list_is_ignored() {
|
||||
// arboard splits text/uri-list on LF, so GNOME Files' CRLF list leaves a '\r' behind.
|
||||
let dir = temp("crlf");
|
||||
let png = dir.join("red image.png");
|
||||
std::fs::write(&png, red_png(2, 2)).unwrap();
|
||||
let with_cr = PathBuf::from(format!("{}\r", png.display()));
|
||||
assert_eq!(image_from_files(&[with_cr]).map(|(w, h, _)| (w, h)), Some((2, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_images_are_skipped_for_the_first_image() {
|
||||
let dir = temp("mixed");
|
||||
let (txt, png) = (dir.join("notes.txt"), dir.join("red.png"));
|
||||
std::fs::write(&txt, b"not an image").unwrap();
|
||||
std::fs::write(&png, red_png(4, 1)).unwrap();
|
||||
let missing = dir.join("gone.png");
|
||||
assert_eq!(image_from_files(&[missing.clone(), txt.clone(), dir.clone(), png]).map(|(w, h, _)| (w, h)), Some((4, 1)));
|
||||
assert!(image_from_files(&[missing, txt, dir]).is_none());
|
||||
assert!(image_from_files(&[]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_damaged_image_is_skipped_not_a_panic() {
|
||||
let dir = temp("damaged");
|
||||
let bad = dir.join("cut.png");
|
||||
std::fs::write(&bad, &red_png(8, 8)[..20]).unwrap();
|
||||
assert!(image_from_files(&[bad]).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tga_is_trusted_only_with_its_extension() {
|
||||
let dir = temp("tga");
|
||||
let (tga, bin) = (dir.join("red.tga"), dir.join("red.bin"));
|
||||
std::fs::write(&tga, tga_1x1()).unwrap();
|
||||
std::fs::write(&bin, tga_1x1()).unwrap();
|
||||
assert_eq!(image_from_files(&[tga]), Some((1, 1, vec![255, 0, 0, 255])));
|
||||
assert!(image_from_files(&[bin]).is_none());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user