mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-27 03:47:09 -06:00
Recognize the Symbol PCL6 font alias (#2521)
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Brandon Thomas
parent
74c1f26c4e
commit
db0affc0d3
@@ -22,7 +22,7 @@
|
||||
/// The symbol fonts this module knows, by lowercase family name.
|
||||
fn table(family: &str) -> Option<&'static [u32; 224]> {
|
||||
let f = family.trim();
|
||||
if f.eq_ignore_ascii_case("symbol") {
|
||||
if f.eq_ignore_ascii_case("symbol") || f.eq_ignore_ascii_case("symbol (pcl6)") {
|
||||
Some(&SYMBOL)
|
||||
} else if f.eq_ignore_ascii_case("wingdings") {
|
||||
Some(&WINGDINGS)
|
||||
@@ -243,6 +243,21 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcl6_symbol_encoding() {
|
||||
for family in ["Symbol (PCL6)", " symbol (pcl6) "] {
|
||||
assert!(is_symbol_font(family));
|
||||
for code in 0x20..=0xFF {
|
||||
let c = char::from_u32(code).unwrap();
|
||||
assert_eq!(symbol_char(family, c), symbol_char("Symbol", c));
|
||||
let pua = char::from_u32(0xF000 + code).unwrap();
|
||||
assert_eq!(symbol_char(family, pua), symbol_char("Symbol", pua));
|
||||
}
|
||||
assert_eq!(symbol_char(family, '\u{F0B3}'), Some('≥'));
|
||||
}
|
||||
assert!(!is_symbol_font("Symbol (unknown)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_uses_the_adobe_symbol_encoding() {
|
||||
assert_eq!(symbol_char("Symbol", '\u{F061}'), Some('α'));
|
||||
|
||||
@@ -236,6 +236,19 @@ fn symbol_font_text_reads_as_unicode_and_round_trips_unchanged() {
|
||||
assert_eq!(q[0].shown_text(), p[0].shown_text());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcl6_symbol_roundtrip() {
|
||||
let body = r#"<w:p><w:r><w:sym w:font="Symbol (PCL6)" w:char="F0B3"/></w:r></w:p>"#;
|
||||
let d = wordcraft_docx::read(&docx(body, &[], &[])).unwrap();
|
||||
let again = wordcraft_docx::read(&wordcraft_docx::write(&d).unwrap()).unwrap();
|
||||
for doc in [&d, &again] {
|
||||
let p = paras(doc)[0];
|
||||
assert_eq!(p.text, "\u{F0B3}");
|
||||
assert_eq!(p.props_of_char(0).font.as_deref(), Some("Symbol (PCL6)"));
|
||||
assert_eq!(p.shown_text(), "≥");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alternate_content_choice_and_fallback() {
|
||||
let body = r#"
|
||||
|
||||
@@ -227,7 +227,8 @@ pub fn resolve(family: &str, bold: bool, italic: bool) -> Resolved {
|
||||
return *r;
|
||||
}
|
||||
}
|
||||
let base = charset_base_family(family);
|
||||
// Printer-driver Symbol aliases use the same encoding as Symbol.
|
||||
let base = if family.trim().eq_ignore_ascii_case("Symbol (PCL6)") { "Symbol" } else { charset_base_family(family) };
|
||||
let (fam, substituted) = if db.has_family(family) {
|
||||
(family.to_string(), false)
|
||||
} else if base != family && db.has_family(base) {
|
||||
@@ -325,6 +326,20 @@ mod tests {
|
||||
assert_eq!(substitutes(charset_base_family("Times New Roman Cyr")), substitutes("Times New Roman"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcl6_symbol_resolves_as_symbol() {
|
||||
let actual = resolve("Symbol (PCL6)", false, false);
|
||||
let db = FontDb::global();
|
||||
if db.has_family("Symbol (PCL6)") {
|
||||
assert_eq!(actual.face.id(), db.face("Symbol (PCL6)", "Regular").id());
|
||||
assert!(!actual.substituted);
|
||||
return;
|
||||
}
|
||||
let expected = resolve("Symbol", false, false);
|
||||
assert_eq!(actual.face.id(), expected.face.id());
|
||||
assert_eq!(actual.substituted, expected.substituted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symbol_fonts_substitute_symbol_faces() {
|
||||
for f in ["Symbol", "Wingdings", "Wingdings 2", "Wingdings 3", "Webdings"] {
|
||||
|
||||
@@ -674,7 +674,15 @@ const MISSING_ROMAN_FONT: &str = "Cambria";
|
||||
fn lookup_family<'f>(doc: &Document, font: &'f str) -> &'f str {
|
||||
let db = wordcraft_fonts::FontDb::global();
|
||||
let roman = doc.settings.roman_fonts.iter().any(|f| f.eq_ignore_ascii_case(font));
|
||||
if roman && !db.has_family(font) && !db.has_family(wordcraft_fonts::word::charset_base_family(font)) { MISSING_ROMAN_FONT } else { font }
|
||||
if roman
|
||||
&& !wordcraft_doc::symbol_fonts::is_symbol_font(font)
|
||||
&& !db.has_family(font)
|
||||
&& !db.has_family(wordcraft_fonts::word::charset_base_family(font))
|
||||
{
|
||||
MISSING_ROMAN_FONT
|
||||
} else {
|
||||
font
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Builder<'a> {
|
||||
@@ -2796,3 +2804,18 @@ mod label_spacing_tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn symbols_ignore_roman_hint() {
|
||||
let mut d = Document::new();
|
||||
d.settings.roman_fonts = vec!["Symbol (PCL6)".into(), "Wingdings".into(), "No Such Roman Font".into()];
|
||||
for family in ["Symbol (PCL6)", "Wingdings"] {
|
||||
assert_eq!(lookup_family(&d, family), family);
|
||||
}
|
||||
assert_eq!(lookup_family(&d, "No Such Roman Font"), MISSING_ROMAN_FONT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1043,6 +1043,17 @@ fn symbol_font_bullet_without_the_font_draws_a_bullet() {
|
||||
assert_eq!(label.glyphs.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pcl6_symbol_draws_greater_equal() {
|
||||
let mut d = Document::from_text("\u{F0B3}");
|
||||
d.format_range(&Pos::body(0, 0), &Pos::body(0, 3), &|c| c.font = Some("Symbol (PCL6)".into())).unwrap();
|
||||
let l = lay(&d);
|
||||
let items = display::page_display(&d, &l.pages[0], &display::DisplayOptions::default());
|
||||
let text: String = items.iter().filter_map(|i| if let display::Draw::Glyphs { text, .. } = i { Some(text.as_str()) } else { None }).collect();
|
||||
assert_eq!(text, "≥");
|
||||
assert_eq!(d.plain_text(StoryRef::Body), "\u{F0B3}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wingdings_text_draws_and_reads_as_its_unicode_symbols() {
|
||||
// Word stores symbol-font text both ways: plain codes ("J" = smiley) and private-use codes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Layout and pagination parity with Microsoft Word
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** trivial (exact full font names select their installed face; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-16 · **Change:** trivial (Symbol PCL6 alias uses Symbol encoding; previously trivial: exact full font names select their installed face; previously trivial: consistent cell widths override a stale table grid; previously trivial: multi-paragraph field instructions no longer add blank lines; previously trivial: page-break-only list paragraphs hide their labels; previously trivial: floating-table overlap ignores empty line-break lines; previously trivial: paragraph spacing adds up when the document turns HTML auto spacing off; previously trivial: an inherited footer sizes its percentage tables by its own section; previously trivial: an empty section mark stays on a full page; previously trivial: a fitting row with less room than its minimum height moves on whole; oversized rows still split; previously trivial: text past an exact-height row is cut off; previously trivial: a list number stays with text after a leading page break; previously trivial: a bookmark after a page break takes no line; previously trivial: a page break followed by a page break before or a next-page section starts one page; previously trivial: All Caps and Small Caps draw ß as SS, #1581; previously trivial: justified vertical page alignment, #1320; previously trivial: page numbers restart at 0; previously trivial: a list label moves with its first line when the paragraph is centred or end-aligned (#1159); previously trivial: PAGE fields in a paragraph split across pages show the page they are on, #1352; previously trivial: a dragged floating object anchors in the column it lands in, #884; previously trivial: a paragraph ending on a page with footnotes keeps its space after above the separator, #1116; previously trivial: symbol-font text drawn as Unicode symbols, #776; previously trivial: large-table scrolling measured and fixed, #811; previously trivial: text boxes use their own margins; Resize shape to fit text never cuts text, #743; previously trivial: complex-script shaping and Myanmar line breaking, #604; previously trivial: body page totals, table-cell footnotes and bookmark link pages fixed, #450 #451 #455; previously major: first version; line breaking, pagination and object placement checked against Word's behaviour from the layout source on origin/main) · **Target:** Microsoft Word (Microsoft 365) for Mac 16.113.4
|
||||
|
||||
A document only "opens correctly" if its pages break where Word breaks them: the same lines, the
|
||||
same page count, footnotes and pictures on the same pages. This checklist covers line breaking,
|
||||
@@ -26,7 +26,7 @@ font can't match Word's metrics.
|
||||
| Default font metrics (Aptos, Calibri, Cambria…) | 🟡 | Carlito/Caladea/Liberation are metric-compatible for Calibri/Cambria/Arial/Times/Courier; **Aptos (Word's default since 2024) has no metric-compatible open substitute**, so new Word documents wrap differently ([typing-parity.md](typing-parity.md) Known gaps). Needs an owner decision (commission or license a metric-matched font) | 6–10 + owner |
|
||||
| Colour emoji and colour fonts on screen and in page renders: COLR v0/v1 paint graphs (solid fills, linear, radial and sweep gradients, composite modes) and bitmap strikes (Apple Color Emoji `sbix`, Noto Color Emoji `CBDT`) drawn in the font's colours at the layout's position and size; a dark page doesn't invert them, the same as pictures | 🟡 | `fonts::color` (skrifa), `render::color_glyph`; `render::tests_color_glyphs::emoji_are_drawn_in_colour` and `emoji_keep_their_colours_on_a_dark_page` (system font, skip without one), `render::color_glyph::tests`, `fonts::color::tests` (#1269). CBDT is verified on a real page with Noto Color Emoji; COLR v0/v1 and sbix are implemented to the OpenType spec and tested with synthetic data, but not yet seen on a real font (Segoe UI Emoji, COLRv1 Noto, Apple Color Emoji). PDF export already drew them through krilla | 1–2 |
|
||||
| Full font names in document font requests | ✅ | Exact English (or first available) OpenType name ID4 lookup in loaded faces and the native lazy catalog; family and legacy-family matches keep priority. No style-suffix guessing; named variable-instance aliases remain unsupported | 0 |
|
||||
| Symbol fonts (Symbol, Wingdings, Wingdings 2/3, Webdings) without the proprietary font: each character, stored as U+F020–U+F0FF or a plain 8-bit code, is drawn as its Unicode equivalent from a face that has it; PDF text and copies read as those symbols | ✅ | `doc::symbol_fonts` tables (Adobe Symbol table and Unicode L2/12-368), `layout::tests::wingdings_text_draws_and_reads_as_its_unicode_symbols` (#776). Shapes come from Noto Sans Symbols / Symbols 2, DejaVu Sans or Segoe UI Symbol, so they differ from Word's; Wingdings 0xFF (not in Unicode) still has no symbol | — |
|
||||
| Symbol fonts (Symbol including its PCL6 alias, Wingdings, Wingdings 2/3, Webdings) without the proprietary font: each character, stored as U+F020–U+F0FF or a plain 8-bit code, is drawn as its Unicode equivalent from a face that has it; PDF text and copies read as those symbols | ✅ | `doc::symbol_fonts` tables (Adobe Symbol table and Unicode L2/12-368), `layout::tests::wingdings_text_draws_and_reads_as_its_unicode_symbols` (#776). Shapes come from Noto Sans Symbols / Symbols 2, DejaVu Sans or Segoe UI Symbol, so they differ from Word's; Wingdings 0xFF (not in Unicode) still has no symbol | 0 |
|
||||
| Indents in characters (`w:firstLineChars`, `w:leftChars`…), measured in the paragraph mark's font size | ✅ | #534, `layout::tests::character_indents_use_the_paragraph_mark_size`; spacing in lines (`w:beforeLines`/`w:afterLines`) uses the stored points (needs the document grid) | — |
|
||||
| Line spacing (single, 1.5, double, at least, exactly, multiple), spacing before/after, contextual spacing | 🟡 | Contextual spacing measured in body paragraphs and headers (below), with a four-paragraph table-cell regression; list-label ascent overflow is unscaled by multiple spacing and uses descent for automatic and at-least spacing (#601); other line-spacing combinations remain (#108, #135 open) | 3–5 |
|
||||
| Kerning only where Kerning for fonts asks for it (`w:kern`, at or above its size) | ✅ | Measured in Word for Windows: no `w:kern` or `w:kern="0"` is unkerned; WordCraft kerned every run, so its lines held more text than Word's. `layout::tests::kerning_follows_kerning_for_fonts_like_word` | — |
|
||||
@@ -161,6 +161,7 @@ No overall parity estimate changes from these probes.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-16 | trivial | Exact OpenType full names (name ID4) select installed static faces after family names; variable-instance aliases remain unsupported |
|
||||
| 2026-10-16 | trivial | Symbol (PCL6) uses the existing Symbol mapping and Symbol face/fallback despite stale Roman font metadata; stored codes/fonts stay unchanged |
|
||||
| 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows |
|
||||
| 2026-10-14 | trivial | Consistent cell widths override a stale table grid |
|
||||
| 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |
|
||||
|
||||
Reference in New Issue
Block a user