mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-12 22:02:33 +00:00
Merge PR #3515: M12: XFA text honors its horizontal font scale
crates/xfa/src/layout.rs: both sides added doc comments to value_height, caption_reserve, field_height, unique_name and draw_field; kept HEAD's, with #3542's natural_width.
This commit is contained in:
commit
be69bfadf9
6 files changed
+201
-10
No files matched your search
@@ -1131,6 +1131,14 @@ impl Layouter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The face a field's text is laid out in: at its horizontal scale when the layout draws
|
||||
/// the text itself (a non-interactive field), as designed when the forms layer draws it in
|
||||
/// a widget, whose appearances have no horizontal scaling.
|
||||
fn field_text_face(f: &Field, font: &Font) -> Face {
|
||||
let face = Face::of(font);
|
||||
if f.access == Access::NonInteractive { face } else { face.unscaled() }
|
||||
}
|
||||
|
||||
/// The height the text of a multi-line text field takes (its value in the data, else the
|
||||
/// template's), for a field without a fixed height, which grows to hold its text. `ctx`
|
||||
/// is the field's own (its SOM ends in the field). Zero for anything else.
|
||||
@@ -1147,9 +1155,11 @@ impl Layouter<'_> {
|
||||
};
|
||||
let w = (inner_w - beside - f.ui_margin.horizontal() - 4.0).max(1.0);
|
||||
let font = ctx.font.apply(&f.common.font);
|
||||
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Face::of(&font), &|_| String::new())
|
||||
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Self::field_text_face(f, &font), &|_| String::new())
|
||||
}
|
||||
|
||||
/// The height of an exclusion group's fields at `inner_w`, by the group's layout: stacked
|
||||
/// top to bottom, at their own positions, or packed in rows.
|
||||
fn group_height(&self, g: &ExclGroup, inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
|
||||
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
|
||||
match g.layout {
|
||||
@@ -1657,14 +1667,16 @@ impl Layouter<'_> {
|
||||
right: f.ui_margin.right + 2.0,
|
||||
};
|
||||
let para = Para { v_align: VAlign::Top, ..fctx.para.clone() };
|
||||
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), ¶, &Face::of(&fctx.font), &|_| String::new());
|
||||
// In the face it was measured in, so it takes the lines it was given.
|
||||
let face = Self::field_text_face(f, &fctx.font);
|
||||
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), ¶, &face, &|_| String::new());
|
||||
return self.push_block(block);
|
||||
}
|
||||
}
|
||||
let face = match (&f.ui, &f.caption) {
|
||||
// Buttons show their caption in the caption's font.
|
||||
(Ui::Button, Some(cap)) => Face::of(&ctx.font.apply(&cap.font)),
|
||||
_ => Face::of(&fctx.font),
|
||||
(Ui::Button, Some(cap)) => Face::of(&ctx.font.apply(&cap.font)).unscaled(),
|
||||
_ => Face::of(&fctx.font).unscaled(),
|
||||
};
|
||||
let h_align = match (&f.ui, &f.caption) {
|
||||
(Ui::Button, Some(cap)) => ctx.para.apply(&cap.para).h_align,
|
||||
|
||||
+10
-1
@@ -155,6 +155,8 @@ pub struct FontSpec {
|
||||
pub italic: Option<bool>,
|
||||
pub underline: Option<bool>,
|
||||
pub color: Option<Color>,
|
||||
/// `fontHorizontalScale`, as a fraction (0.9 for `90%`).
|
||||
pub h_scale: Option<f64>,
|
||||
}
|
||||
|
||||
/// A font with every property resolved.
|
||||
@@ -166,16 +168,20 @@ pub struct Font {
|
||||
pub italic: bool,
|
||||
pub underline: bool,
|
||||
pub color: Color,
|
||||
/// Horizontal scale of the glyphs, as a fraction (1.0: as designed).
|
||||
pub h_scale: f64,
|
||||
}
|
||||
|
||||
impl Default for Font {
|
||||
/// XFA's default font: Courier 10 pt.
|
||||
fn default() -> Self {
|
||||
Font { typeface: "Courier".into(), size: 10.0, bold: false, italic: false, underline: false, color: Color::BLACK }
|
||||
Font { typeface: "Courier".into(), size: 10.0, bold: false, italic: false, underline: false, color: Color::BLACK, h_scale: 1.0 }
|
||||
}
|
||||
}
|
||||
|
||||
impl Font {
|
||||
/// This font with the properties `spec` sets in place of its own; those `spec` leaves
|
||||
/// `None` are inherited.
|
||||
pub fn apply(&self, spec: &FontSpec) -> Font {
|
||||
Font {
|
||||
typeface: spec.typeface.clone().unwrap_or_else(|| self.typeface.clone()),
|
||||
@@ -184,6 +190,7 @@ impl Font {
|
||||
italic: spec.italic.unwrap_or(self.italic),
|
||||
underline: spec.underline.unwrap_or(self.underline),
|
||||
color: spec.color.unwrap_or(self.color),
|
||||
h_scale: spec.h_scale.unwrap_or(self.h_scale),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -617,6 +624,8 @@ pub struct Run {
|
||||
pub italic: Option<bool>,
|
||||
pub underline: Option<bool>,
|
||||
pub size: Option<f64>,
|
||||
/// `xfa-font-horizontal-scale`, as a fraction.
|
||||
pub h_scale: Option<f64>,
|
||||
/// A floating field embedded in the text (`xfa:embed="#id"`), shown by its value.
|
||||
pub embed: Option<String>,
|
||||
}
|
||||
|
||||
+30
-1
@@ -40,10 +40,20 @@ pub fn measure(s: &str) -> Option<f64> {
|
||||
Some((v * k).clamp(-100_000.0, 100_000.0))
|
||||
}
|
||||
|
||||
/// The attribute `name` of `n` read as a measurement in points (see [`measure`]); `None`
|
||||
/// when it is missing or unreadable.
|
||||
fn attr_measure(n: XmlNode, name: &str) -> Option<f64> {
|
||||
n.attribute(name).and_then(measure)
|
||||
}
|
||||
|
||||
/// A horizontal scale such as `90%` (a bare number is a percentage too), as a fraction, within
|
||||
/// 1% and 1000%; `None` when it isn't a positive number.
|
||||
fn scale(v: &str) -> Option<f64> {
|
||||
let v: f64 = v.trim().trim_end_matches('%').trim().parse().ok()?;
|
||||
(v.is_finite() && v > 0.0).then(|| (v / 100.0).clamp(0.01, 10.0))
|
||||
}
|
||||
|
||||
/// The first child element of `n` named `name` (by local name, in any namespace).
|
||||
fn child<'a, 'd>(n: XmlNode<'a, 'd>, name: &str) -> Option<XmlNode<'a, 'd>> {
|
||||
n.children().find(|c| c.is_element() && c.tag_name().name() == name)
|
||||
}
|
||||
@@ -139,6 +149,8 @@ fn parse_border(n: Option<XmlNode>) -> Option<Border> {
|
||||
Some(Border { presence: Presence::parse(n.attribute("presence")), edges: four, fill: parse_fill(child(n, "fill")) })
|
||||
}
|
||||
|
||||
/// `<font>`: typeface, size, weight, posture, underline, fill color and horizontal scale.
|
||||
/// What the element doesn't set, or a missing element, is left `None` to inherit.
|
||||
fn parse_font(n: Option<XmlNode>) -> FontSpec {
|
||||
let Some(n) = n else { return FontSpec::default() };
|
||||
FontSpec {
|
||||
@@ -148,6 +160,7 @@ fn parse_font(n: Option<XmlNode>) -> FontSpec {
|
||||
italic: n.attribute("posture").map(|p| p == "italic"),
|
||||
underline: n.attribute("underline").map(|u| u != "0"),
|
||||
color: parse_color(child(n, "fill").and_then(|f| child(f, "color"))),
|
||||
h_scale: n.attribute("fontHorizontalScale").and_then(scale),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -196,7 +209,10 @@ fn parse_rich(body: XmlNode) -> RichText {
|
||||
italic: Option<bool>,
|
||||
underline: Option<bool>,
|
||||
size: Option<f64>,
|
||||
h_scale: Option<f64>,
|
||||
}
|
||||
/// `base` with the CSS declarations in `css` applied: weight, style, underline, size and
|
||||
/// `xfa-font-horizontal-scale`. Anything else is ignored.
|
||||
fn styled(base: &Style, css: Option<&str>) -> Style {
|
||||
let mut s = base.clone();
|
||||
for decl in css.unwrap_or("").split(';') {
|
||||
@@ -207,6 +223,7 @@ fn parse_rich(body: XmlNode) -> RichText {
|
||||
"font-style" => s.italic = Some(v == "italic" || v == "oblique"),
|
||||
"text-decoration" => s.underline = Some(v.contains("underline")),
|
||||
"font-size" => s.size = measure(v).filter(|x| *x > 0.0),
|
||||
"xfa-font-horizontal-scale" => s.h_scale = scale(v),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -214,6 +231,9 @@ fn parse_rich(body: XmlNode) -> RichText {
|
||||
}
|
||||
// Paragraph boundaries from `p` are implicit (dropped when empty); `br` makes an explicit
|
||||
// line break that stays even when the line is blank.
|
||||
/// Add the text under `n` to `out` as runs in `style`, as the elements it meets restyle it;
|
||||
/// `p` and `br` start paragraphs (`explicit` marks the `br` ones) and `embed` spans add
|
||||
/// embedded fields. Nothing deeper than `MAX_DEPTH` is read.
|
||||
fn walk(n: XmlNode, style: &Style, out: &mut RichText, explicit: &mut Vec<bool>, depth: usize) {
|
||||
if depth > MAX_DEPTH {
|
||||
return;
|
||||
@@ -232,6 +252,7 @@ fn parse_rich(body: XmlNode) -> RichText {
|
||||
italic: style.italic,
|
||||
underline: style.underline,
|
||||
size: style.size,
|
||||
h_scale: style.h_scale,
|
||||
embed: None,
|
||||
},
|
||||
);
|
||||
@@ -271,7 +292,15 @@ fn parse_rich(body: XmlNode) -> RichText {
|
||||
push_run(
|
||||
out,
|
||||
explicit,
|
||||
Run { text: String::new(), bold: st.bold, italic: st.italic, underline: st.underline, size: st.size, embed: Some(e) },
|
||||
Run {
|
||||
text: String::new(),
|
||||
bold: st.bold,
|
||||
italic: st.italic,
|
||||
underline: st.underline,
|
||||
size: st.size,
|
||||
h_scale: st.h_scale,
|
||||
embed: Some(e),
|
||||
},
|
||||
);
|
||||
}
|
||||
walk(c, &st, out, explicit, depth + 1);
|
||||
|
||||
+14
-2
@@ -509,6 +509,9 @@ impl Emitter<'_> {
|
||||
Some(r)
|
||||
}
|
||||
|
||||
/// Write a new image XObject for a picture, tagged with its `hash`: a JPEG as it is, a PNG
|
||||
/// or GIF decoded to samples with a soft mask for any transparency. `None`, with a warning,
|
||||
/// when the picture can't be read or is another kind.
|
||||
fn new_image_xobject(&mut self, data: &[u8], content_type: &str, hash: &[u8; 32]) -> Option<ObjRef> {
|
||||
let what = if content_type.is_empty() { "picture" } else { content_type };
|
||||
let mut d = Dict::new();
|
||||
@@ -570,6 +573,9 @@ impl Emitter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Write one laid-out page under `pages_ref`: its fills, lines, text and pictures in a
|
||||
/// content stream, the fonts and images it uses as resources, and its widgets as
|
||||
/// annotations. Returns the page object.
|
||||
fn page(&mut self, page: &Page, pages_ref: ObjRef) -> Result<ObjRef, XfaError> {
|
||||
let page_ref = self.doc.add(Object::Dict(Dict::new()));
|
||||
let mut content: Vec<u8> = Vec::new();
|
||||
@@ -612,11 +618,17 @@ impl Emitter<'_> {
|
||||
}
|
||||
let name = s.face.resource_name();
|
||||
fonts.insert(name, s.face.family.base_font(s.face.bold, s.face.italic));
|
||||
// Horizontal scaling is text state, which outlives the text object: a
|
||||
// condensed span gets a graphics state of its own, so the next isn't.
|
||||
let scaled = (s.face.h_scale - 1.0).abs() > 1e-6;
|
||||
let (open, tz, close) =
|
||||
if scaled { ("q BT", format!(" {} Tz", n(s.face.h_scale * 100.0)), " ET Q") } else { ("BT", String::new(), " ET") };
|
||||
content.extend(
|
||||
format!("BT /{name} {} Tf {} rg 1 0 0 1 {} {} Tm ", n(s.face.size), rgb(s.face.color), n(s.x), n(h - s.baseline)).bytes(),
|
||||
format!("{open} /{name} {} Tf {} rg{tz} 1 0 0 1 {} {} Tm ", n(s.face.size), rgb(s.face.color), n(s.x), n(h - s.baseline))
|
||||
.bytes(),
|
||||
);
|
||||
content.extend(literal(&win_ansi(&s.text)));
|
||||
content.extend_from_slice(b" Tj ET\n");
|
||||
content.extend_from_slice(format!(" Tj{close}\n").as_bytes());
|
||||
}
|
||||
Item::Image { rect, data, content_type } => {
|
||||
let Some(r) = self.image_xobject(data, content_type) else { continue };
|
||||
|
||||
@@ -16,14 +16,18 @@ use crate::layout::WidgetKind;
|
||||
use crate::model::*;
|
||||
use crate::script::{NodeKind, Overrides, apply_overrides, fields_by_som, form_tree, overrides, rerender, set_overrides};
|
||||
|
||||
/// The widgets laid out on `page`, in order.
|
||||
fn widgets(page: &Page) -> Vec<&Widget> {
|
||||
page.items.iter().filter_map(|i| if let Item::Widget(w) = i { Some(w.as_ref()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// The text of each text span laid out on `page`, in order.
|
||||
fn texts(page: &Page) -> Vec<String> {
|
||||
page.items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s.text.clone()) } else { None }).collect()
|
||||
}
|
||||
|
||||
/// Measurements in points, inches, millimeters and centimeters convert to points, a bare
|
||||
/// number is points, and an `em` length, a non-number or a NaN is `None`.
|
||||
#[test]
|
||||
fn measurements_convert_to_points() {
|
||||
assert_eq!(measure("72pt"), Some(72.0));
|
||||
@@ -36,6 +40,113 @@ fn measurements_convert_to_points() {
|
||||
assert_eq!(measure("NaNpt"), None);
|
||||
}
|
||||
|
||||
/// Two draws of the same text, one at `fontHorizontalScale="80%"`, each 90% as wide as the
|
||||
/// text is at full width, and a rich-text draw condensed to half by its span style.
|
||||
fn scaled_text_template() -> (String, &'static str) {
|
||||
const TEXT: &str = "Approving authority name and title";
|
||||
let w = pdfcraft_fonts::helvetica_width(TEXT, 10.0) * 0.9;
|
||||
let xdp = format!(
|
||||
r##"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
<draw name="narrow" w="{w}pt" h="0.5in"><value><text>{TEXT}</text></value><font typeface="Arial" size="10pt" fontHorizontalScale="80%"/></draw>
|
||||
<draw name="plain" w="{w}pt" h="0.5in"><value><text>{TEXT}</text></value><font typeface="Arial" size="10pt"/></draw>
|
||||
<draw name="rich" w="7in" h="0.3in"><value><exData contentType="text/html"><body xmlns="http://www.w3.org/1999/xhtml"><p>Full <span style="xfa-font-horizontal-scale:50%">half</span></p></body></exData></value><font typeface="Arial" size="10pt"/></draw>
|
||||
</subform>
|
||||
</template></xdp:xdp>"##
|
||||
);
|
||||
(xdp, TEXT)
|
||||
}
|
||||
|
||||
/// `fontHorizontalScale` is read as a percentage and text is measured at it: the 80% draw
|
||||
/// fits its box on one line where the full-width one takes two, and a span's own scale
|
||||
/// applies to that span alone.
|
||||
#[test]
|
||||
fn horizontally_scaled_text_is_measured_and_drawn_narrower() {
|
||||
let (xdp, text) = scaled_text_template();
|
||||
let (tpl, _) = parse(&xdp).unwrap();
|
||||
let Node::Draw(narrow) = &tpl.root.children[0] else { panic!() };
|
||||
assert_eq!(narrow.common.font.h_scale, Some(0.8), "fontHorizontalScale is a percentage");
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
let spans: Vec<&text::Span> = form.pages[0].items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s) } else { None }).collect();
|
||||
let lines = |scale: f64, words: &[&str]| {
|
||||
let mut b: Vec<i64> = spans
|
||||
.iter()
|
||||
.filter(|s| s.face.h_scale == scale && words.iter().any(|w| s.text.contains(w)))
|
||||
.map(|s| (s.baseline * 100.0).round() as i64)
|
||||
.collect();
|
||||
b.dedup();
|
||||
b.len()
|
||||
};
|
||||
let words: Vec<&str> = text.split(' ').collect();
|
||||
// At 80% the text is 72% of its full width: it fits the 90% box on one line.
|
||||
assert_eq!(lines(0.8, &words), 1, "{spans:?}");
|
||||
// At full width it doesn't: two lines.
|
||||
assert_eq!(lines(1.0, &["Approving", "title"]), 2, "{spans:?}");
|
||||
// A span's own scale applies to it alone.
|
||||
let half = spans.iter().find(|s| s.text.contains("half")).expect("the half span");
|
||||
let full = spans.iter().find(|s| s.text.contains("Full")).expect("the full span");
|
||||
assert_eq!((half.face.h_scale, full.face.h_scale), (0.5, 1.0));
|
||||
assert!((half.face.width("half") - full.face.width("half") / 2.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
/// A growing non-interactive field is measured in the face its text is drawn in: condensed,
|
||||
/// its text takes fewer lines and the field grows only to hold them.
|
||||
#[test]
|
||||
fn a_non_interactive_field_is_measured_at_the_scale_its_text_is_drawn() {
|
||||
// A growing, non-interactive text field (drawn as text by the layout, not as a widget),
|
||||
// then a draw below it. However condensed the field's text is, the draw sits the same
|
||||
// distance below its last line: the field is as tall as the lines it shows.
|
||||
// Long enough to take more than a multi-line field's three lines at either scale.
|
||||
const TEXT: &str = "Remarks that run over several lines of a narrow box, and then over several more lines again, until the box has grown well past the three lines it starts with";
|
||||
let gap = |scale: u32| {
|
||||
let xdp = format!(
|
||||
r##"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
<field name="note" w="100pt" access="nonInteractive"><ui><textEdit multiLine="1"/></ui><value><text>{TEXT}</text></value><font typeface="Arial" size="10pt" fontHorizontalScale="{scale}%"/></field>
|
||||
<draw name="after" w="100pt" h="20pt"><value><text>AFTER</text></value><font typeface="Arial" size="10pt"/></draw>
|
||||
</subform>
|
||||
</template></xdp:xdp>"##
|
||||
);
|
||||
let form = layout_xml(&xdp).unwrap();
|
||||
let spans: Vec<&text::Span> = form.pages[0].items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s) } else { None }).collect();
|
||||
let after = spans.iter().find(|s| s.text.contains("AFTER")).expect("the draw").baseline;
|
||||
let lines: std::collections::BTreeSet<i64> =
|
||||
spans.iter().filter(|s| !s.text.contains("AFTER")).map(|s| (s.baseline * 100.0).round() as i64).collect();
|
||||
let last = *lines.iter().next_back().expect("the field's lines") as f64 / 100.0;
|
||||
(lines.len(), after - last)
|
||||
};
|
||||
let (full_lines, full_gap) = gap(100);
|
||||
let (narrow_lines, narrow_gap) = gap(60);
|
||||
assert!(narrow_lines < full_lines, "condensed, the text takes fewer lines: {narrow_lines} vs {full_lines}");
|
||||
assert!((narrow_gap - full_gap).abs() < 0.01, "the field shrinks with its text: gap {narrow_gap} vs {full_gap}");
|
||||
}
|
||||
|
||||
/// Condensed text is written with `Tz` inside a `q`/`Q` pair of its own, and full-width text
|
||||
/// sets no scale, so none carries over from the text before it.
|
||||
#[test]
|
||||
fn horizontally_scaled_text_is_written_with_tz_inside_its_own_graphics_state() {
|
||||
let (xdp, _) = scaled_text_template();
|
||||
let mut doc = Document::open(Arc::new(shell(&xdp))).unwrap();
|
||||
render_into(&mut doc).unwrap();
|
||||
let root = doc.get(doc.root().unwrap());
|
||||
let pages = doc.resolve(root.as_dict().unwrap().get(b"Pages").unwrap());
|
||||
let kid = pages.as_dict().unwrap().get(b"Kids").unwrap().as_array().unwrap()[0].clone();
|
||||
let page = doc.resolve(&kid);
|
||||
let contents = doc.resolve(page.as_dict().unwrap().get(b"Contents").unwrap());
|
||||
let Object::Stream(c) = &*contents else { panic!("a content stream") };
|
||||
let ops = String::from_utf8_lossy(&c.decoded().unwrap()).into_owned();
|
||||
let scaled: Vec<&str> = ops.lines().filter(|l| l.contains(" Tz ")).collect();
|
||||
assert!(!scaled.is_empty() && scaled.iter().all(|l| l.starts_with("q BT ") && l.ends_with(" ET Q")), "{ops}");
|
||||
assert!(scaled.iter().any(|l| l.contains(" 80 Tz ")) && scaled.iter().any(|l| l.contains(" 50 Tz ")), "{ops}");
|
||||
// Full-width text sets no scale, and none is left over from the condensed text before it.
|
||||
assert!(ops.lines().filter(|l| l.contains(" Tj ") && !l.contains(" Tz ")).all(|l| l.starts_with("BT ")), "{ops}");
|
||||
}
|
||||
|
||||
/// The sample template parses without warnings into what the layout uses: its page areas,
|
||||
/// page-number draws, rich-text title, field font, caption and widget border, check box,
|
||||
/// date field, radio group, table columns and page break all come through.
|
||||
#[test]
|
||||
fn the_template_parses_with_fonts_captions_and_rich_text() {
|
||||
let (tpl, warnings) = parse(&template(2)).unwrap();
|
||||
|
||||
+20
-2
@@ -75,9 +75,14 @@ pub struct Face {
|
||||
pub size: f64,
|
||||
pub color: [f64; 3],
|
||||
pub underline: bool,
|
||||
/// Horizontal scale of the glyphs (`fontHorizontalScale`), as a fraction: widths are
|
||||
/// measured at it and the text is written with `Tz`.
|
||||
pub h_scale: f64,
|
||||
}
|
||||
|
||||
impl Face {
|
||||
/// The face a resolved font is drawn in: the standard-14 family its typeface maps to,
|
||||
/// with its size and horizontal scale clamped to a drawable range.
|
||||
pub fn of(font: &Font) -> Face {
|
||||
Face {
|
||||
family: Family::of(&font.typeface),
|
||||
@@ -86,9 +91,12 @@ impl Face {
|
||||
size: font.size.clamp(1.0, 300.0),
|
||||
color: font.color.0,
|
||||
underline: font.underline,
|
||||
h_scale: font.h_scale.clamp(0.01, 10.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// This face with what a rich-text run sets in place of its own: weight, posture,
|
||||
/// underline, size and horizontal scale. The family and color stay.
|
||||
pub fn with_run(&self, run: &Run) -> Face {
|
||||
Face {
|
||||
family: self.family,
|
||||
@@ -97,20 +105,30 @@ impl Face {
|
||||
size: run.size.unwrap_or(self.size).clamp(1.0, 300.0),
|
||||
color: self.color,
|
||||
underline: run.underline.unwrap_or(self.underline),
|
||||
h_scale: run.h_scale.unwrap_or(self.h_scale).clamp(0.01, 10.0),
|
||||
}
|
||||
}
|
||||
|
||||
/// This face as designed, without a horizontal scale: field text is drawn by the forms
|
||||
/// layer, whose appearances have none, so it is measured that way too.
|
||||
pub fn unscaled(self) -> Face {
|
||||
Face { h_scale: 1.0, ..self }
|
||||
}
|
||||
|
||||
/// The resource name of this face's standard font, as content streams and `/DA` strings
|
||||
/// refer to it.
|
||||
pub fn resource_name(&self) -> &'static str {
|
||||
self.family.resource_name(self.bold, self.italic)
|
||||
}
|
||||
|
||||
/// Advance of `s` at this face's size.
|
||||
pub fn width(&self, s: &str) -> f64 {
|
||||
match self.family {
|
||||
let designed = match self.family {
|
||||
Family::Courier => s.chars().count() as f64 * 0.6 * self.size,
|
||||
Family::Helvetica => pdfcraft_fonts::helvetica_width(s, self.size) * if self.bold { 1.04 } else { 1.0 },
|
||||
Family::Times => pdfcraft_fonts::helvetica_width(s, self.size) * if self.bold { 0.96 } else { 0.92 },
|
||||
}
|
||||
};
|
||||
designed * self.h_scale
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user