@signiphi/pdf-compose 0.1.0-beta.7 → 0.1.0-beta.8

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/dist/index.d.ts CHANGED
@@ -6,7 +6,8 @@ export type { Acknowledgement, DocumentGeneratorField, DocumentGeneratorVariable
6
6
  export { FormFieldType } from './types';
7
7
  export { suppressZeroContent, isZeroLike, expandRepeatContent, extractVariablesFromContent } from './utils/variable-helpers';
8
8
  export { generatePdfFromMarkdown, generatePdfFromTiptap } from './utils/template-pipeline';
9
- export type { HeadlessPdfResult } from './utils/template-pipeline';
9
+ export type { HeadlessPdfResult, HeadlessPdfOptions } from './utils/template-pipeline';
10
+ export type { PdfTheme, PdfPageHeader, PdfPageFooter } from './utils/pdf-generator';
10
11
  export { markdownToTiptap } from './utils/markdown-parser';
11
12
  export { tiptapToMarkdown } from './utils/markdown-writer';
12
13
  export { RepeatNode } from './extensions/repeat-node';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAG7H,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AAGnE,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AAGnE,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAGzD,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AAGpE,YAAY,EACV,eAAe,EACf,sBAAsB,EACtB,yBAAyB,EACzB,gBAAgB,EAChB,cAAc,EACd,iBAAiB,EACjB,6BAA6B,EAC7B,kBAAkB,EAClB,sBAAsB,EACtB,kBAAkB,EAClB,SAAS,EACT,WAAW,EACX,OAAO,GACR,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAGxC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,mBAAmB,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAG7H,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAC3F,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AACvF,YAAY,EAAE,QAAQ,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAGpF,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAG3D,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAG5D,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAC/D,YAAY,EAAE,sBAAsB,EAAE,MAAM,6BAA6B,CAAC;AAG1E,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAClE,YAAY,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC"}
package/dist/index.js CHANGED
@@ -608,6 +608,11 @@ function replaceVariablesInContent(content, values) {
608
608
  }
609
609
  return textNode;
610
610
  }
611
+ if (node.type === "imageNode" && node.attrs?.varName) {
612
+ const varName = node.attrs.varName;
613
+ const resolved = values[varName] || node.attrs.src || "";
614
+ return { ...node, attrs: { ...node.attrs, src: resolved } };
615
+ }
611
616
  if (node.content) {
612
617
  return { ...node, content: node.content.map(walkNode) };
613
618
  }
@@ -736,7 +741,8 @@ function expandRepeatContent(content, values) {
736
741
  values: enrichedValues
737
742
  };
738
743
  }
739
- var TOKEN_REGEX = /\{\{(field|var)\|([^}]+)\}\}/g;
744
+ var TOKEN_REGEX = /\{\{(field|var|image)\|([^}]+)\}\}/g;
745
+ var BLOCK_IMAGE_REGEX = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
740
746
  var DIRECTIVE_OPEN = /^:::(panel|columns|col|watermark|repeat|subtotals)(?:\{([^}]*)\})?$/;
741
747
  var DIRECTIVE_CLOSE = /^:::$/;
742
748
  function parseDirectiveAttrs(str) {
@@ -881,6 +887,44 @@ function parseVariableToken(tokenBody) {
881
887
  }
882
888
  return attrs;
883
889
  }
