77 * triggers chrome.downloads.download({ saveAs: true }).
88 */
99import { calculateExportSize } from './export-size.js' ;
10+ import pako from 'pako' ;
1011import { extensionApi } from '../shared/extension-api.js' ;
1112import { EXPORT_STREAM_CHUNK_BYTES , createExportTransferId } from '../shared/export-transfer.js' ;
1213import { normalizeTransferredFontAssets } from '../shared/font-assets.js' ;
@@ -20,6 +21,11 @@ import {
2021
2122const PX_TO_MM = 25.4 / 96 ;
2223const FONT_ASSET_FORMATS = new Set ( [ 'html' , 'svg' , 'pdf' ] ) ;
24+ const PDF_FONT_SOURCE_PRIORITY = [ 'ttf' , 'otf' , 'woff' , 'woff2' ] ;
25+ const PDF_CONVERTIBLE_FONT_SOURCE_FORMATS = new Set ( [ 'otf' , 'woff' , 'woff2' ] ) ;
26+
27+ let fontEditorCorePromise = null ;
28+ let woff2InitPromise = null ;
2329
2430( async ( ) => {
2531 try {
@@ -181,11 +187,14 @@ const FONT_ASSET_FORMATS = new Set(['html', 'svg', 'pdf']);
181187
182188 /* ── Document ── */
183189 case 'pdf' : {
190+ const pdfFontAssets = await preparePdfFontAssets ( fontAssets , exportOptions . pdfUseFontEditorCore ) ;
191+
184192 const w = new writers . PDFWriter ( {
185193 pageWidth : width * PX_TO_MM ,
186194 pageHeight : height * PX_TO_MM ,
187- fontAssets,
188- useFontEditorCore : exportOptions . pdfUseFontEditorCore ,
195+ fontAssets : pdfFontAssets ,
196+ // We normalize assets to TTF up-front so writer-level conversion is not required.
197+ useFontEditorCore : false ,
189198 } ) ;
190199 const doc = await renderIR ( ir , w ) ;
191200 await doc . finalize ( ) ;
@@ -410,3 +419,226 @@ function shouldCollectFontAssets(format, exportOptions) {
410419function normalizeFontAssets ( fontAssets ) {
411420 return normalizeTransferredFontAssets ( fontAssets ) ;
412421}
422+
423+ async function preparePdfFontAssets ( fontAssets , allowConversion ) {
424+ if ( ! fontAssets || ! Array . isArray ( fontAssets . faces ) || fontAssets . faces . length === 0 ) {
425+ return undefined ;
426+ }
427+
428+ const faces = [ ] ;
429+
430+ for ( const face of fontAssets . faces ) {
431+ const convertedSource = await pickPdfFontSource ( face , allowConversion ) ;
432+ if ( ! convertedSource ) continue ;
433+
434+ faces . push ( {
435+ ...face ,
436+ sources : [ convertedSource ] ,
437+ } ) ;
438+ }
439+
440+ return faces . length > 0 ? { faces } : undefined ;
441+ }
442+
443+ async function pickPdfFontSource ( face , allowConversion ) {
444+ const candidates = rankPdfFontSources ( face ?. sources ) ;
445+
446+ for ( const source of candidates ) {
447+ const data = toUint8Array ( source ?. data ) ;
448+ const format = resolveFontSourceFormat ( source ?. format , data ) ;
449+ if ( ! format || ! data ) continue ;
450+
451+ if ( format === 'ttf' ) {
452+ return {
453+ ...source ,
454+ format : 'ttf' ,
455+ mimeType : 'font/ttf' ,
456+ data,
457+ } ;
458+ }
459+
460+ if ( ! allowConversion || ! PDF_CONVERTIBLE_FONT_SOURCE_FORMATS . has ( format ) ) {
461+ continue ;
462+ }
463+
464+ try {
465+ const convertedData = await convertFontSourceToTtf ( data , format ) ;
466+ if ( ! convertedData ) continue ;
467+
468+ return {
469+ ...source ,
470+ format : 'ttf' ,
471+ mimeType : 'font/ttf' ,
472+ data : convertedData ,
473+ } ;
474+ } catch ( error ) {
475+ console . warn (
476+ `[Web2Vector] Failed to convert font source to TTF for PDF (${ face ?. family || 'unknown family' } , ${ format } ):` ,
477+ error
478+ ) ;
479+ }
480+ }
481+
482+ return null ;
483+ }
484+
485+ function rankPdfFontSources ( sources ) {
486+ if ( ! Array . isArray ( sources ) || sources . length === 0 ) return [ ] ;
487+
488+ return [ ...sources ]
489+ . map ( ( source ) => ( {
490+ source,
491+ format : normalizeFontSourceFormat ( source ?. format ) ,
492+ } ) )
493+ . filter ( ( entry ) => Boolean ( entry . format ) )
494+ . sort ( ( left , right ) => {
495+ const leftRank = getPdfFontSourcePriority ( left . format ) ;
496+ const rightRank = getPdfFontSourcePriority ( right . format ) ;
497+
498+ return leftRank - rightRank ;
499+ } )
500+ . map ( ( entry ) => entry . source ) ;
501+ }
502+
503+ function getPdfFontSourcePriority ( format ) {
504+ const rank = PDF_FONT_SOURCE_PRIORITY . indexOf ( format ) ;
505+ return rank === - 1 ? Number . MAX_SAFE_INTEGER : rank ;
506+ }
507+
508+ function normalizeFontSourceFormat ( format ) {
509+ if ( typeof format !== 'string' ) return null ;
510+ return format . trim ( ) . toLowerCase ( ) ;
511+ }
512+
513+ function resolveFontSourceFormat ( declaredFormat , data ) {
514+ const detectedFormat = detectFontBinaryFormat ( data ) ;
515+ if ( detectedFormat ) {
516+ return detectedFormat ;
517+ }
518+
519+ return normalizeFontSourceFormat ( declaredFormat ) ;
520+ }
521+
522+ function detectFontBinaryFormat ( data ) {
523+ if ( ! ( data instanceof Uint8Array ) || data . length < 4 ) return null ;
524+
525+ const signature = String . fromCharCode ( data [ 0 ] , data [ 1 ] , data [ 2 ] , data [ 3 ] ) ;
526+ if ( signature === 'wOFF' ) return 'woff' ;
527+ if ( signature === 'wOF2' ) return 'woff2' ;
528+ if ( signature === 'OTTO' ) return 'otf' ;
529+ if ( signature === 'true' ) return 'ttf' ;
530+ if ( data [ 0 ] === 0x00 && data [ 1 ] === 0x01 && data [ 2 ] === 0x00 && data [ 3 ] === 0x00 ) return 'ttf' ;
531+
532+ return null ;
533+ }
534+
535+ function toUint8Array ( value ) {
536+ if ( value instanceof Uint8Array ) {
537+ return value ;
538+ }
539+
540+ if ( ArrayBuffer . isView ( value ) ) {
541+ return new Uint8Array ( value . buffer . slice ( value . byteOffset , value . byteOffset + value . byteLength ) ) ;
542+ }
543+
544+ if ( value instanceof ArrayBuffer ) {
545+ return new Uint8Array ( value . slice ( 0 ) ) ;
546+ }
547+
548+ if ( Array . isArray ( value ) ) {
549+ return Uint8Array . from ( value ) ;
550+ }
551+
552+ return null ;
553+ }
554+
555+ function toArrayBufferCopy ( value ) {
556+ const data = toUint8Array ( value ) ;
557+ if ( ! data ) return null ;
558+
559+ return data . buffer . slice ( data . byteOffset , data . byteOffset + data . byteLength ) ;
560+ }
561+
562+ async function convertFontSourceToTtf ( data , format ) {
563+ const normalizedFormat = normalizeFontSourceFormat ( format ) ;
564+ if ( normalizedFormat === 'ttf' ) {
565+ return data ;
566+ }
567+
568+ if ( ! PDF_CONVERTIBLE_FONT_SOURCE_FORMATS . has ( normalizedFormat ) ) {
569+ return null ;
570+ }
571+
572+ const fontEditorCore = await getFontEditorCore ( ) ;
573+ if ( ! fontEditorCore ?. createFont ) {
574+ return null ;
575+ }
576+
577+ if ( normalizedFormat === 'woff2' ) {
578+ await ensureWoff2Runtime ( fontEditorCore ) ;
579+ }
580+
581+ const fontBuffer = toArrayBufferCopy ( data ) ;
582+ if ( ! fontBuffer ) {
583+ return null ;
584+ }
585+
586+ const readOptions = {
587+ type : normalizedFormat ,
588+ } ;
589+
590+ if ( normalizedFormat === 'woff' ) {
591+ readOptions . inflate = ( compressedData ) => pako . inflate ( compressedData ) ;
592+ }
593+
594+ const font = fontEditorCore . createFont ( fontBuffer , readOptions ) ;
595+ const ttfBuffer = font . write ( {
596+ type : 'ttf' ,
597+ toBuffer : false ,
598+ } ) ;
599+
600+ return toUint8Array ( ttfBuffer ) ;
601+ }
602+
603+ async function getFontEditorCore ( ) {
604+ if ( ! fontEditorCorePromise ) {
605+ fontEditorCorePromise = import ( '../../node_modules/fonteditor-core/lib/main.js' )
606+ . then ( ( mod ) => mod ?. default ?? mod )
607+ . catch ( ( error ) => {
608+ fontEditorCorePromise = null ;
609+ throw error ;
610+ } ) ;
611+ }
612+
613+ return fontEditorCorePromise ;
614+ }
615+
616+ async function ensureWoff2Runtime ( fontEditorCore ) {
617+ if ( fontEditorCore ?. woff2 ?. isInited ?. ( ) ) {
618+ return ;
619+ }
620+
621+ if ( ! fontEditorCore ?. woff2 ?. init ) {
622+ throw new Error ( 'fonteditor-core WOFF2 runtime is unavailable' ) ;
623+ }
624+
625+ if ( ! woff2InitPromise ) {
626+ const wasmUrl = resolveWoff2WasmUrl ( ) ;
627+ if ( ! wasmUrl ) {
628+ throw new Error ( 'Unable to resolve WOFF2 wasm URL' ) ;
629+ }
630+
631+ woff2InitPromise = fontEditorCore . woff2 . init ( wasmUrl ) . catch ( ( error ) => {
632+ woff2InitPromise = null ;
633+ throw error ;
634+ } ) ;
635+ }
636+
637+ await woff2InitPromise ;
638+ }
639+
640+ function resolveWoff2WasmUrl ( ) {
641+ return typeof extensionApi ?. runtime ?. getURL === 'function'
642+ ? extensionApi . runtime . getURL ( 'woff2.wasm' )
643+ : null ;
644+ }
0 commit comments