@formepdf/renderer 0.7.1 → 0.7.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.
@@ -19,7 +19,7 @@
19
19
  --accent-hover: #2563eb;
20
20
  --inspector-width: 320px;
21
21
  --left-sidebar-width: 280px;
22
- --toolbar-height: 44px;
22
+ --toolbar-height: 78px;
23
23
  --font-mono: 'SF Mono', 'Fira Code', 'Cascadia Code', 'JetBrains Mono', monospace;
24
24
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
25
25
  }
@@ -44,12 +44,21 @@
44
44
  height: var(--toolbar-height);
45
45
  background: var(--surface);
46
46
  border-bottom: 1px solid var(--border);
47
+ display: flex;
48
+ flex-direction: column;
49
+ font-size: 13px;
50
+ -webkit-app-region: drag;
51
+ }
52
+ .toolbar-row {
47
53
  display: flex;
48
54
  align-items: center;
49
55
  padding: 0 12px;
50
56
  gap: 12px;
51
- font-size: 13px;
52
- -webkit-app-region: drag;
57
+ height: 39px;
58
+ flex-shrink: 0;
59
+ }
60
+ .toolbar-row:first-child {
61
+ border-bottom: 1px solid var(--border);
53
62
  }
54
63
 
55
64
  .toolbar-group {
@@ -785,70 +794,77 @@
785
794
  <body>
786
795
 
787
796
  <div id="toolbar">
788
- <div class="toolbar-group">
789
- <div class="status-dot" id="status-dot"></div>
790
- <div class="wordmark">forme <span>preview</span></div>
791
- </div>
797
+ <div class="toolbar-row">
798
+ <div class="toolbar-group">
799
+ <div class="status-dot" id="status-dot"></div>
800
+ <div class="wordmark">forme <span>preview</span></div>
801
+ </div>
792
802
 
793
- <div class="toolbar-separator"></div>
803
+ <div class="toolbar-separator"></div>
794
804
 
795
- <button class="toolbar-btn" id="tree-toggle" title="Toggle component tree (T)">Tree</button>
805
+ <button class="toolbar-btn" id="tree-toggle" title="Toggle component tree (T)">Tree</button>
796
806
 
797
- <div class="toolbar-separator"></div>
807
+ <div class="toolbar-separator"></div>
798
808
 
799
- <div class="segmented-control" id="mode-control">
800
- <button data-mode="preview" class="active">Preview <span class="shortcut">1</span></button>
801
- <button data-mode="layout">Layout <span class="shortcut">2</span></button>
802
- <button data-mode="margins">Margins <span class="shortcut">3</span></button>
803
- <button data-mode="breaks">Breaks <span class="shortcut">4</span></button>
804
- </div>
809
+ <div class="segmented-control" id="mode-control">
810
+ <button data-mode="preview" class="active">Preview <span class="shortcut">1</span></button>
811
+ <button data-mode="layout">Layout <span class="shortcut">2</span></button>
812
+ <button data-mode="margins">Margins <span class="shortcut">3</span></button>
813
+ <button data-mode="breaks">Breaks <span class="shortcut">4</span></button>
814
+ </div>
805
815
 
806
- <div class="toolbar-separator"></div>
807
-
808
- <div class="toolbar-group">
809
- <select id="page-size-select" title="Page size override">
810
- <option value="default">Default</option>
811
- <option value="letter">Letter (612 x 792)</option>
812
- <option value="a4">A4 (595 x 842)</option>
813
- <option value="legal">Legal (612 x 1008)</option>
814
- <option value="tabloid">Tabloid (792 x 1224)</option>
815
- <option value="a3">A3 (842 x 1191)</option>
816
- <option value="a5">A5 (420 x 595)</option>
817
- <option value="custom">Custom...</option>
818
- </select>
819
- <div class="custom-size-inputs" id="custom-size-inputs">
820
- <input type="number" id="custom-width" placeholder="W" value="612" min="72" max="4000">
821
- <span>x</span>
822
- <input type="number" id="custom-height" placeholder="H" value="792" min="72" max="4000">
823
- <span>pt</span>
816
+ <div class="toolbar-spacer"></div>
817
+
818
+ <div class="toolbar-group">
819
+ <select id="editor-select" title="Editor for Open in Editor">
820
+ <option value="vscode">VS Code</option>
821
+ <option value="cursor">Cursor</option>
822
+ <option value="webstorm">WebStorm</option>
823
+ </select>
824
824
  </div>
825
825
  </div>
826
826
 
827
- <div class="toolbar-spacer"></div>
827
+ <div class="toolbar-row">
828
+ <div class="toolbar-group">
829
+ <select id="page-size-select" title="Page size override">
830
+ <option value="default">Default</option>
831
+ <option value="letter">Letter (612 x 792)</option>
832
+ <option value="a4">A4 (595 x 842)</option>
833
+ <option value="legal">Legal (612 x 1008)</option>
834
+ <option value="tabloid">Tabloid (792 x 1224)</option>
835
+ <option value="a3">A3 (842 x 1191)</option>
836
+ <option value="a5">A5 (420 x 595)</option>
837
+ <option value="custom">Custom...</option>
838
+ </select>
839
+ <div class="custom-size-inputs" id="custom-size-inputs">
840
+ <input type="number" id="custom-width" placeholder="W" value="612" min="72" max="4000">
841
+ <span>x</span>
842
+ <input type="number" id="custom-height" placeholder="H" value="792" min="72" max="4000">
843
+ <span>pt</span>
844
+ </div>
845
+ </div>
828
846
 
829
- <div class="badge render-time" id="render-badge" style="display:none">
830
- <span id="render-time">0ms</span>
831
- </div>
832
- <div class="badge page-count" id="page-badge" style="display:none">
833
- <span id="page-count">0</span> pages
834
- </div>
847
+ <div class="toolbar-separator"></div>
848
+
849
+ <div class="badge render-time" id="render-badge" style="display:none">
850
+ <span id="render-time">0ms</span>
851
+ </div>
852
+ <div class="badge page-count" id="page-badge" style="display:none">
853
+ <span id="page-count">0</span> pages
854
+ </div>
835
855
 
836
- <div class="toolbar-separator"></div>
856
+ <div class="toolbar-separator"></div>
837
857
 
838
- <div class="zoom-controls">
839
- <button id="zoom-out" title="Zoom out (Cmd -)">&#x2212;</button>
840
- <span class="zoom-level" id="zoom-level">100%</span>
841
- <button id="zoom-in" title="Zoom in (Cmd +)">+</button>
842
- <button id="zoom-fit" title="Fit to window (Cmd 0)" style="font-size:11px; width:auto; padding: 0 8px;">Fit</button>
843
- </div>
858
+ <div class="zoom-controls">
859
+ <button id="zoom-out" title="Zoom out (Cmd -)">&#x2212;</button>
860
+ <span class="zoom-level" id="zoom-level">100%</span>
861
+ <button id="zoom-in" title="Zoom in (Cmd +)">+</button>
862
+ <button id="zoom-fit" title="Fit to window (Cmd 0)" style="font-size:11px; width:auto; padding: 0 8px;">Fit</button>
863
+ </div>
844
864
 
845
- <div class="toolbar-separator"></div>
846
- <div class="toolbar-group">
847
- <select id="editor-select" title="Editor for Open in Editor">
848
- <option value="vscode">VS Code</option>
849
- <option value="cursor">Cursor</option>
850
- <option value="webstorm">WebStorm</option>
851
- </select>
865
+ <div class="toolbar-spacer"></div>
866
+
867
+ <button class="toolbar-btn" id="download-btn" title="Download PDF" style="display:none">&#x2913; PDF</button>
852
868
  </div>
853
869
  </div>
854
870
 
@@ -1900,6 +1916,26 @@
1900
1916
  return { element: best, ancestors: bestAncestorNames, ancestorElements: bestAncestorElements };
1901
1917
  }