890
+ function parseImageToken(tokenBody) {
891
+ const attrs = {
892
+ src: "",
893
+ varName: "",
894
+ alt: "",
895
+ width: "",
896
+ height: "",
897
+ align: ""
898
+ };
899
+ const pairs = tokenBody.split("|");
900
+ for (const pair of pairs) {
901
+ const colonIdx = pair.indexOf(":");
902
+ if (colonIdx === -1) continue;
903
+ const key = pair.slice(0, colonIdx).trim();
904
+ const value = pair.slice(colonIdx + 1).trim();
905
+ switch (key) {
906
+ case "src":
907
+ attrs.src = value;
908
+ break;
909
+ case "var":
910
+ attrs.varName = value;
911
+ break;
912
+ case "alt":
913
+ attrs.alt = value;
914
+ break;
915
+ case "width":
916
+ attrs.width = value;
917
+ break;
918
+ case "height":
919
+ attrs.height = value;
920
+ break;
921
+ case "align":
922
+ attrs.align = value;
923
+ break;
924
+ }
925
+ }
926
+ return attrs;
927
+ }
884
928
  function parseInline(text) {
885
929
  text = preprocessMarkedTokens(text);
886
930
  const nodes = [];
@@ -904,6 +948,8 @@ function parseInline(text) {
904
948
  const node = { type: "variableNode", attrs };
905
949
  if (marks.length > 0) node.marks = marks;
906
950
  nodes.push(node);
951
+ } else if (match[1] === "image") {
952
+ nodes.push({ type: "imageNode", attrs: parseImageToken(match[2]) });
907
953
  }
908
954
  lastIndex = match.index + match[0].length;
909
955
  }
@@ -1217,10 +1263,24 @@ function parseBlocks(lines, startIdx, stopCondition) {
1217
1263
  i++;
1218
1264
  continue;
1219
1265
  }
1220
- content.push({
1221
- type: "paragraph",
1222
- content: parseInline(line)
1223
- });
1266
+ const imageMatch = line.trim().match(BLOCK_IMAGE_REGEX);
1267
+ if (imageMatch) {
1268
+ content.push({
1269
+ type: "imageNode",
1270
+ attrs: { src: imageMatch[2], varName: "", alt: imageMatch[1], width: "", height: "", align: "" }
1271
+ });
1272
+ i++;
1273
+ continue;
1274
+ }
1275
+ const inlineNodes = parseInline(line);
1276
+ const nonWhitespace = inlineNodes.filter(
1277
+ (n) => !(n.type === "text" && !(n.text || "").trim())
1278
+ );
1279
+ if (nonWhitespace.length === 1 && nonWhitespace[0].type === "imageNode") {
1280
+ content.push(nonWhitespace[0]);
1281
+ } else {
1282
+ content.push({ type: "paragraph", content: inlineNodes });
1283
+ }
1224
1284
  i++;
1225
1285
  }
1226
1286
  return { blocks: content, nextIdx: i };
@@ -1371,6 +1431,34 @@ var CONTENT_WIDTH = PAGE_WIDTH - 2 * MARGIN;
1371
1431
  var CONTENT_HEIGHT = PAGE_HEIGHT - 2 * MARGIN;
1372
1432
  var LINE_HEIGHT_FACTOR = 1.4;
1373
1433
  var BODY_FONT_SIZE = 10;
