satoru-render 0.0.20 → 0.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -1
- package/dist/core.d.ts +5 -0
- package/dist/core.js +117 -7
- package/dist/index.js +6 -6
- package/dist/satoru-single.js +0 -0
- package/dist/satoru-single.wasm +0 -0
- package/dist/satoru.wasm +0 -0
- package/dist/web-workers.js +106 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -135,7 +135,37 @@ const pdf = await render({
|
|
|
135
135
|
});
|
|
136
136
|
```
|
|
137
137
|
|
|
138
|
-
### 2.
|
|
138
|
+
### 2. Avoiding CORS Issues with Proxy
|
|
139
|
+
|
|
140
|
+
If you encounter CORS errors when fetching images or fonts from other domains, you can use a proxy service within the `resolveResource` callback.
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
const png = await render({
|
|
144
|
+
value: html,
|
|
145
|
+
width: 800,
|
|
146
|
+
format: "png",
|
|
147
|
+
resolveResource: async (resource, defaultResolver) => {
|
|
148
|
+
// Intercept external images to avoid CORS issues
|
|
149
|
+
if (resource.type === "image" && !resource.url.startsWith("data:")) {
|
|
150
|
+
const proxyUrl = `https://your-proxy-service.com/?url=${encodeURIComponent(resource.url)}`;
|
|
151
|
+
try {
|
|
152
|
+
const resp = await fetch(proxyUrl);
|
|
153
|
+
if (resp.ok) {
|
|
154
|
+
const buf = await resp.arrayBuffer();
|
|
155
|
+
return new Uint8Array(buf);
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn(`Failed to fetch via proxy: ${resource.url}`, e);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Fallback to default resolver for other resources
|
|
163
|
+
return defaultResolver(resource);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### 3. Multi-page PDF Generation
|
|
139
169
|
|
|
140
170
|
Generate complex documents by passing an array of HTML strings. Each element in the array becomes a new page.
|
|
141
171
|
|
package/dist/core.d.ts
CHANGED
|
@@ -82,4 +82,9 @@ export declare abstract class SatoruBase {
|
|
|
82
82
|
format?: "svg";
|
|
83
83
|
}): Promise<string>;
|
|
84
84
|
render(options: RenderOptions): Promise<string | Uint8Array>;
|
|
85
|
+
/**
|
|
86
|
+
* Combines multiple simple PDF files into one.
|
|
87
|
+
* This is a minimal implementation specialized for Skia-generated PDFs.
|
|
88
|
+
*/
|
|
89
|
+
private mergeSimplePDFs;
|
|
85
90
|
}
|
package/dist/core.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { LogLevel } from "./log-level.js";
|
|
2
2
|
export const DEFAULT_FONT_MAP = {
|
|
3
|
-
"sans-serif": "https://
|
|
4
|
-
serif: "https://
|
|
5
|
-
monospace: "https://
|
|
6
|
-
cursive: "https://
|
|
7
|
-
fantasy: "https://
|
|
3
|
+
"sans-serif": "https://fonts.googleapis.com/css2?family=Noto+Sans+JP",
|
|
4
|
+
serif: "https://fonts.googleapis.com/css2?family=Noto+Serif+JP",
|
|
5
|
+
monospace: "https://fonts.googleapis.com/css2?family=M+PLUS+1+Code",
|
|
6
|
+
cursive: "https://fonts.googleapis.com/css2?family=Yuji+Syuku",
|
|
7
|
+
fantasy: "https://fonts.googleapis.com/css2?family=Reggae+One",
|
|
8
8
|
emoji: "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
|
|
9
9
|
"Noto Color Emoji": "https://cdn.jsdelivr.net/npm/@fontsource/noto-color-emoji/files/noto-color-emoji-emoji-400-normal.woff2",
|
|
10
10
|
};
|
|
@@ -161,8 +161,23 @@ export class SatoruBase {
|
|
|
161
161
|
}
|
|
162
162
|
}
|
|
163
163
|
async render(options) {
|
|
164
|
+
let { format = "svg", value, url, baseUrl } = options;
|
|
165
|
+
if (format === "pdf" && Array.isArray(value) && value.length > 1) {
|
|
166
|
+
const pagePdfs = [];
|
|
167
|
+
const SatoruClass = this.constructor;
|
|
168
|
+
for (const html of value) {
|
|
169
|
+
const engine = await SatoruClass.create();
|
|
170
|
+
const pagePdf = await engine.render({
|
|
171
|
+
...options,
|
|
172
|
+
value: html,
|
|
173
|
+
format: "pdf",
|
|
174
|
+
});
|
|
175
|
+
pagePdfs.push(pagePdf);
|
|
176
|
+
}
|
|
177
|
+
return this.mergeSimplePDFs(pagePdfs);
|
|
178
|
+
}
|
|
164
179
|
const mod = await this.getModule();
|
|
165
|
-
|
|
180
|
+
const { width, height = 0, fonts, images, css, logLevel, onLog, } = options;
|
|
166
181
|
if (!options.userAgent) {
|
|
167
182
|
options.userAgent =
|
|
168
183
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
|
@@ -240,7 +255,9 @@ export class SatoruBase {
|
|
|
240
255
|
const uint8 = data instanceof Uint8Array
|
|
241
256
|
? data
|
|
242
257
|
: new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
243
|
-
if (r.type === "image" &&
|
|
258
|
+
if (r.type === "image" &&
|
|
259
|
+
typeof createImageBitmap !== "undefined" &&
|
|
260
|
+
typeof OffscreenCanvas !== "undefined") {
|
|
244
261
|
try {
|
|
245
262
|
const blob = new Blob([uint8.buffer]);
|
|
246
263
|
const bitmap = await createImageBitmap(blob);
|
|
@@ -308,4 +325,97 @@ export class SatoruBase {
|
|
|
308
325
|
this.currentFontMap = prevFontMap;
|
|
309
326
|
}
|
|
310
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Combines multiple simple PDF files into one.
|
|
330
|
+
* This is a minimal implementation specialized for Skia-generated PDFs.
|
|
331
|
+
*/
|
|
332
|
+
mergeSimplePDFs(pdfs) {
|
|
333
|
+
if (pdfs.length === 0)
|
|
334
|
+
return new Uint8Array();
|
|
335
|
+
if (pdfs.length === 1)
|
|
336
|
+
return pdfs[0];
|
|
337
|
+
const decoder = new TextDecoder();
|
|
338
|
+
const encoder = new TextEncoder();
|
|
339
|
+
let maxObjNum = 0;
|
|
340
|
+
const allObjects = [];
|
|
341
|
+
const pageIds = [];
|
|
342
|
+
for (const pdf of pdfs) {
|
|
343
|
+
const offset = maxObjNum;
|
|
344
|
+
const content = decoder.decode(pdf);
|
|
345
|
+
// Find all objects
|
|
346
|
+
const objRegex = /(\d+)\s+0\s+obj([\s\S]*?)endobj/g;
|
|
347
|
+
let match;
|
|
348
|
+
let currentPdfMax = 0;
|
|
349
|
+
while ((match = objRegex.exec(content)) !== null) {
|
|
350
|
+
const oldId = parseInt(match[1], 10);
|
|
351
|
+
const objBody = match[2];
|
|
352
|
+
const newId = oldId + offset;
|
|
353
|
+
if (oldId > currentPdfMax)
|
|
354
|
+
currentPdfMax = oldId;
|
|
355
|
+
// Shift references within the object body: "N 0 R" -> "(N+offset) 0 R"
|
|
356
|
+
const shiftedBody = objBody.replace(/(\d+)\s+0\s+R/g, (_, id) => {
|
|
357
|
+
return `${parseInt(id, 10) + offset} 0 R`;
|
|
358
|
+
});
|
|
359
|
+
// Detect if this is a Page object
|
|
360
|
+
if (shiftedBody.includes("/Type /Page") &&
|
|
361
|
+
!shiftedBody.includes("/Type /Pages")) {
|
|
362
|
+
pageIds.push(newId);
|
|
363
|
+
}
|
|
364
|
+
// We skip Catalog and original Pages root, we'll create new ones
|
|
365
|
+
if (!shiftedBody.includes("/Type /Catalog") &&
|
|
366
|
+
!shiftedBody.includes("/Type /Pages")) {
|
|
367
|
+
allObjects.push({
|
|
368
|
+
id: newId,
|
|
369
|
+
content: encoder.encode(`${newId} 0 obj${shiftedBody}endobj\n`),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
maxObjNum += currentPdfMax;
|
|
374
|
+
}
|
|
375
|
+
// Create new Catalog and Pages root
|
|
376
|
+
const catalogId = ++maxObjNum;
|
|
377
|
+
const pagesRootId = ++maxObjNum;
|
|
378
|
+
const newPagesRoot = encoder.encode(`${pagesRootId} 0 obj\n<< /Type /Pages /Kids [${pageIds
|
|
379
|
+
.map((id) => `${id} 0 R`)
|
|
380
|
+
.join(" ")}] /Count ${pageIds.length} >>\nendobj\n`);
|
|
381
|
+
const newCatalog = encoder.encode(`${catalogId} 0 obj\n<< /Type /Catalog /Pages ${pagesRootId} 0 R >>\nendobj\n`);
|
|
382
|
+
allObjects.push({ id: pagesRootId, content: newPagesRoot });
|
|
383
|
+
allObjects.push({ id: catalogId, content: newCatalog });
|
|
384
|
+
// Build the final PDF
|
|
385
|
+
const resultParts = [encoder.encode("%PDF-1.4\n")];
|
|
386
|
+
const xref = [];
|
|
387
|
+
let currentOffset = resultParts[0].length;
|
|
388
|
+
// Sort objects by ID for a clean xref table
|
|
389
|
+
allObjects.sort((a, b) => a.id - b.id);
|
|
390
|
+
for (const obj of allObjects) {
|
|
391
|
+
xref.push({ id: obj.id, offset: currentOffset });
|
|
392
|
+
resultParts.push(obj.content);
|
|
393
|
+
currentOffset += obj.content.length;
|
|
394
|
+
}
|
|
395
|
+
const startXref = currentOffset;
|
|
396
|
+
resultParts.push(encoder.encode("xref\n"));
|
|
397
|
+
resultParts.push(encoder.encode(`0 ${maxObjNum + 1}\n`));
|
|
398
|
+
resultParts.push(encoder.encode("0000000000 65535 f \n"));
|
|
399
|
+
const xrefMap = new Map(xref.map((x) => [x.id, x.offset]));
|
|
400
|
+
for (let i = 1; i <= maxObjNum; i++) {
|
|
401
|
+
const offset = xrefMap.get(i);
|
|
402
|
+
if (offset !== undefined) {
|
|
403
|
+
resultParts.push(encoder.encode(`${offset.toString().padStart(10, "0")} 00000 n \n`));
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
resultParts.push(encoder.encode("0000000000 00001 f \n"));
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
resultParts.push(encoder.encode(`trailer\n<< /Size ${maxObjNum + 1} /Root ${catalogId} 0 R >>\n`));
|
|
410
|
+
resultParts.push(encoder.encode(`startxref\n${startXref}\n%%EOF`));
|
|
411
|
+
// Combine all parts
|
|
412
|
+
const totalLength = resultParts.reduce((acc, part) => acc + part.length, 0);
|
|
413
|
+
const merged = new Uint8Array(totalLength);
|
|
414
|
+
let pos = 0;
|
|
415
|
+
for (const part of resultParts) {
|
|
416
|
+
merged.set(part, pos);
|
|
417
|
+
pos += part.length;
|
|
418
|
+
}
|
|
419
|
+
return merged;
|
|
420
|
+
}
|
|
311
421
|
}
|
package/dist/index.js
CHANGED
|
@@ -77,16 +77,16 @@ export class Satoru extends SatoruBase {
|
|
|
77
77
|
handlePseudo("::before");
|
|
78
78
|
handlePseudo("::after");
|
|
79
79
|
// Special handling for specific elements
|
|
80
|
-
if (src instanceof HTMLCanvasElement) {
|
|
80
|
+
if (typeof HTMLCanvasElement !== "undefined" && src instanceof HTMLCanvasElement) {
|
|
81
81
|
const img = document.createElement("img");
|
|
82
82
|
img.src = src.toDataURL();
|
|
83
83
|
img.setAttribute("style", dest.getAttribute("style") || "");
|
|
84
84
|
dest.replaceWith(img);
|
|
85
85
|
}
|
|
86
|
-
else if (src instanceof HTMLImageElement) {
|
|
86
|
+
else if (typeof HTMLImageElement !== "undefined" && src instanceof HTMLImageElement) {
|
|
87
87
|
dest.setAttribute("src", absoluteUrl(src.src));
|
|
88
88
|
}
|
|
89
|
-
else if (src instanceof HTMLInputElement) {
|
|
89
|
+
else if (typeof HTMLInputElement !== "undefined" && src instanceof HTMLInputElement) {
|
|
90
90
|
if (src.type === "checkbox" || src.type === "radio") {
|
|
91
91
|
if (src.checked)
|
|
92
92
|
dest.setAttribute("checked", "");
|
|
@@ -95,10 +95,10 @@ export class Satoru extends SatoruBase {
|
|
|
95
95
|
dest.setAttribute("value", src.value);
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
else if (src instanceof HTMLTextAreaElement) {
|
|
98
|
+
else if (typeof HTMLTextAreaElement !== "undefined" && src instanceof HTMLTextAreaElement) {
|
|
99
99
|
dest.innerText = src.value;
|
|
100
100
|
}
|
|
101
|
-
else if (src instanceof HTMLSelectElement) {
|
|
101
|
+
else if (typeof HTMLSelectElement !== "undefined" && src instanceof HTMLSelectElement) {
|
|
102
102
|
// Find the selected option and mark it
|
|
103
103
|
const index = src.selectedIndex;
|
|
104
104
|
const clonedSelect = dest;
|
|
@@ -149,7 +149,7 @@ export class Satoru extends SatoruBase {
|
|
|
149
149
|
const processedValues = [];
|
|
150
150
|
let hasElement = false;
|
|
151
151
|
for (const val of values) {
|
|
152
|
-
if (val instanceof HTMLElement) {
|
|
152
|
+
if (typeof HTMLElement !== "undefined" && val instanceof HTMLElement) {
|
|
153
153
|
processedValues.push(this.serializeElement(val));
|
|
154
154
|
hasElement = true;
|
|
155
155
|
}
|
package/dist/satoru-single.js
CHANGED
|
Binary file
|
|
Binary file
|
package/dist/satoru.wasm
CHANGED
|
Binary file
|