@@ -120,13 +120,6 @@ async function convertHtmlToPdf(html: string): Promise<Buffer> {
120120 // Load the HTML
121121 await page . setContent ( html , { waitUntil : 'load' , timeout : PAGE_TIMEOUT_MS } ) ;
122122
123- // Chromium 146 renders characters like ↩ (U+21A9) as color emoji by
124- // default. Force text presentation so they stay as simple glyphs —
125- // important for academic reference back-links.
126- await page . addStyleTag ( {
127- content : `*, *::before, *::after { font-variant-emoji: text; }` ,
128- } ) ;
129-
130123 // Wait for all web fonts before paged.js measures text.
131124 // (Key fix from the pubpub/pagedjs-cli fork.)
132125 await page . evaluate ( ( ) => document . fonts . ready ) ;
@@ -206,6 +199,32 @@ async function convertHtmlToPdf(html: string): Promise<Buffer> {
206199 ( ) => new Promise ( ( r ) => requestAnimationFrame ( ( ) => requestAnimationFrame ( r ) ) ) ,
207200 ) ;
208201
202+ // Chromium 146+ renders certain Unicode characters (e.g. ↩ U+21A9 in
203+ // footnote back-links) as color emoji instead of simple text glyphs.
204+ // Fix at the Unicode level by appending U+FE0E (Variation Selector 15 =
205+ // "text presentation") to every affected character in text nodes.
206+ // This is more reliable than CSS font-variant-emoji because page.pdf()
207+ // uses Chromium's internal print path which may ignore that property.
208+ await page . evaluate ( ( ) => {
209+ const VS15 = '\uFE0E' ;
210+ // Characters with dual text/emoji presentation that commonly appear
211+ // in academic documents and should remain as simple text glyphs.
212+ const EMOJI_CAPABLE =
213+ / [ \u2139 \u21A9 \u21AA \u2194 - \u2199 \u23CF \u23ED - \u23EF \u23F1 \u23F2 \u25AA \u25AB \u25B6 \u25C0 \u25FB - \u25FE \u2600 - \u27BF \u2934 \u2935 \u2B05 - \u2B07 \u2B1B \u2B1C \u2B50 \u2B55 \u3030 \u303D ] / ;
214+ const walker = document . createTreeWalker ( document . body , NodeFilter . SHOW_TEXT ) ;
215+ let node : Text | null ;
216+ while ( ( node = walker . nextNode ( ) as Text | null ) ) {
217+ const text = node . nodeValue ;
218+ if ( text && EMOJI_CAPABLE . test ( text ) ) {
219+ // Append VS15 after each matching char (skip if already followed by VS15)
220+ node . nodeValue = text . replace (
221+ / ( [ \u2139 \u21A9 \u21AA \u2194 - \u2199 \u23CF \u23ED - \u23EF \u23F1 \u23F2 \u25AA \u25AB \u25B6 \u25C0 \u25FB - \u25FE \u2600 - \u27BF \u2934 \u2935 \u2B05 - \u2B07 \u2B1B \u2B1C \u2B50 \u2B55 \u3030 \u303D ] ) (? ! \uFE0E ) / g,
222+ `$1${ VS15 } ` ,
223+ ) ;
224+ }
225+ }
226+ } ) ;
227+
209228 // Extract <meta> tags for PDF metadata
210229 const meta : PdfMeta = await page . evaluate ( ( ) => {
211230 const m : Record < string , string > = { } ;
0 commit comments