1434
+ function hexToRgbColor(hex) {
1435
+ if (!hex) return null;
1436
+ const m = hex.trim().match(/^#?([0-9a-fA-F]{6})$/);
1437
+ if (!m) return null;
1438
+ const n = parseInt(m[1], 16);
1439
+ return pdfLib.rgb((n >> 16 & 255) / 255, (n >> 8 & 255) / 255, (n & 255) / 255);
1440
+ }
1441
+ function contrastTextColor(fill) {
1442
+ const c = fill;
1443
+ const luminance = 0.299 * c.red + 0.587 * c.green + 0.114 * c.blue;
1444
+ return luminance > 0.6 ? pdfLib.rgb(0, 0, 0) : pdfLib.rgb(1, 1, 1);
1445
+ }
1446
+ function resolveTheme(theme) {
1447
+ const primary = hexToRgbColor(theme?.primaryColor);
1448
+ const accent = hexToRgbColor(theme?.accentColor);
1449
+ const border = hexToRgbColor(theme?.borderColor);
1450
+ return {
1451
+ tableHeaderBg: primary ?? pdfLib.rgb(0.3, 0.3, 0.3),
1452
+ tableHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1453
+ tableBorder: border ?? pdfLib.rgb(0.75, 0.75, 0.75),
1454
+ panelDarkHeaderBg: primary ?? pdfLib.rgb(0.25, 0.25, 0.25),
1455
+ panelDarkHeaderText: primary ? contrastTextColor(primary) : pdfLib.rgb(1, 1, 1),
1456
+ panelBorder: border ?? pdfLib.rgb(0.6, 0.6, 0.6),
1457
+ rule: primary ?? pdfLib.rgb(0, 0, 0),
1458
+ pageHeaderText: primary ?? pdfLib.rgb(0, 0, 0),
1459
+ pageHeaderRule: accent ?? primary ?? pdfLib.rgb(0.75, 0.75, 0.75)
1460
+ };
1461
+ }
1374
1462
  var DEFAULT_REGION = { leftX: MARGIN, width: CONTENT_WIDTH };
1375
1463
  var FIELD_DISPLAY = {
1376
1464
  ["text" /* TEXT */]: { label: "Text", width: 120, height: 30 },
@@ -1402,6 +1490,53 @@ function getFieldDimensions(fieldType, attrs, font, fontSize) {
1402
1490
  const height = textLineHeight + 4;
1403
1491
  return { width, height };
1404
1492
  }
1493
+ async function loadImageBytes(src) {
1494
+ if (src.startsWith("data:")) {
1495
+ const comma = src.indexOf(",");
1496
+ if (comma === -1) throw new Error("malformed data URL");
1497
+ const meta = src.slice(0, comma);
1498
+ if (!/;base64$/i.test(meta)) throw new Error("data URL must be base64-encoded");
1499
+ const bin = atob(src.slice(comma + 1));
1500
+ const bytes = new Uint8Array(bin.length);
1501
+ for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
1502
+ return bytes;
1503
+ }
1504
+ const res = await fetch(src);
1505
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
1506
+ return new Uint8Array(await res.arrayBuffer());
1507
+ }
1508
+ function describeImageSrc(src) {
1509
+ return src.length > 80 ? `${src.slice(0, 77)}...` : src;
1510
+ }
1511
+ async function preloadImages(pdfDoc, content) {
1512
+ const srcs = /* @__PURE__ */ new Set();
1513
+ (function walk(node) {
1514
+ if (node.type === "imageNode") {
1515
+ const src = node.attrs?.src || "";
1516
+ if (src) srcs.add(src);
1517
+ }
1518
+ for (const child of node.content || []) walk(child);
1519
+ })(content);
1520
+ const images = /* @__PURE__ */ new Map();
1521
+ const warnings = [];
1522
+ for (const src of srcs) {
1523
+ try {
1524
+ const bytes = await loadImageBytes(src);
1525
+ const isPng = bytes.length > 4 && bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71;
1526
+ const isJpeg = bytes.length > 2 && bytes[0] === 255 && bytes[1] === 216;
1527
+ if (isPng) {
1528
+ images.set(src, await pdfDoc.embedPng(bytes));
1529
+ } else if (isJpeg) {
1530
+ images.set(src, await pdfDoc.embedJpg(bytes));
1531
+ } else {
1532
+ warnings.push(`Image skipped (unsupported format \u2014 only PNG/JPEG embed): ${describeImageSrc(src)}`);
1533
+ }
1534
+ } catch (err) {
1535
+ warnings.push(`Image skipped (${getErrorMessage(err)}): ${describeImageSrc(src)}`);
1536
+ }
1537
+ }
1538
+ return { images, warnings };
1539
+ }
1405
1540
  function ensureSpace(state, neededHeight) {
1406
1541
  if (state.y + neededHeight > CONTENT_HEIGHT) {
1407
1542
  const newPage = state.pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
@@ -1749,9 +1884,9 @@ function renderTable(state, node, region = DEFAULT_REGION) {
1749
1884
  const cellPadding = borderless ? 2 : 4;
1750
1885
  const lineHeight = fontSize * LINE_HEIGHT_FACTOR;
1751
1886
  const rowPadding = cellPadding * 2;
1752
- const borderColor = pdfLib.rgb(0.75, 0.75, 0.75);
1753
- const headerBg = pdfLib.rgb(0.3, 0.3, 0.3);
1754
- const headerTextColor = pdfLib.rgb(1, 1, 1);
1887
+ const borderColor = state.theme.tableBorder;
1888
+ const headerBg = state.theme.tableHeaderBg;
1889
+ const headerTextColor = state.theme.tableHeaderText;
1755
1890
  const firstRow = tableRows[0];
1756
1891
  const colCount = firstRow.content?.length || 0;
1757
1892
  if (colCount === 0) return;
@@ -1999,7 +2134,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
1999
2134
  start: { x: region.leftX, y: ruleY },
2000
2135
  end: { x: region.leftX + region.width, y: ruleY },
2001
2136
  thickness: 2.5,
2002
- color: pdfLib.rgb(0, 0, 0)
2137
+ color: state.theme.rule
2003
2138
  });
2004
2139
  state.y += 4;
2005
2140
  break;
@@ -2032,6 +2167,39 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2032
2167
  renderTable(state, node, region);
2033
2168
  break;
2034
2169
  }
2170
+ case "imageNode": {
2171
+ const src = node.attrs?.src || "";
2172
+ const image = src ? state.images.get(src) : void 0;
2173
+ if (!image) break;
2174
+ const natW = image.width;
2175
+ const natH = image.height;
2176
+ let w = parseFloat(node.attrs?.width || "") || 0;
2177
+ let h = parseFloat(node.attrs?.height || "") || 0;
2178
+ if (w && !h) h = natH / natW * w;
2179
+ else if (h && !w) w = natW / natH * h;
2180
+ else if (!w && !h) {
2181
+ w = natW;
2182
+ h = natH;
2183
+ }
2184
+ if (w > region.width) {
2185
+ h = h * (region.width / w);
2186
+ w = region.width;
2187
+ }
2188
+ const maxH = CONTENT_HEIGHT * 0.6;
2189
+ if (h > maxH) {
2190
+ w = w * (maxH / h);
2191
+ h = maxH;
2192
+ }
2193
+ ensureSpace(state, h + 4);
2194
+ const align = node.attrs?.align || "left";
2195
+ let x = region.leftX;
2196
+ if (align === "center") x = region.leftX + (region.width - w) / 2;
2197
+ else if (align === "right") x = region.leftX + region.width - w;
2198
+ const pdfY = PAGE_HEIGHT - MARGIN - state.y - h;
2199
+ state.currentPage.drawImage(image, { x, y: pdfY, width: w, height: h });
2200
+ state.y += h + 4;
2201
+ break;
2202
+ }
2035
2203
  // ─── Layout directives ─────────────────────────────────────────
2036
2204
  case "panel": {
2037
2205
  const title = node.attrs?.title || "";
@@ -2068,8 +2236,8 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2068
2236
  y: titleBarY,
2069
2237
  width: region.width,
2070
2238
  height: titleHeight,
2071
- color: isDarkHeader ? pdfLib.rgb(0.25, 0.25, 0.25) : pdfLib.rgb(0.93, 0.93, 0.93),
2072
- borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2239
+ color: isDarkHeader ? state.theme.panelDarkHeaderBg : pdfLib.rgb(0.93, 0.93, 0.93),
2240
+ borderColor: state.theme.panelBorder,
2073
2241
  borderWidth: 0.75
2074
2242
  });
2075
2243
  startPage.drawText(title, {
@@ -2077,7 +2245,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2077
2245
  y: titleBarY + 4,
2078
2246
  size: titleFontSize,
2079
2247
  font: state.fonts.bold,
2080
- color: isDarkHeader ? pdfLib.rgb(1, 1, 1) : pdfLib.rgb(0, 0, 0)
2248
+ color: isDarkHeader ? state.theme.panelDarkHeaderText : pdfLib.rgb(0, 0, 0)
2081
2249
  });
2082
2250
  }
