mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-14 19:27:04 -04:00
M7: preserve Japanese Gothic fallback family and weight (#394)
* M7: select real Japanese fallback faces by family and weight Refs #184. Keep Type 3 font-name reporting separate in #387; missing weights and the small web font input remain explicit limitations. Assisted-by: OpenAI GPT-6.1 (gpt-6.1-sol) * Test the Japanese fallback name for a sans source: the Gothic face since #394 matches the source style Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Brandon Thomas <[email protected]> Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
9 files changed
+179
-22
No files matched your search
@@ -420,6 +420,7 @@ impl Automation {
|
||||
let block = self.doc(&a)?.text_blocks(page)[k as usize - 1].clone();
|
||||
let text = a.opt_str("text")?.map(str::to_owned).unwrap_or(block.text);
|
||||
let mut style = pdfcraft_engine::BlockStyle {
|
||||
bold: a.opt_bool("bold")?,
|
||||
size: a.opt_num("size")?,
|
||||
underline: a.opt_bool("underline")?,
|
||||
line_spacing: a.opt_num("line_spacing")?,
|
||||
|
||||
@@ -998,7 +998,7 @@ pub fn tools() -> Vec<ToolDef> {
|
||||
.ro()
|
||||
.cmd("edit.edit_text")
|
||||
.with(schema(json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 } }), &["doc", "page"])),
|
||||
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character; otherwise a standard font or the Japanese Type 3 fallback is used (the result shows the font used). The Japanese fallback does not yet preserve the source's family or weight. Text that no available font can show is refused. Undoable.")
|
||||
t("text_edit", "Edit text", "Replace the text of one paragraph (paragraph, from text_paragraphs: rewrapped to the paragraph's width with its line spacing) or one line (line, from text_lines) in place, keeping position, size and colour. For a paragraph, also change its formatting: font (helvetica, times, courier) with bold/italic, size (points), color (#rrggbb), align (left, center, right, justify), underline, line_spacing (× size), char_spacing (points) and scale (horizontal, percent), or move it (dx, dy in points; up is +dy) and rewrap it to a new width (points); text may then be omitted. Its own font is reused when it can show every character and no font/weight change is requested; otherwise it uses a standard font matching the source style or, for Japanese, real Mincho/Gothic fallback outlines matching the requested/source family and available weight (the result shows the font used). Missing Japanese weights use Regular, not synthetic bold; the small web build has only Gothic Regular. Paragraph bold may be set without font to keep the source family. Text that no available font can show is refused. Undoable.")
|
||||
.cmd("edit.edit_text")
|
||||
.with(schema(
|
||||
json!({ "doc": doc(), "page": { "type": "integer", "minimum": 1 }, "line": { "type": "integer", "minimum": 1 },
|
||||
|
||||
@@ -1288,6 +1288,20 @@ fn editing_existing_text_through_tools() {
|
||||
assert_eq!(p["lines"].as_array().map(Vec::len), Some(2), "{p}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn paragraph_bold_without_font_keeps_the_source_family() {
|
||||
let dir = workdir("paragraph-bold");
|
||||
let mut a = auto(&dir);
|
||||
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
|
||||
ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "font": "times" }));
|
||||
let bold = ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": true }));
|
||||
assert_eq!(bold["paragraph"]["font"], "Times-Bold");
|
||||
let regular = ok(&mut a, "text_edit", json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": false }));
|
||||
assert_eq!(regular["paragraph"]["font"], "Times-Roman");
|
||||
assert_eq!(regular["paragraph"]["text"], "Page 2");
|
||||
assert!(matches!(a.call("text_edit", &json!({ "doc": doc, "page": 2, "paragraph": 1, "bold": "yes" })), Err(ToolError::InvalidArgs(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_page_images_through_tools() {
|
||||
let dir = workdir("page-images");
|
||||
|
||||
@@ -296,7 +296,7 @@ fn japanese_line_uses_unicode_type3_fallback() {
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
|
||||
let replacement = "25362738こんにちは、お元気ですか? 2.3444";
|
||||
let result = text::replace_line(&mut doc, 0, 0, replacement).unwrap();
|
||||
assert_eq!(result.substituted.as_deref(), Some("Shippori Mincho Type3"));
|
||||
assert_eq!(result.substituted.as_deref(), Some("BIZ UDPGothic Type3"));
|
||||
let bytes = page_content_bytes(&doc, 0);
|
||||
let content = String::from_utf8_lossy(&bytes);
|
||||
assert!(content.contains("/PCJp"), "{content}");
|
||||
@@ -304,12 +304,98 @@ fn japanese_line_uses_unicode_type3_fallback() {
|
||||
assert_eq!(text::text_lines(&reopened, 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
|
||||
fn styled_text_page(base: &str) -> Document {
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Original) Tj ET");
|
||||
let r = pdfcraft_cos::ObjRef::new(5, 0);
|
||||
let mut font = doc.get(r).as_dict().unwrap().clone();
|
||||
font.set(b"BaseFont".to_vec(), pdfcraft_cos::Object::name(base));
|
||||
doc.set(r, pdfcraft_cos::Object::Dict(font));
|
||||
doc
|
||||
}
|
||||
|
||||
fn fallback_paths(doc: &Document) -> Vec<Vec<u8>> {
|
||||
let p = pdfcraft_model::pages(doc).swap_remove(0);
|
||||
let res = doc.resolve(p.dict.get(b"Resources").unwrap());
|
||||
let fonts = doc.resolve(res.as_dict().unwrap().get(b"Font").unwrap());
|
||||
let font = doc.resolve(fonts.as_dict().unwrap().get(b"PCJp").unwrap());
|
||||
let procs = doc.resolve(font.as_dict().unwrap().get(b"CharProcs").unwrap());
|
||||
procs
|
||||
.as_dict()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.map(|(_, object)| match &*doc.resolve(object) {
|
||||
pdfcraft_cos::Object::Stream(s) => s.decoded().unwrap(),
|
||||
other => panic!("expected a glyph stream, got {other:?}"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_line_fallback_uses_real_source_family_and_weight() {
|
||||
if without_craft_fonts("japanese_line_fallback_uses_real_source_family_and_weight") {
|
||||
return;
|
||||
}
|
||||
let mut sans = styled_text_page("Helvetica");
|
||||
let mut bold = descriptor_text_page();
|
||||
let mut serif = styled_text_page("ABCDEF+ShipporiMincho-Regular");
|
||||
let replacement = "見本商会 御中";
|
||||
text::replace_line(&mut sans, 0, 0, replacement).unwrap();
|
||||
text::replace_line(&mut bold, 0, 0, replacement).unwrap();
|
||||
text::replace_line(&mut serif, 0, 0, replacement).unwrap();
|
||||
assert_ne!(fallback_paths(&sans), fallback_paths(&serif), "sans source must not use Mincho outlines");
|
||||
if pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.family == "BIZ UDPGothic" && f.style == "Bold") {
|
||||
assert_ne!(fallback_paths(&sans), fallback_paths(&bold), "bold must change the actual glyph paths");
|
||||
}
|
||||
for doc in [&sans, &bold, &serif] {
|
||||
assert_eq!(text::text_lines(&reopen(doc), 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_paragraph_fallback_honors_requested_family_and_weight() {
|
||||
if without_craft_fonts("japanese_paragraph_fallback_honors_requested_family_and_weight") {
|
||||
return;
|
||||
}
|
||||
let replacement = "見本商会 御中";
|
||||
let mut regular = styled_text_page("Helvetica");
|
||||
let mut bold = styled_text_page("Helvetica");
|
||||
let mut serif = styled_text_page("Helvetica");
|
||||
text::replace_block(&mut regular, 0, 0, replacement).unwrap();
|
||||
text::rewrite_block(
|
||||
&mut bold,
|
||||
0,
|
||||
0,
|
||||
Some(replacement),
|
||||
&text::BlockStyle { family: Some((added::Family::Helvetica, true, false)), ..Default::default() },
|
||||
)
|
||||
.unwrap();
|
||||
text::rewrite_block(
|
||||
&mut serif,
|
||||
0,
|
||||
0,
|
||||
Some(replacement),
|
||||
&text::BlockStyle { family: Some((added::Family::Times, false, false)), ..Default::default() },
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(fallback_paths(®ular), fallback_paths(&serif), "requested serif must change actual outlines");
|
||||
if pdfcraft_fonts::CRAFT_FONTS.iter().any(|f| f.family == "BIZ UDPGothic" && f.style == "Bold") {
|
||||
assert_ne!(fallback_paths(®ular), fallback_paths(&bold), "requested bold must change actual outlines");
|
||||
let mut source_bold = styled_text_page("Helvetica-Bold");
|
||||
text::replace_block(&mut source_bold, 0, 0, replacement).unwrap();
|
||||
assert_eq!(fallback_paths(&bold), fallback_paths(&source_bold));
|
||||
}
|
||||
for doc in [®ular, &bold, &serif] {
|
||||
assert_eq!(text::text_blocks(&reopen(doc), 0).unwrap()[0].text, replacement);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn japanese_fallback_reports_the_face_after_save_and_reopen() {
|
||||
if without_craft_fonts("japanese_fallback_reports_the_face_after_save_and_reopen") {
|
||||
return;
|
||||
}
|
||||
let face = pdfcraft_fonts::document_japanese_font().unwrap();
|
||||
// F2 is (a subset of) Arial: a regular sans face, so the fallback is the regular Gothic.
|
||||
let face = pdfcraft_fonts::document_japanese_font_for_style(false, false).unwrap();
|
||||
let expected = format!("{}-{}", face.family, face.style).replace(' ', "");
|
||||
for paragraph in [false, true] {
|
||||
let mut doc = text_page("BT /F2 12 Tf 72 700 Td (ab) Tj ET");
|
||||
@@ -493,9 +579,9 @@ fn missing_glyphs_substitute_helvetica_and_impossible_text_is_refused() {
|
||||
let doc2 = reopen(&doc);
|
||||
let lines = text::text_lines(&doc2, 0).unwrap();
|
||||
assert_eq!((lines[0].text.as_str(), lines[0].base_font.as_str()), ("abc", "Helvetica"));
|
||||
// Neither font can show Greek.
|
||||
// Neither the standard font nor the craft-fonts Japanese faces have this emoji.
|
||||
let mut doc = text_page("BT /F1 12 Tf 72 700 Td (Hello) Tj ET");
|
||||
assert!(text::replace_line(&mut doc, 0, 0, "Ωmega").is_err());
|
||||
assert!(text::replace_line(&mut doc, 0, 0, "\u{1f4a9}").is_err());
|
||||
assert!(text::replace_line(&mut doc, 0, 5, "x").is_err(), "no such line");
|
||||
}
|
||||
|
||||
|
||||
+16
-11
@@ -16,7 +16,7 @@ use std::rc::Rc;
|
||||
use pdfcraft_content::{Matrix, Op, parse, serialize_ops};
|
||||
use pdfcraft_cos::{Dict, Document, Object, PdfString, Stream};
|
||||
use pdfcraft_fonts::pdf::Metrics;
|
||||
use pdfcraft_fonts::{GlyphError, japanese_glyph};
|
||||
use pdfcraft_fonts::{CraftFont, GlyphError, japanese_glyph_from};
|
||||
|
||||
use crate::EditError;
|
||||
|
||||
@@ -510,7 +510,9 @@ fn source_family(base_font: &str) -> crate::added::Family {
|
||||
let name = base_font.to_ascii_lowercase();
|
||||
if ["courier", "mono", "consolas", "menlo", "monaco", "lucida console"].iter().any(|s| name.contains(s)) {
|
||||
crate::added::Family::Courier
|
||||
} else if !name.contains("sans") && ["times", "serif", "roman", "cambria", "georgia", "palatino", "garamond"].iter().any(|s| name.contains(s)) {
|
||||
} else if !name.contains("sans")
|
||||
&& ["times", "serif", "roman", "mincho", "cambria", "georgia", "palatino", "garamond"].iter().any(|s| name.contains(s))
|
||||
{
|
||||
crate::added::Family::Times
|
||||
} else {
|
||||
crate::added::Family::Helvetica
|
||||
@@ -533,8 +535,8 @@ fn pdf_num(v: f64) -> String {
|
||||
if v.fract() == 0.0 { format!("{v:.0}") } else { format!("{v:.4}").trim_end_matches('0').trim_end_matches('.').to_string() }
|
||||
}
|
||||
|
||||
fn type3_path(ch: char) -> Result<(Vec<u8>, f64), EditError> {
|
||||
let glyph = japanese_glyph(ch).map_err(|e| match e {
|
||||
fn type3_path(face: &CraftFont, ch: char) -> Result<(Vec<u8>, f64), EditError> {
|
||||
let glyph = japanese_glyph_from(face, ch).map_err(|e| match e {
|
||||
GlyphError::NoFont => no_japanese_font(),
|
||||
GlyphError::Missing => EditError::Invalid(format!("Japanese fallback font has no glyph for U+{:04X}", ch as u32)),
|
||||
GlyphError::TooComplex => EditError::Invalid(format!("Japanese fallback glyph U+{:04X} is too complex", ch as u32)),
|
||||
@@ -568,8 +570,8 @@ fn no_japanese_font() -> EditError {
|
||||
)
|
||||
}
|
||||
|
||||
fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Type3Fallback, EditError> {
|
||||
let face = pdfcraft_fonts::document_japanese_font().ok_or_else(no_japanese_font)?;
|
||||
fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str, family: crate::added::Family, bold: bool) -> Result<Type3Fallback, EditError> {
|
||||
let face = pdfcraft_fonts::document_japanese_font_for_style(family == crate::added::Family::Times, bold).ok_or_else(no_japanese_font)?;
|
||||
let family = face.family;
|
||||
let mut chars = Vec::new();
|
||||
for ch in text.chars() {
|
||||
@@ -594,7 +596,7 @@ fn type3_font(doc: &mut Document, fonts_res: &mut Dict, text: &str) -> Result<Ty
|
||||
for (i, ch) in chars.into_iter().enumerate() {
|
||||
let code = u8::try_from(i + 1).map_err(|_| EditError::Invalid("Japanese replacement has too many unique characters".into()))?;
|
||||
let glyph_name = format!("g{code:02X}");
|
||||
let (path, width) = type3_path(ch)?;
|
||||
let (path, width) = type3_path(face, ch)?;
|
||||
let mut pd = Dict::new();
|
||||
pd.set(b"Length".to_vec(), path.len() as i64);
|
||||
let proc_ref = doc.add(Object::Stream(Stream::from_raw(pd, path)));
|
||||
@@ -679,7 +681,7 @@ pub fn replace_line(doc: &mut Document, page: usize, line: usize, text: &str) ->
|
||||
Some(bytes) => replacement.push(Op::new("Tj", vec![Object::String(PdfString::literal(bytes))])),
|
||||
None => {
|
||||
if needs_type3(&text) {
|
||||
let fallback = type3_font(doc, &mut fonts_res, &text)?;
|
||||
let fallback = type3_font(doc, &mut fonts_res, &text, source_family(&target.base_font), target.bold)?;
|
||||
let bytes = type3_encode(&fallback, &text)
|
||||
.ok_or_else(|| EditError::Invalid(format!("\"{text}\" can't be shown by the Japanese fallback")))?;
|
||||
let size = font_size_before(&ops, first).unwrap_or(target.size);
|
||||
@@ -866,6 +868,8 @@ pub fn replace_block(doc: &mut Document, page: usize, block: usize, text: &str)
|
||||
pub struct BlockStyle {
|
||||
/// A standard font family, bold, italic.
|
||||
pub family: Option<(crate::added::Family, bool, bool)>,
|
||||
/// Override weight without choosing a different family (`None` keeps the source weight).
|
||||
pub bold: Option<bool>,
|
||||
/// Font size in points (user space).
|
||||
pub size: Option<f64>,
|
||||
/// Fill colour (RGB 0–1).
|
||||
@@ -913,10 +917,11 @@ pub fn rewrite_block(doc: &mut Document, page: usize, block: usize, text: Option
|
||||
}
|
||||
let o_state = ts_state.clone();
|
||||
let metrics = fonts_res.get(font_name.as_bytes()).and_then(|f| doc.resolve(f).as_dict().cloned()).map(|d| Metrics::from_dict(doc, &d));
|
||||
let reuse = style.family.is_none() && metrics.as_ref().is_some_and(|m| m.encode(&text).is_some());
|
||||
let type3 = if !reuse && needs_type3(&text) { Some(type3_font(doc, &mut fonts_res, &text)?) } else { None };
|
||||
// The standard font used when the paragraph's own can't be (chosen, or substituted).
|
||||
let reuse = style.family.is_none() && style.bold.is_none() && metrics.as_ref().is_some_and(|m| m.encode(&text).is_some());
|
||||
// The chosen style also determines the real Japanese fallback outlines and advances.
|
||||
let (family, bold, italic) = style.family.unwrap_or((source_family(&b.base_font), b.bold, b.italic));
|
||||
let bold = style.bold.unwrap_or(bold);
|
||||
let type3 = if !reuse && needs_type3(&text) { Some(type3_font(doc, &mut fonts_res, &text, family, bold)?) } else { None };
|
||||
let std_width = move |s: &str, size: f64| -> f64 {
|
||||
match family {
|
||||
crate::added::Family::Courier => s.chars().count() as f64 * 0.6 * size,
|
||||
|
||||
@@ -28,14 +28,20 @@ set). Without that variable `CRAFT_FONTS` is empty and everything below copes:
|
||||
- `document_japanese_font` / `japanese_glyph`: the face (Shippori Mincho, then BIZ UDMincho) whose
|
||||
outlines become the Type 3 fallback font for Japanese text written into PDFs. Without it,
|
||||
`japanese_glyph` returns `GlyphError::NoFont` and the editor reports a clear error.
|
||||
- `document_japanese_font_for_style` / `japanese_glyph_from`: select and draw a real Japanese
|
||||
fallback face for edited text. Sans sources prefer BIZ UDPGothic Regular/Bold; serif sources
|
||||
keep Mincho. Type 3 paths and advances both come from that selected face. Missing weights
|
||||
fall back to a real Regular face, never synthetic bold; no italic face is supplied. The
|
||||
default `document_japanese_font` / `japanese_glyph` APIs still use regular document Mincho.
|
||||
- `SHIPPORI_MINCHO`: Shippori Mincho's bytes, or `None`.
|
||||
|
||||
Generated Japanese Type 3 fonts record the fallback face's family and style in an indirect
|
||||
`FontDescriptor`. `pdf::Metrics::base_font` uses its `FontName` when the Type 3 dictionary has
|
||||
no `BaseFont`, so text inspection and edit-tool results identify the fallback after save/reopen.
|
||||
The descriptor does not change glyph programs or replace the Type 3 `FontBBox` metrics when
|
||||
`Ascent`/`Descent` are omitted (PDF Reference 1.7, tables 5.9 and 5.19). Matching the source
|
||||
font's family and weight remains unsupported.
|
||||
`Ascent`/`Descent` are omitted (PDF Reference 1.7, tables 5.9 and 5.19). The small web input has
|
||||
only Gothic Regular, so it cannot preserve bold. No new font assets or changes to that input are
|
||||
needed.
|
||||
|
||||
wasm32 builds embed only BIZ UDPGothic Regular and any `Arab` or `Telu` face, to keep the web build small: the web build
|
||||
currently has no Chinese face, so Chinese there still shows the replacement glyph.
|
||||
|
||||
@@ -85,7 +85,28 @@ fn order_cjk<'a>(faces: impl IntoIterator<Item = &'a CraftFont>, prefer_hans: bo
|
||||
/// The face for Japanese text written into PDFs (serif document text): Shippori Mincho, then
|
||||
/// BIZ UDMincho, then any other regular `Jpan` face. `None` without craft-fonts.
|
||||
pub fn document_japanese_font() -> Option<&'static CraftFont> {
|
||||
let jpan = || CRAFT_FONTS.iter().filter(|f| f.covers("Jpan"));
|
||||
document_face(CRAFT_FONTS, true, false)
|
||||
}
|
||||
|
||||
/// A real Japanese document face matching serif/sans and weight where available.
|
||||
/// Sans text prefers BIZ UDPGothic Bold or Regular. Missing weights fall back to Regular;
|
||||
/// serif text keeps the document Mincho preference. No synthetic weight or slant is applied.
|
||||
/// `None` without craft-fonts. The small web input currently has only Gothic Regular.
|
||||
pub fn document_japanese_font_for_style(serif: bool, bold: bool) -> Option<&'static CraftFont> {
|
||||
document_face(CRAFT_FONTS, serif, bold)
|
||||
}
|
||||
|
||||
fn document_face(faces: &[CraftFont], serif: bool, bold: bool) -> Option<&CraftFont> {
|
||||
let jpan = || faces.iter().filter(|f| f.covers("Jpan"));
|
||||
if !serif {
|
||||
let style = if bold { "Bold" } else { "Regular" };
|
||||
if let Some(face) = jpan()
|
||||
.find(|f| f.family == "BIZ UDPGothic" && f.style == style)
|
||||
.or_else(|| jpan().find(|f| f.family == "BIZ UDPGothic" && f.style == "Regular"))
|
||||
{
|
||||
return Some(face);
|
||||
}
|
||||
}
|
||||
["Shippori Mincho", "BIZ UDMincho"]
|
||||
.iter()
|
||||
.find_map(|family| jpan().find(|f| f.family == *family && f.style == "Regular"))
|
||||
@@ -156,6 +177,24 @@ mod tests {
|
||||
assert_eq!(ja, ["BIZ UDPGothic", "Shippori Mincho", "FakeHans"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_faces_match_style_without_inventing_missing_weights() {
|
||||
let faces = [
|
||||
CraftFont { family: "Shippori Mincho", style: "Regular", scripts: &["Jpan"], bytes: b"serif" },
|
||||
CraftFont { family: "BIZ UDPGothic", style: "Regular", scripts: &["Jpan"], bytes: b"sans" },
|
||||
CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Jpan"], bytes: b"bold" },
|
||||
CraftFont { family: "BIZ UDPGothic", style: "Bold", scripts: &["Latn"], bytes: b"not-japanese" },
|
||||
];
|
||||
assert_eq!(document_face(&faces, false, false).unwrap().bytes, b"sans");
|
||||
assert_eq!(document_face(&faces, false, true).unwrap().bytes, b"bold");
|
||||
assert_eq!(document_face(&faces, true, false).unwrap().bytes, b"serif");
|
||||
assert_eq!(document_face(&faces, true, true).unwrap().bytes, b"serif");
|
||||
assert_eq!(document_face(&faces[..2], false, true).unwrap().bytes, b"sans");
|
||||
assert_eq!(document_face(&faces[..1], false, true).unwrap().bytes, b"serif");
|
||||
assert!(document_face(&faces[3..], false, true).is_none());
|
||||
assert!(document_face(&[], false, true).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arabic_faces_are_picked_by_script_in_manifest_order() {
|
||||
// Synthetic faces: the filter must not depend on the real build input.
|
||||
|
||||
@@ -10,10 +10,10 @@ mod encodings;
|
||||
pub mod pdf;
|
||||
mod script;
|
||||
pub use craft::{
|
||||
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_japanese_fonts,
|
||||
ui_telugu_fonts,
|
||||
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_japanese_font, document_japanese_font_for_style, ui_arabic_fonts, ui_chinese_fonts,
|
||||
ui_cjk_fonts, ui_japanese_fonts, ui_telugu_fonts,
|
||||
};
|
||||
pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, script_outline};
|
||||
pub use script::{GlyphError, GlyphOutline, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_outline};
|
||||
|
||||
/// Approximate advance of `s` in Helvetica (or Arial) at `size` points.
|
||||
pub fn helvetica_width(s: &str, size: f64) -> f64 {
|
||||
|
||||
@@ -129,6 +129,12 @@ impl OutlinePen for Flatten {
|
||||
/// outline data. [`GlyphError::NoFont`] when the build has no Japanese face.
|
||||
pub fn japanese_glyph(ch: char) -> Result<GlyphOutline, GlyphError> {
|
||||
let Some(face) = crate::document_japanese_font() else { return Err(GlyphError::NoFont) };
|
||||
japanese_glyph_from(face, ch)
|
||||
}
|
||||
|
||||
/// The bounded outline and advance from the explicitly selected craft-fonts face.
|
||||
/// Uses the same complexity and missing-glyph checks as [`japanese_glyph`].
|
||||
pub fn japanese_glyph_from(face: &crate::CraftFont, ch: char) -> Result<GlyphOutline, GlyphError> {
|
||||
let Ok(font) = FontRef::new(face.bytes) else { return Err(GlyphError::Missing) };
|
||||
let loc = LocationRef::default();
|
||||
let metrics = font.metrics(Size::unscaled(), loc);
|
||||
|
||||
Reference in new issue
Block a user