@fenfill/mcp 0.2.0 → 0.2.1

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
@@ -138,7 +138,8 @@ workspace, free native-field (AcroForm) ones included.
138
138
  a text/date field's stored `format` and `font_pt` (the size its value starts at; `font_auto`
139
139
  when that follows the box height); a choice's options with their own boxes; a table's cells by
140
140
  `(row, col)`, its `orientation` and headers (also printed on the image inside, or beside, the
141
- first cell of each line as `R: …` / `C: …`); a `table_rows` table's columns and printed cells
141
+ first cell of each line as `R: …` / `C: …`; a character the bundled fonts lack is shown as `?`
142
+ there, with a warning, and the legend keeps the full text); a `table_rows` table's columns and printed cells
142
143
  with their boxes (fill values still go by column id). `warnings` and `placement_warnings` are
143
144
  always present (`[]` = checked and clean): placement covers every box shown, so a plain preview
144
145
  is a read-only placement audit (see the checks under `edit_template`). `crop`
@@ -314,6 +315,7 @@ fenfill web app for that workspace (for example, your logo if you have set one u
314
315
  | `too_many_active_jobs` | Other analyses in the workspace (free native-field ones too) are still running; try again in a minute, or analyze fewer at once. |
315
316
  | `template_conflict` | The template changed in fenfill since your edits began. Your local edits are kept: `edit_template` with `discard: true`, re-apply the ops, save again. |
316
317
  | `invalid_schema` | fenfill (or the local check before it) refused the edited layout; `details` lists each item and reason. Fix them with `edit_template`. |
318
+ | `check_copy_failed` | `preview_page` could not build its outlined copy; `stage` and `id` say where. Fix or remove that field or group with `edit_template`, then preview again. |
317
319
  | `request_too_large` | The edited template is too large to save in one request. |
318
320
  | `answer_in_template` | Edited wording matches an answer from this session, so nothing was sent. Put the form's own wording back with `edit_template`. |
319
321
  | `confirm_wording_needed` | The working copy has wording edited in an earlier session. Review the ids and properties in `changed`, then `save_template` with `confirm_wording: true`. |
@@ -1,4 +1,4 @@
1
- @fenfill/mcp 0.2.0 bundles the following third-party software.
1
+ @fenfill/mcp 0.2.1 bundles the following third-party software.
2
2
 
3
3
  @cantoo/pdf-lib@2.11.1 (MIT)
4
4
  ------------------------------------------------------------------------
package/dist/index.js CHANGED
@@ -135572,6 +135572,37 @@ function layoutMultiline({
135572
135572
  }
135573
135573
 
135574
135574
  // ../../app/t/[template_id]/stampPdf.ts
135575
+ async function embedStampFaces(pdfDoc, fontkit, fonts) {
135576
+ const interFont = await pdfDoc.embedFont(fonts.inter, { subset: true });
135577
+ const dejaFont = await pdfDoc.embedFont(fonts.deja, { subset: true });
135578
+ const interKit = fontkit.create(new Uint8Array(fonts.inter));
135579
+ const covers = (cp) => {
135580
+ try {
135581
+ return interKit.hasGlyphForCodePoint(cp);
135582
+ } catch {
135583
+ return false;
135584
+ }
135585
+ };
135586
+ const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
135587
+ font: r.key,
135588
+ text: r.text
135589
+ }));
135590
+ const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
135591
+ const draw = (page, t, x2, y, s, color) => {
135592
+ let cx2 = x2;
135593
+ for (const r of splitRuns(t)) {
135594
+ page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color });
135595
+ cx2 += r.font.widthOfTextAtSize(r.text, s);
135596
+ }
135597
+ };
135598
+ let dejaSet;
135599
+ const hasGlyph = (cp) => {
135600
+ if (covers(cp)) return true;
135601
+ dejaSet ??= new Set(dejaFont.getCharacterSet());
135602
+ return dejaSet.has(cp);
135603
+ };
135604
+ return { measure, draw, hasGlyph };
135605
+ }
135575
135606
  async function stampPdf({
135576
135607
  pdfBytes,
135577
135608
  marksByPage,
@@ -135605,29 +135636,12 @@ async function stampPdf({
135605
135636
  return r.arrayBuffer();
135606
135637
  })
135607
135638
  ]);
135608
- const interFont = await pdfDoc.embedFont(interBytes, { subset: true });
135609
- const dejaFont = await pdfDoc.embedFont(dejaBytes, { subset: true });
135639
+ const faces = await embedStampFaces(pdfDoc, fontkit, { inter: interBytes, deja: dejaBytes });
135610
135640
  const pdfPages = pdfDoc.getPages();
