mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-24 10:16:59 -06:00
Merge pull request #1217 from MarekSpodBiedry/issue-1207
PDF/X-4 export: add xmp:MetadataDate and xmpMM:VersionID to the XMP (#1207)
This commit is contained in:
@@ -254,7 +254,7 @@ fn export_pdf_with_report_inner(doc: &Document, cache: &Cache, opts: &PdfOptions
|
||||
}
|
||||
if opts.standard == Standard::PdfX4 {
|
||||
// Output intent and PDF/X identification, then check the result.
|
||||
match crate::pdfx::make_pdfx4(&bytes, &title) {
|
||||
match crate::pdfx::make_pdfx4(&bytes, &title, created) {
|
||||
Some(b) => bytes = b,
|
||||
None => warnings.push("PDF/X-4: the output intent couldn't be added".into()),
|
||||
}
|
||||
|
||||
+20
-5
@@ -23,9 +23,11 @@ fn lit(s: &str) -> String {
|
||||
o
|
||||
}
|
||||
|
||||
/// Turn krilla's output into PDF/X-4. `None` when the file isn't the simple kind this
|
||||
/// understands (a classic cross-reference table and plain dictionaries).
|
||||
pub fn make_pdfx4(pdf: &[u8], title: &str) -> Option<Vec<u8>> {
|
||||
/// Turn krilla's output into PDF/X-4. `date` (Unix seconds, UTC) is the XMP metadata date; `None`
|
||||
/// (no clock, as on wasm) writes the epoch, because PDF/X-4 requires the entry. `None` back when
|
||||
/// the file isn't the simple kind this understands (a classic cross-reference table and plain
|
||||
/// dictionaries).
|
||||
pub fn make_pdfx4(pdf: &[u8], title: &str, date: Option<i64>) -> Option<Vec<u8>> {
|
||||
let t_at = rfind(pdf, b"trailer")?;
|
||||
let trailer = String::from_utf8_lossy(&pdf[t_at..]).to_string();
|
||||
let size = int_after(&trailer, "/Size")?;
|
||||
@@ -84,7 +86,8 @@ pub fn make_pdfx4(pdf: &[u8], title: &str) -> Option<Vec<u8>> {
|
||||
i
|
||||
}
|
||||
};
|
||||
// XMP: the PDF/X identification schema, and the trapping state that Info also states.
|
||||
// XMP: the PDF/X identification schema, the trapping state that Info also states, and the
|
||||
// metadata date and version ID that PDF/X-4 requires.
|
||||
if let Some(meta) = int_after(&cat, "/Metadata") {
|
||||
let r = object(pdf, meta)?;
|
||||
let body = &pdf[r];
|
||||
@@ -96,11 +99,17 @@ pub fn make_pdfx4(pdf: &[u8], title: &str) -> Option<Vec<u8>> {
|
||||
if dict.contains("/Filter") {
|
||||
return None;
|
||||
}
|
||||
let (y, mo, d, h, mi, se) = crate::export::civil(date.unwrap_or(0));
|
||||
let stamp = format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{se:02}Z");
|
||||
let add = concat!(
|
||||
"<rdf:Description rdf:about=\"\" xmlns:pdfxid=\"http://www.npes.org/pdfx/ns/id/\"><pdfxid:GTS_PDFXVersion>PDF/X-4</pdfxid:GTS_PDFXVersion></rdf:Description>",
|
||||
"<rdf:Description rdf:about=\"\" xmlns:pdf=\"http://ns.adobe.com/pdf/1.3/\"><pdf:Trapped>False</pdf:Trapped></rdf:Description>"
|
||||
);
|
||||
let xml = xml.replacen("</rdf:RDF>", &format!("{add}</rdf:RDF>"), 1);
|
||||
let dates = format!(
|
||||
"<rdf:Description rdf:about=\"\" xmlns:xmp=\"http://ns.adobe.com/xap/1.0/\" xmlns:xmpMM=\"http://ns.adobe.com/xap/1.0/mm/\">\
|
||||
<xmp:MetadataDate>{stamp}</xmp:MetadataDate><xmpMM:VersionID>1</xmpMM:VersionID></rdf:Description>"
|
||||
);
|
||||
let xml = xml.replacen("</rdf:RDF>", &format!("{add}{dates}</rdf:RDF>"), 1);
|
||||
let dict = {
|
||||
let i = dict.find("/Length")?;
|
||||
let rest = &dict[i + 7..];
|
||||
@@ -216,6 +225,12 @@ pub fn check_pdfx4(pdf: &[u8]) -> Vec<String> {
|
||||
if !text.contains("pdfxid:GTS_PDFXVersion>PDF/X-4<") {
|
||||
issues.push("the XMP metadata doesn't name PDF/X-4".into());
|
||||
}
|
||||
if !text.contains("<xmp:MetadataDate>") {
|
||||
issues.push("the XMP metadata has no xmp:MetadataDate".into());
|
||||
}
|
||||
if !text.contains("<xmpMM:VersionID>") {
|
||||
issues.push("the XMP metadata has no xmpMM:VersionID".into());
|
||||
}
|
||||
if text.contains("/Interpolate true") {
|
||||
issues.push("an image asks for interpolation".into());
|
||||
}
|
||||
|
||||
@@ -163,6 +163,20 @@ fn spreads_and_ranges() {
|
||||
assert_eq!(export_pdf(&d, &Cache::new(), &PdfOptions { pages: Some(vec![9]), ..Default::default() }), Err(PdfError::BadPage(10)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pdfx4_xmp_has_metadata_date_and_version_id() {
|
||||
let d = doc_with_text("Print me");
|
||||
let opts = PdfOptions { standard: Standard::PdfX4, created: Some(1_700_000_000), ..Default::default() };
|
||||
let r = export_pdf_with_report(&d, &Cache::new(), &opts).unwrap();
|
||||
let text = String::from_utf8_lossy(&r.bytes).to_string();
|
||||
assert!(text.contains("<xmp:MetadataDate>2023-11-14T22:13:20Z</xmp:MetadataDate>"), "metadata date");
|
||||
assert!(text.contains("<xmpMM:VersionID>1</xmpMM:VersionID>"), "version id");
|
||||
assert!(crate::check_pdfx4(&r.bytes).is_empty());
|
||||
// Without the two entries the check says so.
|
||||
let stripped = text.replace("xmp:MetadataDate", "xmp:Other").replace("xmpMM:VersionID", "xmpMM:Other");
|
||||
assert_eq!(crate::check_pdfx4(stripped.as_bytes()).iter().filter(|i| i.contains("xmp")).count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn standards() {
|
||||
let d = doc_with_text("Archive me");
|
||||
|
||||
@@ -377,7 +377,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Right-to-left text (World-Ready basics): right-to-left runs are shaped right to left, lines are reordered by the Unicode Bidirectional Algorithm (matched against the reference implementation), Paragraph Direction (`type.para {direction}`, Paragraph panel, IDML ParagraphDirection, EPUB `dir`). Digits (`digits`: Default, Arabic, Hindi, Farsi, Native by language; Character panel and Character Style Options; IDML DigitsType) draw 0–9 in another script. Kashidas (`kashidas` paragraph attribute, on by default; IDML Kashidas): justified Arabic lines stretch at one join per word (after seen/sad when possible, never inside lam–alef) with a drawn tatweel before word spaces widen. Carets follow the drawing: the caret before a right-to-left glyph sits at its right edge, arrow keys move as drawn (by words in the line's direction), clicks find the nearest drawn caret position and selections highlight the selected glyphs. Binding: Right to Left (Document Setup, `layout.documentSetup {binding}`, IDML PageBinding) makes page 1 a left page and lays spreads out right to left.
|
||||
- Interface language (Preferences ▸ General ▸ Interface Language, `app.language`; each language listed by its own name): German, French, Spanish, Italian, Japanese, Simplified Chinese and Arabic menu titles, common menu items and panel names (our own translations). Common dialog, panel and tooltip labels are also localized, with English fallback for untranslated strings. Arabic translations cover every key in the shared interface translation table, including menu commands and utility labels. The Type menu's Story Direction, Horizontal/Vertical, Tate-Chu-Yoko, Ruby… and Kenten are translated too. Arabic labels use Unicode bidi run ordering and contextual shaping; welcome and new-document form layouts follow RTL direction independently of document binding. CJK and Arabic glyphs come from [craft-fonts](https://github.com/storytold/craft-fonts) when built with `CRAFT_FONTS_DIR` (all releases): BIZ UDPGothic and Noto Sans CJK SC for the UI (Chinese forms first in the Chinese interface), Noto Sans Arabic, and Mincho for Japanese documents (the web build embeds BIZ UDPGothic only); otherwise from system fonts. Ukrainian covers all 1,430 existing catalog keys, including publishing dialogs and built-in names, with persisted selection, grammatical count captions, English fallback and bundled Cyrillic UI glyph coverage. Polish covers 1,717 catalog keys with the same category-first count captions and English fallback.
|
||||
- Page transitions (`page.transition/transitions`, Page Transitions panel): twelve transition types with speed and direction per spread, written into exported PDFs as `/Trans` entries through an incremental update.
|
||||
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with the working CMYK space's profile (by default craft-cms's Coated FOGRA39 profile, identifier FOGRA39; Generic CMYK and Device CMYK embed our Generic CMYK profile, lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, `pdf:Trapped` in XMP as in Info, RGB images without a profile tagged sRGB, no image interpolation (none in PDF/A either), and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
|
||||
- PDF/X-4 export (`file.exportPdf {standard: "x4"}`): an output intent with the working CMYK space's profile (by default craft-cms's Coated FOGRA39 profile, identifier FOGRA39; Generic CMYK and Device CMYK embed our Generic CMYK profile, lut16 tables generated from the parametric press model), GTS_PDFXVersion in Info and XMP, `pdf:Trapped` in XMP as in Info, `xmp:MetadataDate` and `xmpMM:VersionID` in XMP, RGB images without a profile tagged sRGB, no image interpolation (none in PDF/A either), and built-in PDF/X-4 checks reported as warnings. qpdf finds no errors in the result.
|
||||
- EPS place: placed at its (high-resolution) bounding box and shown, printed and exported through its TIFF preview, or a placeholder when it has none. PostScript itself isn't interpreted.
|
||||
- Preferences: Dictionary (user dictionary; `spelling.setWords/words`), Spelling (Dynamic Spelling: red squiggles on the canvas, also Edit ▸ Spelling), Autocorrect (word list, applied as you type), Notes, Track Changes, Story Editor Display and File Handling (recovery interval) sections.
|
||||
- Colour management exposed (ICC through the craft-libs `craft-cms` crate): Edit ▸ Color Settings (`color.settings`: RGB/CMYK working spaces, default CMYK Coated FOGRA39 with CMYK swatches shown relative colorimetric with black-point compensation, Generic CMYK and Device CMYK still selectable, intent, black-point compensation; `color.loadProfile` for ICC files; `color.convert` through the working spaces, with gamut checks) and View ▸ Proof Setup / Proof Colors (press, sRGB, legacy Mac RGB, colour-blindness simulations, simulate paper) on the canvas. Placed images are shown through their embedded ICC profiles, and CMYK images through their own profile or the working CMYK space, with the settings' intent and black-point compensation.
|
||||
|
||||
Reference in New Issue
Block a user