1902
1918
 
1919
+ // -- Download PDF -------------------------------------------------
1920
+ let lastPdfBase64 = null;
1921
+ const downloadBtn = document.getElementById('download-btn');
1922
+ if (downloadBtn) {
1923
+ downloadBtn.addEventListener('click', function() {
1924
+ if (!lastPdfBase64) return;
1925
+ if (isVSCode) {
1926
+ vscode.postMessage({ type: 'downloadPdf' });
1927
+ } else {
1928
+ const bytes = Uint8Array.from(atob(lastPdfBase64), c => c.charCodeAt(0));
1929
+ const blob = new Blob([bytes], { type: 'application/pdf' });
1930
+ const a = document.createElement('a');
1931
+ a.href = URL.createObjectURL(blob);
1932
+ a.download = 'document.pdf';
1933
+ a.click();
1934
+ URL.revokeObjectURL(a.href);
1935
+ }
1936
+ });
1937
+ }
1938
+
1903
1939
  // -- Render PDF pages --------------------------------------------
1904
1940
  let currentPdfDoc = null;
1905
1941
 
@@ -2429,6 +2465,8 @@
2429
2465
  if (!msg || !msg.type) return;
2430
2466
 
2431
2467
  if (msg.type === 'pdfData') {
2468
+ lastPdfBase64 = msg.pdf;
2469
+ if (downloadBtn) downloadBtn.style.display = '';
2432
2470
  const bytes = Uint8Array.from(atob(msg.pdf), c => c.charCodeAt(0));
2433
2471
  if (msg.renderTime) {
2434
2472
  renderTimeEl.textContent = msg.renderTime + 'ms';
package/dist/resolve.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export declare function uint8ArrayToBase64(bytes: Uint8Array): string;
2
2
  export declare function resolveFontSources(doc: Record<string, unknown>, basePath?: string): Promise<void>;
3
- export declare function resolveImageSources(doc: Record<string, unknown>): Promise<void>;
3
+ export declare function resolveImageSources(doc: Record<string, unknown>, basePath?: string): Promise<void>;
4
4
  export declare function resolveAllSources(doc: Record<string, unknown>, basePath?: string): Promise<void>;
package/dist/resolve.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { resolve } from 'node:path';
2
+ import { resolve, extname } from 'node:path';
3
3
  export function uint8ArrayToBase64(bytes) {
4
4
  return Buffer.from(bytes).toString('base64');
5
5
  }
@@ -22,19 +22,33 @@ export async function resolveFontSources(doc, basePath) {
22
22
  }
23
23
  }
24
24
  }