135611
135641
  const INK = rgb3(0.1, 0.1, 0.1);
135612
- const interKit = fontkit.create(new Uint8Array(interBytes));
135613
- const covers = (cp) => {
135614
- try {
135615
- return interKit.hasGlyphForCodePoint(cp);
135616
- } catch {
135617
- return false;
135618
- }
135619
- };
135620
- const splitRuns = (t) => splitByCoverage(t, (cp) => covers(cp) ? interFont : dejaFont).map((r) => ({
135621
- font: r.key,
135622
- text: r.text
135623
- }));
135624
- const measure = (t, s) => splitRuns(t).reduce((sum2, r) => sum2 + r.font.widthOfTextAtSize(r.text, s), 0);
135642
+ const { measure } = faces;
135625
135643
  const drawMixed = (page, t, x2, y, s) => {
135626
- let cx2 = x2;
135627
- for (const r of splitRuns(t)) {
135628
- page.drawText(r.text, { x: cx2, y, size: s, font: r.font, color: INK });
135629
- cx2 += r.font.widthOfTextAtSize(r.text, s);
135630
- }
135644
+ faces.draw(page, t, x2, y, s, INK);
135631
135645
  };
135632
135646
  const drawMarkPdf = (page, shape, r, sizePt, overflow) => {
135633
135647
  const fit = Math.min(r.w, r.h);
@@ -136602,9 +136616,34 @@ function checkTagIds(entries) {
136602
136616
  return ids;
136603
136617
  }
136604
136618
  var hits = (a, b) => a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h;
136605
- async function buildCheckPdf(stamped, entries, status, banner = true) {
136619
+ var causeOf = (e4) => e4 instanceof Error && e4.message ? e4.message : String(e4);
136620
+ var oneLine = (s) => s.replace(/[\s\p{Cc}]+/gu, " ").trim();
136621
+ var asciiOnly = (s) => s.replace(/[^\x20-\x7e]/g, "?");
136622
+ var CheckCopyError = class extends Error {
136623
+ at;
136624
+ constructor(at, cause) {
136625
+ super(causeOf(cause));
136626
+ this.name = "CheckCopyError";
136627
+ this.at = { ...at };
136628
+ }
136629
+ };
136630
+ async function buildCheckPdf(stamped, entries, status, fonts, banner = true) {
136631
+ const at = { stage: "load" };
136632
+ try {
136633
+ return await drawCheckPdf(stamped, entries, status, fonts, banner, at);
136634
+ } catch (e4) {
136635
+ throw new CheckCopyError(at, e4);
136636
+ }
136637
+ }
136638
+ async function drawCheckPdf(stamped, entries, status, fonts, banner, at) {
136639
+ const step = (stage, id) => {
136640
+ at.stage = stage;
136641
+ at.id = id;
136642
+ };
136643
+ const notes = [];
136606
136644
  const { PDFDocument: PDFDocument2, StandardFonts: StandardFonts2, rgb: rgb3 } = await Promise.resolve().then(() => __toESM(require_cjs()));
136607
136645
  const doc = await PDFDocument2.load(stamped, { updateMetadata: false });
136646
+ step("fonts");
136608
136647
  const helv = await doc.embedFont(StandardFonts2.Helvetica);
136609
136648
  const pages = doc.getPages();
136610
136649
  const COLORS = {
@@ -136615,6 +136654,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136615
136654
  };
136616
136655
  const boxes = [];
136617
136656
  for (const e4 of entries) {
136657
+ step("outline", e4.id);
136618
136658
  const st = status(e4.id);
136619
136659
  if (e4.kind === "field") {
136620
136660
  boxes.push({ id: e4.id, page: e4.field.page, box: e4.field, st, thin: false, tagged: true });
@@ -136631,6 +136671,7 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136631
136671
  boxes.push({ id: c2.id, page: c2.field.page, box: c2.field, st, thin: true, tagged: false });
136632
136672
  }
136633
136673
  }
136674
+ step("tag");
136634
136675
  const short = shortIds(checkTagIds(entries));
136635
136676
  const SIZE = 5;
136636
136677
  const rectOf = (b, pw, ph) => {
@@ -136640,47 +136681,92 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136640
136681
  };
136641
136682
  const placedTags = /* @__PURE__ */ new Map();
136642
136683
  const HEAD = 4;
136643
- const heads = [];
136644
136684
  const headColor = rgb3(0.15, 0.35, 0.75);
136685
+ const wanted = [];
136645
136686
  for (const e4 of entries) {
136646
136687
  if (e4.kind !== "table") continue;
136688
+ step("table headers", e4.id);
136647
136689
  const g = e4.group;
136648
136690
  const texts = (v) => Array.isArray(v) ? v : [];
136691
+ const lineOf = (raw) => typeof raw === "string" ? oneLine(raw) : "";
136649
136692
  const firstOf = (key, k4) => e4.cells.filter((c2) => c2[key] === k4).sort((a, b) => key === "row" ? a.col - b.col : a.row - b.row).at(0);
136650
- const draw = (prefix, raw, cell, below) => {
136651
- if (typeof raw !== "string" || !raw.trim() || !cell) return;
136693
+ const want = (axis, index2, raw, cell, below) => {
136694
+ const text = lineOf(raw);
136695
+ if (!text || !cell) return;
136652
136696
  const page = pages[cell.field.page - 1];
136653
- if (!page) return;
136654
- const { width: pw, height: ph } = page.getSize();
136655
- const cx2 = cell.field.xpct / 100 * pw;
136656
- const top = ph - cell.field.ypct / 100 * ph;
136657
- const w = cell.field.wpct / 100 * pw;
136658
- const h = cell.field.hpct / 100 * ph;
136659
- const inside = w >= 40 && h >= HEAD + 1;
136660
- const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
136661
- let text = `${prefix}: ${raw.trim()}`;
136662
- while (text.length > 4 && helv.widthOfTextAtSize(text, HEAD) > room) {
136663
- text = `${text.slice(0, -2)}\u2026`;
136664
- }
136665
- const tw = helv.widthOfTextAtSize(text, HEAD);
136666
- const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
136667
- const y = inside ? top - HEAD - 0.4 - below : prefix === "R" ? top - h / 2 - HEAD / 2 : top + 0.6;
136668
- heads.push({ page, text, opts: { x: x2, y, size: HEAD, font: helv, color: headColor } });
136669
- const taken = placedTags.get(cell.field.page) ?? [];
136670
- taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
136671
- placedTags.set(cell.field.page, taken);
136697
+ if (page) wanted.push({ gid: e4.id, axis, index: index2, text, cell, page, below });
136672
136698
  };
136673
136699
  texts(g.header_rows).forEach((t, r) => {
136674
- draw("R", t, firstOf("row", r), 0);
136700
+ want("row", r, t, firstOf("row", r), 0);
136675
136701
  });
136676
136702
  texts(g.header_cols).forEach((t, c2) => {
136677
136703
  const cell = firstOf("col", c2);
136678
136704
  const shared = cell !== void 0 && cell.col === firstOf("row", cell.row)?.col;
136679
- const rowHead = cell ? texts(g.header_rows)[cell.row] : void 0;
136680
- draw("C", t, cell, shared && typeof rowHead === "string" && rowHead.trim() ? HEAD + 0.6 : 0);
136705
+ const rowHead = cell ? lineOf(texts(g.header_rows)[cell.row]) : "";
136706
+ want("column", c2, t, cell, shared && rowHead ? HEAD + 0.6 : 0);
136681
136707
  });
136682
136708
  }
136709
+ const heads = [];
136710
+ const dropped = (h, e4) => `${h.gid}: ${h.axis} header ${String(h.index)} could not be drawn in the check copy: ${causeOf(e4)}`;
136711
+ if (wanted.length > 0) {
136712
+ step("fonts");
136713
+ const { default: fontkit } = await Promise.resolve().then(() => __toESM(require_fontkit_umd()));
136714
+ doc.registerFontkit(fontkit);
136715
+ const faces = await embedStampFaces(doc, fontkit, fonts);
136716
+ const unreadable = /* @__PURE__ */ new Set();
136717
+ for (const h of wanted) {
136718
+ step("table headers", h.gid);
136719
+ try {
136720
+ const { cell, page } = h;
136721
+ const prefix = h.axis === "row" ? "R" : "C";
136722
+ const { width: pw, height: ph } = page.getSize();
136723
+ const cx2 = cell.field.xpct / 100 * pw;
136724
+ const top = ph - cell.field.ypct / 100 * ph;
136725
+ const w = cell.field.wpct / 100 * pw;
136726
+ const ht = cell.field.hpct / 100 * ph;
136727
+ const inside = w >= 40 && ht >= HEAD + 1;
136728
+ const room = inside ? w - 1 : prefix === "R" ? Math.min(120, cx2 - 1) : Math.max(2 * w, 30);
136729
+ const chars3 = Array.from(
136730
+ `${prefix}: ${h.text}`,
136731
+ (ch) => faces.hasGlyph(ch.codePointAt(0) ?? 0) ? ch : "?"
136732
+ );
136733
+ if (chars3.join("") !== `${prefix}: ${h.text}` && !unreadable.has(h.gid)) {
136734
+ unreadable.add(h.gid);
136735
+ notes.push(
136736
+ `${h.gid}: the check copy cannot draw some header characters (shown as "?"); the legend's header_rows / header_cols hold the full text`
136737
+ );
136738
+ }
136739
+ const cut = (n) => `${chars3.slice(0, n).join("")}\u2026`;
136740
+ let text = chars3.join("");
136741
+ if (chars3.length > 4 && faces.measure(text, HEAD) > room) {
136742
+ let lo = 3;
136743
+ let hi = chars3.length - 2;
136744
+ while (lo < hi) {
136745
+ const mid = Math.ceil((lo + hi) / 2);
136746
+ if (faces.measure(cut(mid), HEAD) <= room) lo = mid;
136747
+ else hi = mid - 1;
136748
+ }
136749
+ text = cut(lo);
136750
+ }
136751
+ const tw = faces.measure(text, HEAD);
136752
+ const x2 = inside ? cx2 + 0.6 : prefix === "R" ? cx2 - tw - 0.8 : cx2;
136753
+ const y = inside ? top - HEAD - 0.4 - h.below : prefix === "R" ? top - ht / 2 - HEAD / 2 : top + 0.6;
136754
+ heads.push({
136755
+ head: h,
136756
+ draw: () => {
136757
+ faces.draw(page, text, x2, y, HEAD, headColor);
136758
+ }
136759
+ });
136760
+ const taken = placedTags.get(cell.field.page) ?? [];
136761
+ taken.push({ x: x2, y, w: tw, h: HEAD + 0.6 });
136762
+ placedTags.set(cell.field.page, taken);
136763
+ } catch (e4) {
136764
+ notes.push(dropped(h, e4));
136765
+ }
136766
+ }
136767
+ }
136683
136768
  for (const b of boxes) {
136769
+ step("outline", b.id);
136684
136770
  const page = pages[b.page - 1];
136685
136771
  if (!page) continue;
136686
136772
  const { width: pw, height: ph } = page.getSize();
@@ -136698,7 +136784,8 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136698
136784
  borderOpacity: 0.9
136699
136785
  });
136700
136786
  if (!b.tagged) continue;
136701
- const tag = short.get(b.id) ?? b.id;
136787
+ step("tag", b.id);
136788
+ const tag = asciiOnly(short.get(b.id) ?? b.id);
136702
136789
  const tw = helv.widthOfTextAtSize(tag, SIZE) + 1.6;
136703
136790
  const th = SIZE + 1.2;
136704
136791
  const right = x2 + w - tw;
@@ -136730,14 +136817,23 @@ async function buildCheckPdf(stamped, entries, status, banner = true) {
136730
136817
  });
136731
136818
  page.drawText(tag, { x: spot.x + 0.8, y: spot.y + 0.8, size: SIZE, font: helv, color });
136732
136819
  }
136733
- for (const h of heads) h.page.drawText(h.text, h.opts);
136820
+ for (const h of heads) {
136821
+ step("table headers", h.head.gid);
136822
+ try {
136823
+ h.draw();
136824
+ } catch (e4) {
136825
+ notes.push(dropped(h.head, e4));
136826
+ }
136827
+ }
136828
+ step("banner");
136734
136829
  const first = pages[0];
136735
136830
  if (first && banner) {
136736
136831
  const legend = "fenfill check copy: green = filled, orange = filled with a warning, red = skipped, grey = left empty. Tags are field-id prefixes.";
136737
136832
  const { height: ph } = first.getSize();
136738
136833
  first.drawText(legend, { x: 6, y: ph - 7, size: SIZE, font: helv, color: rgb3(0.3, 0.3, 0.35) });
136739
136834
  }
136740
- return doc.save();
136835
+ step("save");
136836
+ return { pdf: await doc.save(), notes };
136741
136837
  }
