Merge pull request #1919 from Silvertree2010/fix/1881-form-fields-export-with-a-stroke

Export form field borders from the item stroke
This commit is contained in:
Brandon Thomas
2026-10-29 21:53:21 -04:00
committed by GitHub
2 changed files with 49 additions and 6 deletions
+19 -6
View File
@@ -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<Placed> {
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
@@ -96,6 +98,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<Vec<u8>> {
@@ -177,13 +189,14 @@ pub(crate) fn add_fields(pdf: &[u8], fields: &[Placed], media: &[PlacedMedia]) -
put(&mut out, font, b"<</Type/Font/Subtype/Type1/BaseFont/Helvetica/Encoding/WinAnsiEncoding>>");
}
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<</W {bw:.2}>>")) } else { ("", "/BS<</W 0>>".to_string()) };
let common = format!(
"/Type/Annot/Subtype/Widget/T{}/Rect{rect}/F 4/P {page_ref} 0 R/MK<</BC[0.45 0.45 0.45]/BG[1 1 1]>>/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 };
@@ -194,7 +207,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 = 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 = 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),
+30
View File
@@ -798,6 +798,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<</W 0>>"), "no border width");
assert!(!none.contains("/BC"), "no border colour");
let stroked = export(Stroke { weight: 1.0, ..Stroke::default() });
assert!(stroked.contains("/BS<</W 1"), "border width");
assert!(stroked.contains("/BC"), "border colour");
}
#[test]
fn extreme_page_selection_is_a_page_error_without_overflow() {
let doc = doc_with_text("Bounded page selection");