diff --git a/lib/textkit.js b/lib/textkit.js index 0b8b6fa636c48ef0ac9cecb27040d2bac9303ce8..058d96e4d47339482c2c8e8769ca031b8688e3a5 100644 --- a/lib/textkit.js +++ b/lib/textkit.js @@ -697,6 +697,31 @@ const omit = (value, run) => { return Object.assign({}, run, { attributes }); }; +/** + * Resolve a fontkit Font's vertical metrics, preferring the OS/2 + * `sTypoAscender / sTypoDescender / sTypoLineGap` triple when available + * and falling back to hhea. This matches Chromium's font-metrics + * resolution and is what browsers (and the v5.0.x Puppeteer renderer) + * use, giving compact, predictable line boxes for fonts whose hhea + * values are inflated for Windows GDI compatibility (notably Source + * Han Sans/Serif a.k.a. Noto Sans/Serif CJK). + * + * `f['OS/2']` is exposed by fontkit when the font carries an OS/2 table. + * Standard PDF fonts (Helvetica/Courier/Times) and our internal + * EmbeddedFont don't, so they keep their existing hhea-based metrics. + */ +const resolveTypoMetrics = (font) => { + const os2 = font?.['OS/2']; + if (!os2 || typeof os2.typoAscender !== 'number' || typeof os2.typoDescender !== 'number') { + return { ascent: font?.ascent || 0, descent: font?.descent || 0, lineGap: font?.lineGap || 0 }; + } + return { + ascent: os2.typoAscender, + descent: os2.typoDescender, + lineGap: typeof os2.typoLineGap === 'number' ? os2.typoLineGap : (font.lineGap || 0), + }; +}; + /** * Get run ascent * @@ -706,7 +731,7 @@ const omit = (value, run) => { const ascent$1 = (run) => { const { font, attachment } = run.attributes; const attachmentHeight = attachment?.height || 0; - const fontAscent = typeof font === 'string' ? 0 : font?.[0]?.ascent || 0; + const fontAscent = typeof font === 'string' ? 0 : resolveTypoMetrics(font?.[0]).ascent; return Math.max(attachmentHeight, fontAscent * scale(run)); }; @@ -718,7 +743,7 @@ const ascent$1 = (run) => { */ const descent = (run) => { const font = run.attributes?.font; - const fontDescent = typeof font === 'string' ? 0 : font?.[0]?.descent || 0; + const fontDescent = typeof font === 'string' ? 0 : resolveTypoMetrics(font?.[0]).descent; return scale(run) * fontDescent; }; @@ -730,8 +755,8 @@ const descent = (run) => { */ const lineGap = (run) => { const font = run.attributes?.font; - const lineGap = typeof font === 'string' ? 0 : font?.[0]?.lineGap || 0; - return lineGap * scale(run); + const fontLineGap = typeof font === 'string' ? 0 : resolveTypoMetrics(font?.[0]).lineGap; + return fontLineGap * scale(run); }; /** @@ -742,7 +767,8 @@ const lineGap = (run) => { */ const height$1 = (run) => { const lineHeight = run.attributes?.lineHeight; - return lineHeight || lineGap(run) + ascent$1(run) - descent(run); + const intrinsic = lineGap(run) + ascent$1(run) - descent(run); + return Math.max(lineHeight || 0, intrinsic); }; /**