136742
136838
  async function fillAgentPdf(a) {
136743
136839
  if (!isRecord(a.render)) throw new TypeError("render must be the template's schema object");
@@ -136772,12 +136868,20 @@ async function fillAgentPdf(a) {
136772
136868
  const warned = new Set(
136773
136869
  placed.flatMap((w) => w.slice(0, w.indexOf(":")).split(" and ").map(topId))
136774
136870
  );
136775
- res.checkPdf = await buildCheckPdf(
136776
- pdf,
136777
- m.entries,
136778
- (id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
136779
- a.checkBanner !== false
136780
- );
136871
+ try {
136872
+ const built = await buildCheckPdf(
136873
+ pdf,
136874
+ m.entries,
136875
+ (id) => skippedIds.has(id) ? "skipped" : warned.has(id) ? "warned" : m.filledIds.has(id) ? "filled" : "empty",
136876
+ a.fonts,
136877
+ a.checkBanner !== false
136878
+ );
136879
+ res.checkPdf = built.pdf;
136880
+ warnings.push(...built.notes);
136881
+ } catch (e4) {
136882
+ const at = e4 instanceof CheckCopyError ? e4.at : { stage: "build" };
136883
+ res.checkError = { stage: at.stage, ...at.id ? { id: at.id } : {}, message: causeOf(e4) };
136884
+ }
136781
136885
  }
136782
136886
  return res;
136783
136887
  }
@@ -137680,7 +137784,7 @@ function loadFonts(dir = defaultFontsDir()) {
137680
137784
  }
137681
137785
 
137682
137786
  // src/version.ts
137683
- var VERSION = true ? "0.2.0" : "0.0.0-dev";
137787
+ var VERSION = true ? "0.2.1" : "0.0.0-dev";
137684
137788
  var USER_AGENT = `fenfill-mcp/${VERSION}`;
137685
137789
 
137686
137790
  // src/fill.ts
@@ -137864,6 +137968,11 @@ function fillForbiddenError() {
137864
137968
  hint: "fenfill can't fill this file; use a copy whose permissions allow form filling."
137865
137969
  });
