Skip to content

Commit 2f69a62

Browse files
committed
fix(render): Type1 font text rendering via system-font fallback (v1.47.10)
Symptom: PDFs with Type1 fonts (e.g. UniviaProRegular in Word/InDesign- exported documents) rendered with all text invisible. Logos, vectors and "-" bullets did appear, but glyph-encoded text was silently dropped. Root cause: - fonts.rs:287-308 extract_and_parse_font only checked /FontFile2 (TrueType) and /FontFile3 (CFF). The /FontFile (Type1 binary) stream was ignored, leaving parsed=None. - fonts.rs:367-388 try_system_font fell through if no system font matched the BaseFont name (subset prefixes like BAAAAA+UniviaProRegular rarely match). - text_renderer.rs:46-49 early-returns when parsed=None, so no glyph fill commands were emitted by extract_draw_commands. Fix: try_system_font now ends with a last-resort Arial family fallback (arial / arialbd / ariali / arialbi based on bold/italic hints from the font name). Character mapping stays correct because char_to_glyph_id already routes through the font's ToUnicode CMap → Unicode → Arial cmap → GID. Only the visual letter shape changes. CIDFontType2 path (the v1.48.1 fix) is untouched. Verified with new regression test (tests/test_type1_text.rs) on both PDFs: - AC294 (Type1 + TrueType): 0 → 711 text fills emitted - Vloerverwarming (CIDFontType2): 2199 fills, unchanged
1 parent 558e9eb commit 2f69a62

6 files changed

Lines changed: 108 additions & 13 deletions

File tree

