satoru-render 1.0.1 → 1.0.3

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
@@ -350,12 +350,12 @@ npx satoru-render input.html -f webp --verbose
350
350
  | :---------------- | :----------------------------------------- | :------------------------------------------------------ |
351
351
  | `value` | `string \| string[] \| HTMLElement \| ...` | HTML string, array of strings, or DOM element(s). |
352
352
  | `url` | `string` | URL to fetch HTML from. |
353
- | `width` | `number` | **Required.** Output width in pixels. |
354
- | `height` | `number` | Output height. Default: `0` (auto-calculate). |
353
+ | `width` | `number` | **Required.** Canvas width in pixels (used for layout). |
354
+ | `height` | `number` | Canvas height. Default: `0` (auto-calculate). |
355
355
  | `crop` | `{ x, y, width, height }` | Crop parameters to extract a specific region. |
356
- | `outputWidth` | `number` | Target output width when scaling the image. |
357
- | `outputHeight` | `number` | Target output height when scaling the image. |
358
- | `fit` | `"contain" \| "cover" \| "fill"` | Resizing behavior when output scale differs. |
356
+ | `outputWidth` | `number` | Output image width. Default: canvas/crop width. |
357
+ | `outputHeight` | `number` | Output image height. Default: canvas/crop height. |
358
+ | `fit` | `"contain" \| "cover" \| "fill"` | Fit strategy when canvas/crop size differs from output. |
359
359
  | `format` | `"svg" \| "png" \| "webp" \| "pdf"` | Output format. Default: `"svg"`. |
360
360
  | `resolveResource` | `ResourceResolver` | Async callback to fetch assets (fonts, images, CSS). |
361
361
  | `fonts` | `Object[]` | Pre-load fonts: `[{ name, data: Uint8Array }]`. |
package/dist/core.d.ts CHANGED
@@ -36,19 +36,35 @@ export interface ResolvedFontResult {
36
36
  }
37
37
  export type ResourceResolver = (resource: RequiredResource, defaultResolver: (resource: RequiredResource) => Promise<Uint8Array | ResolvedFontResult | null>) => Promise<Uint8Array | ArrayBufferView | ResolvedFontResult | null>;
38
38
  export interface RenderOptions {
39
+ /** Input content (HTML string or state object) */
39
40
  value?: string | string[] | any | any[];
41
+ /** Source URL */
40
42
  url?: string;
43
+ /** Canvas width for layout (used for CSS calculations) */
41
44
  width: number;
45
+ /** Canvas height for layout. If omitted, determined automatically by content height */
42
46
  height?: number;
47
+ /** Cropping options for the source canvas */
43
48
  crop?: {
44
49
  x: number;
45
50
  y: number;
46
51
  width: number;
47
52
  height: number;
48
53
  };
54
+ /** Final output image width. Defaults to width (or crop.width) */
49
55
  outputWidth?: number;
56
+ /** Final output image height. Defaults to height (or crop.height) */
50
57
  outputHeight?: number;
58
+ /** Resizing strategy to fit the canvas into the output size (default: "contain") */
51
59
  fit?: "contain" | "cover" | "fill";
60
+ /** Alignment origin when fitted (default: {x: 0.5, y: 0.5}) */
61
+ fitPosition?: {
62
+ x: number;
63
+ y: number;
64
+ };
65
+ /** Background color of the output canvas. (e.g., "#ffffff", "rgba(0,0,0,0.5)") */
66
+ backgroundColor?: string;
67
+ /** Output format */
52
68
  format?: "svg" | "png" | "webp" | "pdf";
53
69
  textToPaths?: boolean;
54
70
  resolveResource?: ResourceResolver;
@@ -95,11 +111,17 @@ export declare abstract class SatoruBase {
95
111
  outputWidth?: number;
96
112
  outputHeight?: number;
97
113
  fit?: "contain" | "cover" | "fill";
114
+ fitPosition?: {
115
+ x: number;
116
+ y: number;
117
+ };
118
+ backgroundColor?: string;
98
119
  format?: "svg" | "png" | "webp" | "pdf";
99
120
  textToPaths?: boolean;
100
121
  }): Promise<string | Uint8Array>;
101
122
  destroyInstance(inst: any): Promise<void>;
102
123
  loadFallbackFont(data: Uint8Array): Promise<void>;
124
+ protected parseColor(color?: string): number;
103
125
  protected abstract resolveDefaultResource(resource: RequiredResource, baseUrl?: string, userAgent?: string): Promise<Uint8Array | ResolvedFontResult | null>;
104
126
  protected abstract fetchHtml(url: string, userAgent?: string): Promise<string>;