137866
137970
  }
137971
+ function checkCopyFailed(err2) {
137972
+ const what = "The check copy could not be built";
137973
+ if (!err2) return `${what}.`;
137974
+ return `${what} (${[err2.stage, err2.id].filter(Boolean).join(", ")}): ${err2.message}`;
137975
+ }
137867
137976
  async function checkPathFor(outputAbs, opts) {
137868
137977
  return resolveOutputPath(outputAbs.replace(/\.pdf$/i, ".check.pdf"), opts);
137869
137978
  }
@@ -137889,17 +137998,22 @@ async function stampAndWrite(a) {
137889
137998
  await writeOutputAtomic(a.outputAbs, res.pdf, a.overwrite);
137890
137999
  await a.cache.registerOutput(sha256Hex(res.pdf));
137891
138000
  let checkPath;
138001
+ const warnings = [...res.warnings];
137892
138002
  if (a.checkAbs && res.checkPdf) {
137893
138003
  await writeOutputAtomic(a.checkAbs, res.checkPdf, a.overwrite);
137894
138004
  await a.cache.registerOutput(sha256Hex(res.checkPdf));
137895
138005
  checkPath = a.checkAbs;
138006
+ } else if (a.checkAbs) {
138007
+ warnings.push(
138008
+ `No check copy was written; the filled PDF is complete. ${checkCopyFailed(res.checkError)}`
138009
+ );
137896
138010
  }
137897
138011
  return {
137898
138012
  output_path: a.outputAbs,
137899
138013
  ...checkPath ? { check_path: checkPath } : {},
137900
138014
  filled: res.filled,
137901
138015
  skipped: res.skipped,
137902
- warnings: res.warnings
138016
+ warnings
137903
138017
  };
137904
138018
  }
137905
138019
 
@@ -142114,7 +142228,13 @@ async function previewPage(a) {
142114
142228
  if (isFillForbiddenError(e4)) throw fillForbiddenError();
142115
142229
  throw e4;
142116
142230
  }
142117
- if (!res.checkPdf) throw new ToolError("internal_error", "The check copy could not be built.");
142231
+ if (!res.checkPdf) {
142232
+ const err2 = res.checkError;
142233
+ throw new ToolError("check_copy_failed", checkCopyFailed(err2), {
142234
+ hint: err2?.id ? `Fix or remove ${err2.id} with edit_template, then preview again.` : "Preview again; if it persists, fill_form / fill_template still write the filled PDF, without a check copy.",
142235
+ details: { stage: err2?.stage, id: err2?.id }
142236
+ });
142237
+ }
142118
142238
  const legend = legendRows(a.render, a.page);
142119
142239
  const ids = new Set(legend.map((r) => r.id));
142120
142240
  const skipped = res.skipped.filter((s) => ids.has(topId2(s.id)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fenfill/mcp",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "fenfill MCP server: analyze blank PDF forms with fenfill and fill them locally, so your answers never leave your machine.",
5
5
  "license": "MIT",
6
6
  "author": "fenfill",