diff --git a/ROADMAP.md b/ROADMAP.md index 5b2fbba92f..2ae68ebd2f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -143,6 +143,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-12 (M10, file formats):** Create PDF reads BigTIFF scans; a malformed GIF header is no longer detected as an image only to fail conversion. file-format-parity.md now lists XDP data import/export for XFA forms (already shipped under `form.xfa-data`) separately from opening `.xdp` as a document. Estimates unchanged. - **2026-10-12 (gap 6, #1317, #1327, #1302, #1315, #1340):** Flattening keeps a visible comment it cannot draw, such as a cloudy square with no stored appearance, and still draws a solid square, a hidden comment and a cloudy square that already has an appearance. A text box another program drew is left unchanged when a restyle or resize is refused. Make Current Properties Default on an arrow updates the Arrow tool and leaves plain lines alone. A custom text match that ends on a reconstructed word or line separator no longer includes the next glyph. The vertical-writing redaction refusal still fails closed, and its message no longer says the text was under the mark. Estimates unchanged. - **2026-10-12 (M12, text-only auto-tag):** Automatically tag PDF now writes marked text objects, paragraph tags, a Document element and a parent tree for untagged text-only PDFs. The result is undoable and opens the Tags panel for review. Mixed paint, forms, annotations, existing tags and malformed streams are refused before mutation. Synthetic two-page engine and tool tests check the tree, checker, unchanged rendering and save/reopen. Richer content and role classification remain open. Estimates unchanged. - **2026-10-12 (M7, #1316):** Replacing a line or paragraph no longer removes a distinct neighbour whose glyph box overlaps the edited line. A second drawing is removed only when it also sits on the same baseline, so fake-bold and invisible copies still go. Synthetic regressions cover 6 pt leading, the same word on the next line, ordinary spacing, a separate paragraph, same-position copies, and incremental and full save/reopen. Estimates unchanged. diff --git a/crates/create/src/lib.rs b/crates/create/src/lib.rs index 4d21ea5e97..a42ec84fa8 100644 --- a/crates/create/src/lib.rs +++ b/crates/create/src/lib.rs @@ -488,12 +488,21 @@ pub enum SourceKind { /// File extensions Create converts to PDF (images and plain text). pub const CONVERTIBLE: [&str; 12] = ["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx", "txt", "text"]; +/// A classic TIFF or a BigTIFF (version 43), in either byte order. +fn is_tiff(bytes: &[u8]) -> bool { + [b"II*\0", b"MM\0*", b"II+\0", b"MM\0+"].iter().any(|m| bytes.starts_with(*m)) +} + +/// The GIF headers `embed` decodes, so detection and conversion agree. +fn is_gif(bytes: &[u8]) -> bool { + bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") +} + fn is_image(bytes: &[u8]) -> bool { bytes.starts_with(&[0xFF, 0xD8]) || bytes.starts_with(b"\x89PNG\r\n\x1a\n") - || bytes.starts_with(b"II*\0") - || bytes.starts_with(b"MM\0*") - || bytes.starts_with(b"GIF8") + || is_tiff(bytes) + || is_gif(bytes) // JPEG 2000: a JP2 file or a raw codestream. || bytes.starts_with(JP2_SIGNATURE) || bytes.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) @@ -522,11 +531,11 @@ fn embed(name: &str, bytes: &[u8]) -> Result, CreateError> { Ok(vec![jpeg(name, bytes)?]) } else if bytes.starts_with(b"\x89PNG\r\n\x1a\n") { Ok(vec![png_image(name, bytes)?]) - } else if bytes.starts_with(b"II*\0") || bytes.starts_with(b"MM\0*") { + } else if is_tiff(bytes) { tiff_pages(name, bytes) } else if bytes.starts_with(b"BM") { Ok(vec![decoded(name, bytes, image::ImageFormat::Bmp)?]) - } else if bytes.starts_with(b"GIF87a") || bytes.starts_with(b"GIF89a") { + } else if is_gif(bytes) { Ok(vec![decoded(name, bytes, image::ImageFormat::Gif)?]) } else if bytes.starts_with(JP2_SIGNATURE) || bytes.starts_with(&[0xFF, 0x4F, 0xFF, 0x51]) { Ok(vec![jpx(name, bytes)?]) diff --git a/crates/create/src/tests.rs b/crates/create/src/tests.rs index 493f605479..2d76055ec9 100644 --- a/crates/create/src/tests.rs +++ b/crates/create/src/tests.rs @@ -408,6 +408,28 @@ fn source_kind_tells_pdfs_images_and_text_apart() { assert_eq!(source_kind("b.bmp", b"BM"), None); assert_eq!(source_kind("a.docx", b"PK\x03\x04"), None); assert_eq!(source_kind("", b""), None); + // Only the GIF versions Create decodes count as images, so detection never promises a + // conversion that then fails. + assert_eq!(source_kind("a.gif", b"GIF8x not a gif"), None); + assert_eq!(source_kind("notes.txt", b"GIF8 is a word here"), Some(SourceKind::Text)); +} + +/// BigTIFF (version 43, used for large scans) is detected and converted like a classic TIFF. +#[test] +fn bigtiff_images_become_pages() { + let mut tif = std::io::Cursor::new(Vec::new()); + { + let mut enc = tiff::encoder::TiffEncoder::new_big(&mut tif).unwrap(); + let mut im = enc.new_image::(4, 2).unwrap(); + im.resolution(tiff::tags::ResolutionUnit::Inch, tiff::encoder::Rational { n: 144, d: 1 }); + im.write_data(&[128; 8]).unwrap(); + } + let tif = tif.into_inner(); + assert!(tif.starts_with(b"II+\0"), "the encoder wrote a BigTIFF"); + assert_eq!(source_kind("scan.tif", &tif), Some(SourceKind::Image)); + let doc = reopen(&from_images(&[("scan.tif".into(), tif)]).unwrap()); + assert_eq!(pages(&doc).len(), 1); + assert_eq!(media(&pages(&doc)[0]), [0.0, 0.0, 2.0, 1.0], "4×2 px at 144 dpi"); } /// A synthetic ICC profile: a 128-byte header (size, device class, colour space, `acsp`) and diff --git a/docs/file-format-parity.md b/docs/file-format-parity.md index ab959edc4a..9a5f4d20d4 100644 --- a/docs/file-format-parity.md +++ b/docs/file-format-parity.md @@ -1,6 +1,6 @@ # File format parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (Word invoice export, #773; encrypted output opens in Acrobat, #774; XFDF fixes #807, #809, #819) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (XDP data row corrected; BigTIFF in Create PDF) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) Every format Acrobat Pro reads or writes, and PdfCraft's support for it. Acrobat's list comes from its bundle's `Info.plist` `CFBundleDocumentTypes` (what it opens) and its public documentation @@ -38,7 +38,8 @@ features total). | FDF (`.fdf`) comments and form data | R W | R W | good | `crates/xfdf`; comment-exchange fixes for popups, polygon vertices and callouts (#807, #809, #819) | | XFDF (`.xfdf`, ISO 19444-1) | R W | R W | good | as above | | XML form data | R W | R W | good | `form.data-xml-csv-txt` | -| XDP (`.xdp`, XFA data package) | R | none | none | Acrobat opens `.xdp` as a document type | +| XDP data (`.xdp`) for XFA forms | R W | R W: Export Data writes the whole datasets as an XDP; Import Data takes an XDP, datasets or data XML | good | `form.xfa-data` (`data_exports_as_datasets_xml_or_an_xdp_and_imports_from_any_of_them`, `xfa_data_files_export_and_import_through_tools`) | +| XDP opened as a document | R | none | none | Acrobat opens `.xdp` as a document type | | CSV / tab-delimited form data | W (merge to spreadsheet) | R W; merge to spreadsheet | good | `form.merge-to-spreadsheet` | | Comment summary (PDF) | W | W (comments only layout) | basic | `comment.summarize` partial | | Measurement CSV | W | W | good | `comment.measure-export` | @@ -62,7 +63,7 @@ features total). | Source | Acrobat | PdfCraft | Fidelity | Tests and evidence | |---|---|---|---|---| -| JPEG, PNG, TIFF (multipage), GIF, BMP, JPEG 2000 | yes | yes (ICC profiles kept; DPI choices) | good | `create.from-images`, `create.from-multipage-tiff` | +| JPEG, PNG, TIFF (multipage, and BigTIFF), GIF, BMP, JPEG 2000 | yes | yes (ICC profiles kept; DPI choices) | good | `create.from-images`, `create.from-multipage-tiff`, `bigtiff_images_become_pages` | | PCX | yes | no | none | in Acrobat's document types | | Plain text | yes | yes | good | `create.from-text` | | Clipboard (image or text) | yes | yes | good | `create.from-clipboard` | @@ -109,6 +110,7 @@ features total). | Date | Change | Summary | |---|---|---| +| 2026-10-12 | minor | XDP split into data (read and write for XFA forms, already shipped under `form.xfa-data`) and opening `.xdp` as a document (none). Create PDF reads BigTIFF; malformed GIF headers are no longer detected as images. | | 2026-10-11 | minor | #773: inferred tables have no invented borders; rotated or overlapping invoice pages use floating shapes and page sections. Full Office layout fidelity remains partial. | | 2026-10-11 | minor | Encrypted PDF row: written files open in Acrobat DC (#774, fixed by #836). FDF row: #807, #809 and #819 fixed | | 2026-10-10 | major | Created from Acrobat 26.002.21931's `CFBundleDocumentTypes`, Acrobat's public Create/Export documentation, and PdfCraft's code and checklist | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 73c9539025..eb892d892b 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -6784,8 +6784,8 @@ milestone = "M10" status = "shipped" commands = ["create.images", "create.file"] tools = ["doc_create"] -evidence = ["crates/create/src/tests.rs::bmp_gif_and_multi_page_tiff_images"] -notes = "every page; 1-bit scans stay 1-bit; resolution from the TIFF" +evidence = ["crates/create/src/tests.rs::bmp_gif_and_multi_page_tiff_images", "crates/create/src/tests.rs::bigtiff_images_become_pages"] +notes = "every page; 1-bit scans stay 1-bit; resolution from the TIFF; classic TIFF and BigTIFF" [[feature]] id = "create.from-text"