From 516998d4494ee33472359c6eab029e07a7215838 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Tue, 27 Oct 2026 13:33:17 +0000 Subject: [PATCH 1/4] Export form field borders from the item stroke Fixes #1881 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/pdf/src/forms.rs | 16 ++++++++++------ crates/pdf/src/tests.rs | 30 ++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index 07f4cf04c..bf0d955fc 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -10,8 +10,9 @@ use designcraft_doc::{Document, FieldKind, FormField, Item, MediaOptions}; use crate::export::Sheet; use crate::transitions::{bracketed, int_after, object, rfind}; -/// A field placed on an output page: (page index, PDF rectangle, field). -pub(crate) type Placed = (usize, [f64; 4], FormField); +/// A field placed on an output page: (page index, PDF rectangle, field, border width, 0 when the +/// item has no stroke). +pub(crate) type Placed = (usize, [f64; 4], FormField, f64); /// A placed video or sound on an output page: (page index, PDF rectangle, file name, MIME type, /// bytes, options). @@ -37,7 +38,8 @@ pub(crate) fn collect(doc: &Document, sheets: &[Sheet]) -> Vec { let mut out = Vec::new(); visit(doc, sheets, |pi, r, it| { if let Some(f) = &it.form_field { - out.push((pi, r, f.clone())); + let bw = if it.stroke.is_none() || !it.stroke.weight.is_finite() { 0.0 } else { it.stroke.weight }; + out.push((pi, r, f.clone(), bw)); } }); out @@ -158,13 +160,14 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) - put(&mut out, font, b"<>"); } let mut all = Vec::new(); - for (page, r, f) in fields { + for (page, r, f, bw) in fields { let Some(&page_ref) = kids.get(*page) else { continue }; let rect = format!("[{:.2} {:.2} {:.2} {:.2}]", r[0], r[1], r[2], r[3]); let (w, h) = (r[2] - r[0], r[3] - r[1]); let fs = if f.font_size > 0.0 { f.font_size } else { 0.0 }; + let (bc, bs) = if *bw > 0.0 { ("/BC[0.45 0.45 0.45]", format!("/BS<>")) } else { ("", "/BS<>".to_string()) }; let common = format!( - "/Type/Annot/Subtype/Widget/T{}/Rect{rect}/F 4/P {page_ref} 0 R/MK<>/DA(/Helv {fs:.1} Tf 0 g)", + "/Type/Annot/Subtype/Widget/T{}/Rect{rect}/F 4/P {page_ref} 0 R/MK<<{bc}/BG[1 1 1]>>{bs}/DA(/Helv {fs:.1} Tf 0 g)", pdf_string(&f.name) ); let req = if f.required { 2 } else { 0 }; @@ -175,7 +178,8 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) - } FieldKind::CheckBox => { // Appearances: an empty box, and a box with a check mark. - let border = format!("0.45 G 1 w 0.5 0.5 {:.2} {:.2} re S", (w - 1.0).max(0.0), (h - 1.0).max(0.0)); + let border = + if *bw > 0.0 { format!("0.45 G 1 w 0.5 0.5 {:.2} {:.2} re S", (w - 1.0).max(0.0), (h - 1.0).max(0.0)) } else { String::new() }; let check = format!( "{border} 0 g {:.2} w 1 J 1 j {:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l S", (w.min(h) * 0.12).max(0.8), diff --git a/crates/pdf/src/tests.rs b/crates/pdf/src/tests.rs index b06fad47d..9c3a9ff9a 100644 --- a/crates/pdf/src/tests.rs +++ b/crates/pdf/src/tests.rs @@ -791,6 +791,36 @@ fn only_printed_fields_and_buttons_export() { assert_eq!(all.matches("/Link").count(), shown.matches("/Link").count(), "only the shown button"); } +#[test] +fn form_field_border_follows_the_item_stroke() { + use designcraft_doc::{FieldKind, FormField, Shape, Stroke}; + use designcraft_geom::shapes::rectangle; + let export = |stroke: Stroke| { + let mut d = Document::new(&NewDocument::default()); + let lid = d.default_layer(); + let mut it = Item::new(ItemId(d.alloc()), lid, Shape::Rectangle, rectangle(Rect::new(72.0, 72.0, 250.0, 96.0))); + it.stroke = stroke; + it.form_field = Some(FormField { + kind: FieldKind::TextField, + name: "name".into(), + value: String::new(), + options: Vec::new(), + multiline: false, + required: false, + font_size: 0.0, + }); + d.insert_item(SpreadRef::Doc(0), it, None).unwrap(); + uncompressed(&d, PdfOptions::default()) + }; + let none = export(Stroke { weight: 0.0, swatch: designcraft_color::swatch::NONE.into(), ..Stroke::default() }); + assert!(none.contains("/Subtype/Widget")); + assert!(none.contains("/BS<>"), "no border width"); + assert!(!none.contains("/BC"), "no border colour"); + let stroked = export(Stroke { weight: 1.0, ..Stroke::default() }); + assert!(stroked.contains("/BS< Date: Thu, 29 Oct 2026 21:40:04 -0400 Subject: [PATCH 2/4] Draw a checkbox's border at the item's stroke weight Co-Authored-By: Claude Opus 5.5 --- crates/pdf/src/forms.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index bf0d955fc..73165dc7a 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -179,7 +179,7 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) - FieldKind::CheckBox => { // Appearances: an empty box, and a box with a check mark. let border = - if *bw > 0.0 { format!("0.45 G 1 w 0.5 0.5 {:.2} {:.2} re S", (w - 1.0).max(0.0), (h - 1.0).max(0.0)) } else { String::new() }; + if *bw > 0.0 { format!("0.45 G {bw:.2} w {i:.2} {i:.2} {:.2} {:.2} re S", (w - bw).max(0.0), (h - bw).max(0.0), i = bw / 2.0) } else { String::new() }; let check = format!( "{border} 0 g {:.2} w 1 J 1 j {:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l S", (w.min(h) * 0.12).max(0.8), From 680dfdc964b3dd86858326fc44c1f963f3f9fc4c Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 29 Oct 2026 21:41:41 -0400 Subject: [PATCH 3/4] Format the checkbox border Co-Authored-By: Claude Opus 5.5 --- crates/pdf/src/forms.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index 73165dc7a..90234859d 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -178,8 +178,11 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) - } FieldKind::CheckBox => { // Appearances: an empty box, and a box with a check mark. - let border = - if *bw > 0.0 { format!("0.45 G {bw:.2} w {i:.2} {i:.2} {:.2} {:.2} re S", (w - bw).max(0.0), (h - bw).max(0.0), i = bw / 2.0) } else { String::new() }; + let border = if *bw > 0.0 { + format!("0.45 G {bw:.2} w {i:.2} {i:.2} {:.2} {:.2} re S", (w - bw).max(0.0), (h - bw).max(0.0), i = bw / 2.0) + } else { + String::new() + }; let check = format!( "{border} 0 g {:.2} w 1 J 1 j {:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l S", (w.min(h) * 0.12).max(0.8), From eace4735dac32df6785e82718991f391bdc5ac97 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Thu, 29 Oct 2026 21:43:49 -0400 Subject: [PATCH 4/4] Draw the checkbox border in its own function Keeps add_fields under the cognitive complexity limit. Co-Authored-By: Claude Opus 5.5 --- crates/pdf/src/forms.rs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/crates/pdf/src/forms.rs b/crates/pdf/src/forms.rs index 90234859d..e36478971 100644 --- a/crates/pdf/src/forms.rs +++ b/crates/pdf/src/forms.rs @@ -79,6 +79,16 @@ fn pdf_string(s: &str) -> String { o } +/// A checkbox appearance's border: the item's stroke weight, inset by half of it so it stays +/// inside the box; nothing for an unstroked item. +fn checkbox_border(bw: f64, w: f64, h: f64) -> String { + if bw > 0.0 { + format!("0.45 G {bw:.2} w {i:.2} {i:.2} {:.2} {:.2} re S", (w - bw).max(0.0), (h - bw).max(0.0), i = bw / 2.0) + } else { + String::new() + } +} + /// Add `fields` and `media` to `pdf` (page indices in output order). `None` when the file's /// structure isn't the simple kind this understands. pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) -> Option> { @@ -178,11 +188,7 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) - } FieldKind::CheckBox => { // Appearances: an empty box, and a box with a check mark. - let border = if *bw > 0.0 { - format!("0.45 G {bw:.2} w {i:.2} {i:.2} {:.2} {:.2} re S", (w - bw).max(0.0), (h - bw).max(0.0), i = bw / 2.0) - } else { - String::new() - }; + let border = checkbox_border(*bw, w, h); let check = format!( "{border} 0 g {:.2} w 1 J 1 j {:.2} {:.2} m {:.2} {:.2} l {:.2} {:.2} l S", (w.min(h) * 0.12).max(0.8),