mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
M2.6: non-embedded Japanese CID fonts draw in a craft-fonts face (#368)
#260 (page 2): text in a Japanese CID font that the PDF doesn't embed (HeiseiMin-W3 with UniJIS-UCS2-H, Adobe-Japan1) drew nothing, while MuPDF, Poppler, PDFium and Acrobat draw it in a Mincho face. hayro asks its font resolver for a substitute with the CID collection in the query, but the renderer never installed one, so hayro's default answered with a Latin standard-14 font that has no Japanese glyphs. Release builds have embedded Japanese faces from craft-fonts all along; the renderer just didn't use them. RenderConfig::settings now installs a resolver: for an Adobe-Japan1 fallback query it returns a craft-fonts face (Shippori Mincho for Mincho names, BIZ UDPGothic Regular or Bold for Gothic names, the descriptor's serif flag otherwise); every other query keeps hayro's default. hayro already maps such fonts through the Adobe-Japan1-UCS2 CMap to Unicode and the face's cmap. Without craft-fonts nothing changes. Other CJK collections are untouched: the pinned craft-fonts has only Japanese faces, and its later Chinese face is Noto CJK, which AGENTS.md §1.1 rules out. pdfcraft-render now depends on pdfcraft-fonts (L3 to L2). Tests: raster::tests::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face (with CRAFT_FONTS_DIR: 日本 in HeiseiMin-W3 and HeiseiKakuGo-W5 now draws; it drew 0 dark pixels before; skips without craft-fonts, as the Linux CI job has them) and raster::tests::japanese_font_names_pick_mincho_or_gothic. The parity entry view.render-cid-fonts cites the test and notes what is still missing. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
4995da0653
commit
3b8a274f52
Generated
+1
@@ -3750,6 +3750,7 @@ dependencies = [
|
||||
"kurbo 0.13.1",
|
||||
"log",
|
||||
"lopdf",
|
||||
"pdfcraft-fonts",
|
||||
"thiserror 2.0.21",
|
||||
]
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ license.workspace = true
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
pdfcraft-fonts = { workspace = true }
|
||||
hayro = { workspace = true }
|
||||
hayro-syntax = { workspace = true }
|
||||
lopdf = { workspace = true }
|
||||
|
||||
+110
-1
@@ -14,6 +14,8 @@ use std::sync::{Arc, Mutex};
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use hayro::hayro_interpret::InterpreterSettings;
|
||||
use hayro::hayro_interpret::font::{FontData, FontQuery};
|
||||
use hayro::hayro_interpret::hayro_cmap::CidFamily;
|
||||
use hayro::hayro_syntax::Pdf;
|
||||
use hayro::vello_cpu::color::palette::css::WHITE;
|
||||
use hayro::{RenderCache, RenderSettings, render};
|
||||
@@ -50,7 +52,54 @@ pub struct RenderConfig {
|
||||
|
||||
impl RenderConfig {
|
||||
fn settings(&self) -> InterpreterSettings {
|
||||
InterpreterSettings { ocg_overrides: self.layers.clone(), hide_comments: self.hide_comments, ..InterpreterSettings::default() }
|
||||
let standard = InterpreterSettings::default().font_resolver;
|
||||
InterpreterSettings {
|
||||
ocg_overrides: self.layers.clone(),
|
||||
hide_comments: self.hide_comments,
|
||||
font_resolver: Arc::new(move |query| japanese_fallback(query).or_else(|| standard(query))),
|
||||
..InterpreterSettings::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Japanese face from craft-fonts for a CID font of the Adobe-Japan1 collection that the PDF
|
||||
/// doesn't embed (`HeiseiMin-W3`, `KozGoPro-Medium`, …). hayro's own substitutes for fonts that
|
||||
/// aren't embedded are the Latin standard 14, so such text drew nothing. `None` for every other
|
||||
/// font, and when PdfCraft was built without craft-fonts. Only Japanese: the pinned craft-fonts
|
||||
/// has no other CJK faces, and its later Chinese face is Noto CJK, which AGENTS.md §1.1 rules out.
|
||||
fn japanese_fallback(query: &FontQuery) -> Option<(FontData, u32)> {
|
||||
let FontQuery::Fallback(f) = query else { return None };
|
||||
if f.character_collection.as_ref()?.family != CidFamily::AdobeJapan1 {
|
||||
return None;
|
||||
}
|
||||
let face = match japanese_face(f.post_script_name.as_deref().unwrap_or_default(), f.is_serif, f.is_bold || f.font_weight >= 600) {
|
||||
JapaneseFace::Mincho => pdfcraft_fonts::document_japanese_font(),
|
||||
JapaneseFace::Gothic { bold } => {
|
||||
let faces = pdfcraft_fonts::ui_japanese_fonts();
|
||||
let style = if bold { "Bold" } else { "Regular" };
|
||||
faces.iter().find(|c| c.family == "BIZ UDPGothic" && c.style == style).or(faces.first()).copied()
|
||||
}
|
||||
}?;
|
||||
Some((Arc::new(face.bytes), 0))
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum JapaneseFace {
|
||||
Mincho,
|
||||
Gothic { bold: bool },
|
||||
}
|
||||
|
||||
/// Which kind of Japanese face stands in for the font named `name`: Mincho names (`HeiseiMin`,
|
||||
/// `KozMin`, `Ryumin`, `MS-Mincho`) a serif Mincho; Gothic names (`…Gothic…`, `HeiseiKakuGo`,
|
||||
/// `KozGo`, `…Maru…`) a sans Gothic; any other name by the font descriptor's serif flag.
|
||||
fn japanese_face(name: &str, serif: bool, bold: bool) -> JapaneseFace {
|
||||
let name = name.to_ascii_lowercase();
|
||||
if name.contains("min") {
|
||||
JapaneseFace::Mincho
|
||||
} else if ["goth", "kakugo", "kozgo", "kaku", "maru"].iter().any(|k| name.contains(k)) || !serif {
|
||||
JapaneseFace::Gothic { bold }
|
||||
} else {
|
||||
JapaneseFace::Mincho
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1459,6 +1508,66 @@ trailer << /Root 1 0 R >>
|
||||
}
|
||||
}
|
||||
|
||||
/// A Japanese CID font that isn't embedded (Adobe-Japan1, as `HeiseiMin-W3` with
|
||||
/// `UniJIS-UCS2-H` in #260's test file) drew nothing: hayro's substitutes for fonts that
|
||||
/// aren't embedded are Latin-only. With craft-fonts (the build input release builds embed),
|
||||
/// such text now draws in a Japanese face; without it, nothing changes.
|
||||
#[test]
|
||||
fn non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face() {
|
||||
let pdf = |base_font: &str| {
|
||||
let content = "BT /F1 40 Tf 5 15 Td <65E5672C> Tj ET";
|
||||
format!(
|
||||
"%PDF-1.7
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 100 60] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj
|
||||
4 0 obj << /Length {} >> stream
|
||||
{content}
|
||||
endstream endobj
|
||||
5 0 obj << /Type /Font /Subtype /Type0 /BaseFont /{base_font} /Encoding /UniJIS-UCS2-H /DescendantFonts [6 0 R] >> endobj
|
||||
6 0 obj << /Type /Font /Subtype /CIDFontType0 /BaseFont /{base_font} /CIDSystemInfo << /Registry (Adobe) /Ordering (Japan1) /Supplement 2 >>
|
||||
/FontDescriptor 7 0 R /DW 1000 >> endobj
|
||||
7 0 obj << /Type /FontDescriptor /FontName /{base_font} /Flags 6 /FontBBox [0 -141 1000 859] /ItalicAngle 0 /Ascent 859 /Descent -141 /CapHeight 700 /StemV 80 >> endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF",
|
||||
content.len()
|
||||
)
|
||||
};
|
||||
for base_font in ["HeiseiMin-W3", "HeiseiKakuGo-W5"] {
|
||||
let mut r = PageRenderer::new(Arc::new(pdf(base_font).into_bytes()), RenderConfig::default());
|
||||
let p = r.render(RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 });
|
||||
assert!(p.error.is_none(), "{base_font}: {:?}", p.error);
|
||||
let inked = p.rgba.as_chunks::<4>().0.iter().filter(|c| c[0] < 128).count();
|
||||
if pdfcraft_fonts::document_japanese_font().is_none() {
|
||||
eprintln!("built without craft-fonts (CRAFT_FONTS_DIR unset): no Japanese face to check");
|
||||
continue;
|
||||
}
|
||||
// 日本 at 40 pt covers a few hundred dark pixels; a blank or missing-glyph run doesn't.
|
||||
assert!(inked > 300, "{base_font}: 日本 is drawn ({inked} dark pixels)");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_font_names_pick_mincho_or_gothic() {
|
||||
use super::JapaneseFace::{Gothic, Mincho};
|
||||
for (name, serif, bold, face) in [
|
||||
("HeiseiMin-W3", false, false, Mincho),
|
||||
("KozMinPro-Regular", false, false, Mincho),
|
||||
("Ryumin-Light", false, false, Mincho),
|
||||
("MS-PMincho", false, false, Mincho),
|
||||
("HiraMinProN-W3", false, false, Mincho),
|
||||
("HeiseiKakuGo-W5", true, false, Gothic { bold: false }),
|
||||
("KozGoPro-Bold", true, true, Gothic { bold: true }),
|
||||
("GothicBBB-Medium", true, false, Gothic { bold: false }),
|
||||
("MS-Gothic", false, false, Gothic { bold: false }),
|
||||
("HiraKakuProN-W6", false, true, Gothic { bold: true }),
|
||||
("Unknown-Japanese", true, false, Mincho),
|
||||
("Unknown-Japanese", false, false, Gothic { bold: false }),
|
||||
] {
|
||||
assert_eq!(super::japanese_face(name, serif, bold), face, "{name}");
|
||||
}
|
||||
}
|
||||
|
||||
/// An alpha soft mask whose transparency group has no /CS (as Chrome writes gradient text)
|
||||
/// must still mask. Regression test for the vendored hayro-interpret patch.
|
||||
#[test]
|
||||
|
||||
@@ -746,8 +746,8 @@ milestone = "M2"
|
||||
status = "partial"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate"]
|
||||
notes = "CID-to-Unicode tables are M2.4"
|
||||
evidence = ["crates/render/src/raster.rs::huge_cid_width_ranges_terminate", "crates/render/src/raster.rs::non_embedded_japanese_cid_fonts_draw_with_a_craft_fonts_face"]
|
||||
notes = "CID-to-Unicode tables are M2.4; a non-embedded Adobe-Japan1 font is drawn in a craft-fonts Japanese face (Mincho or Gothic by name) when built with craft-fonts; non-embedded GB1, CNS1 and Korea1 fonts still draw nothing (no allowed face)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.render-image-codecs"
|
||||
|
||||
Reference in New Issue
Block a user