Skip to content

Commit 5c5f68b

Browse files
committed
add font support
1 parent e47cbde commit 5c5f68b

9 files changed

Lines changed: 285 additions & 33 deletions

File tree

package-lock.json

Lines changed: 24 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
},
1818
"dependencies": {
1919
"@chenglou/pretext": "^0.0.6",
20-
"@node-projects/layout2vector": "^5.19.0",
20+
"@node-projects/layout2vector": "^5.21.0",
21+
"fonteditor-core": "^2.6.3",
2122
"get-box-quads-polyfill": "^4.36.0"
2223
},
2324
"devDependencies": {

src/background/service-worker.js

Lines changed: 65 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,11 @@ const popupPorts = new Set();
1010
const IMAGE_FETCH_TIMEOUT_MS = 12000;
1111
const FRAME_COLLECTION_TIMEOUT_MS = 45000;
1212
const PRECOMPUTED_IR_TRANSFER_MAX_BYTES = 8 * 1024 * 1024;
13+
const FONT_ASSET_FORMATS = new Set(['html', 'svg', 'pdf']);
1314
const DEFAULT_EXPORT_OPTIONS = Object.freeze({
1415
rootScrollBehavior: 'clip',
16+
embedFonts: true,
17+
pdfUseFontEditorCore: true,
1518
});
1619

1720
const FRAME_EXTRACTION_OPTIONS = {
@@ -127,27 +130,35 @@ async function startExport(tabId, format, exportOptions = DEFAULT_EXPORT_OPTIONS
127130

128131
try {
129132
const normalizedOptions = normalizeExportOptions(exportOptions);
130-
const mergedIr = selectPrecomputedIrForTransfer(await collectMergedIrForTab(tabId, normalizedOptions));
133+
const precomputedExport = selectPrecomputedExportForTransfer(
134+
await collectMergedIrForTab(tabId, format, normalizedOptions)
135+
);
131136

132137
// 1. Set the requested format and precomputed IR in the content-script world
133138
await extensionApi.scripting.executeScript({
134139
target: { tabId },
135-
func: (f, ir, options) => {
140+
func: (f, precomputed, options) => {
136141
globalThis.__web2vector_format = f;
137142

138-
if (Array.isArray(ir)) {
139-
globalThis.__web2vector_precomputed_ir = ir;
143+
if (Array.isArray(precomputed?.ir)) {
144+
globalThis.__web2vector_precomputed_ir = precomputed.ir;
140145
} else {
141146
delete globalThis.__web2vector_precomputed_ir;
142147
}
143148

149+
if (precomputed?.fontAssets && Array.isArray(precomputed.fontAssets.faces)) {
150+
globalThis.__web2vector_precomputed_font_assets = precomputed.fontAssets;
151+
} else {
152+
delete globalThis.__web2vector_precomputed_font_assets;
153+
}
154+
144155
if (options && typeof options === 'object') {
145156
globalThis.__web2vector_export_options = options;
146157
} else {
147158
delete globalThis.__web2vector_export_options;
148159
}
149160
},
150-
args: [format, mergedIr, normalizedOptions],
161+
args: [format, precomputedExport, normalizedOptions],
151162
});
152163

153164
// 2. Lazy-load writer bundle when required
@@ -173,9 +184,11 @@ async function startExport(tabId, format, exportOptions = DEFAULT_EXPORT_OPTIONS
173184
}
174185
}
175186

176-
async function collectMergedIrForTab(tabId, exportOptions = DEFAULT_EXPORT_OPTIONS) {
187+
async function collectMergedIrForTab(tabId, format, exportOptions = DEFAULT_EXPORT_OPTIONS) {
177188
try {
178189
return await withTimeout((async () => {
190+
const normalizedOptions = normalizeExportOptions(exportOptions);
191+
179192
await extensionApi.scripting.executeScript({
180193
target: { tabId, allFrames: true },
181194
files: ['core-lib.js', 'frame-support.js'],
@@ -191,11 +204,12 @@ async function collectMergedIrForTab(tabId, exportOptions = DEFAULT_EXPORT_OPTIO
191204
func: (options) => globalThis.__web2vectorFrameSupport?.collectFrameData?.(options) ?? null,
192205
args: [{
193206
...FRAME_EXTRACTION_OPTIONS,
194-
rootScrollBehavior: normalizeExportOptions(exportOptions).rootScrollBehavior,
207+
includeFonts: shouldCollectFontAssets(format, normalizedOptions),
208+
rootScrollBehavior: normalizedOptions.rootScrollBehavior,
195209
}],
196210
});
197211

198-
return mergeFrameExtractionResults(frameResults, { rootFrameId: 0 })?.ir ?? null;
212+
return mergeFrameExtractionResults(frameResults, { rootFrameId: 0 });
199213
})(), FRAME_COLLECTION_TIMEOUT_MS, () => {
200214
console.warn('[Web2Vector] Timed out while collecting merged frame IR; falling back to in-page extraction.');
201215
return null;
@@ -205,12 +219,12 @@ async function collectMergedIrForTab(tabId, exportOptions = DEFAULT_EXPORT_OPTIO
205219
}
206220
}
207221

208-
function selectPrecomputedIrForTransfer(ir) {
209-
if (!Array.isArray(ir)) return null;
222+
function selectPrecomputedExportForTransfer(precomputedExport) {
223+
if (!Array.isArray(precomputedExport?.ir)) return null;
210224

211-
const estimatedBytes = estimateSerializedSize(ir);
225+
const estimatedBytes = estimateTransferSize(precomputedExport);
212226
if (estimatedBytes <= PRECOMPUTED_IR_TRANSFER_MAX_BYTES) {
213-
return ir;
227+
return precomputedExport;
214228
}
215229

216230
console.warn(
@@ -219,20 +233,54 @@ function selectPrecomputedIrForTransfer(ir) {
219233
return null;
220234
}
221235

222-
function estimateSerializedSize(value) {
223-
try {
224-
return JSON.stringify(value)?.length ?? Number.POSITIVE_INFINITY;
225-
} catch {
226-
return Number.POSITIVE_INFINITY;
236+
function estimateTransferSize(value, seen = new Set()) {
237+
if (value == null) return 0;
238+
239+
const valueType = typeof value;
240+
if (valueType === 'string') return value.length;
241+
if (valueType === 'number') return 8;
242+
if (valueType === 'boolean') return 4;
243+
244+
if (ArrayBuffer.isView(value)) {
245+
return value.byteLength;
227246
}
247+
248+
if (value instanceof ArrayBuffer) {
249+
return value.byteLength;
250+
}
251+
252+
if (valueType !== 'object') {
253+
return 0;
254+
}
255+
256+
if (seen.has(value)) {
257+
return 0;
258+
}
259+
260+
seen.add(value);
261+
262+
if (Array.isArray(value)) {
263+
return value.reduce((total, entry) => total + estimateTransferSize(entry, seen), 0);
264+
}
265+
266+
return Object.entries(value).reduce(
267+
(total, [key, entry]) => total + key.length + estimateTransferSize(entry, seen),
268+
0,
269+
);
228270
}
229271

230272
function normalizeExportOptions(options) {
231273
return {
232274
rootScrollBehavior: options?.rootScrollBehavior === 'expand' ? 'expand' : 'clip',
275+
embedFonts: options?.embedFonts !== false,
276+
pdfUseFontEditorCore: options?.pdfUseFontEditorCore !== false,
233277
};
234278
}
235279

280+
function shouldCollectFontAssets(format, exportOptions) {
281+
return FONT_ASSET_FORMATS.has(format) && exportOptions?.embedFonts !== false;
282+
}
283+
236284
async function withTimeout(promise, timeoutMs, onTimeout) {
237285
let timeoutId = null;
238286

src/content/core-lib.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import { addPolyfill } from 'get-box-quads-polyfill';
99
import {
1010
extractIR,
11+
extractIRWithAssets,
1112
renderIR,
1213
flattenStackingOrder,
1314
getElementQuad,
@@ -25,6 +26,7 @@ if (!globalThis.__web2vector) {
2526

2627
globalThis.__web2vector = {
2728
extractIR,
29+
extractIRWithAssets,
2830
flattenStackingOrder,
2931
getElementQuad,
3032
renderIR,

src/content/frame-support.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ async function collectFrameData(options = {}) {
2727
return {
2828
frameKey: getFrameKey(),
2929
ir: [],
30+
fontAssets: undefined,
3031
childFrames: [],
3132
paintOrder: [],
3233
};
@@ -36,6 +37,7 @@ async function collectFrameData(options = {}) {
3637
boxType: options.boxType ?? 'border',
3738
includeText: options.includeText ?? true,
3839
includeImages: options.includeImages ?? true,
40+
includeFonts: options.includeFonts ?? false,
3941
includeSourceMetadata: options.includeSourceMetadata ?? true,
4042
includeInvisible: options.includeInvisible ?? false,
4143
walkIframes: false,
@@ -45,17 +47,28 @@ async function collectFrameData(options = {}) {
4547
};
4648

4749
const paintOrder = collectPaintOrder(root, extractOptions, lib);
48-
const ir = await lib.extractIR(root, extractOptions);
50+
const extractionResult = extractOptions.includeFonts && typeof lib.extractIRWithAssets === 'function'
51+
? await lib.extractIRWithAssets(root, extractOptions)
52+
: { ir: await lib.extractIR(root, extractOptions) };
4953
const childFrames = await collectChildFrameMappings(root, extractOptions.includeInvisible ?? false, lib);
5054

5155
return {
5256
frameKey: getFrameKey(),
53-
ir,
57+
ir: Array.isArray(extractionResult?.ir) ? extractionResult.ir : [],
58+
fontAssets: normalizeFontAssets(extractionResult?.fontAssets),
5459
childFrames,
5560
paintOrder,
5661
};
5762
}
5863

64+
function normalizeFontAssets(fontAssets) {
65+
if (!fontAssets || !Array.isArray(fontAssets.faces) || fontAssets.faces.length === 0) {
66+
return undefined;
67+
}
68+
69+
return { faces: fontAssets.faces };
70+
}
71+
5972
function collectPaintOrder(root, extractOptions, lib) {
6073
if (typeof lib.traverseDOM !== 'function' || typeof lib.flattenStackingOrder !== 'function') {
6174
return [];

0 commit comments

Comments
 (0)