open-pdf-render/src/fonts.rs

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,11 @@ impl FontRegistry {
364364
/// Try to load a system font matching the PDF BaseFont name.
365365
/// Strips subset prefix (e.g., "ESYDQT+SegoeUI-Bold" → "SegoeUI-Bold")
366366
/// then maps to Windows font files.
367+
///
368+
/// As a last resort (e.g. for Type1 fonts whose embedded /FontFile we
369+
/// cannot parse, like UniviaPro), falls back to Arial so text is at
370+
/// least visible — character mapping still works through the font's
371+
/// ToUnicode CMap → Arial's Unicode cmap → glyph IDs.
367372
fn try_system_font(base_font: &str) -> Option<ParsedFont> {
368373
// Strip subset prefix: 6 uppercase letters + '+'
369374
let clean_name = if base_font.len() > 7 && base_font.as_bytes()[6] == b'+' {
@@ -373,18 +378,39 @@ impl FontRegistry {
373378
};
374379

375380
// Try to find system font file
376-
let font_path = Self::find_system_font(clean_name)?;
377-
let font_data = std::fs::read(&font_path).ok()?;
378-
match font_parser::parse_truetype(&font_data) {
379-
Ok(parsed) => {
380-
eprintln!("[fonts] Loaded system font: {} → {}", clean_name, font_path);
381-
Some(parsed)
381+
if let Some(font_path) = Self::find_system_font(clean_name) {
382+
if let Ok(font_data) = std::fs::read(&font_path) {
383+
match font_parser::parse_truetype(&font_data) {
384+
Ok(parsed) => {
385+
eprintln!("[fonts] Loaded system font: {} → {}", clean_name, font_path);
386+
return Some(parsed);
387+
}
388+
Err(e) => {
389+
eprintln!("[fonts] Failed to parse system font {}: {}", font_path, e);
390+
}
391+
}
382392
}
383-
Err(e) => {
384-
eprintln!("[fonts] Failed to parse system font {}: {}", font_path, e);
385-
None
393+
}
394+
395+
// Last-resort generic fallback — pick a sane default based on the
396+
// font name's hints (bold/italic) so substitutions look reasonable.
397+
let lower = clean_name.to_lowercase();
398+
let is_bold = lower.contains("bold") || lower.contains("black") || lower.contains("heavy");
399+
let is_italic = lower.contains("italic") || lower.contains("oblique");
400+
let fallback_file = match (is_bold, is_italic) {
401+
(true, true) => "arialbi.ttf",
402+
(true, false) => "arialbd.ttf",
403+
(false, true) => "ariali.ttf",
404+
(false, false) => "arial.ttf",
405+
};
406+
let path = format!(r"C:\Windows\Fonts\{}", fallback_file);
407+
if let Ok(font_data) = std::fs::read(&path) {
408+
if let Ok(parsed) = font_parser::parse_truetype(&font_data) {
409+
eprintln!("[fonts] Generic fallback: {} → {}", clean_name, fallback_file);
410+
return Some(parsed);
386411
}
387412
}
413+
None
388414
}
389415

390416
/// Find a system font file matching a PDF font name.
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
// Verifies Type1 fonts (e.g. UniviaPro) emit glyph outlines via system-font fallback.
2+
use open_pdf_render::PdfRenderer;
3+
use std::fs;
4+
5+
fn run(path: &str) -> (usize, usize) {
6+
let bytes = fs::read(path).expect("read pdf");
7+
let r = PdfRenderer::new();
8+
let doc = r.load_document(&bytes).expect("load");
9+
let buf = doc.extract_draw_commands(0, 0).expect("extract");
10+
let raw = buf.into_bytes();
11+
let mut fills = 0usize;
12+
let mut transforms = 0usize;
13+
// Header: f32 x0, y0, w, h = 16 bytes
14+
let mut i = 16usize;
15+
while i < raw.len() {
16+
let op = raw[i]; i += 1;
17+
match op {
18+
0 | 1 => i += 8, // MoveTo / LineTo
19+
2 => i += 24, // CubicTo
20+
3 => i += 16, // Rect
21+
4 => {} // ClosePath
22+
5 => i += 8, // SetStroke (rgba+width)
23+
6 => i += 4, // SetFill
24+
7 => {} // Stroke
25+
8 => fills += 1, // Fill
26+
9 => {} // FillEvenOdd
27+
10 | 11 => {} // Save/Restore
28+
12 => { transforms += 1; i += 24; }
29+
13 | 14 => i += 1,
30+
15 => i += 4,
31+
16 => {
32+
if i >= raw.len() { break; }
33+
let n = raw[i] as usize; i += 1;
34+
i += n * 4 + 4;
35+
}
36+
17 => {}
37+
18 => {
38+
if i + 17 > raw.len() { break; }
39+
i += 16;
40+
let len = raw[i] as usize; i += 1;
41+
i += len;
42+
}
43+
19 => {
44+
// DrawImage: u16 w + u16 h + u32 dataLen + bytes
45+
if i + 8 > raw.len() { break; }
46+
i += 4;
47+
let len = u32::from_le_bytes([raw[i],raw[i+1],raw[i+2],raw[i+3]]) as usize;
48+
i += 4 + len;
49+
}
50+
20 | 21 => {} // Clip / ClipEvenOdd
51+
_ => { eprintln!("unknown opcode {} at {}", op, i - 1); break; }
52+
}
53+
}
54+
(fills, transforms)
55+
}
56+
57+
#[test]
58+
fn ac294_emits_glyph_fills() {
59+
let (fills, transforms) = run("../test pdf-bestanden/AC294_offerte_stalen_bak_Vincent_Christe.pdf");
60+
eprintln!("AC294 page0: fills={} transforms={}", fills, transforms);
61+
assert!(fills > 100, "AC294 should emit many glyph fills, got {}", fills);
62+
}
63+
64+
#[test]
65+
fn vloerverwarming_still_renders() {
66+
let (fills, transforms) = run("../test pdf-bestanden/Vloerverwarming Woning Bert van Dorp - opm BvD.pdf");
67+
eprintln!("Vloerverwarming page0: fills={} transforms={}", fills, transforms);
68+
assert!(fills > 50, "Vloerverwarming regression: only {} fills", fills);
69+
}

open-pdf-studio/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "open-pdf-studio",
3-
"version": "1.47.9",
3+
"version": "1.47.10",
44
"description": "A free, open-source PDF annotation editor built with Tauri",
55
"scripts": {
66
"dev": "vite",

open-pdf-studio/src-tauri/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

open-pdf-studio/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "open-pdf-studio"
3-
version = "1.47.9"
3+
version = "1.47.10"
44
description = "A free, open-source PDF annotation editor"
55
authors = ["OpenAEC Foundation"]
66
license = "MIT"

open-pdf-studio/src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Open PDF Studio",
4-
"version": "1.47.9",
4+
"version": "1.47.10",
55
"identifier": "org.openaec.openpdfstudio",
66
"build": {
77
"frontendDist": "../dist",

0 commit comments

Comments
 (0)