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 CHANGED
@@ -135,7 +135,37 @@ const pdf = await render({
135
135
  });
136
136
  ```
137
137
 
138
- ### 2. Multi-page PDF Generation
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://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp/files/noto-sans-jp-japanese-400-normal.woff2",
4
- serif: "https://cdn.jsdelivr.net/npm/@fontsource/noto-serif-jp/files/noto-serif-jp-japanese-400-normal.woff2",
5
- monospace: "https://cdn.jsdelivr.net/npm/@fontsource/m-plus-1-code/files/m-plus-1-code-japanese-400-normal.woff2",
6
- cursive: "https://cdn.jsdelivr.net/npm/@fontsource/yuji-syuku/files/yuji-syuku-japanese-400-normal.woff2",
7
- fantasy: "https://cdn.jsdelivr.net/npm/@fontsource/reggae-one/files/reggae-one-japanese-400-normal.woff2",
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
- let { value, url, width, height = 0, format = "svg", fonts, images, css, baseUrl, logLevel, onLog, } = options;
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" && typeof createImageBitmap !== "undefined" && typeof OffscreenCanvas !== "undefined") {
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
  }
Binary file
Binary file
package/dist/satoru.wasm CHANGED
Binary file