25
- /// Resolve image sources — converts HTTP/HTTPS URLs to base64 data URIs.
26
- /// Walks the document tree recursively.
27
- export async function resolveImageSources(doc) {
25
+ const MIME_BY_EXT = {
26
+ '.png': 'image/png',
27
+ '.jpg': 'image/jpeg',
28
+ '.jpeg': 'image/jpeg',
29
+ '.gif': 'image/gif',
30
+ '.webp': 'image/webp',
31
+ '.svg': 'image/svg+xml',
32
+ '.bmp': 'image/bmp',
33
+ '.ico': 'image/x-icon',
34
+ '.avif': 'image/avif',
35
+ };
36
+ /// Resolve image sources — converts HTTP/HTTPS URLs and local file paths to base64 data URIs.
37
+ /// Walks the document tree recursively. File paths are resolved relative to `basePath`.
38
+ export async function resolveImageSources(doc, basePath) {
28
39
  const children = doc.children;
29
40
  if (!children?.length)
30
41
  return;
31
- await Promise.all(children.map(resolveImageSourcesInNode));
42
+ await Promise.all(children.map((n) => resolveImageSourcesInNode(n, basePath)));
32
43
  }
33
- async function resolveImageSourcesInNode(node) {
44
+ async function resolveImageSourcesInNode(node, basePath) {
34
45
  const kind = node.kind;
35
46
  if (kind?.type === 'Image' && typeof kind.src === 'string') {
36
47
  const src = kind.src;
37
- if (src.startsWith('http://') || src.startsWith('https://')) {
48
+ if (src.startsWith('data:')) {
49
+ // Already a data URI — pass through
50
+ }
51
+ else if (src.startsWith('http://') || src.startsWith('https://')) {
38
52
  const res = await fetch(src);
39
53
  if (!res.ok)
40
54
  throw new Error(`Failed to fetch image: ${src} (${res.status})`);
@@ -42,16 +56,23 @@ async function resolveImageSourcesInNode(node) {
42
56
  const buf = new Uint8Array(await res.arrayBuffer());
43
57
  kind.src = `data:${contentType};base64,${uint8ArrayToBase64(buf)}`;
44
58
  }
59
+ else if (basePath) {
60
+ const filePath = resolve(basePath, src);
61
+ const ext = extname(filePath).toLowerCase();
62
+ const mime = MIME_BY_EXT[ext] || 'application/octet-stream';
63
+ const bytes = await readFile(filePath);
64
+ kind.src = `data:${mime};base64,${uint8ArrayToBase64(new Uint8Array(bytes))}`;
65
+ }
45
66
  }
46
67
  const children = node.children;
47
68
  if (children?.length) {
48
- await Promise.all(children.map(resolveImageSourcesInNode));
69
+ await Promise.all(children.map((n) => resolveImageSourcesInNode(n, basePath)));
49
70
  }
50
71
  }
51
72
  /// Resolve all asset sources (fonts + images) in parallel.
52
73
  export async function resolveAllSources(doc, basePath) {
53
74
  await Promise.all([
54
75
  resolveFontSources(doc, basePath),
55
- resolveImageSources(doc),
76
+ resolveImageSources(doc, basePath),
56
77
  ]);
57
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formepdf/renderer",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "File-to-PDF rendering pipeline for Forme — bundles TSX, resolves assets, renders via WASM",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,8 +17,8 @@
17
17
  "test": "vitest run"
18
18
  },
19
19
  "dependencies": {
20
- "@formepdf/core": "0.7.0",
21
- "@formepdf/react": "0.7.0",
20
+ "@formepdf/core": "0.7.3",
21
+ "@formepdf/react": "0.7.3",
22
22
  "esbuild": "^0.24.0"
23
23
  },
24
24
  "peerDependencies": {