mirror of
https://github.com/storytold/photocraft.git
synced 2026-10-27 03:17:05 -06:00
* Flat exports embed no document XMP by default; Export As gains a Metadata choice (#647) The document's XMP packet lists the text of every type layer (photoshop:TextLayers) and one id per placed document (photoshop:DocumentAncestors) - data a flat web image should not carry: 14-116 KB per export on real designer PSDs, and layer text leaking into images that show only part of the design. - io: ExportOptions.xmp (XmpEmbed::All | None, default None) drops the packet from flat exports before the codecs see it. Layered saves (PSD, PSB, .pcraft) always keep everything; DPI, ICC and EXIF are unaffected. Export As, Save a Copy, the CLI, Layers/Artboards to Files and Batch all share this path and get the safe default. - The Export As dialog gains a Metadata dropdown (None / All, default None, like the source application's Export As); the file-size estimate follows the choice. - Tests: new xmp_export.rs covers the default (png/jpg/webp/tif carry no packet), the All opt-in and the layered always-keep; metadata_roundtrip_png and the stale-orientation test document the new contract (with All, an embedded stale orientation is still rewritten upright). * Export As alone starts at Metadata: None; Save As and conversions keep the XMP ExportSettings::default() and ExportOptions::default() also serve File > Save As, file.saveAs, the CLI and automation, which keep the document's metadata (as Photoshop's Save As does). Only the Export As dialog defaults to None. A layered TIFF honours the choice too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Brandon Thomas <echelon@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
57 lines
2.6 KiB
Rust
57 lines
2.6 KiB
Rust
//! #647: Export As's Metadata: None (`XmpEmbed::None`) embeds none of the document's XMP (the
|
|
//! packet lists the text of every type layer and one id per placed document); the default,
|
|
//! `XmpEmbed::All`, keeps it as Save As does, and layered saves (PSD, `.pcraft`) always keep it.
|
|
|
|
use photocraft_color::{ColorMode, SampleType};
|
|
use photocraft_doc::{Document, Layer, LayerContent, Size};
|
|
use photocraft_io::{ExportOptions, XmpEmbed, export};
|
|
use photocraft_raster::Surface;
|
|
|
|
const XMP: &str = "<x:xmpmeta xmlns:x=\"adobe:ns:meta/\"><rdf:RDF>\
|
|
<photoshop:DocumentAncestors><rdf:Bag><rdf:li>xmp.did:11111111-2222-3333-4444-555555555555</rdf:li></rdf:Bag></photoshop:DocumentAncestors>\
|
|
<photoshop:TextLayers><rdf:Bag><rdf:li>Secret campaign copy</rdf:li></rdf:Bag></photoshop:TextLayers>\
|
|
</rdf:RDF></x:xmpmeta>";
|
|
|
|
fn doc_with_xmp() -> Document {
|
|
let mut d = Document::new("x", Size::new(8, 6), ColorMode::Rgb, SampleType::U8);
|
|
let mut s = Surface::new(d.pixel_format());
|
|
s.fill_rect(photocraft_geom::Rect::new(0, 0, 8, 6), &[0.5, 0.4, 0.3, 1.0]);
|
|
d.layers.push(Layer::new("flat", LayerContent::Raster(s)));
|
|
d.metadata.xmp = Some(XMP.to_string());
|
|
d
|
|
}
|
|
|
|
fn leaks_xmp(bytes: &[u8]) -> bool {
|
|
let s = String::from_utf8_lossy(bytes);
|
|
s.contains("x:xmpmeta") || s.contains("DocumentAncestors") || s.contains("Secret campaign copy")
|
|
}
|
|
|
|
#[test]
|
|
fn metadata_none_embeds_no_xmp() {
|
|
let opts = ExportOptions { xmp: XmpEmbed::None, ..ExportOptions::default() };
|
|
for ext in ["png", "jpg", "webp", "tif"] {
|
|
let r = export(&doc_with_xmp(), &format!("out.{ext}"), &opts).expect(ext);
|
|
assert!(!leaks_xmp(&r.bytes), "{ext}: the document XMP leaked into the export");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn all_embeds_the_whole_packet_and_is_the_default() {
|
|
assert_eq!(ExportOptions::default().xmp, XmpEmbed::All);
|
|
let opts = ExportOptions { xmp: XmpEmbed::All, ..ExportOptions::default() };
|
|
for ext in ["png", "jpg", "webp"] {
|
|
let r = export(&doc_with_xmp(), &format!("out.{ext}"), &opts).expect(ext);
|
|
assert!(leaks_xmp(&r.bytes), "{ext}: XmpEmbed::All must embed the packet");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn layered_saves_keep_the_xmp_whatever_the_option() {
|
|
for opts in [ExportOptions::default(), ExportOptions { xmp: XmpEmbed::None, ..ExportOptions::default() }] {
|
|
let psd = export(&doc_with_xmp(), "out.psd", &opts).expect("psd");
|
|
assert!(leaks_xmp(&psd.bytes), "PSD always keeps the document XMP");
|
|
let pcraft = export(&doc_with_xmp(), "out.pcraft", &opts).expect("pcraft");
|
|
assert!(leaks_xmp(&pcraft.bytes), ".pcraft always keeps the document XMP");
|
|
}
|
|
}
|