2083
2251
  if (border !== "none") {
@@ -2090,7 +2258,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2090
2258
  y: borderY,
2091
2259
  width: region.width,
2092
2260
  height: panelHeight,
2093
- borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2261
+ borderColor: state.theme.panelBorder,
2094
2262
  borderWidth: 0.75,
2095
2263
  borderDashArray
2096
2264
  });
@@ -2102,7 +2270,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2102
2270
  y: startBorderBottom,
2103
2271
  width: region.width,
2104
2272
  height: startPanelHeight,
2105
- borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2273
+ borderColor: state.theme.panelBorder,
2106
2274
  borderWidth: 0.75,
2107
2275
  borderDashArray
2108
2276
  });
@@ -2114,7 +2282,7 @@ function processBlock(state, node, region = DEFAULT_REGION) {
2114
2282
  y: contBorderY,
2115
2283
  width: region.width,
2116
2284
  height: contHeight,
2117
- borderColor: pdfLib.rgb(0.6, 0.6, 0.6),
2285
+ borderColor: state.theme.panelBorder,
2118
2286
  borderWidth: 0.75,
2119
2287
  borderDashArray
2120
2288
  });
@@ -2418,6 +2586,24 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
2418
2586
  pdfLib.PDFString.of(field.label)
2419
2587
  );