105
127
  render(options: RenderOptions & {
package/dist/core.js CHANGED
@@ -237,6 +237,9 @@ export class SatoruBase {
237
237
  cropY: options.crop?.y ?? 0,
238
238
  cropWidth: options.crop?.width ?? 0,
239
239
  cropHeight: options.crop?.height ?? 0,
240
+ fitPositionX: options.fitPosition?.x ?? 0.5,
241
+ fitPositionY: options.fitPosition?.y ?? 0.5,
242
+ backgroundColor: this.parseColor(options.backgroundColor),
240
243
  });
241
244
  if (!result) {
242
245
  if (options.format === "svg")
@@ -262,6 +265,36 @@ export class SatoruBase {
262
265
  mod.destroy_instance(inst);
263
266
  }
264
267
  }
268
+ parseColor(color) {
269
+ if (!color)
270
+ return 0x00000000;
271
+ if (color.startsWith("#")) {
272
+ let hex = color.slice(1);
273
+ if (hex.length === 3) {
274
+ hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
275
+ }
276
+ if (hex.length === 6) {
277
+ return (0xff000000 | parseInt(hex, 16)) >>> 0;
278
+ }
279
+ if (hex.length === 8) {
280
+ // RRGGBBAA -> AARRGGBB
281
+ const r = hex.slice(0, 2);
282
+ const g = hex.slice(2, 4);
283
+ const b = hex.slice(4, 6);
284
+ const a = hex.slice(6, 8);
285
+ return parseInt(a + r + g + b, 16) >>> 0;
286
+ }
287
+ }
288
+ const m = color.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);
289
+ if (m) {
290
+ const r = parseInt(m[1]);
291
+ const g = parseInt(m[2]);
292
+ const b = parseInt(m[3]);
293
+ const a = m[4] ? Math.round(parseFloat(m[4]) * 255) : 255;
294
+ return ((a << 24) | (r << 16) | (g << 8) | b) >>> 0;
295
+ }
296
+ return 0x00000000;
297
+ }
265
298
  async render(options) {
266
299
  let { format = "svg", value, url, baseUrl } = options;
267
300
  if (format === "pdf" && Array.isArray(value) && value.length > 1) {
@@ -374,7 +407,7 @@ export class SatoruBase {
374
407
  catch (e) {
375
408
  // fall through
376
409
  }
377
- let typeInt = 1;
410
+ let typeInt = 1; // Font
378
411
  if (r.type === "image")
379
412
  typeInt = 2;
380
413
  if (r.type === "css")
@@ -382,7 +415,7 @@ export class SatoruBase {
382
415
  mod.add_resource(instancePtr, r.url, typeInt, uint8);
383
416
  })();
384
417
  }
385
- let typeInt = 1;
418
+ let typeInt = 1; // Font
386
419
  if (r.type === "image")
387
420
  typeInt = 2;
388
421
  if (r.type === "css")
@@ -422,7 +455,7 @@ export class SatoruBase {
422
455
  "fonts" in data) {
423
456
  const fontResult = data;
424
457
  // Load the CSS first so C++ can parse @font-face
425
- mod.add_resource(instancePtr, r.url, 1, // Font type
458
+ mod.add_resource(instancePtr, r.url, 3, // Css type
426
459
  fontResult.css);
427
460
  // Then load all prefetched font binaries directly
428
461
  for (const font of fontResult.fonts) {
@@ -476,6 +509,9 @@ export class SatoruBase {
476
509
  cropY: options.crop?.y ?? 0,
477
510
  cropWidth: options.crop?.width ?? 0,
478
511
  cropHeight: options.crop?.height ?? 0,
512
+ fitPositionX: options.fitPosition?.x ?? 0.5,
513
+ fitPositionY: options.fitPosition?.y ?? 0.5,
514
+ backgroundColor: this.parseColor(options.backgroundColor),
479
515
  });
480
516
  if (!result) {
481
517
  if (format === "svg")
package/dist/node.js CHANGED
@@ -12,7 +12,7 @@ export class Satoru extends SatoruBase {
12
12
  if (resource.url.startsWith("provider:google-fonts")) {
13
13
  return resolveGoogleFonts(resource, userAgent);
14
14
  }
15
- const isAbsolute = /^[a-z][a-z0-9+.-]*:/i.test(resource.url);
15
+ const isAbsolute = /^[a-z][a-z0-9+.-]*:\/\//i.test(resource.url) || resource.url.startsWith("data:");
16
16
  let baseDir = baseUrl
17
17
  ? baseUrl.startsWith("file://")
18
18
  ? baseUrl.slice(7)
@@ -24,7 +24,7 @@ export class Satoru extends SatoruBase {
24
24
  if (!isAbsolute &&
25
25
  !/^[a-z][a-z0-9+.-]*:\/\//i.test(baseDir) &&
26
26
  !baseDir.startsWith("data:")) {
27
- const filePath = path.join(baseDir, resource.url);
27
+ const filePath = path.isAbsolute(resource.url) ? resource.url : path.join(baseDir, resource.url);
28
28
  if (fs.existsSync(filePath)) {
29
29
  return new Uint8Array(fs.readFileSync(filePath));
30
30
  }
@@ -33,8 +33,16 @@ export class Satoru extends SatoruBase {
33
33
  if (isAbsolute) {
34
34
  finalUrl = resource.url;
35
35
  }
36
- else if (baseUrl && /^[a-z][a-z0-9+.-]*:/i.test(baseUrl)) {
37
- finalUrl = new URL(resource.url, baseUrl).href;
36
+ else if (baseUrl) {
37
+ try {
38
+ const base = /^[a-z][a-z0-9+.-]*:\/\//i.test(baseUrl)
39
+ ? baseUrl
40
+ : new URL(`file:///${baseUrl.replace(/\\/g, "/")}`).href;
41
+ finalUrl = new URL(resource.url, base).href;
42
+ }
43
+ catch (e) {
44
+ // ignore
45
+ }
38
46
  }
39
47
  if (!finalUrl)
40
48
  return null;
Binary file