Merge branch 'storytold:main' into fix/paper-size-driver-5861

This commit is contained in:
Harsh Verma authored and GitHub committed 2026-11-10 19:58:34 +05:30
commit de24a4ddba
6 files changed
+120 -4

No files matched your search

+8
View File
@@ -39,6 +39,7 @@
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M1: refresh object-generation reference identity against current upstream; estimates unchanged)
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
> **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: vector-print appearance transforms retain small coefficients; estimates unchanged)
> **Last reviewed:** 2026-10-29 | **Last updated:** 2026-10-29 | **Change:** minor (M2: scans keep their strokes at screen size and are smoothed when zoomed; M7 #2517: a narrowed then widened paragraph keeps the typed spaces the wrap consumed, after save and reopen; estimates unchanged)
> **Last reviewed:** 2026-10-28 | **Last updated:** 2026-10-28 | **Change:** minor (M10, #4119 partial: Enhance scanned file cleans scanned pages in place: background removal, edge shadow removal, despeckle; estimates unchanged)
> **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: OCR model downloads use verified ONNX pairs with legacy discovery fallback; estimates unchanged)
@@ -243,6 +244,9 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
- **2026-11-09 (M10, #5130):** Windows Printer default sends native two-sided bindings only when automatic duplex support is confirmed, preserving manual driver defaults (#5130/#5772); physical output remains unqualified. Both Windows submission paths preserve DLL guards and current spooler fixes. Review follow-up validates both duplex scopes through craft-print 9fe50c94 and corrects CI formatting. Estimates unchanged.
- **2026-10-27 (M10, annotation print precision):** Vector printing preserves small placement coefficients for existing form, comment and stamp appearances with large bounding boxes or matrices.
Synthetic render and save/reopen regressions cover the change; estimates unchanged.
- **2026-10-13 (M8, redact by regex):** `redact_mark` takes `regex` (caller expression, linear-time engine, length/size/nesting caps, empty-matching refused) with optional `validate` (luhn, mod97, iban). Headless only; issue #1886.
- **2026-11-08 (M5, comment exchange):** Comments keep their formatting and appearance through XFDF and FDF. XFDF export and import carry rich text, default style, border width, style and dash pattern, rotation, justification, fringe, line leaders and captions, the caret symbol and redaction overlay text as XFDF 3.0 (ISO 19444-1) names them, and a stamp's appearance as `<appearance>` (the DICT/STREAM/DATA encoding, bounded); copy and paste carries every comment's appearance. FDF export keeps a stamp's /AP with its forms and images, as XFDF does, and FDF import keeps any /AP the file carries (ISO 32000-2 §12.7.8.3.4). The intent goes out as both `intent` and `IT`. An unreadable XFDF appearance is left out, said in the import report and the repair log, and the comment is drawn by PdfCraft; an FDF past the import budget is refused whole. A redaction mark is always drawn by PdfCraft.
@@ -832,6 +836,10 @@ Newest first: the date, what moved, and the new overall estimate.
## Revision history
**2026-10-27 · minor:** M10: vector printing retains small appearance placement coefficients.
Synthetic rendering and save/reopen regressions cover the change.
Estimates unchanged.
**2026-10-25 · minor:** M14 localization current state: Arabic catalogs stay logical, Arabic/Kurdish labels take an explicit RTL direction, and stock chrome mirrors; native/browser RTL input, IME and complex workflows remain unqualified. Native Simplified Chinese uses LXGW WenKai Lite; the web build has no Hans face. Estimates unchanged.
**2026-10-15 · minor:** M14/#2383 candidate: GPU device requests clamp unsupported limits while retaining the adapter texture extent. Synthetic V3D/downlevel regressions and Windows GPU-required tests pass; Raspberry Pi hardware remains unverified. Estimates unchanged.
+2
View File
@@ -54,6 +54,8 @@ Each source page becomes a Form XObject (its content wrapped in q/Q, plus the pr
annotations); sheets place them with a clip. The result is a fresh, unencrypted,
garbage-collected file (callers check the print permission).
Annotation appearances receive unused XObject names, preserving existing page resources even when they use the same naming prefix.
Appearance placement keeps the full PDF number precision, including small coefficients needed by large appearance bounding boxes or matrices.
Missing or unusable appearance streams still cannot be printed.
The GUI Print dialog remembers its last selected paper size across application sessions,
including when Print is cancelled. The preference stores a canonical name such as `A4`;
+13 -2
View File
@@ -3,7 +3,7 @@ use super::settings::{Content, Settings};
use super::sheets::{Sheet, layout};
use super::{DecodeBudget, Dict, Document, Matrix, ObjRef, Object, SaveOptions, Stream, write_full};
// Model-derived transforms and clipping bounds need their full PDF decimal precision.
// Transforms and clipping bounds need their full PDF decimal precision.
fn geometry_number(value: f64) -> String {
let mut bytes = Vec::new();
pdfcraft_cos::serialize(&Object::Real(value), &mut bytes);
@@ -218,7 +218,18 @@ pub(crate) fn page_form(
}
};
let [a0, b0, c0, d0, e0, f0] = m.0;
body.extend(format!("q {} {} {} {} {} {} cm /{name} Do Q\n", n(a0), n(b0), n(c0), n(d0), n(e0), n(f0)).bytes());
body.extend(
format!(
"q {} {} {} {} {} {} cm /{name} Do Q\n",
geometry_number(a0),
geometry_number(b0),
geometry_number(c0),
geometry_number(d0),
geometry_number(e0),
geometry_number(f0)
)
.bytes(),
);
xobjects.set(name.into_bytes(), Object::Ref(ap));
}
// With no appearance added the form keeps the page's own Resources, shared by reference,
+91
View File
@@ -242,6 +242,97 @@ fn missing_subtype_appearance_survives_vector_print() {
assert_eq!(actual_bytes, expected_bytes);
}
/// Appearance coordinates are independent of page units: a large BBox or Matrix can require
/// placement coefficients far below four decimal places even on an ordinary 100-point page.
#[test]
fn annotation_placement_preserves_small_transform_coefficients() {
let (control, _, _) = widget_fixture(Some(Object::name("Form")), None);
let control = render(saved(&control));
for scale in [1.0, 4096.0, 75000.0] {
for scaled_matrix in [false, true] {
let (mut doc, ap, widget) = widget_fixture(Some(Object::name("Form")), None);
let mut form = doc.get(ap).as_dict().unwrap().clone();
let coordinates = if scaled_matrix { 1.0 } else { scale };
form.set(b"BBox".to_vec(), numbers(&[0.0, 0.0, 20.0 * coordinates, 10.0 * coordinates]));
if scaled_matrix {
form.set(b"Matrix".to_vec(), numbers(&[scale, 0.0, 0.0, scale, 7.0, -13.0]));
}
doc.set(
ap,
Object::Stream(Stream::flate(form, format!("/Ink cs 0 1 0 scn 0 0 {} {} re f", 20.0 * coordinates, 10.0 * coordinates).as_bytes())),
);
for (subtype, content) in [
("Widget", Content::Document),
("Widget", Content::FormFieldsOnly),
("Square", Content::DocumentAndMarkups),
("Stamp", Content::DocumentAndStamps),
] {
doc.update_dict(widget, |d| d.set(b"Subtype".to_vec(), Object::name(subtype))).unwrap();
let before = saved(&doc);
let source = render(before.clone());
assert!(source.rgba == control.rgba, "equivalent source, scale={scale}, matrix={scaled_matrix}, {subtype}");
let printed = impose(&doc, &print_settings(content)).unwrap();
let reopened = Document::open(Arc::new(printed.clone())).unwrap();
assert_eq!(pdfcraft_model::pages(&reopened).len(), 1);
let printed = render(printed);
assert_eq!(pixel(&printed, 30, 70), [0, 255, 0, 255], "scale={scale}, matrix={scaled_matrix}, {subtype}");
if content != Content::FormFieldsOnly {
assert!(printed.rgba == source.rgba, "scale={scale}, matrix={scaled_matrix}, {subtype}");
}
// Check the serialized placement too: sub-pixel translation rounding may be
// invisible at 72 dpi while still moving the appearance's physical corners.
let page = &pdfcraft_model::pages(&reopened)[0];
let page_form = reopened.get(xobject(&reopened, &page.dict, b"P0"));
let Object::Stream(page_form) = &*page_form else { panic!("page form") };
let parsed = pdfcraft_content::parse(&page_form.decoded().unwrap());
assert_eq!(parsed.skipped, 0);
let matrices: Vec<_> = parsed.ops.iter().filter(|op| op.is("cm")).collect();
assert_eq!(matrices.len(), 1);
let placement = Matrix(matrices[0].nums::<6>().unwrap());
let (x, y) = if scaled_matrix { (7.0, -13.0) } else { (0.0, 0.0) };
for ((x, y), expected) in [(placement.apply(x, y), (10.0, 20.0)), (placement.apply(x + 20.0 * scale, y + 10.0 * scale), (50.0, 40.0))]
{
assert!((x - expected.0).abs() < 1e-10 && (y - expected.1).abs() < 1e-10, "corner ({x}, {y}) != {expected:?}");
}
assert_eq!(saved(&doc), before, "printing preserves the source");
}
}
}
}
#[test]
fn unusable_appearance_geometry_keeps_page_artwork() {
let (mut control, _, _) = widget_fixture(Some(Object::name("Form")), None);
let page = pdfcraft_model::pages(&control)[0].obj;
control
.update_dict(page, |d| {
d.remove(b"Annots");
})
.unwrap();
let control = render(saved(&control));
for (bbox, matrix) in [
(None, None),
(Some(numbers(&[0.0, 0.0, 20.0])), None),
(Some(numbers(&[0.0, 0.0, 0.0, 10.0])), None),
(Some(numbers(&[0.0, 0.0, 20.0, 0.0])), None),
(Some(numbers(&[0.0, 0.0, 20.0, 10.0])), Some([0.0; 6])),
] {
let (mut doc, ap, _) = widget_fixture(Some(Object::name("Form")), matrix);
let mut form = doc.get(ap).as_dict().unwrap().clone();
match bbox {
Some(bbox) => form.set(b"BBox".to_vec(), bbox),
None => {
form.remove(b"BBox");
}
}
doc.set(ap, Object::Stream(Stream::flate(form, b"/Ink cs 0 1 0 scn 0 0 20 10 re f")));
let before = saved(&doc);
let printed = render(impose(&doc, &print_settings(Content::Document)).unwrap());
assert!(printed.rgba == control.rgba, "unusable appearance is omitted without losing page artwork");
assert_eq!(saved(&doc), before);
}
}
#[test]
fn promotion_preserves_streams_and_explicit_subtypes() {
for subtype in [None, Some(Object::name("Form")), Some(Object::name("Image")), Some(Object::Null), Some(Object::Int(7))] {
+4
View File
@@ -1,5 +1,6 @@
# PdfCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-27 | **Last updated:** 2026-10-27 | **Change:** minor (M10: vector-print appearance transforms retain small coefficients; estimates unchanged)
> **Last reviewed:** 2026-11-09 | **Last updated:** 2026-11-09 | **Change:** patch (M10 #5130: preserve manual duplex defaults; validate DocumentDuplex review fix and CI formatting; estimates unchanged)
>
> **Last reviewed:** 2026-11-07 | **Last updated:** 2026-11-07 | **Change:** patch (M7 #2964 / #3185: grouped source text and Thai font/style preservation, with reported deep or unsafe groups and a per-glyph check of the Thai map repair; estimates unchanged)
@@ -302,6 +303,9 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
## Revision history
**2026-10-27 · minor:** M10: vector printing retains small appearance placement coefficients.
Synthetic rendering and save/reopen regressions cover the change.
Estimates unchanged.
**2026-10-31 · minor:** M3/M4 #5330: direct quick-toolbar Crop pages action reuses command permissions and the existing crop tool. Estimates unchanged.
**2026-10-26 · patch:** M2/PR #4172: reconcile current main and preserve upstream Japanese serif/weight selection alongside the installed CJK viewer and preview fallback. Estimates unchanged.
+2 -2
View File
@@ -8345,8 +8345,8 @@ milestone = "M10"
status = "shipped"
commands = ["print.dialog"]
tools = ["doc_print"]
evidence = ["crates/print/src/tests.rs::imposed_pdf_has_the_sheets_and_honours_comments_and_forms", "crates/print/src/tests/appearances.rs::annotation_appearances_do_not_replace_page_xobjects", "crates/print/src/tests/appearances.rs::printable_link_appearance_survives_vector_print"]
notes = "Print and Hidden flags honoured; existing Link appearances print as document content, but missing Link appearances are not synthesized. Appearance resource names preserve existing page XObjects and distinct annotations, including inherited resources"
evidence = ["crates/print/src/tests.rs::imposed_pdf_has_the_sheets_and_honours_comments_and_forms", "crates/print/src/tests/appearances.rs::annotation_appearances_do_not_replace_page_xobjects", "crates/print/src/tests/appearances.rs::printable_link_appearance_survives_vector_print", "crates/print/src/tests/appearances.rs::annotation_placement_preserves_small_transform_coefficients"]
notes = "Print and Hidden flags honoured; existing Link appearances print as document content, but missing Link appearances are not synthesized. Appearance resource names preserve existing page XObjects and distinct annotations, including inherited resources. Print flag honoured; appearance resource names preserve existing page XObjects and distinct annotations, including inherited resources. Placement retains small transform coefficients for large appearance bounding boxes and matrices. Missing or unusable appearance streams remain outside this path."
[[feature]]
id = "print.print-with-comment-summary"