2420
2588
  }
2589
+ const initialsFontSize = Math.min(
2590
+ 72,
2591
+ Math.max(8, Math.round(position.height * 0.55))
2592
+ );
2593
+ try {
2594
+ initialsField.setFontSize(initialsFontSize);
2595
+ const acro = initialsField;
2596
+ const daRef = acro.acroField.dict.get(pdfLib.PDFName.of("DA"));
2597
+ if (daRef) {
2598
+ const daStr = daRef.value;
2599
+ if (typeof daStr === "string") {
2600
+ const newDa = daStr.replace(/\/Helv\b/, "/TiBo").replace(/\/HeBo\b/, "/TiBo");
2601
+ acro.acroField.dict.set(pdfLib.PDFName.of("DA"), pdfLib.PDFString.of(newDa));
2602
+ }
2603
+ }
2604
+ } catch (fontError) {
2605
+ console.warn(`Failed to set initials font:`, fontError);
2606
+ }
2421
2607
  initialsField.enableReadOnly();
2422
2608
  if (field.required) initialsField.enableRequired();
2423
2609
  break;
@@ -2528,7 +2714,8 @@ async function addFormFields(pdfDoc, fieldPositions, fields) {
2528
2714
  return warnings;
2529
2715
  }
2530
2716
  async function generatePdfFromContent(content, options = {}) {
2531
- const { drawFieldPlaceholders = false, embedFormFields = false, fields = [] } = options;
2717
+ const { drawFieldPlaceholders = false, embedFormFields = false, fields = [], header, footer } = options;
2718
+ const theme = resolveTheme(options.theme);
2532
2719
  const pdfDoc = await pdfLib.PDFDocument.create();
2533
2720
  const firstPage = pdfDoc.addPage([PAGE_WIDTH, PAGE_HEIGHT]);
2534
2721
  const fonts = {
@@ -2538,6 +2725,7 @@ async function generatePdfFromContent(content, options = {}) {
2538
2725
  boldItalic: await pdfDoc.embedFont(pdfLib.StandardFonts.HelveticaBoldOblique)
2539
2726
  };
2540
2727
  const watermarkNodes = collectWatermarkNodes(content);
2728
+ const { images, warnings: imageWarnings } = await preloadImages(pdfDoc, content);
2541
2729
  const state = {
2542
2730
  currentPage: firstPage,
2543
2731
  pageIndex: 0,
@@ -2546,7 +2734,9 @@ async function generatePdfFromContent(content, options = {}) {
2546
2734
  pdfDoc,
2547
2735
  fieldPositions: /* @__PURE__ */ new Map(),
2548
2736
  drawFieldPlaceholders,
2549
- watermarkNodes
2737
+ watermarkNodes,
2738
+ theme,
2739
+ images
2550
2740
  };
2551
2741
  renderWatermarksOnPage(firstPage, fonts, watermarkNodes);
2552
2742
  if (content.content) {
@@ -2557,6 +2747,8 @@ async function generatePdfFromContent(content, options = {}) {
2557
2747
  const allPages = pdfDoc.getPages();
2558
2748
  const totalPages = allPages.length;
2559
2749
  const pageNumFontSize = 9;
2750
+ const hfFontSize = 8;
2751
+ const hfColor = pdfLib.rgb(0.45, 0.45, 0.45);
2560
2752
  for (let i = 0; i < totalPages; i++) {
2561
2753
  const page = allPages[i];
2562
2754
  const label = `Page ${i + 1} of ${totalPages}`;
@@ -2568,6 +2760,43 @@ async function generatePdfFromContent(content, options = {}) {
2568
2760
  font: fonts.bold,
2569
2761
  color: pdfLib.rgb(0, 0, 0)
2570
2762
  });
2763
+ if (header && (header.left || header.right) && !(header.skipFirstPage && i === 0)) {
2764
+ const headerBaseY = PAGE_HEIGHT - MARGIN + 10;
2765
+ if (header.left) {
2766
+ page.drawText(header.left, {
2767
+ x: MARGIN,
2768
+ y: headerBaseY,
2769
+ size: pageNumFontSize,
2770
+ font: fonts.bold,
2771
+ color: theme.pageHeaderText
2772
+ });
2773
+ }
2774
+ if (header.right) {
2775
+ const rightWidth = fonts.regular.widthOfTextAtSize(header.right, hfFontSize);
2776
+ page.drawText(header.right, {
2777
+ x: PAGE_WIDTH - MARGIN - rightWidth,
2778
+ y: headerBaseY,
2779
+ size: hfFontSize,
2780
+ font: fonts.regular,
2781
+ color: hfColor
2782
+ });
2783
+ }
2784
+ page.drawLine({
2785
+ start: { x: MARGIN, y: headerBaseY - 6 },
2786
+ end: { x: PAGE_WIDTH - MARGIN, y: headerBaseY - 6 },
2787
+ thickness: 0.75,
2788
+ color: theme.pageHeaderRule
2789
+ });
2790
+ }
2791
+ if (footer?.left) {
2792
+ page.drawText(footer.left, {
2793
+ x: MARGIN,
2794
+ y: MARGIN / 2 - pageNumFontSize / 2,
2795
+ size: hfFontSize,
2796
+ font: fonts.regular,
2797
+ color: hfColor
2798
+ });
2799
+ }
2571
2800
  }
2572
2801
  let fieldWarnings;
2573
2802
  if (embedFormFields && fields.length > 0) {
@@ -2578,25 +2807,30 @@ async function generatePdfFromContent(content, options = {}) {
2578
2807
  return {
2579
2808
  pdfBytes,
2580
2809
  fieldPositions: state.fieldPositions,
2581
- fieldWarnings
2810
+ fieldWarnings,
2811
+ imageWarnings: imageWarnings.length > 0 ? imageWarnings : void 0
2582
2812
  };
2583
2813
  }
2584
2814
 
2585
2815
  // src/utils/template-pipeline.ts
2586
- async function generatePdfFromTiptap(content, values) {
2816
+ async function generatePdfFromTiptap(content, values, options = {}) {
2587
2817
  const { content: expanded, values: enrichedValues } = expandRepeatContent(content, values);
2588
2818
  const suppressed = suppressZeroContent(expanded, enrichedValues);
2589
2819
  const replaced = replaceVariablesInContent(suppressed, enrichedValues);
2590
2820
  const fields = extractFieldsFromContent(replaced);
2591
2821
  const result = await generatePdfFromContent(replaced, {
2592
2822
  embedFormFields: fields.length > 0,
2593
- fields
2823
+ fields,
2824
+ theme: options.theme,
2825
+ header: options.header,
2826
+ footer: options.footer,
2827
+ drawFieldPlaceholders: options.drawFieldPlaceholders
2594
2828
  });
2595
- return { pdfBytes: result.pdfBytes };
2829
+ return { pdfBytes: result.pdfBytes, imageWarnings: result.imageWarnings };
2596
2830
  }
2597
- async function generatePdfFromMarkdown(markdown, values) {
2831
+ async function generatePdfFromMarkdown(markdown, values, options = {}) {
2598
2832
  const content = markdownToTiptap(markdown);
2599
- return generatePdfFromTiptap(content, values);
2833
+ return generatePdfFromTiptap(content, values, options);
2600
2834
  }
2601
2835
 
2602
2836
  // src/utils/markdown-writer.ts
@@ -2616,6 +2850,20 @@ function serializeFieldToken(attrs) {
2616
2850
  if (attrs.acknowledgements) parts.push(`acks:${btoa(attrs.acknowledgements)}`);
2617
2851
  return `{{${parts.join("|")}}}`;
2618
2852
  }
2853
+ function serializeImageNode(attrs) {
2854
+ const hasExtras = attrs.varName || attrs.width || attrs.height || attrs.align;
2855
+ if (!hasExtras && attrs.src) {
2856
+ return `![${attrs.alt || ""}](${attrs.src})`;
2857
+ }
2858
+ const parts = ["image"];
2859
+ if (attrs.varName) parts.push(`var:${attrs.varName}`);
2860
+ else if (attrs.src) parts.push(`src:${attrs.src}`);
2861
+ if (attrs.alt) parts.push(`alt:${attrs.alt}`);
2862
+ if (attrs.width) parts.push(`width:${attrs.width}`);
2863
+ if (attrs.height) parts.push(`height:${attrs.height}`);
2864
+ if (attrs.align) parts.push(`align:${attrs.align}`);
2865
+ return `{{${parts.join("|")}}}`;
2866
+ }
2619
2867
  function serializeVariableToken(attrs) {
2620
2868
  const parts = ["var"];
2621
2869
  if (attrs.varName) parts.push(`name:${attrs.varName}`);
@@ -2633,6 +2881,10 @@ function serializeInline(content) {
2633
2881
  result += serializeFieldToken(node.attrs || {});
2634
2882
  continue;
2635
2883
  }
2884
+ if (node.type === "imageNode") {
2885
+ result += serializeImageNode(node.attrs || {});
2886
+ continue;
2887
+ }
2636
2888
  if (node.type === "variableNode") {
2637
2889
  let token = serializeVariableToken(node.attrs || {});
2638
2890
  const varMarks = node.marks || [];
@@ -2739,6 +2991,9 @@ ${text}
2739
2991
  case "tableCell": {
2740
2992
  return (node.content || []).map(serializeBlock).join("");
2741
2993
  }
2994
+ case "imageNode": {
2995
+ return serializeImageNode(node.attrs || {});
2996
+ }
2742
2997
  case "watermark": {
2743
2998
  const attrs = node.attrs || {};
2744
2999
  const parts = [];
@@ -4802,8 +5057,8 @@ function validateMarkdown(markdown) {
4802
5057
  errors.push(`Malformed token at line ${lineNum}: missing | delimiter`);
4803
5058
  } else {
4804
5059
  const tokenType = tokenContent.slice(0, pipeIdx);
4805
- if (tokenType !== "field" && tokenType !== "var") {
4806
- errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field" or "var")`);
5060
+ if (tokenType !== "field" && tokenType !== "var" && tokenType !== "image") {
5061
+ errors.push(`Unknown token type "${tokenType}" at line ${lineNum} (expected "field", "var" or "image")`);
4807
5062
  } else {
4808
5063
  const body = tokenContent.slice(pipeIdx + 1);
4809
5064
  const pairs = body.split("|");
@@ -4817,6 +5072,11 @@ function validateMarkdown(markdown) {
4817
5072
  if (!hasName) {
4818
5073
  errors.push(`Variable token at line ${lineNum} is missing required "name" attribute`);
4819
5074
  }
5075
+ } else if (tokenType === "image") {
5076
+ const hasSource = pairs.some((p) => p.startsWith("src:") || p.startsWith("var:"));
5077
+ if (!hasSource) {
5078
+ errors.push(`Image token at line ${lineNum} needs a "src" or "var" attribute`);
5079
+ }
4820
5080
  }
4821
5081
  }
4822
5082
  }