aldus-pdf 0.3.0
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 +70 -0
- package/dist/cli.js +4260 -0
- package/dist/editor/assets/index-BnKlq-l5.js +364 -0
- package/dist/editor/assets/index-Db8qJ_wC.css +1 -0
- package/dist/editor/assets/index-L30_R8mi.js +60 -0
- package/dist/editor/assets/pdf.worker.min-yatZIOMy.mjs +21 -0
- package/dist/editor/index.html +13 -0
- package/dist/index.js +4016 -0
- package/dist/server.mjs +4393 -0
- package/package.json +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,4016 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// ../core/src/model.ts
|
|
12
|
+
var FIELD_DEFAULT_SIZE;
|
|
13
|
+
var init_model = __esm({
|
|
14
|
+
"../core/src/model.ts"() {
|
|
15
|
+
FIELD_DEFAULT_SIZE = {
|
|
16
|
+
text: { width: 160, height: 20 },
|
|
17
|
+
checkbox: { width: 14, height: 14 },
|
|
18
|
+
radio: { width: 14, height: 14 },
|
|
19
|
+
select: { width: 140, height: 20 },
|
|
20
|
+
list: { width: 140, height: 60 },
|
|
21
|
+
button: { width: 90, height: 24 },
|
|
22
|
+
signature: { width: 200, height: 50 }
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ../core/src/tokens.ts
|
|
28
|
+
function avgCharWidth(run) {
|
|
29
|
+
const w = run.width / Math.max(1, run.text.length);
|
|
30
|
+
return Math.max(w, run.fontSize * 0.2);
|
|
31
|
+
}
|
|
32
|
+
function classifyGap(gap, prev, next) {
|
|
33
|
+
if (gap <= 0) return "none";
|
|
34
|
+
const ref = (avgCharWidth(prev) + avgCharWidth(next)) / 2;
|
|
35
|
+
if (gap > 2 * ref) return "column";
|
|
36
|
+
if (gap > 0.5 * ref) return "space";
|
|
37
|
+
return "none";
|
|
38
|
+
}
|
|
39
|
+
function splitSegments(runsSorted) {
|
|
40
|
+
const segments = [];
|
|
41
|
+
let current = [];
|
|
42
|
+
for (const run of runsSorted) {
|
|
43
|
+
if (current.length === 0) {
|
|
44
|
+
current.push(run);
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const prev = current[current.length - 1];
|
|
48
|
+
const gap = run.x - (prev.x + prev.width);
|
|
49
|
+
if (classifyGap(gap, prev, run) === "column") {
|
|
50
|
+
segments.push(current);
|
|
51
|
+
current = [run];
|
|
52
|
+
} else {
|
|
53
|
+
current.push(run);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (current.length) segments.push(current);
|
|
57
|
+
return segments;
|
|
58
|
+
}
|
|
59
|
+
function segmentText(runsSorted) {
|
|
60
|
+
let text = "";
|
|
61
|
+
for (let i = 0; i < runsSorted.length; i++) {
|
|
62
|
+
const run = runsSorted[i];
|
|
63
|
+
if (i > 0) {
|
|
64
|
+
const prev = runsSorted[i - 1];
|
|
65
|
+
const gap = run.x - (prev.x + prev.width);
|
|
66
|
+
if (classifyGap(gap, prev, run) === "space" && !text.endsWith(" ") && !run.text.startsWith(" ")) {
|
|
67
|
+
text += " ";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
text += run.text;
|
|
71
|
+
}
|
|
72
|
+
return text;
|
|
73
|
+
}
|
|
74
|
+
var init_tokens = __esm({
|
|
75
|
+
"../core/src/tokens.ts"() {
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ../core/src/extractGraph.ts
|
|
80
|
+
function mergeBlockSegments(lines) {
|
|
81
|
+
const out = [];
|
|
82
|
+
let i = 0;
|
|
83
|
+
while (i < lines.length) {
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
if (line.segments.length !== 1) {
|
|
86
|
+
out.push(...line.segments);
|
|
87
|
+
i++;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const chain = [line.segments[0]];
|
|
91
|
+
let j = i + 1;
|
|
92
|
+
while (j < lines.length) {
|
|
93
|
+
const prev = chain[chain.length - 1];
|
|
94
|
+
const next = lines[j].segments.length === 1 ? lines[j].segments[0] : null;
|
|
95
|
+
if (!next) break;
|
|
96
|
+
const step = prev.baseline - next.baseline;
|
|
97
|
+
const lead = prev.fontSize * 1.2;
|
|
98
|
+
const match = Math.abs(next.x - prev.x) <= 0.5 && Math.abs(next.fontSize - prev.fontSize) <= 0.1 && Math.abs(step - lead) <= prev.fontSize * 0.06;
|
|
99
|
+
if (!match) break;
|
|
100
|
+
chain.push(next);
|
|
101
|
+
j++;
|
|
102
|
+
}
|
|
103
|
+
if (chain.length === 1) {
|
|
104
|
+
out.push(chain[0]);
|
|
105
|
+
} else {
|
|
106
|
+
const first = chain[0];
|
|
107
|
+
const last = chain[chain.length - 1];
|
|
108
|
+
out.push({
|
|
109
|
+
...first,
|
|
110
|
+
text: chain.map((s) => s.text).join("\n"),
|
|
111
|
+
runs: chain.flatMap((s) => s.runs),
|
|
112
|
+
width: Math.max(...chain.map((s) => s.width)),
|
|
113
|
+
y: last.y,
|
|
114
|
+
height: first.y + first.height - last.y
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
i = j;
|
|
118
|
+
}
|
|
119
|
+
return out;
|
|
120
|
+
}
|
|
121
|
+
function extractLinks(annots, page, x0, y0) {
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const raw of annots) {
|
|
124
|
+
if (raw?.subtype !== "Link" || !Array.isArray(raw.rect)) continue;
|
|
125
|
+
const url = raw.url ?? raw.unsafeUrl;
|
|
126
|
+
if (!url) continue;
|
|
127
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
128
|
+
out.push({
|
|
129
|
+
id: `p${page}-link${out.length}`,
|
|
130
|
+
kind: "link",
|
|
131
|
+
page,
|
|
132
|
+
url,
|
|
133
|
+
x: Math.min(ax, bx) - x0,
|
|
134
|
+
y: Math.min(ay, by) - y0,
|
|
135
|
+
width: Math.abs(bx - ax),
|
|
136
|
+
height: Math.abs(by - ay)
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
function extractHighlights(annots, page, x0, y0) {
|
|
142
|
+
const out = [];
|
|
143
|
+
const hx = (v) => Math.max(0, Math.min(255, Math.round(v <= 1 ? v * 255 : v))).toString(16).padStart(2, "0");
|
|
144
|
+
for (const raw of annots) {
|
|
145
|
+
if (raw?.subtype !== "Highlight" || !Array.isArray(raw.rect)) continue;
|
|
146
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
147
|
+
const c = raw.color && raw.color.length >= 3 ? raw.color : null;
|
|
148
|
+
out.push({
|
|
149
|
+
id: `p${page}-hl${out.length}`,
|
|
150
|
+
kind: "highlight",
|
|
151
|
+
page,
|
|
152
|
+
x: Math.min(ax, bx) - x0,
|
|
153
|
+
y: Math.min(ay, by) - y0,
|
|
154
|
+
width: Math.abs(bx - ax),
|
|
155
|
+
height: Math.abs(by - ay),
|
|
156
|
+
color: c ? `#${hx(c[0])}${hx(c[1])}${hx(c[2])}` : "#ffd400"
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
function widgetKindOf(a2) {
|
|
162
|
+
if (a2.fieldType === "Tx") return "text";
|
|
163
|
+
if (a2.fieldType === "Sig") return "signature";
|
|
164
|
+
if (a2.fieldType === "Ch") return a2.combo ? "select" : "list";
|
|
165
|
+
if (a2.fieldType === "Btn") return a2.checkBox ? "checkbox" : a2.radioButton ? "radio" : "button";
|
|
166
|
+
return "text";
|
|
167
|
+
}
|
|
168
|
+
function extractWidgets(annots, page, x0, y0) {
|
|
169
|
+
const out = [];
|
|
170
|
+
for (const raw of annots) {
|
|
171
|
+
if (raw?.subtype !== "Widget" || !Array.isArray(raw.rect) || raw.hidden) continue;
|
|
172
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
173
|
+
const kind = widgetKindOf(raw);
|
|
174
|
+
out.push({
|
|
175
|
+
id: `p${page}-w${out.length}`,
|
|
176
|
+
kind: "widget",
|
|
177
|
+
page,
|
|
178
|
+
fieldName: raw.fieldName ?? "",
|
|
179
|
+
widgetType: kind,
|
|
180
|
+
readOnly: raw.readOnly === true,
|
|
181
|
+
options: (kind === "select" || kind === "list") && Array.isArray(raw.options) ? raw.options.map((o) => o.displayValue || o.exportValue || "").filter(Boolean) : void 0,
|
|
182
|
+
// Valor actual (/V). pdf.js entrega '' para vacío y 'Off' para un
|
|
183
|
+
// checkbox/radio sin marcar → los normalizamos a "ausente".
|
|
184
|
+
value: (() => {
|
|
185
|
+
const v = raw.fieldValue;
|
|
186
|
+
if (Array.isArray(v)) return v.length ? v : void 0;
|
|
187
|
+
if (typeof v === "string" && v && v !== "Off") return v;
|
|
188
|
+
return void 0;
|
|
189
|
+
})(),
|
|
190
|
+
x: Math.min(ax, bx) - x0,
|
|
191
|
+
y: Math.min(ay, by) - y0,
|
|
192
|
+
width: Math.abs(bx - ax),
|
|
193
|
+
height: Math.abs(by - ay)
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
function extractImages(fnArray, argsArray, page, x0, y0) {
|
|
199
|
+
const images = [];
|
|
200
|
+
let ctm = [1, 0, 0, 1, 0, 0];
|
|
201
|
+
const stack = [];
|
|
202
|
+
const seen = /* @__PURE__ */ new Map();
|
|
203
|
+
for (let i = 0; i < fnArray.length; i++) {
|
|
204
|
+
const fn = fnArray[i];
|
|
205
|
+
if (fn === OP_SAVE) stack.push(ctm);
|
|
206
|
+
else if (fn === OP_RESTORE) ctm = stack.pop() ?? [1, 0, 0, 1, 0, 0];
|
|
207
|
+
else if (fn === OP_TRANSFORM) {
|
|
208
|
+
const a2 = argsArray[i];
|
|
209
|
+
ctm = mulMat([a2[0], a2[1], a2[2], a2[3], a2[4], a2[5]], ctm);
|
|
210
|
+
} else if (PAINT_OPS.has(fn)) {
|
|
211
|
+
const [a2, b, c, d, e, f] = ctm;
|
|
212
|
+
const xs = [e, a2 + e, c + e, a2 + c + e];
|
|
213
|
+
const ys = [f, b + f, d + f, b + d + f];
|
|
214
|
+
const minX = Math.min(...xs);
|
|
215
|
+
const minY = Math.min(...ys);
|
|
216
|
+
const arg0 = argsArray[i][0];
|
|
217
|
+
const objId = (fn === OP_PAINT_IMAGE || fn === OP_PAINT_IMAGE_REPEAT) && typeof arg0 === "string" ? arg0 : void 0;
|
|
218
|
+
let id;
|
|
219
|
+
if (objId) {
|
|
220
|
+
const n = seen.get(objId) ?? 0;
|
|
221
|
+
seen.set(objId, n + 1);
|
|
222
|
+
id = n === 0 ? `p${page}-${objId}` : `p${page}-${objId}#${n}`;
|
|
223
|
+
} else {
|
|
224
|
+
id = `p${page}-img${images.length}`;
|
|
225
|
+
}
|
|
226
|
+
images.push({
|
|
227
|
+
id,
|
|
228
|
+
kind: "image",
|
|
229
|
+
page,
|
|
230
|
+
x: minX - x0,
|
|
231
|
+
y: minY - y0,
|
|
232
|
+
width: Math.max(...xs) - minX,
|
|
233
|
+
height: Math.max(...ys) - minY,
|
|
234
|
+
rotated: Math.abs(b) > 0.01 || Math.abs(c) > 0.01,
|
|
235
|
+
objId
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return images;
|
|
240
|
+
}
|
|
241
|
+
function styleFromName(name) {
|
|
242
|
+
const n = name.toLowerCase();
|
|
243
|
+
const bold = /bold|black|heavy|semibold|demibold|extrabold|-bd\b|,bd\b/.test(n);
|
|
244
|
+
const italic = /italic|oblique/.test(n);
|
|
245
|
+
const bucket = /(mono|courier|consol|menlo|typewriter|fixed)/.test(n) ? "mono" : /(times|georgia|garamond|serif|roman|minion|antiqua|palatino|cambria|bodoni)/.test(n) ? "serif" : "sans";
|
|
246
|
+
return { bold, italic, bucket };
|
|
247
|
+
}
|
|
248
|
+
function fontInfoFor(page, loadedName, cache) {
|
|
249
|
+
const hit = cache.get(loadedName);
|
|
250
|
+
if (hit) return hit;
|
|
251
|
+
let raw = null;
|
|
252
|
+
try {
|
|
253
|
+
raw = page.commonObjs.get(loadedName);
|
|
254
|
+
} catch {
|
|
255
|
+
raw = null;
|
|
256
|
+
}
|
|
257
|
+
const ps = raw?.name || raw?.fallbackName || loadedName;
|
|
258
|
+
const style = styleFromName(ps);
|
|
259
|
+
const info = {
|
|
260
|
+
loadedName,
|
|
261
|
+
postScriptName: ps,
|
|
262
|
+
bold: raw?.bold ?? style.bold,
|
|
263
|
+
italic: raw?.italic ?? style.italic,
|
|
264
|
+
bucket: style.bucket,
|
|
265
|
+
ascent: typeof raw?.ascent === "number" && raw.ascent > 0 ? raw.ascent : 0.8,
|
|
266
|
+
descent: typeof raw?.descent === "number" && raw.descent < 0 ? raw.descent : -0.2,
|
|
267
|
+
embedded: raw != null && raw.missingFile !== true
|
|
268
|
+
};
|
|
269
|
+
cache.set(loadedName, info);
|
|
270
|
+
return info;
|
|
271
|
+
}
|
|
272
|
+
async function extractPageGraph(page) {
|
|
273
|
+
const opList = await page.getOperatorList();
|
|
274
|
+
const tc = await page.getTextContent({ disableNormalization: true });
|
|
275
|
+
const annots = await page.getAnnotations().catch(() => []);
|
|
276
|
+
const [x0, y0, x1, y1] = page.view;
|
|
277
|
+
const fontCache = /* @__PURE__ */ new Map();
|
|
278
|
+
const runs = [];
|
|
279
|
+
let i = 0;
|
|
280
|
+
for (const item of tc.items) {
|
|
281
|
+
if (typeof item.str !== "string" || item.str.trim().length === 0) continue;
|
|
282
|
+
const [a2, b, c, d, e, f] = item.transform;
|
|
283
|
+
runs.push({
|
|
284
|
+
id: `p${page.pageNumber}-r${i++}`,
|
|
285
|
+
kind: "text",
|
|
286
|
+
page: page.pageNumber,
|
|
287
|
+
text: item.str,
|
|
288
|
+
x: e - x0,
|
|
289
|
+
baseline: f - y0,
|
|
290
|
+
width: item.width,
|
|
291
|
+
fontSize: Math.hypot(c, d),
|
|
292
|
+
angle: Math.atan2(b, a2),
|
|
293
|
+
font: fontInfoFor(page, item.fontName, fontCache)
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const lines = groupIntoLines(runs, page.pageNumber);
|
|
297
|
+
return {
|
|
298
|
+
page: page.pageNumber,
|
|
299
|
+
width: x1 - x0,
|
|
300
|
+
height: y1 - y0,
|
|
301
|
+
runs,
|
|
302
|
+
lines,
|
|
303
|
+
segments: mergeBlockSegments(lines),
|
|
304
|
+
images: extractImages(opList.fnArray, opList.argsArray, page.pageNumber, x0, y0),
|
|
305
|
+
widgets: extractWidgets(annots, page.pageNumber, x0, y0),
|
|
306
|
+
links: extractLinks(annots, page.pageNumber, x0, y0),
|
|
307
|
+
highlights: extractHighlights(annots, page.pageNumber, x0, y0)
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
function groupIntoLines(runs, page) {
|
|
311
|
+
const horizontal = runs.filter((r2) => Math.abs(r2.angle) < 0.01);
|
|
312
|
+
const rotated = runs.filter((r2) => Math.abs(r2.angle) >= 0.01);
|
|
313
|
+
const sorted = [...horizontal].sort((p, q) => q.baseline - p.baseline || p.x - q.x);
|
|
314
|
+
const groups = [];
|
|
315
|
+
for (const r2 of sorted) {
|
|
316
|
+
const current = groups[groups.length - 1];
|
|
317
|
+
const tol = Math.max(1, r2.fontSize * 0.35);
|
|
318
|
+
if (current && Math.abs(current[0].baseline - r2.baseline) <= tol) current.push(r2);
|
|
319
|
+
else groups.push([r2]);
|
|
320
|
+
}
|
|
321
|
+
for (const r2 of rotated) groups.push([r2]);
|
|
322
|
+
return groups.map((g, i) => lineFromRuns(g, page, i));
|
|
323
|
+
}
|
|
324
|
+
function bboxOf(runs) {
|
|
325
|
+
const x = runs[0].x;
|
|
326
|
+
const right = Math.max(...runs.map((r2) => r2.x + r2.width));
|
|
327
|
+
const baseline = runs[0].baseline;
|
|
328
|
+
const fontSize = Math.max(...runs.map((r2) => r2.fontSize));
|
|
329
|
+
const ascent = Math.max(...runs.map((r2) => r2.font.ascent * r2.fontSize));
|
|
330
|
+
const descent = Math.min(...runs.map((r2) => r2.font.descent * r2.fontSize));
|
|
331
|
+
return { x, baseline, width: right - x, y: baseline + descent, height: ascent - descent, fontSize };
|
|
332
|
+
}
|
|
333
|
+
function lineFromRuns(group, page, index) {
|
|
334
|
+
const runs = [...group].sort((a2, b) => a2.x - b.x);
|
|
335
|
+
void index;
|
|
336
|
+
const lineId = `p${page}-y${Math.round(runs[0].baseline)}`;
|
|
337
|
+
const segments = splitSegments(runs).map((segRuns) => ({
|
|
338
|
+
id: `${lineId}-x${Math.round(segRuns[0].x)}`,
|
|
339
|
+
kind: "segment",
|
|
340
|
+
page,
|
|
341
|
+
text: segmentText(segRuns),
|
|
342
|
+
runs: segRuns,
|
|
343
|
+
...bboxOf(segRuns)
|
|
344
|
+
}));
|
|
345
|
+
return {
|
|
346
|
+
id: lineId,
|
|
347
|
+
kind: "line",
|
|
348
|
+
page,
|
|
349
|
+
text: segments.map((s) => s.text).join(" "),
|
|
350
|
+
segments,
|
|
351
|
+
...bboxOf(runs)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
var OP_SAVE, OP_RESTORE, OP_TRANSFORM, OP_PAINT_IMAGE, OP_PAINT_INLINE_IMAGE, OP_PAINT_IMAGE_MASK, OP_PAINT_IMAGE_REPEAT, PAINT_OPS, mulMat;
|
|
355
|
+
var init_extractGraph = __esm({
|
|
356
|
+
"../core/src/extractGraph.ts"() {
|
|
357
|
+
init_tokens();
|
|
358
|
+
OP_SAVE = 10;
|
|
359
|
+
OP_RESTORE = 11;
|
|
360
|
+
OP_TRANSFORM = 12;
|
|
361
|
+
OP_PAINT_IMAGE = 85;
|
|
362
|
+
OP_PAINT_INLINE_IMAGE = 86;
|
|
363
|
+
OP_PAINT_IMAGE_MASK = 83;
|
|
364
|
+
OP_PAINT_IMAGE_REPEAT = 88;
|
|
365
|
+
PAINT_OPS = /* @__PURE__ */ new Set([OP_PAINT_IMAGE, OP_PAINT_INLINE_IMAGE, OP_PAINT_IMAGE_MASK, OP_PAINT_IMAGE_REPEAT]);
|
|
366
|
+
mulMat = (m, n) => [
|
|
367
|
+
m[0] * n[0] + m[1] * n[2],
|
|
368
|
+
m[0] * n[1] + m[1] * n[3],
|
|
369
|
+
m[2] * n[0] + m[3] * n[2],
|
|
370
|
+
m[2] * n[1] + m[3] * n[3],
|
|
371
|
+
m[4] * n[0] + m[5] * n[2] + n[4],
|
|
372
|
+
m[4] * n[1] + m[5] * n[3] + n[5]
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// ../core/src/edits.ts
|
|
378
|
+
function originalStyledRuns(seg) {
|
|
379
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
380
|
+
for (const r2 of seg.runs) {
|
|
381
|
+
const key = Math.round(r2.baseline * 10) / 10;
|
|
382
|
+
byLine.set(key, [...byLine.get(key) ?? [], r2]);
|
|
383
|
+
}
|
|
384
|
+
const lineKeys = [...byLine.keys()].sort((a2, b) => b - a2);
|
|
385
|
+
const out = [];
|
|
386
|
+
for (let li = 0; li < lineKeys.length; li++) {
|
|
387
|
+
const runs = [...byLine.get(lineKeys[li]) ?? []].sort((a2, b) => a2.x - b.x);
|
|
388
|
+
if (li > 0 && out.length) out[out.length - 1].text += "\n";
|
|
389
|
+
for (let i = 0; i < runs.length; i++) {
|
|
390
|
+
const r2 = runs[i];
|
|
391
|
+
let space = "";
|
|
392
|
+
if (i > 0) {
|
|
393
|
+
const prev = runs[i - 1];
|
|
394
|
+
const gap = r2.x - (prev.x + prev.width);
|
|
395
|
+
if (classifyGap(gap, prev, r2) === "space" && !out[out.length - 1]?.text.endsWith(" ") && !r2.text.startsWith(" ")) {
|
|
396
|
+
space = " ";
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const last = out[out.length - 1];
|
|
400
|
+
if (last && last.bold === r2.font.bold && last.italic === r2.font.italic && last.color === r2.color) {
|
|
401
|
+
last.text += space + r2.text;
|
|
402
|
+
} else {
|
|
403
|
+
if (last && space) last.text += space;
|
|
404
|
+
out.push({ text: r2.text, bold: r2.font.bold, italic: r2.font.italic, color: r2.color, dx: r2.x - seg.x });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return out;
|
|
409
|
+
}
|
|
410
|
+
function styledRunsEqual(a2, b) {
|
|
411
|
+
if (a2.length !== b.length) return false;
|
|
412
|
+
for (let i = 0; i < a2.length; i++) {
|
|
413
|
+
if (a2[i].text !== b[i].text || a2[i].bold !== b[i].bold || a2[i].italic !== b[i].italic || a2[i].color !== b[i].color || !!a2[i].underline !== !!b[i].underline) return false;
|
|
414
|
+
}
|
|
415
|
+
return true;
|
|
416
|
+
}
|
|
417
|
+
function applyTextDiff(runs, newText) {
|
|
418
|
+
const oldText = styledText(runs);
|
|
419
|
+
if (oldText === newText) return runs;
|
|
420
|
+
if (!runs.length) return newText ? [{ text: newText, bold: false, italic: false, dx: 0 }] : runs;
|
|
421
|
+
let p = 0;
|
|
422
|
+
while (p < oldText.length && p < newText.length && oldText[p] === newText[p]) p++;
|
|
423
|
+
let s = 0;
|
|
424
|
+
while (s < oldText.length - p && s < newText.length - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
|
|
425
|
+
const inserted = newText.slice(p, newText.length - s);
|
|
426
|
+
let styleSrc = runs[runs.length - 1];
|
|
427
|
+
let pos = 0;
|
|
428
|
+
for (const r2 of runs) {
|
|
429
|
+
if (p < pos + r2.text.length || p === pos + r2.text.length && r2 === runs[runs.length - 1]) {
|
|
430
|
+
styleSrc = r2;
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
pos += r2.text.length;
|
|
434
|
+
}
|
|
435
|
+
const out = [];
|
|
436
|
+
let cursor = 0;
|
|
437
|
+
const pushPiece = (piece) => {
|
|
438
|
+
const last = out[out.length - 1];
|
|
439
|
+
if (last && sameStyle(last, piece)) last.text += piece.text;
|
|
440
|
+
else out.push({ ...piece });
|
|
441
|
+
};
|
|
442
|
+
for (const r2 of runs) {
|
|
443
|
+
if (cursor >= p) break;
|
|
444
|
+
const take = Math.min(r2.text.length, p - cursor);
|
|
445
|
+
if (take > 0) pushPiece({ ...r2, text: r2.text.slice(0, take) });
|
|
446
|
+
cursor += r2.text.length;
|
|
447
|
+
}
|
|
448
|
+
const oldMid = oldText.slice(p, oldText.length - s);
|
|
449
|
+
if (inserted && oldMid && oldMid.length * inserted.length <= 25e4) {
|
|
450
|
+
const styleAt = [];
|
|
451
|
+
for (const r2 of runs) for (let i = 0; i < r2.text.length; i++) styleAt.push(r2);
|
|
452
|
+
let match;
|
|
453
|
+
if (oldMid.length === inserted.length) {
|
|
454
|
+
match = [...inserted].map((ch, i) => oldMid[i] === ch ? i : null);
|
|
455
|
+
} else {
|
|
456
|
+
const n = oldMid.length, m2 = inserted.length;
|
|
457
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m2 + 1));
|
|
458
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
459
|
+
for (let j = m2 - 1; j >= 0; j--) {
|
|
460
|
+
dp[i][j] = oldMid[i] === inserted[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
match = new Array(m2).fill(null);
|
|
464
|
+
for (let i = 0, j = 0; i < n && j < m2; ) {
|
|
465
|
+
if (oldMid[i] === inserted[j]) {
|
|
466
|
+
match[j] = i;
|
|
467
|
+
i++;
|
|
468
|
+
j++;
|
|
469
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) i++;
|
|
470
|
+
else j++;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
let inherit = styleSrc;
|
|
474
|
+
for (let j = 0; j < inserted.length; j++) {
|
|
475
|
+
const oi = match[j];
|
|
476
|
+
const st = oi != null ? styleAt[p + oi] ?? inherit : inherit;
|
|
477
|
+
if (oi != null) inherit = st;
|
|
478
|
+
pushPiece({ ...st, text: inserted[j] });
|
|
479
|
+
}
|
|
480
|
+
} else if (inserted) {
|
|
481
|
+
pushPiece({ ...styleSrc, text: inserted });
|
|
482
|
+
}
|
|
483
|
+
const sufStart = oldText.length - s;
|
|
484
|
+
cursor = 0;
|
|
485
|
+
for (const r2 of runs) {
|
|
486
|
+
const end = cursor + r2.text.length;
|
|
487
|
+
if (end > sufStart) {
|
|
488
|
+
const from = Math.max(0, sufStart - cursor);
|
|
489
|
+
pushPiece({ ...r2, text: r2.text.slice(from) });
|
|
490
|
+
}
|
|
491
|
+
cursor = end;
|
|
492
|
+
}
|
|
493
|
+
return out.filter((r2) => r2.text.length > 0);
|
|
494
|
+
}
|
|
495
|
+
function segmentOriginal(seg) {
|
|
496
|
+
const dom = seg.runs.reduce((a2, b) => b.width > a2.width ? b : a2);
|
|
497
|
+
const baselines = [...new Set(seg.runs.map((r2) => Math.round(r2.baseline * 10) / 10))].sort((a2, b) => b - a2);
|
|
498
|
+
return {
|
|
499
|
+
text: seg.text,
|
|
500
|
+
x: seg.x,
|
|
501
|
+
baseline: seg.baseline,
|
|
502
|
+
width: seg.width,
|
|
503
|
+
fontSize: seg.fontSize,
|
|
504
|
+
bucket: dom.font.bucket,
|
|
505
|
+
bold: dom.font.bold,
|
|
506
|
+
italic: dom.font.italic,
|
|
507
|
+
runs: [...seg.runs].sort((a2, b) => a2.x - b.x).map((r2) => ({ x: r2.x, bold: r2.font.bold, italic: r2.font.italic })),
|
|
508
|
+
...baselines.length > 1 ? { baselines } : {}
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function mergeSegmentEdit(seg, prev, patch) {
|
|
512
|
+
const next = prev ? { ...prev } : {
|
|
513
|
+
segmentId: seg.id,
|
|
514
|
+
page: seg.page,
|
|
515
|
+
text: seg.text,
|
|
516
|
+
original: segmentOriginal(seg)
|
|
517
|
+
};
|
|
518
|
+
if (patch.runs !== void 0) {
|
|
519
|
+
if (patch.runs === null) delete next.runs;
|
|
520
|
+
else next.runs = patch.runs;
|
|
521
|
+
}
|
|
522
|
+
if (patch.text !== void 0) next.text = patch.text;
|
|
523
|
+
if (next.runs) {
|
|
524
|
+
if (styledRunsEqual(next.runs, originalStyledRuns(seg))) delete next.runs;
|
|
525
|
+
else next.text = styledText(next.runs);
|
|
526
|
+
}
|
|
527
|
+
for (const key of OVERRIDE_KEYS) {
|
|
528
|
+
const value = patch[key];
|
|
529
|
+
if (value === void 0) continue;
|
|
530
|
+
if (value === null) delete next[key];
|
|
531
|
+
else next[key] = value;
|
|
532
|
+
}
|
|
533
|
+
const noop = next.text === seg.text && next.runs === void 0 && OVERRIDE_KEYS.every((k) => next[k] === void 0);
|
|
534
|
+
return noop ? null : next;
|
|
535
|
+
}
|
|
536
|
+
function mergeImageEdit(img, prev, patch) {
|
|
537
|
+
const next = prev ? { ...prev } : {
|
|
538
|
+
imageId: img.id,
|
|
539
|
+
page: img.page,
|
|
540
|
+
original: { x: img.x, y: img.y, width: img.width, height: img.height }
|
|
541
|
+
};
|
|
542
|
+
for (const key of IMAGE_KEYS) {
|
|
543
|
+
const value = patch[key];
|
|
544
|
+
if (value === void 0) continue;
|
|
545
|
+
if (value === null) delete next[key];
|
|
546
|
+
else next[key] = value;
|
|
547
|
+
}
|
|
548
|
+
return IMAGE_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
549
|
+
}
|
|
550
|
+
function promoteMovedImages(edits) {
|
|
551
|
+
return edits.map((e) => !e.remove && !e.zOrder && (e.x != null || e.y != null || e.width != null || e.height != null) ? { ...e, zOrder: "front" } : e);
|
|
552
|
+
}
|
|
553
|
+
function mergeWidgetEdit(w, prev, patch) {
|
|
554
|
+
const next = prev ? { ...prev } : {
|
|
555
|
+
widgetId: w.id,
|
|
556
|
+
page: w.page,
|
|
557
|
+
original: { fieldName: w.fieldName, x: w.x, y: w.y, width: w.width, height: w.height }
|
|
558
|
+
};
|
|
559
|
+
for (const key of WIDGET_KEYS) {
|
|
560
|
+
const value = patch[key];
|
|
561
|
+
if (value === void 0) continue;
|
|
562
|
+
if (value === null) delete next[key];
|
|
563
|
+
else next[key] = value;
|
|
564
|
+
}
|
|
565
|
+
return WIDGET_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
566
|
+
}
|
|
567
|
+
function mergeHighlightEdit(h, prev, patch) {
|
|
568
|
+
const next = prev ? { ...prev } : {
|
|
569
|
+
highlightId: h.id,
|
|
570
|
+
page: h.page,
|
|
571
|
+
original: { x: h.x, y: h.y, width: h.width, height: h.height, color: h.color }
|
|
572
|
+
};
|
|
573
|
+
for (const key of HIGHLIGHT_KEYS) {
|
|
574
|
+
const value = patch[key];
|
|
575
|
+
if (value === void 0) continue;
|
|
576
|
+
if (value === null) delete next[key];
|
|
577
|
+
else next[key] = value;
|
|
578
|
+
}
|
|
579
|
+
return HIGHLIGHT_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
580
|
+
}
|
|
581
|
+
function mergeLinkEdit(l, prev, patch) {
|
|
582
|
+
const next = prev ? { ...prev } : {
|
|
583
|
+
linkId: l.id,
|
|
584
|
+
page: l.page,
|
|
585
|
+
original: { url: l.url, x: l.x, y: l.y, width: l.width, height: l.height }
|
|
586
|
+
};
|
|
587
|
+
for (const key of LINK_KEYS) {
|
|
588
|
+
const value = patch[key];
|
|
589
|
+
if (value === void 0) continue;
|
|
590
|
+
if (value === null) delete next[key];
|
|
591
|
+
else next[key] = value;
|
|
592
|
+
}
|
|
593
|
+
return LINK_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
594
|
+
}
|
|
595
|
+
function effectiveGeometry(seg, edit) {
|
|
596
|
+
const fontSize = edit?.fontSize ?? seg.fontSize;
|
|
597
|
+
const ratio = fontSize / seg.fontSize;
|
|
598
|
+
const x = edit?.x ?? seg.x;
|
|
599
|
+
const baseline = edit?.baseline ?? seg.baseline;
|
|
600
|
+
return {
|
|
601
|
+
x,
|
|
602
|
+
baseline,
|
|
603
|
+
fontSize,
|
|
604
|
+
y: baseline + (seg.y - seg.baseline) * ratio,
|
|
605
|
+
height: seg.height * ratio,
|
|
606
|
+
width: seg.width,
|
|
607
|
+
moved: edit?.x !== void 0 || edit?.baseline !== void 0
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
var OVERRIDE_KEYS, styledText, sameStyle, IMAGE_KEYS, WIDGET_KEYS, HIGHLIGHT_KEYS, LINK_KEYS;
|
|
611
|
+
var init_edits = __esm({
|
|
612
|
+
"../core/src/edits.ts"() {
|
|
613
|
+
init_tokens();
|
|
614
|
+
OVERRIDE_KEYS = ["fontSize", "font", "x", "baseline", "remove", "charSpacing", "hScale", "color", "align"];
|
|
615
|
+
styledText = (runs) => runs.map((r2) => r2.text).join("");
|
|
616
|
+
sameStyle = (a2, b) => a2.bold === b.bold && a2.italic === b.italic && a2.color === b.color && !!a2.underline === !!b.underline;
|
|
617
|
+
IMAGE_KEYS = ["x", "y", "width", "height", "remove", "zOrder"];
|
|
618
|
+
WIDGET_KEYS = ["x", "y", "width", "height", "remove"];
|
|
619
|
+
HIGHLIGHT_KEYS = ["x", "y", "width", "height", "color", "remove"];
|
|
620
|
+
LINK_KEYS = ["x", "y", "width", "height", "remove"];
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
// ../core/src/coords.ts
|
|
625
|
+
var init_coords = __esm({
|
|
626
|
+
"../core/src/coords.ts"() {
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
// ../core/src/log.ts
|
|
631
|
+
var enabled, createLogger;
|
|
632
|
+
var init_log = __esm({
|
|
633
|
+
"../core/src/log.ts"() {
|
|
634
|
+
enabled = () => {
|
|
635
|
+
try {
|
|
636
|
+
if (typeof process !== "undefined" && process.env?.ALDUS_DEBUG) return true;
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
if (typeof localStorage !== "undefined" && localStorage.getItem("aldusDebug")) return true;
|
|
641
|
+
} catch {
|
|
642
|
+
}
|
|
643
|
+
return false;
|
|
644
|
+
};
|
|
645
|
+
createLogger = (namespace) => (...args) => {
|
|
646
|
+
if (enabled()) console.log(`[${namespace}]`, ...args);
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
// ../core/src/index.ts
|
|
652
|
+
var init_src = __esm({
|
|
653
|
+
"../core/src/index.ts"() {
|
|
654
|
+
init_model();
|
|
655
|
+
init_extractGraph();
|
|
656
|
+
init_tokens();
|
|
657
|
+
init_edits();
|
|
658
|
+
init_coords();
|
|
659
|
+
init_log();
|
|
660
|
+
}
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
// ../agent/src/graph.ts
|
|
664
|
+
import { readFile } from "node:fs/promises";
|
|
665
|
+
async function graphFromBytes(bytes, path = "(memoria)") {
|
|
666
|
+
const { getDocument } = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
667
|
+
const doc = await getDocument({ data: bytes.slice(), verbosity: 0 }).promise;
|
|
668
|
+
const pages = [];
|
|
669
|
+
for (let n = 1; n <= doc.numPages; n++) {
|
|
670
|
+
const page = await doc.getPage(n);
|
|
671
|
+
pages.push(await extractPageGraph(page));
|
|
672
|
+
}
|
|
673
|
+
await doc.destroy();
|
|
674
|
+
return { path, bytes, pages };
|
|
675
|
+
}
|
|
676
|
+
async function loadDoc(path) {
|
|
677
|
+
return graphFromBytes(new Uint8Array(await readFile(path)), path);
|
|
678
|
+
}
|
|
679
|
+
var init_graph = __esm({
|
|
680
|
+
"../agent/src/graph.ts"() {
|
|
681
|
+
init_src();
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
// ../agent/src/serialize.ts
|
|
686
|
+
function styleOf(s) {
|
|
687
|
+
const run = s.runs.reduce((a2, b) => b.fontSize > a2.fontSize ? b : a2, s.runs[0]);
|
|
688
|
+
if (!run) return "";
|
|
689
|
+
const bits = [];
|
|
690
|
+
if (run.font.bold) bits.push("bold");
|
|
691
|
+
if (run.font.italic) bits.push("italic");
|
|
692
|
+
bits.push(run.font.postScriptName || run.font.bucket);
|
|
693
|
+
if (run.color) bits.push(run.color);
|
|
694
|
+
return bits.join(" ");
|
|
695
|
+
}
|
|
696
|
+
function nearestLabel(w, segs) {
|
|
697
|
+
const cy = w.y + w.height / 2;
|
|
698
|
+
let best = null;
|
|
699
|
+
for (const s of segs) {
|
|
700
|
+
const sy = s.baseline;
|
|
701
|
+
const dyLine = Math.abs(sy - cy);
|
|
702
|
+
const leftGap = w.x - (s.x + s.width);
|
|
703
|
+
const aboveGap = sy - (w.y + w.height);
|
|
704
|
+
let score;
|
|
705
|
+
const belowGap = w.y - sy;
|
|
706
|
+
const hOverlap = s.x < w.x + w.width && s.x + s.width > w.x - 40;
|
|
707
|
+
if (dyLine <= w.height + 6 && leftGap >= -2 && leftGap < 220) score = leftGap;
|
|
708
|
+
else if (aboveGap >= -2 && aboveGap < 26 && hOverlap) score = 300 + aboveGap;
|
|
709
|
+
else if (belowGap >= -2 && belowGap < 20 && hOverlap) score = 600 + belowGap;
|
|
710
|
+
else continue;
|
|
711
|
+
if (!best || score < best.score) best = { s, score };
|
|
712
|
+
}
|
|
713
|
+
if (!best) return void 0;
|
|
714
|
+
const t = best.s.text.replace(/\s+/g, " ").trim();
|
|
715
|
+
return t.length > 40 ? t.slice(0, 40) + "\u2026" : t;
|
|
716
|
+
}
|
|
717
|
+
function readingView(p) {
|
|
718
|
+
const items = [];
|
|
719
|
+
const placed = /* @__PURE__ */ new Set();
|
|
720
|
+
for (const s of p.segments) {
|
|
721
|
+
const inline = p.widgets.filter((w) => Math.abs(w.y + w.height / 2 - (s.baseline + 4)) < Math.max(12, w.height) && w.x + w.width > s.x && w.x < s.x + s.width).sort((a2, b) => a2.x - b.x);
|
|
722
|
+
const runs = [];
|
|
723
|
+
for (const m of s.text.matchAll(/_{2,}/g)) {
|
|
724
|
+
const start = m.index;
|
|
725
|
+
const end = start + m[0].length;
|
|
726
|
+
runs.push({
|
|
727
|
+
start,
|
|
728
|
+
end,
|
|
729
|
+
x0: s.x + s.width * start / s.text.length,
|
|
730
|
+
x1: s.x + s.width * end / s.text.length,
|
|
731
|
+
ids: []
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
for (const w of inline) {
|
|
735
|
+
if (!runs.length) break;
|
|
736
|
+
const cx = w.x + w.width / 2;
|
|
737
|
+
const best = runs.reduce((a2, b) => Math.abs(cx - (a2.x0 + a2.x1) / 2) <= Math.abs(cx - (b.x0 + b.x1) / 2) ? a2 : b);
|
|
738
|
+
if (cx > best.x0 - 35 && cx < best.x1 + 35) {
|
|
739
|
+
best.ids.push(w.id);
|
|
740
|
+
placed.add(w.id);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
let text = "";
|
|
744
|
+
let cursor = 0;
|
|
745
|
+
for (const run of runs) {
|
|
746
|
+
text += s.text.slice(cursor, run.start);
|
|
747
|
+
text += run.ids.length ? run.ids.map((id) => `[[${id}]]`).join(" ") : " ";
|
|
748
|
+
cursor = run.end;
|
|
749
|
+
}
|
|
750
|
+
text += s.text.slice(cursor);
|
|
751
|
+
if (text.trim()) items.push({ x: s.x, y: s.baseline, text });
|
|
752
|
+
}
|
|
753
|
+
for (const w of p.widgets) {
|
|
754
|
+
if (!placed.has(w.id)) items.push({ x: w.x, y: w.y + w.height / 2 - 4, text: `[[${w.id}]]` });
|
|
755
|
+
}
|
|
756
|
+
items.sort((a2, b) => b.y - a2.y || a2.x - b.x);
|
|
757
|
+
const lines = [];
|
|
758
|
+
for (const it of items) {
|
|
759
|
+
const line = lines[lines.length - 1];
|
|
760
|
+
if (line && Math.abs(line[0].y - it.y) <= 8) line.push(it);
|
|
761
|
+
else lines.push([it]);
|
|
762
|
+
}
|
|
763
|
+
const out = [];
|
|
764
|
+
for (const line of lines) {
|
|
765
|
+
const txt = line.sort((a2, b) => a2.x - b.x).map((i) => i.text).join(" ").replace(/\s+/g, " ").trim();
|
|
766
|
+
if (txt) out.push(txt);
|
|
767
|
+
}
|
|
768
|
+
return out;
|
|
769
|
+
}
|
|
770
|
+
function serializeDoc(doc, pages) {
|
|
771
|
+
const out = [];
|
|
772
|
+
const want = pages == null ? null : new Set(Array.isArray(pages) ? pages : [pages]);
|
|
773
|
+
const list = want ? doc.pages.filter((p) => want.has(p.page)) : doc.pages;
|
|
774
|
+
for (const p of list) {
|
|
775
|
+
out.push(`## P\xE1gina ${p.page} \u2014 ${r(p.width)}\xD7${r(p.height)} pt`);
|
|
776
|
+
if (p.widgets.length) {
|
|
777
|
+
out.push("### Lectura (texto con los campos [[id]] intercalados donde caen \u2014 as\xED se entiende qu\xE9 va en cada uno)");
|
|
778
|
+
out.push(...readingView(p));
|
|
779
|
+
}
|
|
780
|
+
if (p.segments.length) {
|
|
781
|
+
out.push('### Texto (id @(x,baseline) ancho\xD7alto tama\xF1o estilo: "contenido")');
|
|
782
|
+
out.push(' (si un nodo tiene varios tramos, "tramos:" da la x y el ancho EXACTOS de cada uno \u2014 para geometr\xEDa fina, p. ej. ubicar un placeholder dentro del nodo)');
|
|
783
|
+
const segs = [...p.segments].sort((a2, b) => b.baseline - a2.baseline || a2.x - b.x);
|
|
784
|
+
for (const s of segs) {
|
|
785
|
+
const t = s.text.replace(/\n/g, "\\n");
|
|
786
|
+
out.push(`- ${s.id} @(${r(s.x)},${r(s.baseline)}) ${r(s.width)}\xD7${r(s.height)} ${r(s.fontSize)}pt ${styleOf(s)}: ${JSON.stringify(t)}`);
|
|
787
|
+
if (s.runs.length > 1) {
|
|
788
|
+
const tr = s.runs.map((run) => `@${r(run.x)} w${r(run.width)}${run.font.bold ? " bold" : ""}${run.font.italic ? " italic" : ""} ${JSON.stringify(run.text)}`).join(" | ");
|
|
789
|
+
out.push(` tramos: ${tr}`);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
if (p.images.length) {
|
|
794
|
+
out.push("### Im\xE1genes (id @(x,y) ancho\xD7alto)");
|
|
795
|
+
for (const im of p.images) {
|
|
796
|
+
out.push(`- ${im.id} @(${r(im.x)},${r(im.y)}) ${r(im.width)}\xD7${r(im.height)}`);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
if (p.widgets.length) {
|
|
800
|
+
out.push('### Campos de formulario (nombre tipo [valor] near "label" @(x,y) ancho\xD7alto)');
|
|
801
|
+
out.push(' (los nombres suelen ser opacos \u2192 us\xE1 "near" (el texto pegado al campo) para saber QU\xC9 va en cada uno)');
|
|
802
|
+
for (const w of p.widgets) {
|
|
803
|
+
const val = w.value != null ? ` = ${JSON.stringify(Array.isArray(w.value) ? w.value.join(", ") : w.value)}` : " (vac\xEDo)";
|
|
804
|
+
const opts = w.options?.length ? ` opciones:[${w.options.join(", ")}]` : "";
|
|
805
|
+
const label = nearestLabel(w, p.segments);
|
|
806
|
+
const near = label ? ` near ${JSON.stringify(label)}` : "";
|
|
807
|
+
out.push(`- ${w.id} ${JSON.stringify(w.fieldName)} ${w.widgetType}${val}${opts}${near} @(${r(w.x)},${r(w.y)}) ${r(w.width)}\xD7${r(w.height)}${w.readOnly ? " read-only" : ""}`);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
if (p.highlights.length) {
|
|
811
|
+
out.push("### Resaltados (id color @(x,y) ancho\xD7alto)");
|
|
812
|
+
for (const h of p.highlights) out.push(`- ${h.id} ${h.color} @(${r(h.x)},${r(h.y)}) ${r(h.width)}\xD7${r(h.height)}`);
|
|
813
|
+
}
|
|
814
|
+
if (p.links.length) {
|
|
815
|
+
out.push("### Links (id \u2192 url @(x,y) ancho\xD7alto)");
|
|
816
|
+
for (const l of p.links) out.push(`- ${l.id} \u2192 ${JSON.stringify(l.url)} @(${r(l.x)},${r(l.y)}) ${r(l.width)}\xD7${r(l.height)}`);
|
|
817
|
+
}
|
|
818
|
+
out.push("");
|
|
819
|
+
}
|
|
820
|
+
return out.join("\n").trimEnd();
|
|
821
|
+
}
|
|
822
|
+
var r;
|
|
823
|
+
var init_serialize = __esm({
|
|
824
|
+
"../agent/src/serialize.ts"() {
|
|
825
|
+
r = (n) => Math.round(n);
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// ../agent/src/tools.ts
|
|
830
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
831
|
+
import { z } from "zod";
|
|
832
|
+
function buildToolServer(session) {
|
|
833
|
+
const ok = (t) => ({ content: [{ type: "text", text: t }] });
|
|
834
|
+
const tools = TOOL_DEFS.map(
|
|
835
|
+
(d) => tool(d.name, d.description, d.shape, async (args) => ok(await d.run(session, args)))
|
|
836
|
+
);
|
|
837
|
+
return createSdkMcpServer({ name: "aldus", version: "0.0.1", tools });
|
|
838
|
+
}
|
|
839
|
+
function buildRouterServer(onRoute) {
|
|
840
|
+
return createSdkMcpServer({
|
|
841
|
+
name: "aldus",
|
|
842
|
+
version: "0.0.1",
|
|
843
|
+
tools: [
|
|
844
|
+
tool("edit_document", ROUTE_DESC, ROUTE_SHAPE, async (args) => {
|
|
845
|
+
onRoute({ pages: args.pages ?? [], request: String(args.request ?? "") });
|
|
846
|
+
return { content: [{ type: "text", text: "\u2713 delegado al editor \u2014 las ediciones corren a continuaci\xF3n; no repitas la llamada." }] };
|
|
847
|
+
})
|
|
848
|
+
]
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
function openaiTools() {
|
|
852
|
+
return TOOL_DEFS.map((d) => ({
|
|
853
|
+
type: "function",
|
|
854
|
+
function: { name: d.name, description: d.description, parameters: z.toJSONSchema(z.object(d.shape)) }
|
|
855
|
+
}));
|
|
856
|
+
}
|
|
857
|
+
function openaiRouterTool() {
|
|
858
|
+
return { type: "function", function: { name: "edit_document", description: ROUTE_DESC, parameters: z.toJSONSchema(z.object(ROUTE_SHAPE)) } };
|
|
859
|
+
}
|
|
860
|
+
async function runTool(session, name, args) {
|
|
861
|
+
const d = TOOL_DEFS.find((x) => x.name === name);
|
|
862
|
+
if (!d) return `\u26A0\uFE0F tool desconocida: ${name}`;
|
|
863
|
+
try {
|
|
864
|
+
return await d.run(session, args);
|
|
865
|
+
} catch (err) {
|
|
866
|
+
return `\u26A0\uFE0F ${name}: ${err instanceof Error ? err.message : "error"}`;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
var FIELD_TYPES, a, TOOL_DEFS, TOOL_NAMES, ROUTE_SHAPE, ROUTE_DESC;
|
|
870
|
+
var init_tools = __esm({
|
|
871
|
+
"../agent/src/tools.ts"() {
|
|
872
|
+
FIELD_TYPES = ["text", "checkbox", "radio", "select", "list", "button", "signature"];
|
|
873
|
+
a = (o) => o;
|
|
874
|
+
TOOL_DEFS = [
|
|
875
|
+
// ── texto existente ──
|
|
876
|
+
{
|
|
877
|
+
name: "edit_text",
|
|
878
|
+
description: "Reemplaza el CONTENIDO de un nodo de texto (conserva su estilo). Si el texto nuevo es M\xC1S LARGO que el rengl\xF3n, el p\xE1rrafo se reconstruye solo (reflow) \u2014 no calcules nada, escrib\xED el texto final.",
|
|
879
|
+
shape: { id: z.string().describe("id del nodo de texto, p. ej. p1-y708-x72"), text: z.string().describe("texto nuevo") },
|
|
880
|
+
run: (s, { id, text }) => s.editText(id, text)
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
name: "set_text_style",
|
|
884
|
+
description: "Pone o saca NEGRITA/IT\xC1LICA a un nodo de texto entero. Si el PDF no trae embebida esa variante de la fuente, se usa la est\xE1ndar equivalente (se reporta).",
|
|
885
|
+
shape: { id: z.string(), bold: z.boolean().optional(), italic: z.boolean().optional() },
|
|
886
|
+
run: (s, { id, bold, italic }) => s.styleText(id, { bold, italic })
|
|
887
|
+
},
|
|
888
|
+
{
|
|
889
|
+
name: "move_text",
|
|
890
|
+
description: "Mueve un nodo de texto. Coordenadas en puntos PDF: x\u2192derecha, y (baseline)\u2192arriba, origen abajo-izquierda. Omit\xED la coordenada que no cambia.",
|
|
891
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional() },
|
|
892
|
+
run: (s, { id, x, y }) => s.moveText(id, x, y)
|
|
893
|
+
},
|
|
894
|
+
{
|
|
895
|
+
name: "set_text_color",
|
|
896
|
+
description: "Cambia el color de un nodo de texto (hex #rrggbb).",
|
|
897
|
+
shape: { id: z.string(), color: z.string().describe("#rrggbb") },
|
|
898
|
+
run: (s, { id, color }) => s.colorText(id, color)
|
|
899
|
+
},
|
|
900
|
+
{
|
|
901
|
+
name: "set_text_size",
|
|
902
|
+
description: "Cambia el tama\xF1o de fuente de un nodo de texto (puntos).",
|
|
903
|
+
shape: { id: z.string(), size: z.number().positive() },
|
|
904
|
+
run: (s, { id, size }) => s.resizeText(id, size)
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
name: "delete_text",
|
|
908
|
+
description: "Elimina un nodo de texto del documento.",
|
|
909
|
+
shape: { id: z.string() },
|
|
910
|
+
run: (s, { id }) => s.deleteText(id)
|
|
911
|
+
},
|
|
912
|
+
// ── imagen existente ──
|
|
913
|
+
{
|
|
914
|
+
name: "move_image",
|
|
915
|
+
description: "Mueve y/o escala una imagen. Coordenadas/tama\xF1o en puntos PDF (origen abajo-izquierda). Omit\xED lo que no cambia.",
|
|
916
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional(), width: z.number().positive().optional(), height: z.number().positive().optional() },
|
|
917
|
+
run: (s, { id, x, y, width, height }) => s.moveImage(id, a({ x, y, width, height }))
|
|
918
|
+
},
|
|
919
|
+
{
|
|
920
|
+
name: "delete_image",
|
|
921
|
+
description: "Elimina una imagen del documento.",
|
|
922
|
+
shape: { id: z.string() },
|
|
923
|
+
run: (s, { id }) => s.deleteImage(id)
|
|
924
|
+
},
|
|
925
|
+
// ── resaltados (/Annots) ──
|
|
926
|
+
{
|
|
927
|
+
name: "highlight_text",
|
|
928
|
+
description: "Resalta (marcador) un nodo de texto EXISTENTE por su id. El resaltado cubre el texto y lo sigue si lo mov\xE9s. Color hex opcional (default amarillo).",
|
|
929
|
+
shape: { id: z.string().describe("id del nodo de texto a resaltar"), color: z.string().optional().describe("#rrggbb") },
|
|
930
|
+
run: (s, { id, color }) => s.highlightText(id, color)
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: "set_highlight_color",
|
|
934
|
+
description: "Cambia el color de un resaltado YA EXISTENTE (id de highlight del grafo, p. ej. p1-hl0). Hex #rrggbb.",
|
|
935
|
+
shape: { id: z.string(), color: z.string().describe("#rrggbb") },
|
|
936
|
+
run: (s, { id, color }) => s.recolorHighlight(id, color)
|
|
937
|
+
},
|
|
938
|
+
{
|
|
939
|
+
name: "delete_highlight",
|
|
940
|
+
description: "Elimina un resaltado existente (id de highlight del grafo).",
|
|
941
|
+
shape: { id: z.string() },
|
|
942
|
+
run: (s, { id }) => s.deleteHighlight(id)
|
|
943
|
+
},
|
|
944
|
+
// ── links (/Annots) ──
|
|
945
|
+
{
|
|
946
|
+
name: "add_link",
|
|
947
|
+
description: "Pone un LINK clickeable sobre un nodo de texto EXISTENTE (por su id) hacia una URL.",
|
|
948
|
+
shape: { id: z.string().describe("id del nodo de texto"), url: z.string().describe("URL destino") },
|
|
949
|
+
run: (s, { id, url }) => s.linkText(id, url)
|
|
950
|
+
},
|
|
951
|
+
{
|
|
952
|
+
name: "delete_link",
|
|
953
|
+
description: "Elimina un link existente (id de link del grafo, p. ej. p1-link0).",
|
|
954
|
+
shape: { id: z.string() },
|
|
955
|
+
run: (s, { id }) => s.deleteLink(id)
|
|
956
|
+
},
|
|
957
|
+
// ── creación de contenido nuevo ──
|
|
958
|
+
{
|
|
959
|
+
name: "add_text",
|
|
960
|
+
description: "Agrega un p\xE1rrafo de texto NUEVO. (x,y) = esquina superior-izquierda en puntos PDF (origen abajo-izq). Si la posici\xF3n pisa texto existente, BAJA sola hasta un hueco libre (se reporta). Se re-extrae como un nodo editable.",
|
|
961
|
+
shape: {
|
|
962
|
+
page: z.number().int().min(1),
|
|
963
|
+
x: z.number(),
|
|
964
|
+
y: z.number(),
|
|
965
|
+
text: z.string(),
|
|
966
|
+
size: z.number().positive().optional(),
|
|
967
|
+
bold: z.boolean().optional(),
|
|
968
|
+
italic: z.boolean().optional(),
|
|
969
|
+
color: z.string().optional().describe("#rrggbb")
|
|
970
|
+
},
|
|
971
|
+
run: (s, { page, x, y, text, size, bold, italic, color }) => s.addTextNode(a({ page, x, y, text, size, bold, italic, color }))
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "insert_image",
|
|
975
|
+
description: "Inserta una imagen (PNG/JPEG) desde una RUTA de archivo local. (x,y) = esquina superior-izquierda en puntos PDF.",
|
|
976
|
+
shape: { page: z.number().int().min(1), x: z.number(), y: z.number(), path: z.string().describe("ruta a un .png/.jpg local"), maxWidth: z.number().positive().optional() },
|
|
977
|
+
run: (s, { page, x, y, path, maxWidth }) => s.insertImageFile(page, x, y, path, maxWidth)
|
|
978
|
+
},
|
|
979
|
+
{
|
|
980
|
+
name: "add_watermark",
|
|
981
|
+
description: "Marca de agua de texto diagonal en TODAS las p\xE1ginas.",
|
|
982
|
+
shape: { text: z.string(), color: z.string().optional().describe("#rrggbb"), opacity: z.number().min(0).max(1).optional() },
|
|
983
|
+
run: (s, { text, color, opacity }) => s.watermark(text, color, opacity)
|
|
984
|
+
},
|
|
985
|
+
{
|
|
986
|
+
name: "add_header_footer",
|
|
987
|
+
description: "Agrega encabezado y/o pie de p\xE1gina (texto) y opcionalmente n\xFAmeros de p\xE1gina, en todas las p\xE1ginas.",
|
|
988
|
+
shape: { header: z.string().optional(), footer: z.string().optional(), pageNumbers: z.boolean().optional() },
|
|
989
|
+
run: (s, { header, footer, pageNumbers }) => s.headerFooter(a({ header, footer, pageNumbers }))
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: "add_form_field",
|
|
993
|
+
description: "Crea un campo de formulario nuevo (text/checkbox/radio/select/list/button/signature). (x,y) = esquina inferior-izquierda en puntos PDF.",
|
|
994
|
+
shape: {
|
|
995
|
+
type: z.enum(FIELD_TYPES),
|
|
996
|
+
page: z.number().int().min(1),
|
|
997
|
+
x: z.number(),
|
|
998
|
+
y: z.number(),
|
|
999
|
+
width: z.number().positive().optional(),
|
|
1000
|
+
height: z.number().positive().optional(),
|
|
1001
|
+
name: z.string().optional()
|
|
1002
|
+
},
|
|
1003
|
+
run: (s, { type, page, x, y, width, height, name }) => s.addField(type, page, x, y, width, height, name)
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: "placeholders_to_fields",
|
|
1007
|
+
description: "Convierte placeholders (XXXX/xxx/***/____ que VOS detect\xE1s) en campos de formulario. Reconstruye el P\xC1RRAFO ENTERO de `id`: llamala UNA SOLA VEZ por p\xE1rrafo con TODOS sus placeholders (de todas las l\xEDneas) en fields[], en orden de lectura \u2014 NO una vez por l\xEDnea (rehace el reflow y se pisan). El c\xF3digo calcula la geometr\xEDa exacta y es imposible que pise texto. NO pases coordenadas; nunca edit_text+add_form_field a mano para esto.",
|
|
1008
|
+
shape: {
|
|
1009
|
+
id: z.string().describe("id del nodo de texto con los placeholders"),
|
|
1010
|
+
fields: z.array(z.object({
|
|
1011
|
+
placeholder: z.string().describe('el substring EXACTO del placeholder tal como aparece en el nodo, p. ej. "XXXXXXXXX" o "xxxxxxx"'),
|
|
1012
|
+
name: z.string().describe('nombre descriptivo del campo (snake_case), p. ej. "razon_social"'),
|
|
1013
|
+
width: z.number().positive().optional().describe("ancho \xDATIL deseado en puntos seg\xFAn lo que se va a escribir (nombre/direcci\xF3n ~110, DNI/RUC ~55). Opcional: si lo omit\xEDs se estima por el nombre. El campo se ensancha hasta ah\xED empujando el texto, con tope en el borde de p\xE1gina.")
|
|
1014
|
+
})).min(1).describe("un item por hueco, EN ORDEN de aparici\xF3n")
|
|
1015
|
+
},
|
|
1016
|
+
run: (s, { id, fields }) => s.placeholdersToFields(id, fields)
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: "fill_field",
|
|
1020
|
+
description: 'COMPLETA un campo de formulario por su fieldName O por su id de widget (el [[id]] de la Lectura, p. ej. p1-w3): texto para text/select/radio; para checkbox pas\xE1 "true"/"false". Pod\xE9s llamarla varias veces para completar todo el form.',
|
|
1021
|
+
shape: { name: z.string().describe("fieldName o id de widget (p1-w3)"), value: z.string().describe('valor (para checkbox: "true"/"false")') },
|
|
1022
|
+
run: (s, { name, value }) => s.fillField(name, value === "true" ? true : value === "false" ? false : value)
|
|
1023
|
+
},
|
|
1024
|
+
{
|
|
1025
|
+
name: "fill_fields",
|
|
1026
|
+
description: 'Completa VARIOS campos DE UNA SOLA VEZ (prefer\xED esta sobre llamar fill_field N veces \u2014 es mucho m\xE1s r\xE1pido). Pas\xE1 una lista {name, value}; name = fieldName o id de widget ([[p1-w3]]); para checkbox value = "true"/"false".',
|
|
1027
|
+
shape: { fields: z.array(z.object({ name: z.string(), value: z.string() })).describe("lista de campos a completar") },
|
|
1028
|
+
run: (s, { fields }) => s.fillFields(
|
|
1029
|
+
fields.map((f) => ({ name: f.name, value: f.value === "true" ? true : f.value === "false" ? false : f.value }))
|
|
1030
|
+
)
|
|
1031
|
+
},
|
|
1032
|
+
{
|
|
1033
|
+
name: "move_field",
|
|
1034
|
+
description: "Mueve un campo de formulario EXISTENTE (id de widget del grafo). Coordenadas en puntos PDF; omit\xED lo que no cambia.",
|
|
1035
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional() },
|
|
1036
|
+
run: (s, { id, x, y }) => s.moveField(id, x, y)
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
name: "delete_field",
|
|
1040
|
+
description: "Elimina un campo de formulario existente (id de widget del grafo).",
|
|
1041
|
+
shape: { id: z.string() },
|
|
1042
|
+
run: (s, { id }) => s.deleteField(id)
|
|
1043
|
+
}
|
|
1044
|
+
];
|
|
1045
|
+
TOOL_NAMES = TOOL_DEFS.map((d) => `mcp__aldus__${d.name}`);
|
|
1046
|
+
ROUTE_SHAPE = {
|
|
1047
|
+
pages: z.array(z.number().int().min(1)).min(1).describe("n\xFAmeros de p\xE1gina del PDF donde van las ediciones, p. ej. [1,3,4]"),
|
|
1048
|
+
request: z.string().describe("la instrucci\xF3n COMPLETA y autocontenida para el editor (inclu\xED todos los datos/valores que dio el usuario, en su idioma)")
|
|
1049
|
+
};
|
|
1050
|
+
ROUTE_DESC = "Deleg\xE1 TODA modificaci\xF3n del PDF (editar/mover/borrar texto, resaltar, links, im\xE1genes, watermark, encabezados, campos de formulario, completar valores) al agente EDITOR. Llamala UNA sola vez con todas las p\xE1ginas a tocar y el pedido completo.";
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
1054
|
+
// ../agent/src/verify.ts
|
|
1055
|
+
function charXMap(s) {
|
|
1056
|
+
const map = new Array(s.text.length + 1).fill(s.x);
|
|
1057
|
+
let cursor = 0;
|
|
1058
|
+
let lastEnd = s.x;
|
|
1059
|
+
for (const r2 of s.runs) {
|
|
1060
|
+
const at = s.text.indexOf(r2.text, cursor);
|
|
1061
|
+
if (at < 0) continue;
|
|
1062
|
+
for (let k = cursor; k <= at; k++) map[k] = lastEnd + (r2.x - lastEnd) * (k - cursor) / Math.max(1, at - cursor);
|
|
1063
|
+
const w = r2.width / Math.max(1, r2.text.length);
|
|
1064
|
+
for (let k = 0; k <= r2.text.length; k++) map[at + k] = r2.x + w * k;
|
|
1065
|
+
cursor = at + r2.text.length;
|
|
1066
|
+
lastEnd = r2.x + r2.width;
|
|
1067
|
+
}
|
|
1068
|
+
for (let k = cursor; k <= s.text.length; k++) map[k] = lastEnd;
|
|
1069
|
+
return map;
|
|
1070
|
+
}
|
|
1071
|
+
function pageIssues(p) {
|
|
1072
|
+
const out = [];
|
|
1073
|
+
for (const w of p.widgets) {
|
|
1074
|
+
for (const s of p.segments) {
|
|
1075
|
+
if (s.baseline < w.y - 2 || s.baseline > w.y + Math.min(w.height, 9)) continue;
|
|
1076
|
+
const map = charXMap(s);
|
|
1077
|
+
let hit = "";
|
|
1078
|
+
for (let i = 0; i < s.text.length; i++) {
|
|
1079
|
+
if (s.text[i] === " ") continue;
|
|
1080
|
+
const mid = (map[i] + map[i + 1]) / 2;
|
|
1081
|
+
if (mid > w.x + 1 && mid < w.x + w.width - 1) hit += s.text[i];
|
|
1082
|
+
}
|
|
1083
|
+
const onlyPlaceholder = /^[_.·\-–—\s]*$/.test(hit);
|
|
1084
|
+
if (hit.trim().length > 0 && !onlyPlaceholder) {
|
|
1085
|
+
out.push(
|
|
1086
|
+
`- el campo "${w.fieldName}" (p${p.page}, x ${Math.round(w.x)}\u2013${Math.round(w.x + w.width)}) PISA el texto ${JSON.stringify(hit.trim().slice(0, 30))} del nodo ${s.id} (que arranca en x=${Math.round(s.x)}). Arreglo: move_text ${s.id} a x=${Math.round(w.x + w.width + 6)}, o mov\xE9/achic\xE1 el campo.`
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
for (const o of p.widgets) {
|
|
1091
|
+
if (o === w || w.x > o.x) continue;
|
|
1092
|
+
const vov = Math.min(w.y + w.height, o.y + o.height) - Math.max(w.y, o.y);
|
|
1093
|
+
if (vov < w.height * 0.5) continue;
|
|
1094
|
+
const ov = Math.min(w.x + w.width, o.x + o.width) - Math.max(w.x, o.x);
|
|
1095
|
+
if (ov > 2) out.push(`- los campos "${w.fieldName}" y "${o.fieldName}" (p${p.page}) se solapan ${Math.round(ov)}pt en el mismo rengl\xF3n \u2014 mov\xE9 uno con move_field.`);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
return out;
|
|
1099
|
+
}
|
|
1100
|
+
async function overlapReport(session) {
|
|
1101
|
+
const { pdf } = await session.bake();
|
|
1102
|
+
const doc = await graphFromBytes(pdf.slice());
|
|
1103
|
+
return doc.pages.flatMap(pageIssues);
|
|
1104
|
+
}
|
|
1105
|
+
function verifyMessage(issues) {
|
|
1106
|
+
return [
|
|
1107
|
+
`Verificaci\xF3n de layout del editor Aldus: horne\xE9 tus cambios y med\xED el grafo real.`,
|
|
1108
|
+
`Detect\xE9 ${issues.length} solape(s) que probablemente no quer\xEDas. Cada l\xEDnea trae`,
|
|
1109
|
+
`el arreglo sugerido (move_text/move_field, o delete_field + add_form_field m\xE1s`,
|
|
1110
|
+
`angosto). Aplic\xE1 los que correspondan; si alguno es intencional, dejalo y segu\xED.`,
|
|
1111
|
+
...issues
|
|
1112
|
+
].join("\n");
|
|
1113
|
+
}
|
|
1114
|
+
var init_verify = __esm({
|
|
1115
|
+
"../agent/src/verify.ts"() {
|
|
1116
|
+
init_graph();
|
|
1117
|
+
}
|
|
1118
|
+
});
|
|
1119
|
+
|
|
1120
|
+
// ../agent/src/config.ts
|
|
1121
|
+
var provider, config;
|
|
1122
|
+
var init_config = __esm({
|
|
1123
|
+
"../agent/src/config.ts"() {
|
|
1124
|
+
provider = process.env.ALDUS_PROVIDER === "openrouter" ? "openrouter" : "subscription";
|
|
1125
|
+
config = {
|
|
1126
|
+
provider,
|
|
1127
|
+
model: process.env.ALDUS_MODEL || "claude-sonnet-5",
|
|
1128
|
+
chatModel: process.env.ALDUS_CHAT_MODEL || "claude-haiku-4-5",
|
|
1129
|
+
maxTurns: Number(process.env.ALDUS_MAX_TURNS || 24),
|
|
1130
|
+
openrouter: {
|
|
1131
|
+
key: process.env.OPENROUTER_API_KEY || "",
|
|
1132
|
+
baseUrl: (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1").replace(/\/$/, ""),
|
|
1133
|
+
// Combo recomendado (mejor calidad/costo, medido): el CHAT ve TODO el doc
|
|
1134
|
+
// (input grande) → conviene el BARATO (flash-lite, $0.25/M in); el EDITOR
|
|
1135
|
+
// hace el tool-calling fino → el bueno (3.5-flash). ~1.8¢/turno en un doc de
|
|
1136
|
+
// 9 págs — la mitad que todo-3.5-flash, misma calidad y MÁS rápido (el
|
|
1137
|
+
// chat-lite rutea en ~1.3s). Todo-lite es ~0.6¢ pero el editor se ensucia
|
|
1138
|
+
// (mete tools de más, llena menos campos).
|
|
1139
|
+
model: process.env.ALDUS_OPENROUTER_MODEL || "google/gemini-3.5-flash",
|
|
1140
|
+
chatModel: process.env.ALDUS_OPENROUTER_CHAT_MODEL || "google/gemini-3.1-flash-lite"
|
|
1141
|
+
}
|
|
1142
|
+
};
|
|
1143
|
+
}
|
|
1144
|
+
});
|
|
1145
|
+
|
|
1146
|
+
// ../agent/src/openrouter.ts
|
|
1147
|
+
var openrouter_exports = {};
|
|
1148
|
+
__export(openrouter_exports, {
|
|
1149
|
+
runTurnOpenRouter: () => runTurnOpenRouter
|
|
1150
|
+
});
|
|
1151
|
+
async function streamCompletion(model, messages, tools, agent, onEvent) {
|
|
1152
|
+
const res = await fetch(`${config.openrouter.baseUrl}/chat/completions`, {
|
|
1153
|
+
method: "POST",
|
|
1154
|
+
headers: {
|
|
1155
|
+
authorization: `Bearer ${config.openrouter.key}`,
|
|
1156
|
+
"content-type": "application/json",
|
|
1157
|
+
"http-referer": "https://bernardocastro.dev",
|
|
1158
|
+
"x-title": "Aldus PDF Agent"
|
|
1159
|
+
},
|
|
1160
|
+
body: JSON.stringify({
|
|
1161
|
+
model,
|
|
1162
|
+
messages,
|
|
1163
|
+
tools,
|
|
1164
|
+
tool_choice: "auto",
|
|
1165
|
+
stream: true,
|
|
1166
|
+
// Sesgo a proveedores de alta throughput — OpenRouter a veces rutea a un
|
|
1167
|
+
// backend encolado (primera llamada de minutos); esto lo evita.
|
|
1168
|
+
provider: { sort: "throughput" }
|
|
1169
|
+
})
|
|
1170
|
+
});
|
|
1171
|
+
if (!res.ok || !res.body) {
|
|
1172
|
+
const body = await res.text().catch(() => "");
|
|
1173
|
+
throw new Error(`OpenRouter ${res.status}: ${body.slice(0, 300)}`);
|
|
1174
|
+
}
|
|
1175
|
+
let content = "";
|
|
1176
|
+
const calls = [];
|
|
1177
|
+
const reader = res.body.getReader();
|
|
1178
|
+
const dec = new TextDecoder();
|
|
1179
|
+
let buf = "";
|
|
1180
|
+
for (; ; ) {
|
|
1181
|
+
const { done, value } = await reader.read();
|
|
1182
|
+
if (done) break;
|
|
1183
|
+
buf += dec.decode(value, { stream: true });
|
|
1184
|
+
const lines = buf.split("\n");
|
|
1185
|
+
buf = lines.pop() ?? "";
|
|
1186
|
+
for (const line of lines) {
|
|
1187
|
+
const t = line.trim();
|
|
1188
|
+
if (!t.startsWith("data:")) continue;
|
|
1189
|
+
const data = t.slice(5).trim();
|
|
1190
|
+
if (!data || data === "[DONE]") continue;
|
|
1191
|
+
let json;
|
|
1192
|
+
try {
|
|
1193
|
+
json = JSON.parse(data);
|
|
1194
|
+
} catch {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
if (json.error) throw new Error(`OpenRouter (stream): ${json.error.message || JSON.stringify(json.error)}`);
|
|
1198
|
+
const delta = json.choices?.[0]?.delta;
|
|
1199
|
+
if (!delta) continue;
|
|
1200
|
+
if (delta.content) {
|
|
1201
|
+
content += delta.content;
|
|
1202
|
+
onEvent?.({ type: "text", delta: delta.content, agent });
|
|
1203
|
+
}
|
|
1204
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
1205
|
+
const i = tc.index ?? 0;
|
|
1206
|
+
calls[i] ??= { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
1207
|
+
if (tc.id) calls[i].id = tc.id;
|
|
1208
|
+
if (tc.function?.name) calls[i].function.name += tc.function.name;
|
|
1209
|
+
if (tc.function?.arguments) calls[i].function.arguments += tc.function.arguments;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
return { content, toolCalls: calls.filter(Boolean) };
|
|
1214
|
+
}
|
|
1215
|
+
async function runTurnOpenRouter(opts) {
|
|
1216
|
+
if (!config.openrouter.key) throw new Error("falta OPENROUTER_API_KEY (o un token de sesi\xF3n del llm-proxy)");
|
|
1217
|
+
const t0 = Date.now();
|
|
1218
|
+
log(`turno OR: "${opts.prompt.slice(0, 80)}" (chat=${config.openrouter.chatModel}, editor=${config.openrouter.model})`);
|
|
1219
|
+
const chatMsgs = [
|
|
1220
|
+
{ role: "system", content: chatSystemPrompt(opts.doc, opts.page) },
|
|
1221
|
+
{ role: "user", content: opts.prompt }
|
|
1222
|
+
];
|
|
1223
|
+
const r1 = await streamCompletion(config.openrouter.chatModel, chatMsgs, [openaiRouterTool()], "chat", opts.onEvent);
|
|
1224
|
+
const routeCall = r1.toolCalls.find((c) => c.function.name === "edit_document");
|
|
1225
|
+
if (!routeCall) {
|
|
1226
|
+
log(`chat-only listo en ${Date.now() - t0}ms`);
|
|
1227
|
+
return { text: r1.content, toolCalls: 0 };
|
|
1228
|
+
}
|
|
1229
|
+
log(`chat \u2192 edit_document (${Date.now() - t0}ms)`);
|
|
1230
|
+
opts.onEvent?.({ type: "tool", name: "mcp__aldus__edit_document", agent: "chat" });
|
|
1231
|
+
let route = { pages: [], request: opts.prompt };
|
|
1232
|
+
try {
|
|
1233
|
+
const args = JSON.parse(routeCall.function.arguments || "{}");
|
|
1234
|
+
route = {
|
|
1235
|
+
pages: Array.isArray(args.pages) ? args.pages.filter((n) => Number.isFinite(n)) : [],
|
|
1236
|
+
request: typeof args.request === "string" && args.request ? args.request : opts.prompt
|
|
1237
|
+
};
|
|
1238
|
+
} catch {
|
|
1239
|
+
}
|
|
1240
|
+
const pages = route.pages.length ? route.pages : opts.page != null ? [opts.page] : void 0;
|
|
1241
|
+
const messages = [
|
|
1242
|
+
{ role: "system", content: systemPrompt(opts.doc, pages) },
|
|
1243
|
+
{ role: "user", content: `${opts.prompt}
|
|
1244
|
+
|
|
1245
|
+
[Plan del asistente]: ${route.request}` }
|
|
1246
|
+
];
|
|
1247
|
+
let text = r1.content;
|
|
1248
|
+
let toolCalls = 0;
|
|
1249
|
+
const usedTools = /* @__PURE__ */ new Set();
|
|
1250
|
+
let verified = false;
|
|
1251
|
+
for (let turn = 0; turn < config.maxTurns; turn++) {
|
|
1252
|
+
const { content, toolCalls: calls } = await streamCompletion(config.openrouter.model, messages, openaiTools(), "editor", opts.onEvent);
|
|
1253
|
+
text += content;
|
|
1254
|
+
if (!calls.length) {
|
|
1255
|
+
const movedByHand = MANUAL_GEOMETRY.some((t) => usedTools.has(t));
|
|
1256
|
+
if (movedByHand && !verified) {
|
|
1257
|
+
verified = true;
|
|
1258
|
+
const issues = await overlapReport(opts.session).catch(() => []);
|
|
1259
|
+
if (issues.length) {
|
|
1260
|
+
log(`verify: ${issues.length} issues \u2192 pasada correctiva`);
|
|
1261
|
+
opts.onEvent?.({ type: "tool", name: "mcp__aldus__verify_layout", agent: "editor" });
|
|
1262
|
+
messages.push({ role: "assistant", content: content || null });
|
|
1263
|
+
messages.push({ role: "user", content: verifyMessage(issues) });
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
messages.push({ role: "assistant", content: content || null, tool_calls: calls });
|
|
1270
|
+
for (const tc of calls) {
|
|
1271
|
+
toolCalls++;
|
|
1272
|
+
usedTools.add(tc.function.name);
|
|
1273
|
+
log(`editor tool #${toolCalls}: ${tc.function.name} (${Date.now() - t0}ms)`);
|
|
1274
|
+
opts.onEvent?.({ type: "tool", name: `mcp__aldus__${tc.function.name}`, agent: "editor" });
|
|
1275
|
+
let args = {};
|
|
1276
|
+
try {
|
|
1277
|
+
args = JSON.parse(tc.function.arguments || "{}");
|
|
1278
|
+
} catch {
|
|
1279
|
+
}
|
|
1280
|
+
const result = await runTool(opts.session, tc.function.name, args);
|
|
1281
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
log(`turno OR listo en ${Date.now() - t0}ms (toolCalls=${toolCalls}, tools=[${[...usedTools].join(",")}])`);
|
|
1285
|
+
return { text, toolCalls };
|
|
1286
|
+
}
|
|
1287
|
+
var log, MANUAL_GEOMETRY;
|
|
1288
|
+
var init_openrouter = __esm({
|
|
1289
|
+
"../agent/src/openrouter.ts"() {
|
|
1290
|
+
init_src();
|
|
1291
|
+
init_config();
|
|
1292
|
+
init_agent();
|
|
1293
|
+
init_tools();
|
|
1294
|
+
init_verify();
|
|
1295
|
+
log = createLogger("aldus:openrouter");
|
|
1296
|
+
MANUAL_GEOMETRY = ["move_text", "move_field", "move_image"];
|
|
1297
|
+
}
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// ../agent/src/agent.ts
|
|
1301
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
1302
|
+
function systemPrompt(doc, page) {
|
|
1303
|
+
const pages = doc.pages.length;
|
|
1304
|
+
const scoped = page == null ? null : Array.isArray(page) ? page : [page];
|
|
1305
|
+
return [
|
|
1306
|
+
"Sos Aldus, un agente experto en documentos PDF. Ten\xE9s EMBEBIDO abajo el",
|
|
1307
|
+
"contenido completo del documento como un grafo. Sos CONSCIENTE de TODO:",
|
|
1308
|
+
"de cada nodo de texto conoc\xE9s su `id`, posici\xF3n (x, baseline), ancho\xD7alto,",
|
|
1309
|
+
"tama\xF1o de fuente, negrita/it\xE1lica y familia (y color si figura); de cada",
|
|
1310
|
+
"imagen, campo, resaltado y link su `id`, rect y datos. Us\xE1 esa geometr\xEDa y",
|
|
1311
|
+
"ese estilo para ubicar y emparejar lo que hagas (p. ej. escribir alineado a",
|
|
1312
|
+
"un label, o crear un campo del tama\xF1o justo).",
|
|
1313
|
+
"",
|
|
1314
|
+
"C\xF3mo trabaj\xE1s:",
|
|
1315
|
+
"- PREGUNTAS sobre el contenido \u2192 respond\xE9 directo leyendo el grafo. NO hay",
|
|
1316
|
+
" tool de lectura: ya ten\xE9s todo el documento ac\xE1. Si el usuario pide los",
|
|
1317
|
+
" datos en un formato (JSON, tabla, lista), devolvelos EXACTAMENTE as\xED.",
|
|
1318
|
+
"- CAMBIOS \u2192 us\xE1 las tools referenciando los `id` EXACTOS del grafo. Pod\xE9s",
|
|
1319
|
+
" encadenar varias. No inventes ids. Ten\xE9s las MISMAS capacidades que un",
|
|
1320
|
+
" humano en el editor:",
|
|
1321
|
+
" \xB7 Texto existente: edit_text (si el texto nuevo es m\xE1s largo, el p\xE1rrafo se",
|
|
1322
|
+
" reconstruye solo \u2014 no calcules nada), move_text, set_text_style (negrita/",
|
|
1323
|
+
" it\xE1lica), set_text_color, set_text_size, delete_text.",
|
|
1324
|
+
" \xB7 Imagen existente: move_image, delete_image.",
|
|
1325
|
+
" \xB7 Resaltar: highlight_text (sobre un id de texto). Sobre resaltados que ya",
|
|
1326
|
+
" existen: set_highlight_color, delete_highlight.",
|
|
1327
|
+
" \xB7 Links: add_link (sobre un id de texto \u2192 URL), delete_link.",
|
|
1328
|
+
" \xB7 Crear: add_text, insert_image (desde una ruta local), add_watermark,",
|
|
1329
|
+
" add_header_footer, add_form_field (type = text/checkbox/radio/select/",
|
|
1330
|
+
" list/button/signature \u2014 pod\xE9s poner inputs NUEVOS: firmas, radios, checks\u2026).",
|
|
1331
|
+
' \xB7 Formularios: las p\xE1ginas con campos traen una secci\xF3n "Lectura" \u2014 el',
|
|
1332
|
+
" texto en orden con cada campo [[id]] intercalado DONDE CAE. Esa lectura",
|
|
1333
|
+
" es LA fuente de verdad para saber qu\xE9 va en cada campo (le\xE9 la oraci\xF3n",
|
|
1334
|
+
" alrededor del [[id]], como un humano). Cada campo muestra su VALOR",
|
|
1335
|
+
' actual (o "(vac\xEDo)") \u2014 para "extraer"/leer un form respond\xE9 desde el',
|
|
1336
|
+
" grafo. Para COMPLETAR VARIOS campos us\xE1 fill_fields (UNA sola llamada con",
|
|
1337
|
+
" la lista {name,value}) \u2014 mucho m\xE1s r\xE1pido que fill_field N veces; us\xE1",
|
|
1338
|
+
" fill_field solo para uno. name = fieldName o el [[id]] de la Lectura.",
|
|
1339
|
+
" Campos existentes: move_field, delete_field.",
|
|
1340
|
+
" Un PDF PLANO (sin campos, con l\xEDneas/labels) se puede volver fillable:",
|
|
1341
|
+
" add_form_field en cada hueco (mir\xE1 los labels y su geometr\xEDa) y opcionalmente",
|
|
1342
|
+
" fill_field. O simplemente escribir la respuesta con add_text al lado del label.",
|
|
1343
|
+
"",
|
|
1344
|
+
"Coordenadas: puntos PDF, origen ABAJO-IZQUIERDA, x\u2192derecha, y\u2192arriba. Para el",
|
|
1345
|
+
"texto la `y` es la baseline. El tama\xF1o de cada p\xE1gina est\xE1 en su encabezado.",
|
|
1346
|
+
"Para NO perder contenido, no coloques nada fuera de los l\xEDmites de la p\xE1gina.",
|
|
1347
|
+
'LLENAR UNA L\xCDNEA "____" YA EXISTENTE (label + rengl\xF3n): el valor se apoya',
|
|
1348
|
+
"ENCIMA del rengl\xF3n, NO debajo. Us\xE1 la MISMA baseline del label de esa l\xEDnea",
|
|
1349
|
+
"(su `y` exacto, o +2pt). NUNCA restes: y menor = el texto cae DEBAJO de la",
|
|
1350
|
+
"l\xEDnea (mal). Si el hueco est\xE1 a la derecha del label, x = x del label + su",
|
|
1351
|
+
'ancho + ~6pt. El texto va SOBRE los "____", no en otro rengl\xF3n.',
|
|
1352
|
+
"",
|
|
1353
|
+
'CONVERTIR PLACEHOLDERS EN INPUTS ("XXXX", "xxx", "____", "***", "\u2026" o',
|
|
1354
|
+
"cualquier relleno de plantilla): DETECTALOS VOS leyendo el texto y us\xE1 SIEMPRE",
|
|
1355
|
+
"placeholders_to_fields(id, fields=[{placeholder,name}]).",
|
|
1356
|
+
"\u26A0\uFE0F CR\xCDTICO \u2014 UNA SOLA LLAMADA POR P\xC1RRAFO (no por l\xEDnea/nodo): la tool",
|
|
1357
|
+
"reconstruye el P\xC1RRAFO ENTERO al que pertenece `id`, as\xED que junt\xE1 TODOS los",
|
|
1358
|
+
"placeholders del p\xE1rrafo (aunque est\xE9n en varias l\xEDneas) en UN solo fields[],",
|
|
1359
|
+
"EN ORDEN DE LECTURA, y llamala UNA vez con el id de cualquiera de esas l\xEDneas.",
|
|
1360
|
+
"Llamarla 2+ veces sobre el mismo p\xE1rrafo rehace todo el reflow cada vez (lento)",
|
|
1361
|
+
"y se pisan entre s\xED. `placeholder` = el substring EXACTO del texto; `name` =",
|
|
1362
|
+
"snake_case (mir\xE1 el label del hueco). NO pases coordenadas, NO uses edit_text/",
|
|
1363
|
+
'add_form_field para esto, NO dejes "_" ni "XXXX". Un "____" ya dibujado YA es el',
|
|
1364
|
+
"campo: complet\xE1 encima. Nunca agregues bold/italic que no exist\xEDa.",
|
|
1365
|
+
"",
|
|
1366
|
+
"Respond\xE9 en el idioma del usuario, conciso. Si una edici\xF3n es ambigua o el id",
|
|
1367
|
+
"no existe, decilo en vez de adivinar.",
|
|
1368
|
+
"",
|
|
1369
|
+
scoped ? `=== DOCUMENTO: ${doc.path} (${pages} p\xE1ginas en total) \u2014 MOSTRANDO SOLO ${scoped.length === 1 ? `LA P\xC1GINA ${scoped[0]}` : `LAS P\xC1GINAS ${scoped.join(", ")}`}. Trabaj\xE1 sobre esas p\xE1ginas. ===` : `=== DOCUMENTO: ${doc.path} (${pages} ${pages === 1 ? "p\xE1gina" : "p\xE1ginas"}) ===`,
|
|
1370
|
+
serializeDoc(doc, page)
|
|
1371
|
+
].join("\n");
|
|
1372
|
+
}
|
|
1373
|
+
function chatSystemPrompt(doc, page) {
|
|
1374
|
+
const total = doc.pages.length;
|
|
1375
|
+
const current = page ?? 1;
|
|
1376
|
+
return [
|
|
1377
|
+
"Sos CASPER, el asistente del editor de PDF Aldus. Ten\xE9s embebido abajo el",
|
|
1378
|
+
`grafo COMPLETO del documento (${total} ${total === 1 ? "p\xE1gina" : "p\xE1ginas"}).`,
|
|
1379
|
+
`El usuario est\xE1 viendo la p\xE1gina ${current}.`,
|
|
1380
|
+
"",
|
|
1381
|
+
"C\xF3mo trabaj\xE1s:",
|
|
1382
|
+
"- PREGUNTAS sobre el contenido (resumir, extraer, listar campos, explicar) \u2192",
|
|
1383
|
+
" respond\xE9 DIRECTO leyendo el grafo. Pod\xE9s mirar CUALQUIER p\xE1gina, no solo la",
|
|
1384
|
+
" que el usuario ve. Devolv\xE9 el formato que pida (JSON, tabla\u2026).",
|
|
1385
|
+
"- CUALQUIER MODIFICACI\xD3N del PDF (editar/mover/borrar texto, resaltar, links,",
|
|
1386
|
+
" im\xE1genes, watermark, encabezados, campos, completar formularios) \u2192 NO la",
|
|
1387
|
+
" hagas vos: llam\xE1 edit_document UNA sola vez con TODO el pedido.",
|
|
1388
|
+
" \xB7 pages = LAS P\xC1GINAS EXACTAS donde hay que trabajar. Mir\xE1 el grafo entero y",
|
|
1389
|
+
" eleg\xED SOLO las que el cambio realmente toca (p. ej. [3] o [1,4]). Al editor",
|
|
1390
|
+
" le inyectamos \xDANICAMENTE esas p\xE1ginas \u2014 si inclu\xEDs de m\xE1s, lo confund\xEDs con",
|
|
1391
|
+
" contenido irrelevante; si te falta una, trabaja a ciegas. Eleg\xED con",
|
|
1392
|
+
` precisi\xF3n. Si el pedido es sobre lo que el usuario ve y no menciona otra,`,
|
|
1393
|
+
` es [${current}].`,
|
|
1394
|
+
" \xB7 request = la instrucci\xF3n COMPLETA y autocontenida para el editor:",
|
|
1395
|
+
" repet\xED todos los datos/valores/textos que dio el usuario, en su idioma, y",
|
|
1396
|
+
" dec\xED expl\xEDcitamente en qu\xE9 p\xE1gina(s) va cada cosa.",
|
|
1397
|
+
" Despu\xE9s de llamar edit_document, dec\xED UNA sola frase corta en presente",
|
|
1398
|
+
' ("Le ped\xED al editor que complete los campos con datos de prueba."). VOS NO',
|
|
1399
|
+
' EDIT\xC1S NADA: NO digas "Listo", NO afirmes que ya se hizo, NO enumeres',
|
|
1400
|
+
" resultados ni valores que no pod\xE9s conocer \u2014 el editor reporta \xE9l mismo al",
|
|
1401
|
+
" terminar, y tu invento quedar\xEDa duplicado y posiblemente mal.",
|
|
1402
|
+
" \u26A0\uFE0F Si el pedido de modificaci\xF3n es claro, DELEG\xC1 YA (llam\xE1 edit_document) \u2014",
|
|
1403
|
+
' NO pidas confirmaci\xF3n ni preguntes "\xBFes esto lo que quer\xE9s?". Solo pregunt\xE1',
|
|
1404
|
+
" si es genuinamente ambiguo qu\xE9 cambiar.",
|
|
1405
|
+
' \u26A0\uFE0F "convertir/reemplazar los XXXX/placeholders por inputs/campos/completable"',
|
|
1406
|
+
" = CREAR campos de formulario VAC\xCDOS. NO necesit\xE1s valores para eso: deleg\xE1",
|
|
1407
|
+
" directo (el editor crea los campos). Solo ped\xED valores si el usuario dice",
|
|
1408
|
+
' expl\xEDcitamente "COMPLET\xC1/LLEN\xC1 con estos datos". Ej: usuario "reemplaza los',
|
|
1409
|
+
' XXXX por inputs" \u2192 edit_document({pages:[1], request:"convertir los',
|
|
1410
|
+
' placeholders XXXX/xxxx de la p\xE1gina 1 en campos de formulario"}).',
|
|
1411
|
+
"",
|
|
1412
|
+
"Respond\xE9 en el idioma del usuario, conciso.",
|
|
1413
|
+
"",
|
|
1414
|
+
`=== DOCUMENTO: ${doc.path} (${total} ${total === 1 ? "p\xE1gina" : "p\xE1ginas"}) \u2014 el usuario ve la ${current} ===`,
|
|
1415
|
+
serializeDoc(doc)
|
|
1416
|
+
].join("\n");
|
|
1417
|
+
}
|
|
1418
|
+
async function runTurn(opts) {
|
|
1419
|
+
if (config.provider === "openrouter") {
|
|
1420
|
+
const { runTurnOpenRouter: runTurnOpenRouter2 } = await Promise.resolve().then(() => (init_openrouter(), openrouter_exports));
|
|
1421
|
+
return runTurnOpenRouter2(opts);
|
|
1422
|
+
}
|
|
1423
|
+
const t0 = Date.now();
|
|
1424
|
+
log2(`turno: "${opts.prompt.slice(0, 80)}" (page=${opts.page}, resume=${!!opts.resume})`);
|
|
1425
|
+
let route = null;
|
|
1426
|
+
let text = "";
|
|
1427
|
+
let sessionId;
|
|
1428
|
+
for await (const message of query({
|
|
1429
|
+
prompt: opts.prompt,
|
|
1430
|
+
options: {
|
|
1431
|
+
model: config.chatModel,
|
|
1432
|
+
systemPrompt: chatSystemPrompt(opts.doc, opts.page),
|
|
1433
|
+
mcpServers: { aldus: buildRouterServer((r2) => {
|
|
1434
|
+
route = r2;
|
|
1435
|
+
}) },
|
|
1436
|
+
includePartialMessages: true,
|
|
1437
|
+
canUseTool: async (name, input) => name === "mcp__aldus__edit_document" ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: "El chat solo puede delegar con edit_document." },
|
|
1438
|
+
maxTurns: 4,
|
|
1439
|
+
...opts.resume ? { resume: opts.resume } : {}
|
|
1440
|
+
}
|
|
1441
|
+
})) {
|
|
1442
|
+
if (message.type === "stream_event") {
|
|
1443
|
+
const ev = message.event;
|
|
1444
|
+
if (ev.type === "content_block_delta" && ev.delta?.type === "text_delta" && ev.delta.text) {
|
|
1445
|
+
text += ev.delta.text;
|
|
1446
|
+
opts.onEvent?.({ type: "text", delta: ev.delta.text, agent: "chat" });
|
|
1447
|
+
} else if (ev.type === "content_block_start" && ev.content_block?.type === "tool_use" && ev.content_block.name === "mcp__aldus__edit_document") {
|
|
1448
|
+
log2(`chat \u2192 edit_document (${Date.now() - t0}ms)`);
|
|
1449
|
+
opts.onEvent?.({ type: "tool", name: "mcp__aldus__edit_document", agent: "chat" });
|
|
1450
|
+
}
|
|
1451
|
+
} else if (message.type === "result") {
|
|
1452
|
+
sessionId = message.session_id;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
if (!route) {
|
|
1456
|
+
log2(`turno chat-only listo en ${Date.now() - t0}ms`);
|
|
1457
|
+
return { text, sessionId, toolCalls: 0 };
|
|
1458
|
+
}
|
|
1459
|
+
const routed = route;
|
|
1460
|
+
const pages = routed.pages.length ? routed.pages : opts.page != null ? [opts.page] : void 0;
|
|
1461
|
+
const server = buildToolServer(opts.session);
|
|
1462
|
+
let toolCalls = 0;
|
|
1463
|
+
const usedTools = /* @__PURE__ */ new Set();
|
|
1464
|
+
let editorSession;
|
|
1465
|
+
const editorPass = async (prompt, resume) => {
|
|
1466
|
+
for await (const message of query({
|
|
1467
|
+
prompt,
|
|
1468
|
+
options: {
|
|
1469
|
+
model: config.model,
|
|
1470
|
+
systemPrompt: systemPrompt(opts.doc, pages),
|
|
1471
|
+
mcpServers: { aldus: server },
|
|
1472
|
+
// Deltas token a token → el panel muestra la respuesta escribiéndose y las
|
|
1473
|
+
// tools ejecutándose, en vez de quedarse mudo 20-40s en "Pensando".
|
|
1474
|
+
includePartialMessages: true,
|
|
1475
|
+
// En headless no hay prompt de permisos interactivo: `canUseTool` es el
|
|
1476
|
+
// ÚNICO gate — auto-aprueba las tools de Aldus y niega cualquier otra (sin
|
|
1477
|
+
// `allowedTools`, que las auto-aprobaría antes y shadowearía este callback).
|
|
1478
|
+
canUseTool: async (name, input) => name.startsWith("mcp__aldus__") ? { behavior: "allow", updatedInput: input } : { behavior: "deny", message: "Aldus solo permite sus propias tools de edici\xF3n." },
|
|
1479
|
+
maxTurns: config.maxTurns,
|
|
1480
|
+
// La fase editora es una conversación propia (el hilo del CHAT vive en
|
|
1481
|
+
// la fase 1 — su sessionId es el que se devuelve al panel).
|
|
1482
|
+
...resume ? { resume } : {}
|
|
1483
|
+
}
|
|
1484
|
+
})) {
|
|
1485
|
+
if (message.type === "stream_event") {
|
|
1486
|
+
const ev = message.event;
|
|
1487
|
+
if (ev.type === "content_block_delta" && ev.delta?.type === "text_delta" && ev.delta.text) {
|
|
1488
|
+
text += ev.delta.text;
|
|
1489
|
+
opts.onEvent?.({ type: "text", delta: ev.delta.text, agent: "editor" });
|
|
1490
|
+
} else if (ev.type === "content_block_start" && ev.content_block?.type === "tool_use") {
|
|
1491
|
+
const name = ev.content_block.name ?? "tool";
|
|
1492
|
+
if (name.startsWith("mcp__aldus__")) {
|
|
1493
|
+
toolCalls++;
|
|
1494
|
+
const bare = name.replace("mcp__aldus__", "");
|
|
1495
|
+
usedTools.add(bare);
|
|
1496
|
+
log2(`editor tool #${toolCalls}: ${bare} (${Date.now() - t0}ms)`);
|
|
1497
|
+
opts.onEvent?.({ type: "tool", name, agent: "editor" });
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
} else if (message.type === "result") {
|
|
1501
|
+
editorSession = message.session_id;
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
await editorPass(`${opts.prompt}
|
|
1506
|
+
|
|
1507
|
+
[Plan del asistente]: ${routed.request}`);
|
|
1508
|
+
const MANUAL_GEOMETRY2 = ["move_text", "move_field", "move_image"];
|
|
1509
|
+
const movedByHand = MANUAL_GEOMETRY2.some((t) => usedTools.has(t));
|
|
1510
|
+
if (movedByHand) {
|
|
1511
|
+
const issues = await overlapReport(opts.session).catch(() => []);
|
|
1512
|
+
if (issues.length) {
|
|
1513
|
+
log2(`verify: ${issues.length} issues \u2192 pasada correctiva`);
|
|
1514
|
+
opts.onEvent?.({ type: "tool", name: "mcp__aldus__verify_layout", agent: "editor" });
|
|
1515
|
+
await editorPass(verifyMessage(issues), editorSession);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
log2(`turno listo en ${Date.now() - t0}ms (toolCalls=${toolCalls}, tools=[${[...usedTools].join(",")}])`);
|
|
1519
|
+
return { text, sessionId, toolCalls };
|
|
1520
|
+
}
|
|
1521
|
+
var log2;
|
|
1522
|
+
var init_agent = __esm({
|
|
1523
|
+
"../agent/src/agent.ts"() {
|
|
1524
|
+
init_src();
|
|
1525
|
+
init_tools();
|
|
1526
|
+
init_serialize();
|
|
1527
|
+
init_verify();
|
|
1528
|
+
init_config();
|
|
1529
|
+
log2 = createLogger("aldus:agent");
|
|
1530
|
+
}
|
|
1531
|
+
});
|
|
1532
|
+
|
|
1533
|
+
// ../agent/src/index.ts
|
|
1534
|
+
init_graph();
|
|
1535
|
+
init_serialize();
|
|
1536
|
+
|
|
1537
|
+
// ../agent/src/session.ts
|
|
1538
|
+
init_src();
|
|
1539
|
+
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
1540
|
+
|
|
1541
|
+
// ../core/src/bake/bake.ts
|
|
1542
|
+
import { PDFDocument as PDFDocument8 } from "pdf-lib";
|
|
1543
|
+
|
|
1544
|
+
// ../core/src/bake/tokenizer.ts
|
|
1545
|
+
var isWs = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
1546
|
+
var isDelim = (c) => c === 40 || c === 41 || c === 60 || c === 62 || c === 91 || c === 93 || c === 123 || c === 125 || c === 47 || c === 37;
|
|
1547
|
+
var latin1 = (bytes, a2, b) => {
|
|
1548
|
+
let s = "";
|
|
1549
|
+
for (let i = a2; i < b; i++) s += String.fromCharCode(bytes[i]);
|
|
1550
|
+
return s;
|
|
1551
|
+
};
|
|
1552
|
+
function tokenizeContentStream(src) {
|
|
1553
|
+
const ops = [];
|
|
1554
|
+
let operands = [];
|
|
1555
|
+
let i = 0;
|
|
1556
|
+
const n = src.length;
|
|
1557
|
+
const skipWs = () => {
|
|
1558
|
+
while (i < n) {
|
|
1559
|
+
if (isWs(src[i])) {
|
|
1560
|
+
i++;
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
if (src[i] === 37) {
|
|
1564
|
+
while (i < n && src[i] !== 10 && src[i] !== 13) i++;
|
|
1565
|
+
continue;
|
|
1566
|
+
}
|
|
1567
|
+
break;
|
|
1568
|
+
}
|
|
1569
|
+
};
|
|
1570
|
+
const parseString = () => {
|
|
1571
|
+
const start = i;
|
|
1572
|
+
i++;
|
|
1573
|
+
const out = [];
|
|
1574
|
+
let depth = 1;
|
|
1575
|
+
while (i < n && depth > 0) {
|
|
1576
|
+
const c = src[i];
|
|
1577
|
+
if (c === 92) {
|
|
1578
|
+
const e = src[i + 1];
|
|
1579
|
+
i += 2;
|
|
1580
|
+
if (e === 110) out.push(10);
|
|
1581
|
+
else if (e === 114) out.push(13);
|
|
1582
|
+
else if (e === 116) out.push(9);
|
|
1583
|
+
else if (e === 98) out.push(8);
|
|
1584
|
+
else if (e === 102) out.push(12);
|
|
1585
|
+
else if (e === 40 || e === 41 || e === 92) out.push(e);
|
|
1586
|
+
else if (e >= 48 && e <= 55) {
|
|
1587
|
+
let v = e - 48;
|
|
1588
|
+
for (let k = 0; k < 2 && src[i] >= 48 && src[i] <= 55; k++) {
|
|
1589
|
+
v = v * 8 + (src[i] - 48);
|
|
1590
|
+
i++;
|
|
1591
|
+
}
|
|
1592
|
+
out.push(v & 255);
|
|
1593
|
+
} else if (e === 10) {
|
|
1594
|
+
} else if (e === 13) {
|
|
1595
|
+
if (src[i] === 10) i++;
|
|
1596
|
+
} else if (e !== void 0) out.push(e);
|
|
1597
|
+
} else {
|
|
1598
|
+
if (c === 40) depth++;
|
|
1599
|
+
else if (c === 41) {
|
|
1600
|
+
depth--;
|
|
1601
|
+
if (depth === 0) {
|
|
1602
|
+
i++;
|
|
1603
|
+
break;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
out.push(c);
|
|
1607
|
+
i++;
|
|
1608
|
+
continue;
|
|
1609
|
+
}
|
|
1610
|
+
}
|
|
1611
|
+
return { kind: "str", raw: latin1(src, start, i), bytes: Uint8Array.from(out), start, end: i };
|
|
1612
|
+
};
|
|
1613
|
+
const parseHex = () => {
|
|
1614
|
+
const start = i;
|
|
1615
|
+
i++;
|
|
1616
|
+
const digits = [];
|
|
1617
|
+
while (i < n && src[i] !== 62) {
|
|
1618
|
+
const c = src[i];
|
|
1619
|
+
if (!isWs(c)) digits.push(c);
|
|
1620
|
+
i++;
|
|
1621
|
+
}
|
|
1622
|
+
i++;
|
|
1623
|
+
let hex = digits.map((c) => String.fromCharCode(c)).join("");
|
|
1624
|
+
if (hex.length % 2) hex += "0";
|
|
1625
|
+
const out = new Uint8Array(hex.length / 2);
|
|
1626
|
+
for (let k = 0; k < out.length; k++) out[k] = parseInt(hex.slice(k * 2, k * 2 + 2), 16);
|
|
1627
|
+
return { kind: "hex", raw: latin1(src, start, i), bytes: out, start, end: i };
|
|
1628
|
+
};
|
|
1629
|
+
const parseName = () => {
|
|
1630
|
+
const start = i;
|
|
1631
|
+
i++;
|
|
1632
|
+
let name = "";
|
|
1633
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) {
|
|
1634
|
+
if (src[i] === 35 && i + 2 < n) {
|
|
1635
|
+
name += String.fromCharCode(parseInt(latin1(src, i + 1, i + 3), 16));
|
|
1636
|
+
i += 3;
|
|
1637
|
+
} else {
|
|
1638
|
+
name += String.fromCharCode(src[i]);
|
|
1639
|
+
i++;
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return { kind: "name", value: name, raw: latin1(src, start, i), start, end: i };
|
|
1643
|
+
};
|
|
1644
|
+
const parseNumber = () => {
|
|
1645
|
+
const start = i;
|
|
1646
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) i++;
|
|
1647
|
+
const raw = latin1(src, start, i);
|
|
1648
|
+
return { kind: "num", value: parseFloat(raw), raw, start, end: i };
|
|
1649
|
+
};
|
|
1650
|
+
const parseDict = () => {
|
|
1651
|
+
const start = i;
|
|
1652
|
+
i += 2;
|
|
1653
|
+
let depth = 1;
|
|
1654
|
+
while (i < n && depth > 0) {
|
|
1655
|
+
if (src[i] === 60 && src[i + 1] === 60) {
|
|
1656
|
+
depth++;
|
|
1657
|
+
i += 2;
|
|
1658
|
+
continue;
|
|
1659
|
+
}
|
|
1660
|
+
if (src[i] === 62 && src[i + 1] === 62) {
|
|
1661
|
+
depth--;
|
|
1662
|
+
i += 2;
|
|
1663
|
+
continue;
|
|
1664
|
+
}
|
|
1665
|
+
if (src[i] === 40) {
|
|
1666
|
+
parseString();
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
i++;
|
|
1670
|
+
}
|
|
1671
|
+
return { kind: "dict", raw: latin1(src, start, i), start, end: i };
|
|
1672
|
+
};
|
|
1673
|
+
const parseArray = () => {
|
|
1674
|
+
const start = i;
|
|
1675
|
+
i++;
|
|
1676
|
+
const items = [];
|
|
1677
|
+
while (i < n) {
|
|
1678
|
+
skipWs();
|
|
1679
|
+
if (src[i] === 93) {
|
|
1680
|
+
i++;
|
|
1681
|
+
break;
|
|
1682
|
+
}
|
|
1683
|
+
items.push(parseOne());
|
|
1684
|
+
}
|
|
1685
|
+
return { kind: "arr", items, raw: latin1(src, start, i), start, end: i };
|
|
1686
|
+
};
|
|
1687
|
+
const parseKeyword = () => {
|
|
1688
|
+
const start = i;
|
|
1689
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) i++;
|
|
1690
|
+
const kw = latin1(src, start, i);
|
|
1691
|
+
return { kind: "kw", value: kw, raw: kw, start, end: i };
|
|
1692
|
+
};
|
|
1693
|
+
const parseOne = () => {
|
|
1694
|
+
const c = src[i];
|
|
1695
|
+
if (c === 40) return parseString();
|
|
1696
|
+
if (c === 60) return src[i + 1] === 60 ? parseDict() : parseHex();
|
|
1697
|
+
if (c === 47) return parseName();
|
|
1698
|
+
if (c === 91) return parseArray();
|
|
1699
|
+
if (c >= 48 && c <= 57 || c === 43 || c === 45 || c === 46) return parseNumber();
|
|
1700
|
+
return parseKeyword();
|
|
1701
|
+
};
|
|
1702
|
+
while (i < n) {
|
|
1703
|
+
skipWs();
|
|
1704
|
+
if (i >= n) break;
|
|
1705
|
+
const tok = parseOne();
|
|
1706
|
+
if (tok.kind === "kw") {
|
|
1707
|
+
const op = tok.value;
|
|
1708
|
+
if (op === "BI") {
|
|
1709
|
+
while (i < n) {
|
|
1710
|
+
if (isWs(src[i - 1]) && src[i] === 69 && src[i + 1] === 73 && (i + 2 >= n || isWs(src[i + 2]) || isDelim(src[i + 2]))) {
|
|
1711
|
+
i += 2;
|
|
1712
|
+
break;
|
|
1713
|
+
}
|
|
1714
|
+
i++;
|
|
1715
|
+
}
|
|
1716
|
+
operands = [];
|
|
1717
|
+
continue;
|
|
1718
|
+
}
|
|
1719
|
+
ops.push({
|
|
1720
|
+
op,
|
|
1721
|
+
operands,
|
|
1722
|
+
start: operands.length ? operands[0].start : tok.start,
|
|
1723
|
+
end: tok.end
|
|
1724
|
+
});
|
|
1725
|
+
operands = [];
|
|
1726
|
+
} else {
|
|
1727
|
+
operands.push(tok);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
return ops;
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// ../core/src/bake/splice.ts
|
|
1734
|
+
var fmt = (v) => {
|
|
1735
|
+
const r2 = Math.round(v * 1e4) / 1e4;
|
|
1736
|
+
return Object.is(r2, -0) ? "0" : String(r2);
|
|
1737
|
+
};
|
|
1738
|
+
var latin12 = (bytes, a2, b) => {
|
|
1739
|
+
let s = "";
|
|
1740
|
+
for (let i = a2; i < b; i++) s += String.fromCharCode(bytes[i]);
|
|
1741
|
+
return s;
|
|
1742
|
+
};
|
|
1743
|
+
var toBytes = (s) => {
|
|
1744
|
+
const out = new Uint8Array(s.length);
|
|
1745
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
1746
|
+
return out;
|
|
1747
|
+
};
|
|
1748
|
+
var hexString = (bytes) => `<${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}>`;
|
|
1749
|
+
function rebuild(src, splices, prepend = "", append = "") {
|
|
1750
|
+
const sorted = [...splices].sort((a2, b) => a2.start - b.start || a2.end - a2.start - (b.end - b.start));
|
|
1751
|
+
let out = prepend ? `${prepend}
|
|
1752
|
+
` : "";
|
|
1753
|
+
let pos = 0;
|
|
1754
|
+
for (const s of sorted) {
|
|
1755
|
+
if (s.start < pos) continue;
|
|
1756
|
+
out += latin12(src, pos, s.start);
|
|
1757
|
+
if (s.text) out += `
|
|
1758
|
+
${s.text}
|
|
1759
|
+
`;
|
|
1760
|
+
pos = s.end;
|
|
1761
|
+
}
|
|
1762
|
+
out += latin12(src, pos, src.length);
|
|
1763
|
+
if (append) out += `
|
|
1764
|
+
${append}
|
|
1765
|
+
`;
|
|
1766
|
+
return toBytes(out);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// ../core/src/bake/color.ts
|
|
1770
|
+
function hexToRg(hex) {
|
|
1771
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
1772
|
+
if (!m) return void 0;
|
|
1773
|
+
const v = parseInt(m[1], 16);
|
|
1774
|
+
const c = (n) => fmt(n / 255);
|
|
1775
|
+
return `${c(v >> 16 & 255)} ${c(v >> 8 & 255)} ${c(v & 255)} rg`;
|
|
1776
|
+
}
|
|
1777
|
+
function rawFillToRgb(raw) {
|
|
1778
|
+
if (!raw) return null;
|
|
1779
|
+
const nums = (raw.match(/-?\d*\.?\d+/g) ?? []).map(Number);
|
|
1780
|
+
if (/\brg\b/.test(raw) && nums.length >= 3) return { r: nums[0], g: nums[1], b: nums[2] };
|
|
1781
|
+
if (/\bg\b/.test(raw) && !/\brg\b/.test(raw) && nums.length >= 1) return { r: nums[0], g: nums[0], b: nums[0] };
|
|
1782
|
+
if (/\bk\b/.test(raw) && nums.length >= 4) {
|
|
1783
|
+
const [c, m, y, kk] = nums;
|
|
1784
|
+
return { r: (1 - c) * (1 - kk), g: (1 - m) * (1 - kk), b: (1 - y) * (1 - kk) };
|
|
1785
|
+
}
|
|
1786
|
+
if (nums.length >= 3) return { r: nums[nums.length - 3], g: nums[nums.length - 2], b: nums[nums.length - 1] };
|
|
1787
|
+
if (nums.length === 1) return { r: nums[0], g: nums[0], b: nums[0] };
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
var hexToRgbObj = (hex) => {
|
|
1791
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
1792
|
+
const v = m ? parseInt(m[1], 16) : 0;
|
|
1793
|
+
return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255 };
|
|
1794
|
+
};
|
|
1795
|
+
var rgbToHex = (c) => {
|
|
1796
|
+
const h = (v) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, "0");
|
|
1797
|
+
return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
|
|
1798
|
+
};
|
|
1799
|
+
function isWhiteFill(rawFill) {
|
|
1800
|
+
const toks = rawFill.trim().split(/\s+/);
|
|
1801
|
+
if (toks.length < 2) return false;
|
|
1802
|
+
const nums = toks.filter((t) => /^[-+.\d]/.test(t)).map(Number).filter(Number.isFinite);
|
|
1803
|
+
const op = toks[toks.length - 1];
|
|
1804
|
+
if (op === "g" && nums.length >= 1) return nums[nums.length - 1] >= 0.99;
|
|
1805
|
+
if (op === "rg" && nums.length >= 3) return nums.slice(-3).every((v) => v >= 0.99);
|
|
1806
|
+
if (op === "k" && nums.length >= 4) return nums.slice(-4).every((v) => v <= 0.01);
|
|
1807
|
+
if ((op === "sc" || op === "scn") && nums.length >= 1) {
|
|
1808
|
+
const vals = nums.slice(-Math.min(nums.length, 3));
|
|
1809
|
+
return vals.every((v) => v >= 0.99);
|
|
1810
|
+
}
|
|
1811
|
+
return false;
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// ../core/src/bake/textWalk.ts
|
|
1815
|
+
var IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
1816
|
+
function mul(m, n) {
|
|
1817
|
+
return [
|
|
1818
|
+
m[0] * n[0] + m[1] * n[2],
|
|
1819
|
+
m[0] * n[1] + m[1] * n[3],
|
|
1820
|
+
m[2] * n[0] + m[3] * n[2],
|
|
1821
|
+
m[2] * n[1] + m[3] * n[3],
|
|
1822
|
+
m[4] * n[0] + m[5] * n[2] + n[4],
|
|
1823
|
+
m[4] * n[1] + m[5] * n[3] + n[5]
|
|
1824
|
+
];
|
|
1825
|
+
}
|
|
1826
|
+
function invert(m) {
|
|
1827
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
1828
|
+
if (!det) return null;
|
|
1829
|
+
const ia = m[3] / det;
|
|
1830
|
+
const ib = -m[1] / det;
|
|
1831
|
+
const ic = -m[2] / det;
|
|
1832
|
+
const id = m[0] / det;
|
|
1833
|
+
return [ia, ib, ic, id, -(m[4] * ia + m[5] * ic), -(m[4] * ib + m[5] * id)];
|
|
1834
|
+
}
|
|
1835
|
+
function walkContent(src) {
|
|
1836
|
+
const ops = tokenizeContentStream(src);
|
|
1837
|
+
const shows = [];
|
|
1838
|
+
const xobjects = [];
|
|
1839
|
+
const fillRects = [];
|
|
1840
|
+
let simple = null;
|
|
1841
|
+
const isRectPoly = (pts) => {
|
|
1842
|
+
if (pts.length !== 4) return false;
|
|
1843
|
+
for (let i = 0; i < 4; i++) {
|
|
1844
|
+
const [x1, y1] = pts[i];
|
|
1845
|
+
const [x2, y2] = pts[(i + 1) % 4];
|
|
1846
|
+
if (Math.abs(y2 - y1) > 0.01 && Math.abs(x2 - x1) > 0.01) return false;
|
|
1847
|
+
}
|
|
1848
|
+
return true;
|
|
1849
|
+
};
|
|
1850
|
+
let ctm = IDENTITY;
|
|
1851
|
+
let clip = null;
|
|
1852
|
+
let clipPending = false;
|
|
1853
|
+
const stack = [];
|
|
1854
|
+
let tm = IDENTITY;
|
|
1855
|
+
let tlm = IDENTITY;
|
|
1856
|
+
let fontName = "";
|
|
1857
|
+
let fontSize = 0;
|
|
1858
|
+
let charSpacing = 0;
|
|
1859
|
+
let wordSpacing = 0;
|
|
1860
|
+
let hScale = 100;
|
|
1861
|
+
let leading = 0;
|
|
1862
|
+
let stale = false;
|
|
1863
|
+
let fillColorRaw = "";
|
|
1864
|
+
let csRaw = "";
|
|
1865
|
+
let backstop = null;
|
|
1866
|
+
let pathStart = null;
|
|
1867
|
+
const markContent = (at) => {
|
|
1868
|
+
if (!backstop) backstop = at;
|
|
1869
|
+
};
|
|
1870
|
+
const raw = (rec) => {
|
|
1871
|
+
let s = "";
|
|
1872
|
+
for (let k = rec.start; k < rec.end; k++) s += String.fromCharCode(src[k]);
|
|
1873
|
+
return s;
|
|
1874
|
+
};
|
|
1875
|
+
const num = (rec, idx) => {
|
|
1876
|
+
const t = rec.operands[idx];
|
|
1877
|
+
return t && t.kind === "num" ? t.value : 0;
|
|
1878
|
+
};
|
|
1879
|
+
const setTd = (tx, ty) => {
|
|
1880
|
+
tlm = mul([1, 0, 0, 1, tx, ty], tlm);
|
|
1881
|
+
tm = tlm;
|
|
1882
|
+
stale = false;
|
|
1883
|
+
};
|
|
1884
|
+
const record = (rec, op) => {
|
|
1885
|
+
const m = mul(tm, ctm);
|
|
1886
|
+
shows.push({
|
|
1887
|
+
record: rec,
|
|
1888
|
+
op,
|
|
1889
|
+
x: m[4],
|
|
1890
|
+
y: m[5],
|
|
1891
|
+
matrix: m,
|
|
1892
|
+
ctm,
|
|
1893
|
+
fontName,
|
|
1894
|
+
fontSize,
|
|
1895
|
+
charSpacing,
|
|
1896
|
+
wordSpacing,
|
|
1897
|
+
hScale,
|
|
1898
|
+
fillColorRaw,
|
|
1899
|
+
stale,
|
|
1900
|
+
clip
|
|
1901
|
+
});
|
|
1902
|
+
stale = true;
|
|
1903
|
+
};
|
|
1904
|
+
const absRectOf = (p) => {
|
|
1905
|
+
const m = p.ctm;
|
|
1906
|
+
const corners = p.rect ? [[p.rect[0], p.rect[1]], [p.rect[0] + p.rect[2], p.rect[1] + p.rect[3]]] : isRectPoly(p.pts) ? p.pts : [];
|
|
1907
|
+
if (!corners.length || Math.abs(m[1]) > 0.01 || Math.abs(m[2]) > 0.01) return null;
|
|
1908
|
+
const xs = corners.map(([px]) => m[0] * px + m[4]);
|
|
1909
|
+
const ys = corners.map(([, py]) => m[3] * py + m[5]);
|
|
1910
|
+
const x = Math.min(...xs);
|
|
1911
|
+
const y = Math.min(...ys);
|
|
1912
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
1913
|
+
};
|
|
1914
|
+
const consumeClip = () => {
|
|
1915
|
+
if (!clipPending) return;
|
|
1916
|
+
clipPending = false;
|
|
1917
|
+
const r2 = simple?.valid ? absRectOf(simple) : null;
|
|
1918
|
+
if (!r2) return;
|
|
1919
|
+
if (!clip) {
|
|
1920
|
+
clip = r2;
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
const x = Math.max(clip.x, r2.x);
|
|
1924
|
+
const y = Math.max(clip.y, r2.y);
|
|
1925
|
+
clip = {
|
|
1926
|
+
x,
|
|
1927
|
+
y,
|
|
1928
|
+
width: Math.max(0, Math.min(clip.x + clip.width, r2.x + r2.width) - x),
|
|
1929
|
+
height: Math.max(0, Math.min(clip.y + clip.height, r2.y + r2.height) - y)
|
|
1930
|
+
};
|
|
1931
|
+
};
|
|
1932
|
+
for (const rec of ops) {
|
|
1933
|
+
switch (rec.op) {
|
|
1934
|
+
case "q":
|
|
1935
|
+
stack.push({ ctm, clip });
|
|
1936
|
+
break;
|
|
1937
|
+
case "Q": {
|
|
1938
|
+
const s = stack.pop();
|
|
1939
|
+
ctm = s?.ctm ?? IDENTITY;
|
|
1940
|
+
clip = s?.clip ?? null;
|
|
1941
|
+
break;
|
|
1942
|
+
}
|
|
1943
|
+
case "cm":
|
|
1944
|
+
ctm = mul([num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3), num(rec, 4), num(rec, 5)], ctm);
|
|
1945
|
+
break;
|
|
1946
|
+
// ── backstop: construcción y pintado de paths ──
|
|
1947
|
+
case "m":
|
|
1948
|
+
if (!pathStart) pathStart = { offset: rec.start, ctm };
|
|
1949
|
+
if (simple) simple.valid = false;
|
|
1950
|
+
else simple = { start: rec.start, ctm, fill: fillColorRaw, pts: [[num(rec, 0), num(rec, 1)]], valid: true };
|
|
1951
|
+
break;
|
|
1952
|
+
case "l":
|
|
1953
|
+
if (simple && simple.valid && !simple.rect && simple.pts.length < 5) simple.pts.push([num(rec, 0), num(rec, 1)]);
|
|
1954
|
+
else if (simple) simple.valid = false;
|
|
1955
|
+
break;
|
|
1956
|
+
case "c":
|
|
1957
|
+
case "v":
|
|
1958
|
+
case "y":
|
|
1959
|
+
if (simple) simple.valid = false;
|
|
1960
|
+
break;
|
|
1961
|
+
case "h":
|
|
1962
|
+
break;
|
|
1963
|
+
// cerrar el subpath no cambia el bbox
|
|
1964
|
+
case "re":
|
|
1965
|
+
if (!pathStart) pathStart = { offset: rec.start, ctm };
|
|
1966
|
+
if (simple) simple.valid = false;
|
|
1967
|
+
else simple = { start: rec.start, ctm, fill: fillColorRaw, rect: [num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3)], pts: [], valid: true };
|
|
1968
|
+
break;
|
|
1969
|
+
case "W":
|
|
1970
|
+
case "W*":
|
|
1971
|
+
clipPending = true;
|
|
1972
|
+
break;
|
|
1973
|
+
case "n":
|
|
1974
|
+
consumeClip();
|
|
1975
|
+
pathStart = null;
|
|
1976
|
+
simple = null;
|
|
1977
|
+
break;
|
|
1978
|
+
// solo clip — no es contenido
|
|
1979
|
+
case "f":
|
|
1980
|
+
case "F":
|
|
1981
|
+
case "f*":
|
|
1982
|
+
case "b":
|
|
1983
|
+
case "b*":
|
|
1984
|
+
case "B":
|
|
1985
|
+
case "B*": {
|
|
1986
|
+
consumeClip();
|
|
1987
|
+
if (!isWhiteFill(fillColorRaw)) markContent(pathStart ?? { offset: rec.start, ctm });
|
|
1988
|
+
if (simple?.valid) {
|
|
1989
|
+
const r2 = absRectOf(simple);
|
|
1990
|
+
if (r2) fillRects.push({ start: simple.start, end: rec.end, ...r2, fillColorRaw: simple.fill });
|
|
1991
|
+
}
|
|
1992
|
+
pathStart = null;
|
|
1993
|
+
simple = null;
|
|
1994
|
+
break;
|
|
1995
|
+
}
|
|
1996
|
+
case "S":
|
|
1997
|
+
case "s":
|
|
1998
|
+
consumeClip();
|
|
1999
|
+
markContent(pathStart ?? { offset: rec.start, ctm });
|
|
2000
|
+
pathStart = null;
|
|
2001
|
+
simple = null;
|
|
2002
|
+
break;
|
|
2003
|
+
case "sh":
|
|
2004
|
+
markContent({ offset: rec.start, ctm });
|
|
2005
|
+
break;
|
|
2006
|
+
case "BI":
|
|
2007
|
+
markContent({ offset: rec.start, ctm });
|
|
2008
|
+
break;
|
|
2009
|
+
case "BT":
|
|
2010
|
+
markContent({ offset: rec.start, ctm });
|
|
2011
|
+
tm = IDENTITY;
|
|
2012
|
+
tlm = IDENTITY;
|
|
2013
|
+
stale = false;
|
|
2014
|
+
break;
|
|
2015
|
+
case "ET":
|
|
2016
|
+
break;
|
|
2017
|
+
case "Tf": {
|
|
2018
|
+
const t = rec.operands[0];
|
|
2019
|
+
fontName = t && t.kind === "name" ? t.value : fontName;
|
|
2020
|
+
fontSize = num(rec, 1);
|
|
2021
|
+
break;
|
|
2022
|
+
}
|
|
2023
|
+
case "Tc":
|
|
2024
|
+
charSpacing = num(rec, 0);
|
|
2025
|
+
break;
|
|
2026
|
+
case "Tw":
|
|
2027
|
+
wordSpacing = num(rec, 0);
|
|
2028
|
+
break;
|
|
2029
|
+
case "Tz":
|
|
2030
|
+
hScale = num(rec, 0);
|
|
2031
|
+
break;
|
|
2032
|
+
case "TL":
|
|
2033
|
+
leading = num(rec, 0);
|
|
2034
|
+
break;
|
|
2035
|
+
case "Td":
|
|
2036
|
+
setTd(num(rec, 0), num(rec, 1));
|
|
2037
|
+
break;
|
|
2038
|
+
case "TD":
|
|
2039
|
+
leading = -num(rec, 1);
|
|
2040
|
+
setTd(num(rec, 0), num(rec, 1));
|
|
2041
|
+
break;
|
|
2042
|
+
case "Tm":
|
|
2043
|
+
tlm = [num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3), num(rec, 4), num(rec, 5)];
|
|
2044
|
+
tm = tlm;
|
|
2045
|
+
stale = false;
|
|
2046
|
+
break;
|
|
2047
|
+
case "T*":
|
|
2048
|
+
setTd(0, -leading);
|
|
2049
|
+
break;
|
|
2050
|
+
case "Tj":
|
|
2051
|
+
record(rec, "Tj");
|
|
2052
|
+
break;
|
|
2053
|
+
case "TJ":
|
|
2054
|
+
record(rec, "TJ");
|
|
2055
|
+
break;
|
|
2056
|
+
case "'":
|
|
2057
|
+
setTd(0, -leading);
|
|
2058
|
+
record(rec, "'");
|
|
2059
|
+
break;
|
|
2060
|
+
case '"':
|
|
2061
|
+
wordSpacing = num(rec, 0);
|
|
2062
|
+
charSpacing = num(rec, 1);
|
|
2063
|
+
setTd(0, -leading);
|
|
2064
|
+
record(rec, '"');
|
|
2065
|
+
break;
|
|
2066
|
+
case "g":
|
|
2067
|
+
case "rg":
|
|
2068
|
+
case "k":
|
|
2069
|
+
fillColorRaw = raw(rec);
|
|
2070
|
+
break;
|
|
2071
|
+
case "cs":
|
|
2072
|
+
csRaw = raw(rec);
|
|
2073
|
+
break;
|
|
2074
|
+
case "sc":
|
|
2075
|
+
case "scn":
|
|
2076
|
+
fillColorRaw = csRaw ? `${csRaw} ${raw(rec)}` : raw(rec);
|
|
2077
|
+
break;
|
|
2078
|
+
case "Do": {
|
|
2079
|
+
const t = rec.operands[0];
|
|
2080
|
+
if (t && t.kind === "name") xobjects.push({ record: rec, name: t.value, matrix: ctm });
|
|
2081
|
+
markContent({ offset: rec.start, ctm });
|
|
2082
|
+
break;
|
|
2083
|
+
}
|
|
2084
|
+
default:
|
|
2085
|
+
break;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return { shows, xobjects, fillRects, backstop: backstop ?? { offset: 0, ctm: IDENTITY } };
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
// ../core/src/bake/pageContent.ts
|
|
2092
|
+
import {
|
|
2093
|
+
PDFArray,
|
|
2094
|
+
PDFName,
|
|
2095
|
+
PDFRawStream,
|
|
2096
|
+
PDFRef,
|
|
2097
|
+
decodePDFRawStream
|
|
2098
|
+
} from "pdf-lib";
|
|
2099
|
+
function pageContentBytes(doc, page) {
|
|
2100
|
+
const ctx = doc.context;
|
|
2101
|
+
const resolve = (o) => o instanceof PDFRef ? ctx.lookup(o) : o;
|
|
2102
|
+
const contents = resolve(page.node.get(PDFName.of("Contents")));
|
|
2103
|
+
const streams = [];
|
|
2104
|
+
if (contents instanceof PDFArray) {
|
|
2105
|
+
for (let i = 0; i < contents.size(); i++) {
|
|
2106
|
+
const s = resolve(contents.get(i));
|
|
2107
|
+
if (s instanceof PDFRawStream) streams.push(s);
|
|
2108
|
+
else throw new Error("Content stream no soportado (no es PDFRawStream).");
|
|
2109
|
+
}
|
|
2110
|
+
} else if (contents instanceof PDFRawStream) {
|
|
2111
|
+
streams.push(contents);
|
|
2112
|
+
} else if (contents != null) {
|
|
2113
|
+
throw new Error("Content stream no soportado.");
|
|
2114
|
+
}
|
|
2115
|
+
const parts = streams.map((s) => decodePDFRawStream(s).decode());
|
|
2116
|
+
const total = parts.reduce((a2, p) => a2 + p.length + 1, 0);
|
|
2117
|
+
const out = new Uint8Array(total);
|
|
2118
|
+
let off = 0;
|
|
2119
|
+
for (const p of parts) {
|
|
2120
|
+
out.set(p, off);
|
|
2121
|
+
off += p.length;
|
|
2122
|
+
out[off++] = 10;
|
|
2123
|
+
}
|
|
2124
|
+
return out;
|
|
2125
|
+
}
|
|
2126
|
+
function setPageContents(doc, page, bytes) {
|
|
2127
|
+
const stream = doc.context.stream(bytes);
|
|
2128
|
+
const ref = doc.context.register(stream);
|
|
2129
|
+
page.node.set(PDFName.of("Contents"), ref);
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// ../core/src/bake/locate.ts
|
|
2133
|
+
import { PDFDict, PDFName as PDFName2, PDFRawStream as PDFRawStream2, PDFRef as PDFRef2 } from "pdf-lib";
|
|
2134
|
+
var Y_TOL = 1.8;
|
|
2135
|
+
var X_TOL = 1.8;
|
|
2136
|
+
function xobjectRect(m) {
|
|
2137
|
+
const [a2, b, c, d, e, f] = m;
|
|
2138
|
+
const xs = [e, a2 + e, c + e, a2 + c + e];
|
|
2139
|
+
const ys = [f, b + f, d + f, b + d + f];
|
|
2140
|
+
const x = Math.min(...xs);
|
|
2141
|
+
const y = Math.min(...ys);
|
|
2142
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y, rotated: Math.abs(b) > 0.01 || Math.abs(c) > 0.01 };
|
|
2143
|
+
}
|
|
2144
|
+
function imageResourceNames(doc, page) {
|
|
2145
|
+
const out = /* @__PURE__ */ new Set();
|
|
2146
|
+
try {
|
|
2147
|
+
const res = page.node.Resources();
|
|
2148
|
+
const xo = res?.lookup(PDFName2.of("XObject"));
|
|
2149
|
+
if (!(xo instanceof PDFDict)) return out;
|
|
2150
|
+
for (const [key, val] of xo.entries()) {
|
|
2151
|
+
const obj = val instanceof PDFRef2 ? doc.context.lookup(val) : val;
|
|
2152
|
+
const dict = obj instanceof PDFRawStream2 ? obj.dict : obj instanceof PDFDict ? obj : null;
|
|
2153
|
+
if (dict?.get(PDFName2.of("Subtype")) === PDFName2.of("Image")) {
|
|
2154
|
+
out.add(key.toString().replace(/^\//, ""));
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
} catch {
|
|
2158
|
+
}
|
|
2159
|
+
return out;
|
|
2160
|
+
}
|
|
2161
|
+
function matchImage(xobjects, orig) {
|
|
2162
|
+
const tol = Math.max(2, orig.width * 0.02, orig.height * 0.02);
|
|
2163
|
+
return xobjects.find((o) => {
|
|
2164
|
+
const r2 = xobjectRect(o.matrix);
|
|
2165
|
+
return Math.abs(r2.x - orig.x) <= tol && Math.abs(r2.y - orig.y) <= tol && Math.abs(r2.width - orig.width) <= tol && Math.abs(r2.height - orig.height) <= tol;
|
|
2166
|
+
}) ?? null;
|
|
2167
|
+
}
|
|
2168
|
+
function matchOps(shows, orig) {
|
|
2169
|
+
const lines = orig.baselines?.length ? orig.baselines : [orig.baseline];
|
|
2170
|
+
const inLine = shows.filter((s) => lines.some((b) => Math.abs(s.y - b) <= Y_TOL));
|
|
2171
|
+
if (inLine.some((s) => s.stale)) {
|
|
2172
|
+
return { ops: [], conflict: "la l\xEDnea tiene shows encadenados sin reposicionar (x desconocida sin widths)" };
|
|
2173
|
+
}
|
|
2174
|
+
const inside = inLine.filter((s) => s.x >= orig.x - X_TOL && s.x <= orig.x + orig.width + X_TOL);
|
|
2175
|
+
if (!inside.length) {
|
|
2176
|
+
return { ops: [], conflict: "ning\xFAn operador de texto arranca dentro del segmento (\xBFun TJ de otra columna lo contiene?)" };
|
|
2177
|
+
}
|
|
2178
|
+
return { ops: inside, conflict: null };
|
|
2179
|
+
}
|
|
2180
|
+
|
|
2181
|
+
// ../core/src/bake/images.ts
|
|
2182
|
+
function applyImageEditsToPage(args) {
|
|
2183
|
+
const { doc, page, pageImgEdits, xobjects, backstop, splices, appendBlocks, report } = args;
|
|
2184
|
+
if (!pageImgEdits.length) return;
|
|
2185
|
+
const imgNames = imageResourceNames(doc, page);
|
|
2186
|
+
const imageOps = xobjects.filter((o) => imgNames.has(o.name));
|
|
2187
|
+
for (const edit of pageImgEdits) {
|
|
2188
|
+
const op = matchImage(imageOps, edit.original);
|
|
2189
|
+
if (!op) {
|
|
2190
|
+
report.warn(`${edit.imageId}: no se encontr\xF3 el XObject en la posici\xF3n original \u2014 sin cambios`);
|
|
2191
|
+
continue;
|
|
2192
|
+
}
|
|
2193
|
+
if (edit.remove) {
|
|
2194
|
+
splices.push({ start: op.record.start, end: op.record.end, text: "" });
|
|
2195
|
+
report.apply(`${edit.imageId}: eliminada`);
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
const r2 = xobjectRect(op.matrix);
|
|
2199
|
+
if (r2.rotated) {
|
|
2200
|
+
report.warn(`${edit.imageId}: la imagen tiene rotaci\xF3n \u2014 mover/escalar no soportado a\xFAn, queda intacta`);
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
const [a2, , , d] = op.matrix;
|
|
2204
|
+
const newW = edit.width ?? r2.width;
|
|
2205
|
+
const newH = edit.height ?? r2.height;
|
|
2206
|
+
const newX = edit.x ?? r2.x;
|
|
2207
|
+
const newY = edit.y ?? r2.y;
|
|
2208
|
+
const na = a2 * (newW / r2.width);
|
|
2209
|
+
const nd = d * (newH / r2.height);
|
|
2210
|
+
const ne = newX - Math.min(0, na);
|
|
2211
|
+
const nf = newY - Math.min(0, nd);
|
|
2212
|
+
const abs = [na, 0, 0, nd, ne, nf];
|
|
2213
|
+
if (edit.zOrder) {
|
|
2214
|
+
splices.push({ start: op.record.start, end: op.record.end, text: "" });
|
|
2215
|
+
if (edit.zOrder === "back") {
|
|
2216
|
+
const binv = invert(backstop.ctm);
|
|
2217
|
+
const m = binv ? mul(abs, binv) : abs;
|
|
2218
|
+
const block = `q ${fmt(m[0])} ${fmt(m[1])} ${fmt(m[2])} ${fmt(m[3])} ${fmt(m[4])} ${fmt(m[5])} cm /${op.name} Do Q`;
|
|
2219
|
+
splices.push({ start: backstop.offset, end: backstop.offset, text: block });
|
|
2220
|
+
} else {
|
|
2221
|
+
appendBlocks.push(`q ${fmt(abs[0])} ${fmt(abs[1])} ${fmt(abs[2])} ${fmt(abs[3])} ${fmt(abs[4])} ${fmt(abs[5])} cm /${op.name} Do Q`);
|
|
2222
|
+
}
|
|
2223
|
+
report.apply(`${edit.imageId}: enviada ${edit.zOrder === "back" ? "al fondo" : "al frente"}`);
|
|
2224
|
+
continue;
|
|
2225
|
+
}
|
|
2226
|
+
const inv = invert(op.matrix);
|
|
2227
|
+
if (!inv) {
|
|
2228
|
+
report.warn(`${edit.imageId}: matriz degenerada \u2014 sin cambios`);
|
|
2229
|
+
continue;
|
|
2230
|
+
}
|
|
2231
|
+
const rel = mul(abs, inv);
|
|
2232
|
+
splices.push({
|
|
2233
|
+
start: op.record.start,
|
|
2234
|
+
end: op.record.end,
|
|
2235
|
+
text: `q ${fmt(rel[0])} ${fmt(rel[1])} ${fmt(rel[2])} ${fmt(rel[3])} ${fmt(rel[4])} ${fmt(rel[5])} cm /${op.name} Do Q`
|
|
2236
|
+
});
|
|
2237
|
+
report.apply(`${edit.imageId}: reubicada/escalada`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
// ../core/src/bake/fonts.ts
|
|
2242
|
+
import { PDFArray as PDFArray2, PDFDict as PDFDict2, PDFName as PDFName3, PDFNumber, PDFRawStream as PDFRawStream3, StandardFonts, decodePDFRawStream as decodePDFRawStream2 } from "pdf-lib";
|
|
2243
|
+
|
|
2244
|
+
// ../core/src/bake/toUnicode.ts
|
|
2245
|
+
var hexToBytes = (hex) => {
|
|
2246
|
+
const out = [];
|
|
2247
|
+
for (let i = 0; i + 1 < hex.length; i += 2) out.push(parseInt(hex.slice(i, i + 2), 16));
|
|
2248
|
+
return out;
|
|
2249
|
+
};
|
|
2250
|
+
var bytesToUnicode = (bytes) => {
|
|
2251
|
+
const units = [];
|
|
2252
|
+
for (let i = 0; i + 1 < bytes.length; i += 2) units.push(bytes[i] << 8 | bytes[i + 1]);
|
|
2253
|
+
return String.fromCharCode(...units);
|
|
2254
|
+
};
|
|
2255
|
+
function parseToUnicode(cmapText) {
|
|
2256
|
+
const map = /* @__PURE__ */ new Map();
|
|
2257
|
+
const hexRe = /<([0-9a-fA-F]+)>/g;
|
|
2258
|
+
const takeHexes = (chunk) => {
|
|
2259
|
+
const out = [];
|
|
2260
|
+
let m2;
|
|
2261
|
+
hexRe.lastIndex = 0;
|
|
2262
|
+
while (m2 = hexRe.exec(chunk)) out.push(m2[1]);
|
|
2263
|
+
return out;
|
|
2264
|
+
};
|
|
2265
|
+
const bfcharRe = /beginbfchar([\s\S]*?)endbfchar/g;
|
|
2266
|
+
let m;
|
|
2267
|
+
while (m = bfcharRe.exec(cmapText)) {
|
|
2268
|
+
const hexes = takeHexes(m[1]);
|
|
2269
|
+
for (let i = 0; i + 1 < hexes.length; i += 2) {
|
|
2270
|
+
const uni = bytesToUnicode(hexToBytes(hexes[i + 1]));
|
|
2271
|
+
if (uni && !map.has(uni)) map.set(uni, hexToBytes(hexes[i]));
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const bfrangeRe = /beginbfrange([\s\S]*?)endbfrange/g;
|
|
2275
|
+
while (m = bfrangeRe.exec(cmapText)) {
|
|
2276
|
+
const body = m[1];
|
|
2277
|
+
const lineRe = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>\s*(<[0-9a-fA-F]+>|\[[\s\S]*?\])/g;
|
|
2278
|
+
let lm;
|
|
2279
|
+
while (lm = lineRe.exec(body)) {
|
|
2280
|
+
const lo = parseInt(lm[1], 16);
|
|
2281
|
+
const hi = parseInt(lm[2], 16);
|
|
2282
|
+
const codeLen = Math.ceil(lm[1].length / 2);
|
|
2283
|
+
const dst = lm[3];
|
|
2284
|
+
const codeBytes = (code) => {
|
|
2285
|
+
const out = [];
|
|
2286
|
+
for (let k = codeLen - 1; k >= 0; k--) out.push(code >> 8 * k & 255);
|
|
2287
|
+
return out;
|
|
2288
|
+
};
|
|
2289
|
+
if (dst.startsWith("[")) {
|
|
2290
|
+
const dsts = takeHexes(dst);
|
|
2291
|
+
for (let c = lo, idx = 0; c <= hi && idx < dsts.length; c++, idx++) {
|
|
2292
|
+
const uni = bytesToUnicode(hexToBytes(dsts[idx]));
|
|
2293
|
+
if (uni && !map.has(uni)) map.set(uni, codeBytes(c));
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
const startBytes = hexToBytes(dst.replace(/[<>]/g, ""));
|
|
2297
|
+
const startUnits = [];
|
|
2298
|
+
for (let i = 0; i + 1 < startBytes.length; i += 2) startUnits.push(startBytes[i] << 8 | startBytes[i + 1]);
|
|
2299
|
+
for (let c = lo; c <= hi && c - lo < 65536; c++) {
|
|
2300
|
+
const units = [...startUnits];
|
|
2301
|
+
units[units.length - 1] += c - lo;
|
|
2302
|
+
const uni = String.fromCharCode(...units);
|
|
2303
|
+
if (uni && !map.has(uni)) map.set(uni, codeBytes(c));
|
|
2304
|
+
}
|
|
2305
|
+
}
|
|
2306
|
+
}
|
|
2307
|
+
}
|
|
2308
|
+
return {
|
|
2309
|
+
encode(text) {
|
|
2310
|
+
const out = [];
|
|
2311
|
+
for (const ch of text) {
|
|
2312
|
+
const bytes = map.get(ch);
|
|
2313
|
+
if (!bytes) return null;
|
|
2314
|
+
out.push(...bytes);
|
|
2315
|
+
}
|
|
2316
|
+
return Uint8Array.from(out);
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
}
|
|
2320
|
+
var MAC_ROMAN_HI = [
|
|
2321
|
+
196,
|
|
2322
|
+
197,
|
|
2323
|
+
199,
|
|
2324
|
+
201,
|
|
2325
|
+
209,
|
|
2326
|
+
214,
|
|
2327
|
+
220,
|
|
2328
|
+
225,
|
|
2329
|
+
224,
|
|
2330
|
+
226,
|
|
2331
|
+
228,
|
|
2332
|
+
227,
|
|
2333
|
+
229,
|
|
2334
|
+
231,
|
|
2335
|
+
233,
|
|
2336
|
+
232,
|
|
2337
|
+
234,
|
|
2338
|
+
235,
|
|
2339
|
+
237,
|
|
2340
|
+
236,
|
|
2341
|
+
238,
|
|
2342
|
+
239,
|
|
2343
|
+
241,
|
|
2344
|
+
243,
|
|
2345
|
+
242,
|
|
2346
|
+
244,
|
|
2347
|
+
246,
|
|
2348
|
+
245,
|
|
2349
|
+
250,
|
|
2350
|
+
249,
|
|
2351
|
+
251,
|
|
2352
|
+
252,
|
|
2353
|
+
8224,
|
|
2354
|
+
176,
|
|
2355
|
+
162,
|
|
2356
|
+
163,
|
|
2357
|
+
167,
|
|
2358
|
+
8226,
|
|
2359
|
+
182,
|
|
2360
|
+
223,
|
|
2361
|
+
174,
|
|
2362
|
+
169,
|
|
2363
|
+
8482,
|
|
2364
|
+
180,
|
|
2365
|
+
168,
|
|
2366
|
+
8800,
|
|
2367
|
+
198,
|
|
2368
|
+
216,
|
|
2369
|
+
8734,
|
|
2370
|
+
177,
|
|
2371
|
+
8804,
|
|
2372
|
+
8805,
|
|
2373
|
+
165,
|
|
2374
|
+
181,
|
|
2375
|
+
8706,
|
|
2376
|
+
8721,
|
|
2377
|
+
8719,
|
|
2378
|
+
960,
|
|
2379
|
+
8747,
|
|
2380
|
+
170,
|
|
2381
|
+
186,
|
|
2382
|
+
937,
|
|
2383
|
+
230,
|
|
2384
|
+
248,
|
|
2385
|
+
191,
|
|
2386
|
+
161,
|
|
2387
|
+
172,
|
|
2388
|
+
8730,
|
|
2389
|
+
402,
|
|
2390
|
+
8776,
|
|
2391
|
+
8710,
|
|
2392
|
+
171,
|
|
2393
|
+
187,
|
|
2394
|
+
8230,
|
|
2395
|
+
160,
|
|
2396
|
+
192,
|
|
2397
|
+
195,
|
|
2398
|
+
213,
|
|
2399
|
+
338,
|
|
2400
|
+
339,
|
|
2401
|
+
8211,
|
|
2402
|
+
8212,
|
|
2403
|
+
8220,
|
|
2404
|
+
8221,
|
|
2405
|
+
8216,
|
|
2406
|
+
8217,
|
|
2407
|
+
247,
|
|
2408
|
+
9674,
|
|
2409
|
+
255,
|
|
2410
|
+
376,
|
|
2411
|
+
8260,
|
|
2412
|
+
8364,
|
|
2413
|
+
8249,
|
|
2414
|
+
8250,
|
|
2415
|
+
64257,
|
|
2416
|
+
64258,
|
|
2417
|
+
8225,
|
|
2418
|
+
183,
|
|
2419
|
+
8218,
|
|
2420
|
+
8222,
|
|
2421
|
+
8240,
|
|
2422
|
+
194,
|
|
2423
|
+
202,
|
|
2424
|
+
193,
|
|
2425
|
+
203,
|
|
2426
|
+
200,
|
|
2427
|
+
205,
|
|
2428
|
+
206,
|
|
2429
|
+
207,
|
|
2430
|
+
204,
|
|
2431
|
+
211,
|
|
2432
|
+
212,
|
|
2433
|
+
63743,
|
|
2434
|
+
210,
|
|
2435
|
+
218,
|
|
2436
|
+
219,
|
|
2437
|
+
217,
|
|
2438
|
+
305,
|
|
2439
|
+
710,
|
|
2440
|
+
732,
|
|
2441
|
+
175,
|
|
2442
|
+
728,
|
|
2443
|
+
729,
|
|
2444
|
+
730,
|
|
2445
|
+
184,
|
|
2446
|
+
733,
|
|
2447
|
+
731,
|
|
2448
|
+
711
|
|
2449
|
+
];
|
|
2450
|
+
var WIN_ANSI_80_9F = [
|
|
2451
|
+
8364,
|
|
2452
|
+
0,
|
|
2453
|
+
8218,
|
|
2454
|
+
402,
|
|
2455
|
+
8222,
|
|
2456
|
+
8230,
|
|
2457
|
+
8224,
|
|
2458
|
+
8225,
|
|
2459
|
+
710,
|
|
2460
|
+
8240,
|
|
2461
|
+
352,
|
|
2462
|
+
8249,
|
|
2463
|
+
338,
|
|
2464
|
+
0,
|
|
2465
|
+
381,
|
|
2466
|
+
0,
|
|
2467
|
+
0,
|
|
2468
|
+
8216,
|
|
2469
|
+
8217,
|
|
2470
|
+
8220,
|
|
2471
|
+
8221,
|
|
2472
|
+
8226,
|
|
2473
|
+
8211,
|
|
2474
|
+
8212,
|
|
2475
|
+
732,
|
|
2476
|
+
8482,
|
|
2477
|
+
353,
|
|
2478
|
+
8250,
|
|
2479
|
+
339,
|
|
2480
|
+
0,
|
|
2481
|
+
382,
|
|
2482
|
+
376
|
|
2483
|
+
];
|
|
2484
|
+
function encodingChar(encoding, code) {
|
|
2485
|
+
if (code < 128) return String.fromCharCode(code);
|
|
2486
|
+
const cp = encoding === "MacRomanEncoding" ? MAC_ROMAN_HI[code - 128] : code <= 159 ? WIN_ANSI_80_9F[code - 128] : code;
|
|
2487
|
+
return cp ? String.fromCodePoint(cp) : "";
|
|
2488
|
+
}
|
|
2489
|
+
function encoderFromSimpleEncoding(encoding, firstChar, lastChar, widths) {
|
|
2490
|
+
const map = /* @__PURE__ */ new Map();
|
|
2491
|
+
for (let code = firstChar; code <= lastChar && code <= 255; code++) {
|
|
2492
|
+
if (widths && !(widths[code - firstChar] > 0)) continue;
|
|
2493
|
+
const uni = encodingChar(encoding, code);
|
|
2494
|
+
if (uni && !map.has(uni)) map.set(uni, code);
|
|
2495
|
+
}
|
|
2496
|
+
return {
|
|
2497
|
+
encode(text) {
|
|
2498
|
+
const out = [];
|
|
2499
|
+
for (const ch of text) {
|
|
2500
|
+
const code = map.get(ch);
|
|
2501
|
+
if (code === void 0) return null;
|
|
2502
|
+
out.push(code);
|
|
2503
|
+
}
|
|
2504
|
+
return Uint8Array.from(out);
|
|
2505
|
+
}
|
|
2506
|
+
};
|
|
2507
|
+
}
|
|
2508
|
+
|
|
2509
|
+
// ../core/src/bake/fonts.ts
|
|
2510
|
+
var STD_FONTS = {
|
|
2511
|
+
sans: [StandardFonts.Helvetica, StandardFonts.HelveticaBold, StandardFonts.HelveticaOblique, StandardFonts.HelveticaBoldOblique],
|
|
2512
|
+
serif: [StandardFonts.TimesRoman, StandardFonts.TimesRomanBold, StandardFonts.TimesRomanItalic, StandardFonts.TimesRomanBoldItalic],
|
|
2513
|
+
mono: [StandardFonts.Courier, StandardFonts.CourierBold, StandardFonts.CourierOblique, StandardFonts.CourierBoldOblique]
|
|
2514
|
+
};
|
|
2515
|
+
function stdFontFor(bucket, bold, italic) {
|
|
2516
|
+
return STD_FONTS[bucket][(bold ? 1 : 0) + (italic ? 2 : 0)];
|
|
2517
|
+
}
|
|
2518
|
+
function encoderForFont(doc, page, fontName, cache) {
|
|
2519
|
+
const hit = cache.get(fontName);
|
|
2520
|
+
if (hit !== void 0) return hit;
|
|
2521
|
+
let enc = null;
|
|
2522
|
+
try {
|
|
2523
|
+
const res = page.node.Resources();
|
|
2524
|
+
const fonts = res?.lookup(PDFName3.of("Font"));
|
|
2525
|
+
const fdict = fonts instanceof PDFDict2 ? fonts.lookup(PDFName3.of(fontName)) : null;
|
|
2526
|
+
const tu = fdict instanceof PDFDict2 ? fdict.lookup(PDFName3.of("ToUnicode")) : null;
|
|
2527
|
+
if (tu instanceof PDFRawStream3) {
|
|
2528
|
+
const decoded = decodePDFRawStream2(tu).decode();
|
|
2529
|
+
enc = parseToUnicode(latin12(decoded, 0, decoded.length));
|
|
2530
|
+
} else if (fdict instanceof PDFDict2) {
|
|
2531
|
+
enc = simpleEncodingEncoder(fdict);
|
|
2532
|
+
}
|
|
2533
|
+
} catch {
|
|
2534
|
+
enc = null;
|
|
2535
|
+
}
|
|
2536
|
+
cache.set(fontName, enc);
|
|
2537
|
+
return enc;
|
|
2538
|
+
}
|
|
2539
|
+
function simpleEncodingEncoder(fdict) {
|
|
2540
|
+
const subtype = fdict.lookupMaybe(PDFName3.of("Subtype"), PDFName3)?.decodeText();
|
|
2541
|
+
if (subtype !== "TrueType" && subtype !== "Type1") return null;
|
|
2542
|
+
const encRaw = fdict.lookup(PDFName3.of("Encoding"));
|
|
2543
|
+
let encName;
|
|
2544
|
+
if (encRaw instanceof PDFName3) encName = encRaw.decodeText();
|
|
2545
|
+
else if (encRaw instanceof PDFDict2) {
|
|
2546
|
+
if (encRaw.has(PDFName3.of("Differences"))) return null;
|
|
2547
|
+
encName = encRaw.lookupMaybe(PDFName3.of("BaseEncoding"), PDFName3)?.decodeText();
|
|
2548
|
+
}
|
|
2549
|
+
if (encName !== "MacRomanEncoding" && encName !== "WinAnsiEncoding") return null;
|
|
2550
|
+
const firstChar = fdict.lookupMaybe(PDFName3.of("FirstChar"), PDFNumber)?.asNumber();
|
|
2551
|
+
const lastChar = fdict.lookupMaybe(PDFName3.of("LastChar"), PDFNumber)?.asNumber();
|
|
2552
|
+
if (firstChar === void 0 || lastChar === void 0) return null;
|
|
2553
|
+
let widths = null;
|
|
2554
|
+
const w = fdict.lookupMaybe(PDFName3.of("Widths"), PDFArray2);
|
|
2555
|
+
if (w) widths = w.asArray().map((v) => v instanceof PDFNumber ? v.asNumber() : 0);
|
|
2556
|
+
return encoderFromSimpleEncoding(encName, firstChar, lastChar, widths);
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// ../core/src/bake/textEmit.ts
|
|
2560
|
+
function relTm(o, ratio, x, y) {
|
|
2561
|
+
const m = o.matrix;
|
|
2562
|
+
const abs = [m[0] * ratio, m[1] * ratio, m[2] * ratio, m[3] * ratio, x, y];
|
|
2563
|
+
const inv = invert(o.ctm);
|
|
2564
|
+
return inv ? mul(abs, inv) : null;
|
|
2565
|
+
}
|
|
2566
|
+
function reemitBlock(o, src, ratio, x, y, ov = {}) {
|
|
2567
|
+
const show = o.op === "Tj" || o.op === "TJ" ? latin12(src, o.record.start, o.record.end) : o.op === "'" ? `${o.record.operands[0]?.raw ?? "()"} Tj` : `${o.record.operands[2]?.raw ?? "()"} Tj`;
|
|
2568
|
+
const t = relTm(o, ratio, x, y);
|
|
2569
|
+
if (!t) return null;
|
|
2570
|
+
const colorRaw = ov.colorRaw ?? o.fillColorRaw;
|
|
2571
|
+
const color = colorRaw ? `${colorRaw} ` : "";
|
|
2572
|
+
const tc = ov.charSpacing ?? o.charSpacing * ratio;
|
|
2573
|
+
const tz = ov.hScale ?? o.hScale;
|
|
2574
|
+
return `q BT ${color}/${o.fontName} ${fmt(o.fontSize)} Tf ${fmt(tc)} Tc ${fmt(o.wordSpacing * ratio)} Tw ${fmt(tz)} Tz ${fmt(t[0])} ${fmt(t[1])} ${fmt(t[2])} ${fmt(t[3])} ${fmt(t[4])} ${fmt(t[5])} Tm ${show} ET Q`;
|
|
2575
|
+
}
|
|
2576
|
+
function newTextBlock(o, ratio, x, y, bytes, ov = {}) {
|
|
2577
|
+
const t = relTm(o, ratio, x, y);
|
|
2578
|
+
if (!t) return null;
|
|
2579
|
+
const colorRaw = ov.colorRaw ?? o.fillColorRaw;
|
|
2580
|
+
const color = colorRaw ? `${colorRaw} ` : "";
|
|
2581
|
+
const tc = ov.charSpacing ?? 0;
|
|
2582
|
+
const tz = ov.hScale ?? o.hScale;
|
|
2583
|
+
return `q BT ${color}/${o.fontName} ${fmt(o.fontSize)} Tf ${fmt(tc)} Tc 0 Tw ${fmt(tz)} Tz ${fmt(t[0])} ${fmt(t[1])} ${fmt(t[2])} ${fmt(t[3])} ${fmt(t[4])} ${fmt(t[5])} Tm ${hexString(bytes)} Tj ET Q`;
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
// ../core/src/bake/text.ts
|
|
2587
|
+
var underlineRectsFor = (edit, fillRects) => {
|
|
2588
|
+
const size = edit.original.fontSize;
|
|
2589
|
+
const lines = edit.original.baselines?.length ? edit.original.baselines : [edit.original.baseline];
|
|
2590
|
+
const x0 = edit.original.x - 2;
|
|
2591
|
+
const x1 = edit.original.x + edit.original.width + 2;
|
|
2592
|
+
return fillRects.filter(
|
|
2593
|
+
(r2) => r2.height <= Math.max(1.5, size * 0.12) && r2.x < x1 && r2.x + r2.width > x0 && lines.some((b) => Math.abs(r2.y - (b - size * 0.11)) <= size * 0.2)
|
|
2594
|
+
);
|
|
2595
|
+
};
|
|
2596
|
+
var escapesClip = (o, x, y) => {
|
|
2597
|
+
if (!o.clip) return false;
|
|
2598
|
+
const m = 1;
|
|
2599
|
+
return x < o.clip.x - m || x > o.clip.x + o.clip.width + m || y < o.clip.y - m || y > o.clip.y + o.clip.height + m;
|
|
2600
|
+
};
|
|
2601
|
+
var atStreamEnd = (o) => ({ ...o, ctm: IDENTITY });
|
|
2602
|
+
var editBasics = (edit) => ({
|
|
2603
|
+
ratio: (edit.fontSize ?? edit.original.fontSize) / edit.original.fontSize,
|
|
2604
|
+
newX: edit.x ?? edit.original.x,
|
|
2605
|
+
newBaseline: edit.baseline ?? edit.original.baseline,
|
|
2606
|
+
styleOv: {
|
|
2607
|
+
charSpacing: edit.charSpacing,
|
|
2608
|
+
hScale: edit.hScale,
|
|
2609
|
+
colorRaw: edit.color ? hexToRg(edit.color) : void 0
|
|
2610
|
+
}
|
|
2611
|
+
});
|
|
2612
|
+
var VerbatimReemit = class {
|
|
2613
|
+
canHandle(edit) {
|
|
2614
|
+
return edit.text === edit.original.text && edit.font === void 0 && !edit.runs;
|
|
2615
|
+
}
|
|
2616
|
+
emit({ edit, ops, src, fillRects, splices, appendBlocks, report }) {
|
|
2617
|
+
const { ratio, newX, newBaseline, styleOv } = editBasics(edit);
|
|
2618
|
+
const editSplices = [];
|
|
2619
|
+
const editAppends = [];
|
|
2620
|
+
for (const o of ops) {
|
|
2621
|
+
const nx = newX + (o.x - edit.original.x) * ratio;
|
|
2622
|
+
const ny = newBaseline + (o.y - edit.original.baseline) * ratio;
|
|
2623
|
+
const escaped = escapesClip(o, nx, ny);
|
|
2624
|
+
const block = reemitBlock(escaped ? atStreamEnd(o) : o, src, ratio, nx, ny, styleOv);
|
|
2625
|
+
if (!block) {
|
|
2626
|
+
report.warn(`${edit.segmentId}: matriz degenerada \u2014 sin cambios`);
|
|
2627
|
+
return;
|
|
2628
|
+
}
|
|
2629
|
+
if (escaped) {
|
|
2630
|
+
editSplices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
2631
|
+
editAppends.push(block);
|
|
2632
|
+
} else {
|
|
2633
|
+
editSplices.push({ start: o.record.start, end: o.record.end, text: block });
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
splices.push(...editSplices);
|
|
2637
|
+
appendBlocks.push(...editAppends);
|
|
2638
|
+
for (const r2 of underlineRectsFor(edit, fillRects)) {
|
|
2639
|
+
splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
2640
|
+
const nx = newX + (r2.x - edit.original.x) * ratio;
|
|
2641
|
+
const ny = newBaseline + (r2.y - edit.original.baseline) * ratio;
|
|
2642
|
+
const fill = r2.fillColorRaw || "0 0 0 rg";
|
|
2643
|
+
appendBlocks.push(`q ${fill} ${fmt(nx)} ${fmt(ny)} ${fmt(r2.width * ratio)} ${fmt(r2.height * ratio)} re f Q`);
|
|
2644
|
+
}
|
|
2645
|
+
report.apply(`${edit.segmentId}: reubicado/escalado (${ops.length} op${ops.length > 1 ? "s" : ""})`);
|
|
2646
|
+
}
|
|
2647
|
+
};
|
|
2648
|
+
var StyledRunsReemit = class {
|
|
2649
|
+
canHandle() {
|
|
2650
|
+
return true;
|
|
2651
|
+
}
|
|
2652
|
+
emit(ctx) {
|
|
2653
|
+
const { edit, ops, doc, page, pageNum, encCache, splices, appendBlocks, fallbackDraws, report } = ctx;
|
|
2654
|
+
const { ratio, newX, newBaseline, styleOv } = editBasics(edit);
|
|
2655
|
+
const familyChanged = edit.font !== void 0;
|
|
2656
|
+
for (const o of ops.slice(1)) splices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
2657
|
+
for (const r2 of underlineRectsFor(edit, ctx.fillRects)) splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
2658
|
+
const firstOp = ops[0];
|
|
2659
|
+
const inlineBlocks = [];
|
|
2660
|
+
const runsToEmit = edit.runs ?? [{
|
|
2661
|
+
text: edit.text,
|
|
2662
|
+
bold: edit.original.bold ?? false,
|
|
2663
|
+
italic: edit.original.italic ?? false,
|
|
2664
|
+
dx: 0
|
|
2665
|
+
}];
|
|
2666
|
+
const fontForStyle = /* @__PURE__ */ new Map();
|
|
2667
|
+
for (const or of edit.original.runs ?? []) {
|
|
2668
|
+
const op = ops.find((o) => Math.abs(o.x - or.x) <= 2.5);
|
|
2669
|
+
const key = `${or.bold}|${or.italic}`;
|
|
2670
|
+
if (op && !fontForStyle.has(key)) fontForStyle.set(key, op.fontName);
|
|
2671
|
+
}
|
|
2672
|
+
const lineRuns = [[]];
|
|
2673
|
+
for (const sr of runsToEmit) {
|
|
2674
|
+
const parts = sr.text.split("\n");
|
|
2675
|
+
parts.forEach((p, i) => {
|
|
2676
|
+
if (i > 0) lineRuns.push([]);
|
|
2677
|
+
if (p) lineRuns[lineRuns.length - 1].push({ ...sr, text: p });
|
|
2678
|
+
});
|
|
2679
|
+
}
|
|
2680
|
+
const leading = (edit.fontSize ?? edit.original.fontSize) * 1.2;
|
|
2681
|
+
const escaped = lineRuns.some((_, li) => escapesClip(firstOp, newX, newBaseline - li * leading));
|
|
2682
|
+
let substituted = 0;
|
|
2683
|
+
for (let li = 0; li < lineRuns.length; li++) {
|
|
2684
|
+
const lineBase = newBaseline - li * leading;
|
|
2685
|
+
for (const sr of lineRuns[li]) {
|
|
2686
|
+
if (!sr.text) continue;
|
|
2687
|
+
const x = newX + sr.dx * ratio;
|
|
2688
|
+
const fontName = familyChanged ? void 0 : fontForStyle.get(`${sr.bold}|${sr.italic}`);
|
|
2689
|
+
const bytes = fontName ? encoderForFont(doc, page, fontName, encCache)?.encode(sr.text) ?? null : null;
|
|
2690
|
+
const runOv = {
|
|
2691
|
+
...styleOv,
|
|
2692
|
+
colorRaw: sr.color ? hexToRg(sr.color) : styleOv.colorRaw
|
|
2693
|
+
};
|
|
2694
|
+
const srcForBlock = ops.find((o) => o.fontName === fontName) ?? firstOp;
|
|
2695
|
+
const inlineBlock = fontName && bytes ? newTextBlock(escaped ? atStreamEnd(srcForBlock) : srcForBlock, ratio, x, lineBase, bytes, runOv) : null;
|
|
2696
|
+
if (inlineBlock) {
|
|
2697
|
+
inlineBlocks.push(inlineBlock);
|
|
2698
|
+
if (sr.underline && sr.w) {
|
|
2699
|
+
const size = edit.fontSize ?? edit.original.fontSize;
|
|
2700
|
+
const c = sr.color ? hexToRgbObj(sr.color) : edit.color ? hexToRgbObj(edit.color) : rawFillToRgb((ops.find((o) => o.fontName === fontName) ?? firstOp)?.fillColorRaw) ?? { r: 0, g: 0, b: 0 };
|
|
2701
|
+
appendBlocks.push(
|
|
2702
|
+
`q ${fmt(c.r)} ${fmt(c.g)} ${fmt(c.b)} rg ${fmt(x)} ${fmt(lineBase - size * 0.11)} ${fmt(sr.w * ratio)} ${fmt(size * 0.055)} re f Q`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
} else {
|
|
2706
|
+
if (!/^\s+$/.test(sr.text)) {
|
|
2707
|
+
const srcOp = ops.find((o) => o.fontName === fontName) ?? firstOp;
|
|
2708
|
+
const color = sr.color ? hexToRgbObj(sr.color) : edit.color ? hexToRgbObj(edit.color) : rawFillToRgb(srcOp?.fillColorRaw) ?? void 0;
|
|
2709
|
+
fallbackDraws.push({
|
|
2710
|
+
page: pageNum,
|
|
2711
|
+
text: sr.text,
|
|
2712
|
+
x,
|
|
2713
|
+
y: lineBase,
|
|
2714
|
+
size: edit.fontSize ?? edit.original.fontSize,
|
|
2715
|
+
bucket: edit.font ?? edit.original.bucket ?? "sans",
|
|
2716
|
+
bold: sr.bold,
|
|
2717
|
+
italic: sr.italic,
|
|
2718
|
+
color: color ?? void 0,
|
|
2719
|
+
underline: sr.underline
|
|
2720
|
+
});
|
|
2721
|
+
}
|
|
2722
|
+
substituted++;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
if (escaped) {
|
|
2727
|
+
splices.push({ start: firstOp.record.start, end: firstOp.record.end, text: "" });
|
|
2728
|
+
if (inlineBlocks.length) appendBlocks.push(inlineBlocks.join("\n"));
|
|
2729
|
+
} else {
|
|
2730
|
+
splices.push({ start: firstOp.record.start, end: firstOp.record.end, text: inlineBlocks.join("\n") });
|
|
2731
|
+
}
|
|
2732
|
+
if (substituted && !familyChanged) {
|
|
2733
|
+
report.warn(`${edit.segmentId}: ${substituted} tramo${substituted > 1 ? "s" : ""} sin fuente original disponible (estilo nuevo o subset insuficiente) \u2014 sustituido por est\xE1ndar`);
|
|
2734
|
+
}
|
|
2735
|
+
report.apply(familyChanged ? `${edit.segmentId}: redibujado con fuente est\xE1ndar (cambio de familia)` : `${edit.segmentId}: reescrito por tramos (${runsToEmit.length})`);
|
|
2736
|
+
}
|
|
2737
|
+
};
|
|
2738
|
+
var textEmitStrategies = [
|
|
2739
|
+
new VerbatimReemit(),
|
|
2740
|
+
new StyledRunsReemit()
|
|
2741
|
+
];
|
|
2742
|
+
function applySegmentEditsToPage(args) {
|
|
2743
|
+
const { doc, page, pageNum, pageEdits, shows, fillRects, src, splices, appendBlocks, fallbackDraws, report } = args;
|
|
2744
|
+
const encCache = /* @__PURE__ */ new Map();
|
|
2745
|
+
for (const edit of pageEdits) {
|
|
2746
|
+
const { ops, conflict } = matchOps(shows, edit.original);
|
|
2747
|
+
if (conflict) {
|
|
2748
|
+
report.warn(`${edit.segmentId}: ${conflict} \u2014 sin cambios`);
|
|
2749
|
+
continue;
|
|
2750
|
+
}
|
|
2751
|
+
const colorOp = ops.find((o) => rawFillToRgb(o.fillColorRaw));
|
|
2752
|
+
if (colorOp) {
|
|
2753
|
+
const c = rawFillToRgb(colorOp.fillColorRaw);
|
|
2754
|
+
if (c) report.color(edit.segmentId, rgbToHex(c));
|
|
2755
|
+
}
|
|
2756
|
+
if (edit.remove) {
|
|
2757
|
+
for (const o of ops) splices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
2758
|
+
for (const r2 of underlineRectsFor(edit, fillRects)) splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
2759
|
+
report.apply(`${edit.segmentId}: eliminado`);
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
const ctx = { doc, page, pageNum, edit, ops, src, encCache, fillRects, splices, appendBlocks, fallbackDraws, report };
|
|
2763
|
+
for (const strategy of textEmitStrategies) {
|
|
2764
|
+
if (strategy.canHandle(edit)) {
|
|
2765
|
+
strategy.emit(ctx);
|
|
2766
|
+
break;
|
|
2767
|
+
}
|
|
2768
|
+
}
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
// ../core/src/bake/widgets.ts
|
|
2773
|
+
function applyWidgetEdits(doc, edits, report) {
|
|
2774
|
+
if (!edits.length) return;
|
|
2775
|
+
let form;
|
|
2776
|
+
try {
|
|
2777
|
+
form = doc.getForm();
|
|
2778
|
+
} catch {
|
|
2779
|
+
report.warn("el documento no tiene AcroForm \u2014 ediciones de campos saltadas");
|
|
2780
|
+
return;
|
|
2781
|
+
}
|
|
2782
|
+
let touched = false;
|
|
2783
|
+
for (const edit of edits) {
|
|
2784
|
+
const tol = 2.5;
|
|
2785
|
+
let matchedField = null;
|
|
2786
|
+
let matchedWidget = null;
|
|
2787
|
+
for (const field of form.getFields()) {
|
|
2788
|
+
if (field.getName() !== edit.original.fieldName) continue;
|
|
2789
|
+
for (const widget of field.acroField.getWidgets()) {
|
|
2790
|
+
const r2 = widget.getRectangle();
|
|
2791
|
+
if (Math.abs(r2.x - edit.original.x) <= tol && Math.abs(r2.y - edit.original.y) <= tol && Math.abs(r2.width - edit.original.width) <= tol && Math.abs(r2.height - edit.original.height) <= tol) {
|
|
2792
|
+
matchedField = field;
|
|
2793
|
+
matchedWidget = widget;
|
|
2794
|
+
break;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (matchedField) break;
|
|
2798
|
+
}
|
|
2799
|
+
if (!matchedField || !matchedWidget) {
|
|
2800
|
+
report.warn(`${edit.widgetId}: campo "${edit.original.fieldName}" no encontrado en su rect \u2014 sin cambios`);
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
if (edit.remove) {
|
|
2804
|
+
try {
|
|
2805
|
+
form.removeField(matchedField);
|
|
2806
|
+
report.apply(`${edit.widgetId}: campo "${edit.original.fieldName}" eliminado`);
|
|
2807
|
+
touched = true;
|
|
2808
|
+
} catch (err) {
|
|
2809
|
+
report.warn(`${edit.widgetId}: no se pudo eliminar (${err instanceof Error ? err.message : "error"})`);
|
|
2810
|
+
}
|
|
2811
|
+
continue;
|
|
2812
|
+
}
|
|
2813
|
+
matchedWidget.setRectangle({
|
|
2814
|
+
x: edit.x ?? edit.original.x,
|
|
2815
|
+
y: edit.y ?? edit.original.y,
|
|
2816
|
+
width: edit.width ?? edit.original.width,
|
|
2817
|
+
height: edit.height ?? edit.original.height
|
|
2818
|
+
});
|
|
2819
|
+
report.apply(`${edit.widgetId}: campo "${edit.original.fieldName}" reubicado/escalado`);
|
|
2820
|
+
touched = true;
|
|
2821
|
+
}
|
|
2822
|
+
if (touched) {
|
|
2823
|
+
try {
|
|
2824
|
+
form.updateFieldAppearances();
|
|
2825
|
+
} catch {
|
|
2826
|
+
}
|
|
2827
|
+
}
|
|
2828
|
+
}
|
|
2829
|
+
|
|
2830
|
+
// ../core/src/bake/highlights.ts
|
|
2831
|
+
import { PDFArray as PDFArray5, PDFDict as PDFDict5, PDFDocument as PDFDocument6, PDFName as PDFName6, PDFNumber as PDFNumber2, PDFRef as PDFRef5 } from "pdf-lib";
|
|
2832
|
+
|
|
2833
|
+
// ../core/src/bake/annotEdits.ts
|
|
2834
|
+
import { PDFArray as PDFArray3, PDFDict as PDFDict3, PDFName as PDFName4, PDFRef as PDFRef3 } from "pdf-lib";
|
|
2835
|
+
var rectNums = (rect) => {
|
|
2836
|
+
if (rect.size() !== 4) return null;
|
|
2837
|
+
const nums = [0, 1, 2, 3].map((k) => Number(rect.get(k).asNumber?.() ?? NaN));
|
|
2838
|
+
return nums.some(Number.isNaN) ? null : nums;
|
|
2839
|
+
};
|
|
2840
|
+
function applyAnnotRectEdits(doc, subtype, label, edits, report, onRect) {
|
|
2841
|
+
if (!edits.length) return;
|
|
2842
|
+
const tol = 2;
|
|
2843
|
+
for (const edit of edits) {
|
|
2844
|
+
const page = doc.getPages()[edit.page - 1];
|
|
2845
|
+
if (!page) {
|
|
2846
|
+
report.warn(`${edit.id}: p\xE1gina ${edit.page} fuera de rango \u2014 sin cambios`);
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
const annots = page.node.lookupMaybe(PDFName4.of("Annots"), PDFArray3);
|
|
2850
|
+
if (!annots) {
|
|
2851
|
+
report.warn(`${edit.id}: la p\xE1gina no tiene /Annots \u2014 sin cambios`);
|
|
2852
|
+
continue;
|
|
2853
|
+
}
|
|
2854
|
+
let done = false;
|
|
2855
|
+
for (let i = 0; i < annots.size(); i++) {
|
|
2856
|
+
const raw = annots.get(i);
|
|
2857
|
+
const dict = raw instanceof PDFRef3 ? doc.context.lookup(raw) : raw;
|
|
2858
|
+
if (!(dict instanceof PDFDict3)) continue;
|
|
2859
|
+
if (dict.get(PDFName4.of("Subtype")) !== PDFName4.of(subtype)) continue;
|
|
2860
|
+
const rect = dict.lookupMaybe(PDFName4.of("Rect"), PDFArray3);
|
|
2861
|
+
const nums = rect ? rectNums(rect) : null;
|
|
2862
|
+
if (!nums) continue;
|
|
2863
|
+
const [ax, ay, bx, by] = nums;
|
|
2864
|
+
const rx = Math.min(ax, bx), ry = Math.min(ay, by), rw = Math.abs(bx - ax), rh = Math.abs(by - ay);
|
|
2865
|
+
if (Math.abs(rx - edit.original.x) <= tol && Math.abs(ry - edit.original.y) <= tol && Math.abs(rw - edit.original.width) <= tol && Math.abs(rh - edit.original.height) <= tol) {
|
|
2866
|
+
if (edit.remove) {
|
|
2867
|
+
annots.remove(i);
|
|
2868
|
+
report.apply(`${edit.id}: ${label} eliminado`);
|
|
2869
|
+
} else {
|
|
2870
|
+
const nx = edit.x ?? rx, ny = edit.y ?? ry, nw = edit.width ?? rw, nh = edit.height ?? rh;
|
|
2871
|
+
dict.set(PDFName4.of("Rect"), doc.context.obj([nx, ny, nx + nw, ny + nh]));
|
|
2872
|
+
onRect?.(dict, nx, ny, nw, nh, edit);
|
|
2873
|
+
report.apply(`${edit.id}: ${label} ${edit.color ? "recoloreado" : "reubicado/escalado"}`);
|
|
2874
|
+
}
|
|
2875
|
+
done = true;
|
|
2876
|
+
break;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
if (!done) report.warn(`${edit.id}: no se encontr\xF3 la anotaci\xF3n en su rect original \u2014 sin cambios`);
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// ../core/src/bake/createNodes.ts
|
|
2884
|
+
init_model();
|
|
2885
|
+
import {
|
|
2886
|
+
PDFArray as PDFArray4,
|
|
2887
|
+
PDFDict as PDFDict4,
|
|
2888
|
+
PDFDocument as PDFDocument5,
|
|
2889
|
+
PDFName as PDFName5,
|
|
2890
|
+
PDFRef as PDFRef4,
|
|
2891
|
+
PDFString,
|
|
2892
|
+
degrees,
|
|
2893
|
+
rgb
|
|
2894
|
+
} from "pdf-lib";
|
|
2895
|
+
var hexToRgb = (hex) => {
|
|
2896
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
2897
|
+
const v = m ? parseInt(m[1], 16) : 0;
|
|
2898
|
+
return rgb((v >> 16 & 255) / 255, (v >> 8 & 255) / 255, (v & 255) / 255);
|
|
2899
|
+
};
|
|
2900
|
+
var FIELD_BASE_NAME = {
|
|
2901
|
+
text: "texto",
|
|
2902
|
+
checkbox: "check",
|
|
2903
|
+
radio: "radio",
|
|
2904
|
+
select: "select",
|
|
2905
|
+
list: "lista",
|
|
2906
|
+
button: "boton",
|
|
2907
|
+
signature: "firma"
|
|
2908
|
+
};
|
|
2909
|
+
function uniqueName(existing, base) {
|
|
2910
|
+
let n = 1;
|
|
2911
|
+
while (existing.has(`${base}_${n}`)) n++;
|
|
2912
|
+
return `${base}_${n}`;
|
|
2913
|
+
}
|
|
2914
|
+
var MODERN_WIDGET = {
|
|
2915
|
+
borderWidth: 1,
|
|
2916
|
+
borderColor: rgb(0.72, 0.77, 0.85),
|
|
2917
|
+
backgroundColor: rgb(0.955, 0.965, 0.985)
|
|
2918
|
+
};
|
|
2919
|
+
function addSignatureField(doc, page, name, rect) {
|
|
2920
|
+
const ctx = doc.context;
|
|
2921
|
+
const dict = ctx.obj({
|
|
2922
|
+
Type: "Annot",
|
|
2923
|
+
Subtype: "Widget",
|
|
2924
|
+
FT: "Sig",
|
|
2925
|
+
T: PDFString.of(name),
|
|
2926
|
+
// string, no name — pdf.js lee el fieldName de acá
|
|
2927
|
+
Rect: rect,
|
|
2928
|
+
F: 4,
|
|
2929
|
+
// print
|
|
2930
|
+
P: page.ref
|
|
2931
|
+
});
|
|
2932
|
+
const ref = ctx.register(dict);
|
|
2933
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray4) ?? ctx.obj([]);
|
|
2934
|
+
annots.push(ref);
|
|
2935
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
2936
|
+
const form = doc.getForm();
|
|
2937
|
+
form.acroForm.addField(ref);
|
|
2938
|
+
}
|
|
2939
|
+
async function addFormField(pdfBytes, spec) {
|
|
2940
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
2941
|
+
const page = doc.getPages()[spec.page - 1];
|
|
2942
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
2943
|
+
const form = doc.getForm();
|
|
2944
|
+
const existing = new Set(form.getFields().map((f) => f.getName()));
|
|
2945
|
+
const name = spec.name && !existing.has(spec.name) ? spec.name : uniqueName(existing, FIELD_BASE_NAME[spec.type]);
|
|
2946
|
+
const size = FIELD_DEFAULT_SIZE[spec.type];
|
|
2947
|
+
const width = spec.width ?? size.width;
|
|
2948
|
+
const height = spec.height ?? size.height;
|
|
2949
|
+
const rect = { x: spec.x, y: spec.y, width, height };
|
|
2950
|
+
const styled = { ...rect, ...MODERN_WIDGET };
|
|
2951
|
+
switch (spec.type) {
|
|
2952
|
+
case "text": {
|
|
2953
|
+
const f = form.createTextField(name);
|
|
2954
|
+
f.addToPage(page, styled);
|
|
2955
|
+
try {
|
|
2956
|
+
f.setFontSize(10);
|
|
2957
|
+
} catch {
|
|
2958
|
+
}
|
|
2959
|
+
break;
|
|
2960
|
+
}
|
|
2961
|
+
case "checkbox": {
|
|
2962
|
+
const f = form.createCheckBox(name);
|
|
2963
|
+
f.addToPage(page, styled);
|
|
2964
|
+
break;
|
|
2965
|
+
}
|
|
2966
|
+
case "radio": {
|
|
2967
|
+
const f = form.createRadioGroup(name);
|
|
2968
|
+
f.addOptionToPage("opcion_1", page, styled);
|
|
2969
|
+
break;
|
|
2970
|
+
}
|
|
2971
|
+
case "select": {
|
|
2972
|
+
const f = form.createDropdown(name);
|
|
2973
|
+
f.addOptions(["Opci\xF3n 1"]);
|
|
2974
|
+
f.addToPage(page, styled);
|
|
2975
|
+
break;
|
|
2976
|
+
}
|
|
2977
|
+
case "list": {
|
|
2978
|
+
const f = form.createOptionList(name);
|
|
2979
|
+
f.addOptions(["Opci\xF3n 1"]);
|
|
2980
|
+
f.addToPage(page, styled);
|
|
2981
|
+
break;
|
|
2982
|
+
}
|
|
2983
|
+
case "button": {
|
|
2984
|
+
const f = form.createButton(name);
|
|
2985
|
+
f.addToPage(name, page, styled);
|
|
2986
|
+
break;
|
|
2987
|
+
}
|
|
2988
|
+
case "signature": {
|
|
2989
|
+
addSignatureField(doc, page, name, [spec.x, spec.y, spec.x + width, spec.y + height]);
|
|
2990
|
+
break;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
try {
|
|
2994
|
+
form.updateFieldAppearances();
|
|
2995
|
+
} catch {
|
|
2996
|
+
}
|
|
2997
|
+
return { pdf: await doc.save(), name };
|
|
2998
|
+
}
|
|
2999
|
+
async function addText(pdfBytes, spec) {
|
|
3000
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3001
|
+
const page = doc.getPages()[spec.page - 1];
|
|
3002
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
3003
|
+
const size = spec.size ?? 11;
|
|
3004
|
+
const font = await doc.embedFont(stdFontFor(spec.bucket ?? "sans", spec.bold ?? false, spec.italic ?? false));
|
|
3005
|
+
page.drawText(spec.text, {
|
|
3006
|
+
x: spec.x,
|
|
3007
|
+
y: spec.y - size,
|
|
3008
|
+
size,
|
|
3009
|
+
font,
|
|
3010
|
+
color: spec.color ? hexToRgb(spec.color) : rgb(0, 0, 0),
|
|
3011
|
+
lineHeight: size * 1.35,
|
|
3012
|
+
maxWidth: Math.max(80, page.getWidth() - spec.x - 40)
|
|
3013
|
+
});
|
|
3014
|
+
return { pdf: await doc.save() };
|
|
3015
|
+
}
|
|
3016
|
+
async function addWatermark(pdfBytes, spec) {
|
|
3017
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3018
|
+
const font = await doc.embedFont(stdFontFor("sans", true, false));
|
|
3019
|
+
for (const page of doc.getPages()) {
|
|
3020
|
+
const w = page.getWidth();
|
|
3021
|
+
const h = page.getHeight();
|
|
3022
|
+
const size = Math.min(84, w * 1.1 / Math.max(4, spec.text.length) / 0.55);
|
|
3023
|
+
page.drawText(spec.text, {
|
|
3024
|
+
x: w * 0.14,
|
|
3025
|
+
y: h * 0.28,
|
|
3026
|
+
size,
|
|
3027
|
+
font,
|
|
3028
|
+
rotate: degrees(38),
|
|
3029
|
+
opacity: spec.opacity ?? 0.14,
|
|
3030
|
+
color: spec.color ? hexToRgb(spec.color) : rgb(0.4, 0.4, 0.45)
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
return { pdf: await doc.save() };
|
|
3034
|
+
}
|
|
3035
|
+
async function addHeaderFooter(pdfBytes, spec) {
|
|
3036
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3037
|
+
const font = await doc.embedFont(stdFontFor("sans", false, false));
|
|
3038
|
+
const pages = doc.getPages();
|
|
3039
|
+
const gray = rgb(0.35, 0.35, 0.4);
|
|
3040
|
+
pages.forEach((page, i) => {
|
|
3041
|
+
const w = page.getWidth();
|
|
3042
|
+
const h = page.getHeight();
|
|
3043
|
+
if (spec.header) page.drawText(spec.header, { x: 40, y: h - 28, size: 9, font, color: gray });
|
|
3044
|
+
if (spec.footer) page.drawText(spec.footer, { x: 40, y: 18, size: 9, font, color: gray });
|
|
3045
|
+
if (spec.pageNumbers) {
|
|
3046
|
+
const label = `P\xE1gina ${i + 1} de ${pages.length}`;
|
|
3047
|
+
const lw = font.widthOfTextAtSize(label, 9);
|
|
3048
|
+
page.drawText(label, { x: w - 40 - lw, y: 18, size: 9, font, color: gray });
|
|
3049
|
+
}
|
|
3050
|
+
});
|
|
3051
|
+
return { pdf: await doc.save() };
|
|
3052
|
+
}
|
|
3053
|
+
function highlightAppearance(ctx, colorHex, w, h) {
|
|
3054
|
+
const c = colorHex ? hexToRgb(colorHex) : rgb(1, 0.84, 0);
|
|
3055
|
+
const gsRef = ctx.register(ctx.obj({ Type: "ExtGState", BM: "Multiply", ca: 0.55, CA: 0.55 }));
|
|
3056
|
+
const ap = ctx.stream(`/GS gs ${fmt(c.red)} ${fmt(c.green)} ${fmt(c.blue)} rg 0 0 ${fmt(w)} ${fmt(h)} re f`, {
|
|
3057
|
+
Type: "XObject",
|
|
3058
|
+
Subtype: "Form",
|
|
3059
|
+
FormType: 1,
|
|
3060
|
+
BBox: [0, 0, w, h],
|
|
3061
|
+
Resources: ctx.obj({ ExtGState: ctx.obj({ GS: gsRef }) })
|
|
3062
|
+
});
|
|
3063
|
+
return { apRef: ctx.register(ap), color: [c.red, c.green, c.blue] };
|
|
3064
|
+
}
|
|
3065
|
+
async function addHighlight(pdfBytes, spec) {
|
|
3066
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3067
|
+
const page = doc.getPages()[spec.page - 1];
|
|
3068
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
3069
|
+
const ctx = doc.context;
|
|
3070
|
+
const x = spec.x - 1, y = spec.y - 1, w = spec.width + 2, h = spec.height + 2;
|
|
3071
|
+
const { apRef, color } = highlightAppearance(ctx, spec.color, w, h);
|
|
3072
|
+
const dict = ctx.obj({
|
|
3073
|
+
Type: "Annot",
|
|
3074
|
+
Subtype: "Highlight",
|
|
3075
|
+
Rect: [x, y, x + w, y + h],
|
|
3076
|
+
// QuadPoints ISO 32000: UL UR LL LR (y crece hacia arriba).
|
|
3077
|
+
QuadPoints: [x, y + h, x + w, y + h, x, y, x + w, y],
|
|
3078
|
+
C: color,
|
|
3079
|
+
CA: 0.55,
|
|
3080
|
+
AP: ctx.obj({ N: apRef })
|
|
3081
|
+
});
|
|
3082
|
+
const ref = ctx.register(dict);
|
|
3083
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray4) ?? ctx.obj([]);
|
|
3084
|
+
annots.push(ref);
|
|
3085
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
3086
|
+
return { pdf: await doc.save() };
|
|
3087
|
+
}
|
|
3088
|
+
async function addLink(pdfBytes, spec) {
|
|
3089
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3090
|
+
const page = doc.getPages()[spec.page - 1];
|
|
3091
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
3092
|
+
const ctx = doc.context;
|
|
3093
|
+
const dict = ctx.obj({
|
|
3094
|
+
Type: "Annot",
|
|
3095
|
+
Subtype: "Link",
|
|
3096
|
+
Rect: [spec.x, spec.y, spec.x + spec.width, spec.y + spec.height],
|
|
3097
|
+
Border: [0, 0, 0],
|
|
3098
|
+
A: ctx.obj({ Type: "Action", S: "URI", URI: PDFString.of(spec.url) })
|
|
3099
|
+
});
|
|
3100
|
+
const ref = ctx.register(dict);
|
|
3101
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray4) ?? ctx.obj([]);
|
|
3102
|
+
annots.push(ref);
|
|
3103
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
3104
|
+
return { pdf: await doc.save() };
|
|
3105
|
+
}
|
|
3106
|
+
async function insertImage(pdfBytes, spec) {
|
|
3107
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
3108
|
+
const page = doc.getPages()[spec.page - 1];
|
|
3109
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
3110
|
+
const image = /png$/i.test(spec.mime) ? await doc.embedPng(spec.bytes) : await doc.embedJpg(spec.bytes);
|
|
3111
|
+
const maxW = spec.maxWidth ?? 240;
|
|
3112
|
+
const ratio = image.width > maxW ? maxW / image.width : 1;
|
|
3113
|
+
const width = image.width * ratio;
|
|
3114
|
+
const height = image.height * ratio;
|
|
3115
|
+
const rect = { x: spec.x, y: spec.y - height, width, height };
|
|
3116
|
+
page.drawImage(image, rect);
|
|
3117
|
+
return { pdf: await doc.save(), rect };
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
// ../core/src/bake/highlights.ts
|
|
3121
|
+
function applyHighlightEdits(doc, edits, report) {
|
|
3122
|
+
applyAnnotRectEdits(
|
|
3123
|
+
doc,
|
|
3124
|
+
"Highlight",
|
|
3125
|
+
"resaltado",
|
|
3126
|
+
edits.map((e) => ({ id: e.highlightId, page: e.page, x: e.x, y: e.y, width: e.width, height: e.height, color: e.color, remove: e.remove, original: e.original })),
|
|
3127
|
+
report,
|
|
3128
|
+
(dict, nx, ny, nw, nh, edit) => {
|
|
3129
|
+
dict.set(PDFName6.of("QuadPoints"), doc.context.obj([nx, ny + nh, nx + nw, ny + nh, nx, ny, nx + nw, ny]));
|
|
3130
|
+
if (edit.color) {
|
|
3131
|
+
const { apRef, color } = highlightAppearance(doc.context, edit.color, nw, nh);
|
|
3132
|
+
dict.set(PDFName6.of("C"), doc.context.obj(color));
|
|
3133
|
+
dict.set(PDFName6.of("AP"), doc.context.obj({ N: apRef }));
|
|
3134
|
+
}
|
|
3135
|
+
}
|
|
3136
|
+
);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
// ../core/src/bake/links.ts
|
|
3140
|
+
function applyLinkEdits(doc, edits, report) {
|
|
3141
|
+
applyAnnotRectEdits(
|
|
3142
|
+
doc,
|
|
3143
|
+
"Link",
|
|
3144
|
+
"link",
|
|
3145
|
+
edits.map((e) => ({ id: e.linkId, page: e.page, x: e.x, y: e.y, width: e.width, height: e.height, remove: e.remove, original: e.original })),
|
|
3146
|
+
report
|
|
3147
|
+
);
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
// ../core/src/bake/fallback.ts
|
|
3151
|
+
import { rgb as rgb2 } from "pdf-lib";
|
|
3152
|
+
async function drawFallbackTexts(doc, draws, report) {
|
|
3153
|
+
if (!draws.length) return;
|
|
3154
|
+
const pages = doc.getPages();
|
|
3155
|
+
const fontCache = /* @__PURE__ */ new Map();
|
|
3156
|
+
for (const d of draws) {
|
|
3157
|
+
const key = `${d.bucket}|${d.bold}|${d.italic}`;
|
|
3158
|
+
let font = fontCache.get(key);
|
|
3159
|
+
if (!font) {
|
|
3160
|
+
font = await doc.embedFont(stdFontFor(d.bucket, d.bold, d.italic));
|
|
3161
|
+
fontCache.set(key, font);
|
|
3162
|
+
}
|
|
3163
|
+
const page = pages[d.page - 1];
|
|
3164
|
+
const color = d.color ? rgb2(d.color.r, d.color.g, d.color.b) : rgb2(0, 0, 0);
|
|
3165
|
+
const drawUnderline = () => {
|
|
3166
|
+
if (!d.underline || !font) return;
|
|
3167
|
+
const w = font.widthOfTextAtSize(d.text, d.size);
|
|
3168
|
+
page.drawRectangle({ x: d.x, y: d.y - d.size * 0.11, width: w, height: d.size * 0.055, color });
|
|
3169
|
+
};
|
|
3170
|
+
try {
|
|
3171
|
+
page.drawText(d.text, { x: d.x, y: d.y, size: d.size, font, color });
|
|
3172
|
+
drawUnderline();
|
|
3173
|
+
} catch {
|
|
3174
|
+
const clean = [...d.text].filter((c) => c.charCodeAt(0) <= 255).join("");
|
|
3175
|
+
try {
|
|
3176
|
+
page.drawText(clean, { x: d.x, y: d.y, size: d.size, font, color });
|
|
3177
|
+
report.warn(`p${d.page}: caracteres no representables descartados en "${d.text.slice(0, 24)}\u2026"`);
|
|
3178
|
+
} catch {
|
|
3179
|
+
report.warn(`p${d.page}: no se pudo dibujar el reemplazo "${d.text.slice(0, 24)}\u2026"`);
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
// ../core/src/bake/report.ts
|
|
3186
|
+
var BakeReport = class {
|
|
3187
|
+
applied = [];
|
|
3188
|
+
warnings = [];
|
|
3189
|
+
colors = {};
|
|
3190
|
+
apply(message) {
|
|
3191
|
+
this.applied.push(message);
|
|
3192
|
+
}
|
|
3193
|
+
warn(message) {
|
|
3194
|
+
this.warnings.push(message);
|
|
3195
|
+
}
|
|
3196
|
+
color(segmentId, hex) {
|
|
3197
|
+
this.colors[segmentId] = hex;
|
|
3198
|
+
}
|
|
3199
|
+
finish(pdf) {
|
|
3200
|
+
return { pdf, applied: this.applied, warnings: this.warnings, colors: this.colors };
|
|
3201
|
+
}
|
|
3202
|
+
};
|
|
3203
|
+
|
|
3204
|
+
// ../core/src/bake/bake.ts
|
|
3205
|
+
var groupByPage = (items) => {
|
|
3206
|
+
const out = /* @__PURE__ */ new Map();
|
|
3207
|
+
for (const item of items) out.set(item.page, [...out.get(item.page) ?? [], item]);
|
|
3208
|
+
return out;
|
|
3209
|
+
};
|
|
3210
|
+
async function bakeSegmentEdits(pdfBytes, edits, imageEdits = [], widgetEdits = [], highlightEdits = [], linkEdits = []) {
|
|
3211
|
+
const doc = await PDFDocument8.load(pdfBytes, { ignoreEncryption: true });
|
|
3212
|
+
const pages = doc.getPages();
|
|
3213
|
+
const report = new BakeReport();
|
|
3214
|
+
const fallbackDraws = [];
|
|
3215
|
+
applyWidgetEdits(doc, widgetEdits, report);
|
|
3216
|
+
applyHighlightEdits(doc, highlightEdits, report);
|
|
3217
|
+
applyLinkEdits(doc, linkEdits, report);
|
|
3218
|
+
const byPage = groupByPage(edits);
|
|
3219
|
+
const imgByPage = groupByPage(imageEdits);
|
|
3220
|
+
const allPages = /* @__PURE__ */ new Set([...byPage.keys(), ...imgByPage.keys()]);
|
|
3221
|
+
for (const pageNum of allPages) {
|
|
3222
|
+
const pageEdits = byPage.get(pageNum) ?? [];
|
|
3223
|
+
const pageImgEdits = imgByPage.get(pageNum) ?? [];
|
|
3224
|
+
const page = pages[pageNum - 1];
|
|
3225
|
+
if (!page) {
|
|
3226
|
+
report.warn(`p\xE1gina ${pageNum} fuera de rango \u2014 ediciones saltadas`);
|
|
3227
|
+
continue;
|
|
3228
|
+
}
|
|
3229
|
+
let src;
|
|
3230
|
+
try {
|
|
3231
|
+
src = pageContentBytes(doc, page);
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
report.warn(`p\xE1gina ${pageNum}: ${err instanceof Error ? err.message : "stream ilegible"}`);
|
|
3234
|
+
continue;
|
|
3235
|
+
}
|
|
3236
|
+
const { shows, xobjects, fillRects, backstop } = walkContent(src);
|
|
3237
|
+
const splices = [];
|
|
3238
|
+
const appendBlocks = [];
|
|
3239
|
+
applyImageEditsToPage({ doc, page, pageImgEdits, xobjects, backstop, splices, appendBlocks, report });
|
|
3240
|
+
applySegmentEditsToPage({ doc, page, pageNum, pageEdits, shows, fillRects, src, splices, appendBlocks, fallbackDraws, report });
|
|
3241
|
+
if (splices.length || appendBlocks.length) {
|
|
3242
|
+
setPageContents(doc, page, rebuild(src, splices, "", appendBlocks.join("\n")));
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
await drawFallbackTexts(doc, fallbackDraws, report);
|
|
3246
|
+
return report.finish(await doc.save());
|
|
3247
|
+
}
|
|
3248
|
+
|
|
3249
|
+
// ../core/src/bake/forms.ts
|
|
3250
|
+
import {
|
|
3251
|
+
PDFArray as PDFArray6,
|
|
3252
|
+
PDFDocument as PDFDocument9,
|
|
3253
|
+
PDFName as PDFName7,
|
|
3254
|
+
PDFRef as PDFRef6,
|
|
3255
|
+
PDFTextField,
|
|
3256
|
+
PDFCheckBox,
|
|
3257
|
+
PDFRadioGroup,
|
|
3258
|
+
PDFDropdown,
|
|
3259
|
+
PDFOptionList,
|
|
3260
|
+
PDFButton
|
|
3261
|
+
} from "pdf-lib";
|
|
3262
|
+
var nz = (s) => s ? s : void 0;
|
|
3263
|
+
function widgetPages(doc) {
|
|
3264
|
+
const map = /* @__PURE__ */ new Map();
|
|
3265
|
+
doc.getPages().forEach((pg, i) => {
|
|
3266
|
+
const annots = pg.node.lookupMaybe(PDFName7.of("Annots"), PDFArray6);
|
|
3267
|
+
if (!annots) return;
|
|
3268
|
+
for (let k = 0; k < annots.size(); k++) {
|
|
3269
|
+
const ref = annots.get(k);
|
|
3270
|
+
map.set(ref instanceof PDFRef6 ? doc.context.lookup(ref) : ref, i + 1);
|
|
3271
|
+
}
|
|
3272
|
+
});
|
|
3273
|
+
return map;
|
|
3274
|
+
}
|
|
3275
|
+
function rectsOf(field, pageBy) {
|
|
3276
|
+
const out = [];
|
|
3277
|
+
for (const w of field.acroField.getWidgets()) {
|
|
3278
|
+
try {
|
|
3279
|
+
const r2 = w.getRectangle();
|
|
3280
|
+
const as = w.dict.get(PDFName7.of("AS"));
|
|
3281
|
+
out.push({
|
|
3282
|
+
page: pageBy.get(w.dict) ?? 1,
|
|
3283
|
+
x: Math.round(r2.x * 10) / 10,
|
|
3284
|
+
y: Math.round(r2.y * 10) / 10,
|
|
3285
|
+
width: Math.round(r2.width * 10) / 10,
|
|
3286
|
+
height: Math.round(r2.height * 10) / 10,
|
|
3287
|
+
export: as instanceof PDFName7 && as.asString() !== "/Off" ? as.asString().slice(1) : void 0
|
|
3288
|
+
});
|
|
3289
|
+
} catch {
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
return out;
|
|
3293
|
+
}
|
|
3294
|
+
async function readFormFields(pdfBytes) {
|
|
3295
|
+
const doc = await PDFDocument9.load(pdfBytes, { ignoreEncryption: true });
|
|
3296
|
+
let form;
|
|
3297
|
+
try {
|
|
3298
|
+
form = doc.getForm();
|
|
3299
|
+
} catch {
|
|
3300
|
+
return [];
|
|
3301
|
+
}
|
|
3302
|
+
const pageBy = widgetPages(doc);
|
|
3303
|
+
const out = [];
|
|
3304
|
+
for (const f of form.getFields()) {
|
|
3305
|
+
const name = f.getName();
|
|
3306
|
+
const readOnly = f.isReadOnly();
|
|
3307
|
+
const rects = rectsOf(f, pageBy);
|
|
3308
|
+
if (f instanceof PDFTextField) out.push({ name, type: "text", value: nz(f.getText() ?? void 0), readOnly, rects });
|
|
3309
|
+
else if (f instanceof PDFCheckBox) out.push({ name, type: "checkbox", value: f.isChecked() ? "On" : void 0, readOnly, rects });
|
|
3310
|
+
else if (f instanceof PDFRadioGroup) out.push({ name, type: "radio", value: nz(f.getSelected() ?? void 0), options: f.getOptions(), readOnly, rects });
|
|
3311
|
+
else if (f instanceof PDFDropdown) out.push({ name, type: "select", value: nz(f.getSelected()[0]), options: f.getOptions(), readOnly, rects });
|
|
3312
|
+
else if (f instanceof PDFOptionList) out.push({ name, type: "list", value: f.getSelected().length ? f.getSelected() : void 0, options: f.getOptions(), readOnly, rects });
|
|
3313
|
+
else if (f instanceof PDFButton) out.push({ name, type: "button", readOnly, rects });
|
|
3314
|
+
else out.push({ name, type: "signature", readOnly, rects });
|
|
3315
|
+
}
|
|
3316
|
+
return out;
|
|
3317
|
+
}
|
|
3318
|
+
var isChecked = (v) => v === true || typeof v === "string" && ["on", "true", "yes", "si", "s\xED", "1", "x", "checked"].includes(v.trim().toLowerCase());
|
|
3319
|
+
async function setFieldValues(pdfBytes, values) {
|
|
3320
|
+
const doc = await PDFDocument9.load(pdfBytes, { ignoreEncryption: true });
|
|
3321
|
+
const applied = [];
|
|
3322
|
+
const warnings = [];
|
|
3323
|
+
let form;
|
|
3324
|
+
try {
|
|
3325
|
+
form = doc.getForm();
|
|
3326
|
+
} catch {
|
|
3327
|
+
return { pdf: pdfBytes, applied, warnings: ["el documento no tiene formulario (AcroForm)"] };
|
|
3328
|
+
}
|
|
3329
|
+
const byName = new Map(form.getFields().map((f) => [f.getName(), f]));
|
|
3330
|
+
for (const [name, value] of Object.entries(values)) {
|
|
3331
|
+
const field = byName.get(name);
|
|
3332
|
+
if (!field) {
|
|
3333
|
+
warnings.push(`campo "${name}" no existe`);
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
if (field.isReadOnly()) {
|
|
3337
|
+
warnings.push(`campo "${name}" es read-only \u2014 saltado`);
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
const badOption = (opts) => {
|
|
3341
|
+
const want = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
3342
|
+
const invalid = want.filter((v) => !opts.includes(v));
|
|
3343
|
+
return invalid.length ? `opci\xF3n(es) inv\xE1lida(s) [${invalid.join(", ")}] \u2014 v\xE1lidas: [${opts.join(", ")}]` : null;
|
|
3344
|
+
};
|
|
3345
|
+
try {
|
|
3346
|
+
if (field instanceof PDFTextField) field.setText(String(value));
|
|
3347
|
+
else if (field instanceof PDFCheckBox) {
|
|
3348
|
+
if (isChecked(value)) field.check();
|
|
3349
|
+
else field.uncheck();
|
|
3350
|
+
} else if (field instanceof PDFRadioGroup) {
|
|
3351
|
+
const b = badOption(field.getOptions());
|
|
3352
|
+
if (b) {
|
|
3353
|
+
warnings.push(`${name}: ${b}`);
|
|
3354
|
+
continue;
|
|
3355
|
+
}
|
|
3356
|
+
field.select(String(value));
|
|
3357
|
+
} else if (field instanceof PDFDropdown) {
|
|
3358
|
+
const b = badOption(field.getOptions());
|
|
3359
|
+
if (b) {
|
|
3360
|
+
warnings.push(`${name}: ${b}`);
|
|
3361
|
+
continue;
|
|
3362
|
+
}
|
|
3363
|
+
field.select(String(value));
|
|
3364
|
+
} else if (field instanceof PDFOptionList) {
|
|
3365
|
+
const b = badOption(field.getOptions());
|
|
3366
|
+
if (b) {
|
|
3367
|
+
warnings.push(`${name}: ${b}`);
|
|
3368
|
+
continue;
|
|
3369
|
+
}
|
|
3370
|
+
field.select(Array.isArray(value) ? value : String(value));
|
|
3371
|
+
} else {
|
|
3372
|
+
warnings.push(`campo "${name}" (${field.constructor.name}) no admite valor`);
|
|
3373
|
+
continue;
|
|
3374
|
+
}
|
|
3375
|
+
applied.push(`${name} = ${JSON.stringify(value)}`);
|
|
3376
|
+
} catch (err) {
|
|
3377
|
+
warnings.push(`${name}: ${err instanceof Error ? err.message : "no se pudo completar"}`);
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
if (applied.length) {
|
|
3381
|
+
try {
|
|
3382
|
+
form.updateFieldAppearances();
|
|
3383
|
+
} catch {
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return { pdf: await doc.save(), applied, warnings };
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
// ../agent/src/session.ts
|
|
3390
|
+
init_graph();
|
|
3391
|
+
var MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg" };
|
|
3392
|
+
var WIDE_FIELD = /nombre|apellido|raz[oó]n|social|domicilio|direcci|empresa|calle|ciudad|cargo/i;
|
|
3393
|
+
var NARROW_FIELD = /ruc|dni|n[uú]m|partida|c[oó]digo|fecha|tel[eé]fono|cuit|nit|zip|cp\b/i;
|
|
3394
|
+
function targetWidthFor(name, width, fontSize) {
|
|
3395
|
+
if (typeof width === "number" && width > 0) return width;
|
|
3396
|
+
if (WIDE_FIELD.test(name)) return fontSize * 11;
|
|
3397
|
+
if (NARROW_FIELD.test(name)) return fontSize * 5.5;
|
|
3398
|
+
return fontSize * 8;
|
|
3399
|
+
}
|
|
3400
|
+
var EditSession = class {
|
|
3401
|
+
constructor(doc) {
|
|
3402
|
+
this.doc = doc;
|
|
3403
|
+
}
|
|
3404
|
+
edits = /* @__PURE__ */ new Map();
|
|
3405
|
+
imageEdits = /* @__PURE__ */ new Map();
|
|
3406
|
+
widgetEdits = /* @__PURE__ */ new Map();
|
|
3407
|
+
highlightEdits = /* @__PURE__ */ new Map();
|
|
3408
|
+
linkEdits = /* @__PURE__ */ new Map();
|
|
3409
|
+
creates = [];
|
|
3410
|
+
/** Valores de formulario a COMPLETAR, por nombre de campo (setFieldValues). */
|
|
3411
|
+
fills = /* @__PURE__ */ new Map();
|
|
3412
|
+
seg(id) {
|
|
3413
|
+
for (const p of this.doc.pages) {
|
|
3414
|
+
const s = p.segments.find((x) => x.id === id);
|
|
3415
|
+
if (s) return s;
|
|
3416
|
+
}
|
|
3417
|
+
return void 0;
|
|
3418
|
+
}
|
|
3419
|
+
img(id) {
|
|
3420
|
+
for (const p of this.doc.pages) {
|
|
3421
|
+
const i = p.images.find((x) => x.id === id);
|
|
3422
|
+
if (i) return i;
|
|
3423
|
+
}
|
|
3424
|
+
return void 0;
|
|
3425
|
+
}
|
|
3426
|
+
widget(id) {
|
|
3427
|
+
for (const p of this.doc.pages) {
|
|
3428
|
+
const w = p.widgets.find((x) => x.id === id);
|
|
3429
|
+
if (w) return w;
|
|
3430
|
+
}
|
|
3431
|
+
return void 0;
|
|
3432
|
+
}
|
|
3433
|
+
hlNode(id) {
|
|
3434
|
+
for (const p of this.doc.pages) {
|
|
3435
|
+
const h = p.highlights.find((x) => x.id === id);
|
|
3436
|
+
if (h) return h;
|
|
3437
|
+
}
|
|
3438
|
+
return void 0;
|
|
3439
|
+
}
|
|
3440
|
+
linkNode(id) {
|
|
3441
|
+
for (const p of this.doc.pages) {
|
|
3442
|
+
const l = p.links.find((x) => x.id === id);
|
|
3443
|
+
if (l) return l;
|
|
3444
|
+
}
|
|
3445
|
+
return void 0;
|
|
3446
|
+
}
|
|
3447
|
+
putSeg(seg, patch) {
|
|
3448
|
+
const m = mergeSegmentEdit(seg, this.edits.get(seg.id) ?? null, patch);
|
|
3449
|
+
if (m) this.edits.set(seg.id, m);
|
|
3450
|
+
else this.edits.delete(seg.id);
|
|
3451
|
+
}
|
|
3452
|
+
putImg(img, patch) {
|
|
3453
|
+
const m = mergeImageEdit(img, this.imageEdits.get(img.id) ?? null, patch);
|
|
3454
|
+
if (m) this.imageEdits.set(img.id, m);
|
|
3455
|
+
else this.imageEdits.delete(img.id);
|
|
3456
|
+
}
|
|
3457
|
+
// ── EDICIONES de texto (nodos existentes) ──
|
|
3458
|
+
/** Edita el texto de un nodo. Si el texto NUEVO es más ancho de lo que entra
|
|
3459
|
+
* en su renglón, el PÁRRAFO se reconstruye (reflow determinístico): lo que
|
|
3460
|
+
* sobra baja al renglón siguiente en cascada — nunca se superpone ni se sale
|
|
3461
|
+
* del borde. Texto igual o más corto = camino simple (verbatim + diff). */
|
|
3462
|
+
async editText(id, text) {
|
|
3463
|
+
const s = this.seg(id);
|
|
3464
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3465
|
+
const styled = applyTextDiff(originalStyledRuns(s), text);
|
|
3466
|
+
const para = this.paragraphOf(s);
|
|
3467
|
+
const avgCharW = s.width / Math.max(1, s.text.length);
|
|
3468
|
+
const fits = text.length <= s.text.length || s.x + text.length * avgCharW <= para.rightEdge + 2;
|
|
3469
|
+
if (fits) {
|
|
3470
|
+
this.putSeg(s, { runs: styled });
|
|
3471
|
+
return `\u2713 Texto ${id}: ${JSON.stringify(s.text)} \u2192 ${JSON.stringify(text)}`;
|
|
3472
|
+
}
|
|
3473
|
+
const toks = this.paragraphToks(para, [], { lineId: s.id, styled, avgCharW });
|
|
3474
|
+
const { extraLines, scale } = await this.reflowApply(s, para, toks);
|
|
3475
|
+
const grew = extraLines ? ` (+${extraLines} rengl\xF3n/es, contenido inferior corrido)` : "";
|
|
3476
|
+
const note = scale < 1 ? " \u26A0 el p\xE1rrafo qued\xF3 justo: revis\xE1 el resultado" : "";
|
|
3477
|
+
return `\u2713 Texto ${id} \u2192 ${JSON.stringify(text)} \u2014 p\xE1rrafo reconstruido${grew}${note}`;
|
|
3478
|
+
}
|
|
3479
|
+
/** Cambia el ESTILO (negrita/itálica) de un nodo de texto entero. El bake
|
|
3480
|
+
* re-encoda con la variante de fuente correspondiente (si el PDF no la trae
|
|
3481
|
+
* embebida, cae a la estándar equivalente y lo reporta). */
|
|
3482
|
+
styleText(id, opts) {
|
|
3483
|
+
const s = this.seg(id);
|
|
3484
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3485
|
+
if (opts.bold === void 0 && opts.italic === void 0) return `\u26A0\uFE0F pas\xE1 bold y/o italic.`;
|
|
3486
|
+
const runs = originalStyledRuns(s).map((r2) => ({
|
|
3487
|
+
...r2,
|
|
3488
|
+
bold: opts.bold ?? r2.bold,
|
|
3489
|
+
italic: opts.italic ?? r2.italic
|
|
3490
|
+
}));
|
|
3491
|
+
this.putSeg(s, { runs });
|
|
3492
|
+
const parts = [opts.bold !== void 0 ? `bold=${opts.bold}` : "", opts.italic !== void 0 ? `italic=${opts.italic}` : ""].filter(Boolean);
|
|
3493
|
+
return `\u2713 Texto ${id} \u2192 ${parts.join(", ")}`;
|
|
3494
|
+
}
|
|
3495
|
+
moveText(id, x, y) {
|
|
3496
|
+
const s = this.seg(id);
|
|
3497
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3498
|
+
this.putSeg(s, { x, baseline: y });
|
|
3499
|
+
return `\u2713 Texto ${id} movido a @(${x ?? Math.round(s.x)},${y ?? Math.round(s.baseline)})`;
|
|
3500
|
+
}
|
|
3501
|
+
colorText(id, color) {
|
|
3502
|
+
const s = this.seg(id);
|
|
3503
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3504
|
+
this.putSeg(s, { color });
|
|
3505
|
+
return `\u2713 Texto ${id} \u2192 color ${color}`;
|
|
3506
|
+
}
|
|
3507
|
+
resizeText(id, fontSize) {
|
|
3508
|
+
const s = this.seg(id);
|
|
3509
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3510
|
+
this.putSeg(s, { fontSize });
|
|
3511
|
+
return `\u2713 Texto ${id} \u2192 ${fontSize}pt`;
|
|
3512
|
+
}
|
|
3513
|
+
deleteText(id) {
|
|
3514
|
+
const s = this.seg(id);
|
|
3515
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3516
|
+
this.putSeg(s, { remove: true });
|
|
3517
|
+
return `\u2713 Texto ${id} eliminado`;
|
|
3518
|
+
}
|
|
3519
|
+
// ── EDICIONES de imagen (nodos existentes) ──
|
|
3520
|
+
moveImage(id, patch) {
|
|
3521
|
+
const im = this.img(id);
|
|
3522
|
+
if (!im) return `\u26A0\uFE0F No existe la imagen "${id}".`;
|
|
3523
|
+
this.putImg(im, patch);
|
|
3524
|
+
return `\u2713 Imagen ${id} \u2192 @(${patch.x ?? Math.round(im.x)},${patch.y ?? Math.round(im.y)}) ${patch.width ?? Math.round(im.width)}\xD7${patch.height ?? Math.round(im.height)}`;
|
|
3525
|
+
}
|
|
3526
|
+
deleteImage(id) {
|
|
3527
|
+
const im = this.img(id);
|
|
3528
|
+
if (!im) return `\u26A0\uFE0F No existe la imagen "${id}".`;
|
|
3529
|
+
this.putImg(im, { remove: true });
|
|
3530
|
+
return `\u2713 Imagen ${id} eliminada`;
|
|
3531
|
+
}
|
|
3532
|
+
// ── EDICIONES de campo / highlight / link existentes ──
|
|
3533
|
+
moveField(id, x, y) {
|
|
3534
|
+
const w = this.widget(id);
|
|
3535
|
+
if (!w) return `\u26A0\uFE0F No existe el campo "${id}".`;
|
|
3536
|
+
const m = mergeWidgetEdit(w, this.widgetEdits.get(id) ?? null, { x, y });
|
|
3537
|
+
if (m) this.widgetEdits.set(id, m);
|
|
3538
|
+
else this.widgetEdits.delete(id);
|
|
3539
|
+
return `\u2713 Campo ${id} movido a @(${x ?? Math.round(w.x)},${y ?? Math.round(w.y)})`;
|
|
3540
|
+
}
|
|
3541
|
+
deleteField(id) {
|
|
3542
|
+
const w = this.widget(id);
|
|
3543
|
+
if (!w) return `\u26A0\uFE0F No existe el campo "${id}".`;
|
|
3544
|
+
const m = mergeWidgetEdit(w, this.widgetEdits.get(id) ?? null, { remove: true });
|
|
3545
|
+
if (m) this.widgetEdits.set(id, m);
|
|
3546
|
+
return `\u2713 Campo ${id} eliminado`;
|
|
3547
|
+
}
|
|
3548
|
+
recolorHighlight(id, color) {
|
|
3549
|
+
const h = this.hlNode(id);
|
|
3550
|
+
if (!h) return `\u26A0\uFE0F No existe el resaltado "${id}".`;
|
|
3551
|
+
const m = mergeHighlightEdit(h, this.highlightEdits.get(id) ?? null, { color });
|
|
3552
|
+
if (m) this.highlightEdits.set(id, m);
|
|
3553
|
+
else this.highlightEdits.delete(id);
|
|
3554
|
+
return `\u2713 Resaltado ${id} \u2192 color ${color}`;
|
|
3555
|
+
}
|
|
3556
|
+
deleteHighlight(id) {
|
|
3557
|
+
const h = this.hlNode(id);
|
|
3558
|
+
if (!h) return `\u26A0\uFE0F No existe el resaltado "${id}".`;
|
|
3559
|
+
const m = mergeHighlightEdit(h, this.highlightEdits.get(id) ?? null, { remove: true });
|
|
3560
|
+
if (m) this.highlightEdits.set(id, m);
|
|
3561
|
+
return `\u2713 Resaltado ${id} eliminado`;
|
|
3562
|
+
}
|
|
3563
|
+
deleteLink(id) {
|
|
3564
|
+
const l = this.linkNode(id);
|
|
3565
|
+
if (!l) return `\u26A0\uFE0F No existe el link "${id}".`;
|
|
3566
|
+
const m = mergeLinkEdit(l, this.linkEdits.get(id) ?? null, { remove: true });
|
|
3567
|
+
if (m) this.linkEdits.set(id, m);
|
|
3568
|
+
return `\u2713 Link ${id} eliminado`;
|
|
3569
|
+
}
|
|
3570
|
+
// ── CREACIONES (nodos nuevos — cola aplicada post-bake) ──
|
|
3571
|
+
highlightText(segId, color) {
|
|
3572
|
+
if (!this.seg(segId)) return `\u26A0\uFE0F No existe el nodo de texto "${segId}".`;
|
|
3573
|
+
this.creates.push({ kind: "highlightSeg", segId, color });
|
|
3574
|
+
return `\u2713 Resaltado sobre ${segId}${color ? ` (${color})` : ""}`;
|
|
3575
|
+
}
|
|
3576
|
+
linkText(segId, url) {
|
|
3577
|
+
if (!this.seg(segId)) return `\u26A0\uFE0F No existe el nodo de texto "${segId}".`;
|
|
3578
|
+
this.creates.push({ kind: "linkSeg", segId, url });
|
|
3579
|
+
return `\u2713 Link sobre ${segId} \u2192 ${url}`;
|
|
3580
|
+
}
|
|
3581
|
+
/** Agrega texto nuevo. ANTI-COLISIÓN determinística: si el rect estimado pisa
|
|
3582
|
+
* texto existente (u otro texto ya encolado), baja renglón a renglón hasta un
|
|
3583
|
+
* hueco libre — el LLM no tiene que acertar la y exacta. */
|
|
3584
|
+
addTextNode(op) {
|
|
3585
|
+
const page = this.doc.pages.find((p) => p.page === op.page);
|
|
3586
|
+
const size = op.size ?? 11;
|
|
3587
|
+
let y = op.y;
|
|
3588
|
+
if (page) {
|
|
3589
|
+
const estW = Math.min(op.text.length * size * 0.55, page.width - op.x - 20);
|
|
3590
|
+
const collides = (yt) => page.segments.some((sg) => sg.baseline > yt - size * 1.5 && sg.baseline < yt + 2 && sg.x < op.x + estW && sg.x + sg.width > op.x) || this.creates.some((c) => c.kind === "text" && c.page === op.page && Math.abs(c.y - yt) < size * 1.3 && c.x < op.x + estW);
|
|
3591
|
+
let guard = 0;
|
|
3592
|
+
while (collides(y) && y - size * 1.3 > 40 && guard++ < 60) y -= size * 1.3;
|
|
3593
|
+
if (collides(y)) y = op.y;
|
|
3594
|
+
}
|
|
3595
|
+
const moved = Math.abs(y - op.y) > 1;
|
|
3596
|
+
this.creates.push({ kind: "text", ...op, y });
|
|
3597
|
+
return `\u2713 Texto nuevo en p${op.page} @(${op.x},${Math.round(y)})${moved ? ` \u2014 BAJADO desde y=${Math.round(op.y)} (pisaba texto existente)` : ""}: ${JSON.stringify(op.text.slice(0, 40))}`;
|
|
3598
|
+
}
|
|
3599
|
+
insertImageFile(page, x, y, path, maxWidth) {
|
|
3600
|
+
this.creates.push({ kind: "image", page, x, y, path, maxWidth });
|
|
3601
|
+
return `\u2713 Imagen "${path}" en p${page} @(${x},${y})`;
|
|
3602
|
+
}
|
|
3603
|
+
watermark(text, color, opacity) {
|
|
3604
|
+
this.creates.push({ kind: "watermark", text, color, opacity });
|
|
3605
|
+
return `\u2713 Marca de agua: ${JSON.stringify(text)}`;
|
|
3606
|
+
}
|
|
3607
|
+
headerFooter(op) {
|
|
3608
|
+
this.creates.push({ kind: "headerFooter", ...op });
|
|
3609
|
+
return `\u2713 Encabezado/pie aplicado`;
|
|
3610
|
+
}
|
|
3611
|
+
addField(fieldType, page, x, y, width, height, name) {
|
|
3612
|
+
this.creates.push({ kind: "field", fieldType, page, x, y, width, height, name });
|
|
3613
|
+
return `\u2713 Campo ${fieldType} en p${page} @(${x},${y})`;
|
|
3614
|
+
}
|
|
3615
|
+
/* ── REFLOW DE PÁRRAFO (compartido por placeholders_to_fields y edit_text) ──
|
|
3616
|
+
* El LLM nunca calcula geometría: estos helpers tokenizan el párrafo con
|
|
3617
|
+
* anchos MEDIDOS, lo re-envuelven, re-emiten cada renglón con runs anclados a
|
|
3618
|
+
* posiciones calculadas, y hornean+miden EN LOOP hasta que ningún renglón se
|
|
3619
|
+
* pasa del borde ni dos tramos chocan. */
|
|
3620
|
+
/** x por CARÁCTER de un segmento, desde sus runs reales. */
|
|
3621
|
+
charXOf(seg) {
|
|
3622
|
+
const cx = new Array(seg.text.length + 1).fill(seg.x);
|
|
3623
|
+
let cur = 0, lastEnd = seg.x;
|
|
3624
|
+
for (const r2 of seg.runs) {
|
|
3625
|
+
const at = seg.text.indexOf(r2.text, cur);
|
|
3626
|
+
if (at < 0) continue;
|
|
3627
|
+
for (let k = cur; k <= at; k++) cx[k] = lastEnd + (r2.x - lastEnd) * (k - cur) / Math.max(1, at - cur);
|
|
3628
|
+
const w = r2.width / Math.max(1, r2.text.length);
|
|
3629
|
+
for (let k = 0; k <= r2.text.length; k++) cx[at + k] = r2.x + w * k;
|
|
3630
|
+
cur = at + r2.text.length;
|
|
3631
|
+
lastEnd = r2.x + r2.width;
|
|
3632
|
+
}
|
|
3633
|
+
for (let k = cur; k <= seg.text.length; k++) cx[k] = lastEnd;
|
|
3634
|
+
return cx;
|
|
3635
|
+
}
|
|
3636
|
+
/** El párrafo de un segmento: líneas consecutivas con el mismo x de anclaje
|
|
3637
|
+
* y paso de interlineado regular. */
|
|
3638
|
+
paragraphOf(s) {
|
|
3639
|
+
const page = this.doc.pages.find((p) => p.page === s.page);
|
|
3640
|
+
const sameCol = page.segments.filter((x) => Math.abs(x.x - s.x) < 4 && Math.abs(x.fontSize - s.fontSize) < 2).sort((a2, b) => b.baseline - a2.baseline);
|
|
3641
|
+
const idx = sameCol.findIndex((x) => x.id === s.id);
|
|
3642
|
+
const maxLead = s.fontSize * 1.7;
|
|
3643
|
+
let lo = idx, hi = idx;
|
|
3644
|
+
while (lo > 0 && sameCol[lo - 1].baseline - sameCol[lo].baseline < maxLead) lo--;
|
|
3645
|
+
while (hi + 1 < sameCol.length && sameCol[hi].baseline - sameCol[hi + 1].baseline < maxLead) hi++;
|
|
3646
|
+
const lines = sameCol.slice(lo, hi + 1);
|
|
3647
|
+
const leading = lines.length > 1 ? (lines[0].baseline - lines[lines.length - 1].baseline) / (lines.length - 1) : s.fontSize * 1.15;
|
|
3648
|
+
const rightEdge = Math.max(...lines.map((l) => l.x + l.width));
|
|
3649
|
+
return {
|
|
3650
|
+
page,
|
|
3651
|
+
lines,
|
|
3652
|
+
leading,
|
|
3653
|
+
rightEdge,
|
|
3654
|
+
capacity: rightEdge - s.x,
|
|
3655
|
+
spaceW: s.fontSize * 0.28,
|
|
3656
|
+
paraBottom: lines[lines.length - 1].baseline
|
|
3657
|
+
};
|
|
3658
|
+
}
|
|
3659
|
+
/** Tokens (palabras con ancho medido + estilo, y huecos) de las líneas del
|
|
3660
|
+
* párrafo. `replace` sustituye el contenido de UNA línea por runs nuevos
|
|
3661
|
+
* (edit_text): sus palabras se estiman con el ancho medio del segmento. */
|
|
3662
|
+
paragraphToks(para, holes, replace) {
|
|
3663
|
+
const toks = [];
|
|
3664
|
+
for (let k = 0; k < para.lines.length; k++) {
|
|
3665
|
+
const line = para.lines[k];
|
|
3666
|
+
if (replace && line.id === replace.lineId) {
|
|
3667
|
+
for (const run of replace.styled) {
|
|
3668
|
+
for (const m of run.text.matchAll(/\S+/g)) {
|
|
3669
|
+
toks.push({ kind: "word", text: m[0], w: m[0].length * replace.avgCharW, bold: run.bold, italic: run.italic });
|
|
3670
|
+
}
|
|
3671
|
+
}
|
|
3672
|
+
continue;
|
|
3673
|
+
}
|
|
3674
|
+
const text = line.text;
|
|
3675
|
+
const cx = this.charXOf(line);
|
|
3676
|
+
const styleAt = new Array(text.length).fill({ bold: false, italic: false });
|
|
3677
|
+
let sc = 0;
|
|
3678
|
+
for (const r2 of line.runs) {
|
|
3679
|
+
const at = text.indexOf(r2.text, sc);
|
|
3680
|
+
if (at < 0) continue;
|
|
3681
|
+
for (let c = at; c < at + r2.text.length; c++) styleAt[c] = { bold: r2.font.bold, italic: r2.font.italic };
|
|
3682
|
+
sc = at + r2.text.length;
|
|
3683
|
+
}
|
|
3684
|
+
const lineHoles = holes.filter((h) => h.li === k).sort((a2, b) => a2.from - b.from);
|
|
3685
|
+
let pos = 0;
|
|
3686
|
+
const pushWords = (from, to) => {
|
|
3687
|
+
for (const m of text.slice(from, to).matchAll(/\S+/g)) {
|
|
3688
|
+
const a2 = from + m.index, b = a2 + m[0].length;
|
|
3689
|
+
toks.push({ kind: "word", text: m[0], w: cx[b] - cx[a2], bold: styleAt[a2]?.bold ?? false, italic: styleAt[a2]?.italic ?? false });
|
|
3690
|
+
}
|
|
3691
|
+
};
|
|
3692
|
+
for (const h of lineHoles) {
|
|
3693
|
+
pushWords(pos, h.from);
|
|
3694
|
+
toks.push({ kind: "hole", w: 0, hole: h });
|
|
3695
|
+
pos = h.to;
|
|
3696
|
+
}
|
|
3697
|
+
pushWords(pos, text.length);
|
|
3698
|
+
}
|
|
3699
|
+
return toks;
|
|
3700
|
+
}
|
|
3701
|
+
/** Re-envuelve, aplica y MIDE EN LOOP: re-emite cada renglón con runs propios
|
|
3702
|
+
* (dx calculado, nunca heredado), hornea un preview, y corrige — overflow del
|
|
3703
|
+
* borde achica los targets de los huecos; tramos que chocan se re-anclan con
|
|
3704
|
+
* el corrimiento EXACTO medido. Si el párrafo crece, corre lo de abajo. */
|
|
3705
|
+
async reflowApply(s, para, toks) {
|
|
3706
|
+
const { page, lines, leading, rightEdge, capacity, spaceW, paraBottom } = para;
|
|
3707
|
+
const slackBelow = Math.max(0, paraBottom - 60);
|
|
3708
|
+
const maxExtraLines = Math.min(3, Math.floor(slackBelow / leading));
|
|
3709
|
+
const holeW = (t, scale2) => Math.max(25, t.hole.target * scale2);
|
|
3710
|
+
const wrap = (scale2, capShrink2) => {
|
|
3711
|
+
const cap = capacity - capShrink2;
|
|
3712
|
+
const rows = [[]];
|
|
3713
|
+
let curW = 0;
|
|
3714
|
+
for (const t of toks) {
|
|
3715
|
+
const w = t.kind === "hole" ? holeW(t, scale2) : t.w;
|
|
3716
|
+
const sep = rows[rows.length - 1].length ? spaceW : 0;
|
|
3717
|
+
if (curW + sep + w > cap && rows[rows.length - 1].length) {
|
|
3718
|
+
rows.push([]);
|
|
3719
|
+
curW = 0;
|
|
3720
|
+
}
|
|
3721
|
+
rows[rows.length - 1].push(t);
|
|
3722
|
+
curW += (rows[rows.length - 1].length > 1 ? spaceW : 0) + w;
|
|
3723
|
+
}
|
|
3724
|
+
return rows;
|
|
3725
|
+
};
|
|
3726
|
+
const BOUNDARY_PAD = 2;
|
|
3727
|
+
const dxFix = /* @__PURE__ */ new Map();
|
|
3728
|
+
const rowRuns = (row, scale2, rowIdx) => {
|
|
3729
|
+
const runs = [];
|
|
3730
|
+
let cursor = 0;
|
|
3731
|
+
for (const t of row) {
|
|
3732
|
+
const isFirst = runs.length === 0;
|
|
3733
|
+
const sep = isFirst ? 0 : spaceW;
|
|
3734
|
+
const text = t.kind === "word" ? t.text : " ".repeat(Math.max(3, Math.round(holeW(t, scale2) / spaceW)));
|
|
3735
|
+
const w = t.kind === "word" ? t.w : holeW(t, scale2);
|
|
3736
|
+
const bold = t.kind === "word" ? !!t.bold : false;
|
|
3737
|
+
const italic = t.kind === "word" ? !!t.italic : false;
|
|
3738
|
+
const last = runs[runs.length - 1];
|
|
3739
|
+
if (last && (t.kind === "hole" || last.bold === bold && last.italic === italic)) {
|
|
3740
|
+
last.text += (isFirst ? "" : " ") + text;
|
|
3741
|
+
} else {
|
|
3742
|
+
runs.push({ text, bold, italic, dx: cursor + sep + (isFirst ? 0 : BOUNDARY_PAD) });
|
|
3743
|
+
}
|
|
3744
|
+
cursor += sep + w;
|
|
3745
|
+
}
|
|
3746
|
+
void rowIdx;
|
|
3747
|
+
for (const r2 of runs) {
|
|
3748
|
+
const fix = dxFix.get(r2.text.trim().slice(0, 30));
|
|
3749
|
+
if (fix) r2.dx += fix;
|
|
3750
|
+
}
|
|
3751
|
+
return runs;
|
|
3752
|
+
};
|
|
3753
|
+
const createStart = this.creates.length;
|
|
3754
|
+
let scale = 1;
|
|
3755
|
+
let capShrink = 0;
|
|
3756
|
+
let layout = [];
|
|
3757
|
+
let extraLines = 0;
|
|
3758
|
+
let rePage;
|
|
3759
|
+
for (let iter = 0; iter < 6; iter++) {
|
|
3760
|
+
layout = wrap(scale, capShrink);
|
|
3761
|
+
while (layout.length > lines.length + maxExtraLines && scale > 0.3) {
|
|
3762
|
+
scale *= 0.9;
|
|
3763
|
+
layout = wrap(scale, capShrink);
|
|
3764
|
+
}
|
|
3765
|
+
extraLines = Math.max(0, layout.length - lines.length);
|
|
3766
|
+
this.creates.length = createStart;
|
|
3767
|
+
for (const l of lines) this.edits.delete(l.id);
|
|
3768
|
+
for (let k = 0; k < lines.length; k++) {
|
|
3769
|
+
const runs = k < layout.length ? rowRuns(layout[k], scale, k) : [];
|
|
3770
|
+
if (runs.length) this.putSeg(lines[k], { runs });
|
|
3771
|
+
else this.putSeg(lines[k], { remove: true });
|
|
3772
|
+
}
|
|
3773
|
+
for (let e = 0; e < extraLines; e++) {
|
|
3774
|
+
const bl = paraBottom - leading * (e + 1);
|
|
3775
|
+
const text = layout[lines.length + e].map((t) => t.kind === "word" ? t.text : " ".repeat(Math.max(3, Math.round(holeW(t, scale) / spaceW)))).join(" ");
|
|
3776
|
+
this.creates.push({ kind: "text", page: s.page, x: s.x, y: bl + s.fontSize, text, size: s.fontSize });
|
|
3777
|
+
}
|
|
3778
|
+
if (extraLines > 0) {
|
|
3779
|
+
const dy = extraLines * leading;
|
|
3780
|
+
for (const other of page.segments) {
|
|
3781
|
+
if (other.baseline < paraBottom - 1 && !lines.some((l) => l.id === other.id)) {
|
|
3782
|
+
this.putSeg(other, { baseline: other.baseline - dy });
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
for (let ci = 0; ci < createStart; ci++) {
|
|
3786
|
+
const c = this.creates[ci];
|
|
3787
|
+
if (c.kind === "field" && c.page === s.page && c.y < paraBottom - 1) c.y -= dy;
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
const { pdf } = await this.bake();
|
|
3791
|
+
const re = await graphFromBytes(pdf.slice());
|
|
3792
|
+
rePage = re.pages.find((p) => p.page === s.page);
|
|
3793
|
+
let maxOver = 0;
|
|
3794
|
+
let collided = false;
|
|
3795
|
+
const MIN_GAP = spaceW * 0.7;
|
|
3796
|
+
for (let k = 0; k < layout.length; k++) {
|
|
3797
|
+
const bl = k < lines.length ? lines[k].baseline : paraBottom - leading * (k - lines.length + 1);
|
|
3798
|
+
const rowSegs = (rePage?.segments ?? []).filter((x) => Math.abs(x.baseline - bl) < 6 && x.x >= s.x - 3).sort((a2, b) => a2.x - b.x);
|
|
3799
|
+
const flat = rowSegs.flatMap((seg) => seg.runs).sort((a2, b) => a2.x - b.x);
|
|
3800
|
+
for (let i = 0; i < flat.length; i++) {
|
|
3801
|
+
maxOver = Math.max(maxOver, flat[i].x + flat[i].width - rightEdge);
|
|
3802
|
+
if (i > 0) {
|
|
3803
|
+
const gap = flat[i].x - (flat[i - 1].x + flat[i - 1].width);
|
|
3804
|
+
if (gap < MIN_GAP) {
|
|
3805
|
+
collided = true;
|
|
3806
|
+
const key = flat[i].text.trim().slice(0, 30);
|
|
3807
|
+
dxFix.set(key, (dxFix.get(key) ?? 0) + (MIN_GAP - gap));
|
|
3808
|
+
}
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
const overflow = maxOver > 3;
|
|
3813
|
+
if (!overflow && !collided) break;
|
|
3814
|
+
if (overflow) {
|
|
3815
|
+
capShrink += maxOver + spaceW;
|
|
3816
|
+
scale *= 0.96;
|
|
3817
|
+
}
|
|
3818
|
+
}
|
|
3819
|
+
return { layout, rePage, scale, extraLines };
|
|
3820
|
+
}
|
|
3821
|
+
/**
|
|
3822
|
+
* DETERMINÍSTICO — el LLM DETECTA (pasa los substrings de placeholder + nombre
|
|
3823
|
+
* y opcionalmente el ancho útil) y el CÓDIGO hace TODO el layout: campos con
|
|
3824
|
+
* ancho ÚTIL (nombre ~110pt, número ~55pt), párrafo reconstruido si hace falta
|
|
3825
|
+
* (renglón extra + contenido inferior corrido), loop de medición hasta que
|
|
3826
|
+
* todo queda dentro, y cada campo sobre el HUECO REAL medido del preview.
|
|
3827
|
+
* `id` puede ser cualquier línea del párrafo; `fields` en orden de lectura.
|
|
3828
|
+
*/
|
|
3829
|
+
async placeholdersToFields(id, fields) {
|
|
3830
|
+
const s = this.seg(id);
|
|
3831
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
3832
|
+
if (!fields.length) return `\u26A0\uFE0F placeholders_to_fields necesita al menos un {placeholder,name}.`;
|
|
3833
|
+
const para = this.paragraphOf(s);
|
|
3834
|
+
const { lines, leading, rightEdge, paraBottom } = para;
|
|
3835
|
+
if (lines.some((l) => this.edits.has(l.id))) {
|
|
3836
|
+
return `\u21A9\uFE0E Ese p\xE1rrafo ya fue convertido en esta sesi\xF3n (sus placeholders ya son campos). No repitas la llamada.`;
|
|
3837
|
+
}
|
|
3838
|
+
const holes = [];
|
|
3839
|
+
let li = 0, off = 0;
|
|
3840
|
+
for (const f of fields) {
|
|
3841
|
+
if (!f.placeholder) return `\u26A0\uFE0F un field vino sin placeholder.`;
|
|
3842
|
+
let at = -1;
|
|
3843
|
+
while (li < lines.length) {
|
|
3844
|
+
at = lines[li].text.indexOf(f.placeholder, off);
|
|
3845
|
+
if (at >= 0) break;
|
|
3846
|
+
li++;
|
|
3847
|
+
off = 0;
|
|
3848
|
+
}
|
|
3849
|
+
if (at < 0) return `\u26A0\uFE0F no encontr\xE9 ${JSON.stringify(f.placeholder)} en el p\xE1rrafo de ${id} (us\xE1 el texto EXACTO, en orden de lectura).`;
|
|
3850
|
+
holes.push({ li, from: at, to: at + f.placeholder.length, name: (f.name || "").trim() || `campo_${holes.length + 1}`, target: targetWidthFor(f.name ?? "", f.width, s.fontSize) });
|
|
3851
|
+
off = at + f.placeholder.length;
|
|
3852
|
+
}
|
|
3853
|
+
const toks = this.paragraphToks(para, holes);
|
|
3854
|
+
const { layout, rePage, scale, extraLines } = await this.reflowApply(s, para, toks);
|
|
3855
|
+
const made = [];
|
|
3856
|
+
let holeCursor = 0;
|
|
3857
|
+
for (let k = 0; k < layout.length && holeCursor < holes.length; k++) {
|
|
3858
|
+
const rowHoles = layout[k].filter((t) => t.kind === "hole").map((t) => t.hole);
|
|
3859
|
+
if (!rowHoles.length) continue;
|
|
3860
|
+
const bl = k < lines.length ? lines[k].baseline : paraBottom - leading * (k - lines.length + 1);
|
|
3861
|
+
const rowSegs = (rePage?.segments ?? []).filter((x) => Math.abs(x.baseline - bl) < 6 && x.x >= s.x - 3 && x.x <= rightEdge + 8).sort((a2, b) => a2.x - b.x);
|
|
3862
|
+
const gaps = [];
|
|
3863
|
+
const GAP_MIN = 10;
|
|
3864
|
+
if (rowSegs.length && rowSegs[0].x - s.x > GAP_MIN) gaps.push({ from: s.x, to: rowSegs[0].x });
|
|
3865
|
+
for (let i = 0; i < rowSegs.length; i++) {
|
|
3866
|
+
const from = rowSegs[i].x + rowSegs[i].width;
|
|
3867
|
+
const to = i + 1 < rowSegs.length ? rowSegs[i + 1].x : rightEdge;
|
|
3868
|
+
if (to - from > GAP_MIN) gaps.push({ from, to });
|
|
3869
|
+
}
|
|
3870
|
+
const size = rowSegs[0]?.fontSize ?? s.fontSize;
|
|
3871
|
+
const rbl = rowSegs[0]?.baseline ?? bl;
|
|
3872
|
+
for (let i = 0; i < rowHoles.length && holeCursor < holes.length; i++, holeCursor++) {
|
|
3873
|
+
const g = gaps[i];
|
|
3874
|
+
if (!g) {
|
|
3875
|
+
made.push(`(\u26A0 sin hueco medible para ${rowHoles[i].name})`);
|
|
3876
|
+
continue;
|
|
3877
|
+
}
|
|
3878
|
+
this.creates.push({
|
|
3879
|
+
kind: "field",
|
|
3880
|
+
fieldType: "text",
|
|
3881
|
+
page: s.page,
|
|
3882
|
+
x: Math.round((g.from + 1) * 100) / 100,
|
|
3883
|
+
y: Math.round((rbl - 2) * 100) / 100,
|
|
3884
|
+
width: Math.round(Math.max(20, g.to - g.from - 2) * 100) / 100,
|
|
3885
|
+
height: Math.round((size + 2) * 100) / 100,
|
|
3886
|
+
name: rowHoles[i].name
|
|
3887
|
+
});
|
|
3888
|
+
made.push(`${rowHoles[i].name} @(${Math.round(g.from + 1)},${Math.round(rbl - 2)}) ${Math.round(g.to - g.from - 2)}pt`);
|
|
3889
|
+
}
|
|
3890
|
+
}
|
|
3891
|
+
const grew = extraLines ? ` \xB7 p\xE1rrafo reconstruido (+${extraLines} rengl\xF3n/es, contenido inferior corrido)` : "";
|
|
3892
|
+
const shrunk = scale < 1 ? ` \xB7 targets al ${Math.round(scale * 100)}% para entrar en p\xE1gina` : "";
|
|
3893
|
+
return `\u2713 ${made.length} campo(s) con ancho \xFAtil sobre huecos REALES (medidos tras hornear)${grew}${shrunk}: ${made.join(" \xB7 ")}`;
|
|
3894
|
+
}
|
|
3895
|
+
/** COMPLETA un campo de formulario por su NOMBRE o por su id de widget
|
|
3896
|
+
* ([[p1-w3]] de la vista de Lectura — se resuelve al fieldName). Valor:
|
|
3897
|
+
* texto para text/select/radio, true/false para checkbox. Determinístico. */
|
|
3898
|
+
fillField(nameOrId, value) {
|
|
3899
|
+
let fieldName = nameOrId;
|
|
3900
|
+
if (!this.doc.pages.some((p) => p.widgets.some((w) => w.fieldName === fieldName))) {
|
|
3901
|
+
const byId = this.widget(nameOrId.replace(/^\[\[|\]\]$/g, ""));
|
|
3902
|
+
if (!byId) return `\u26A0\uFE0F No existe un campo llamado "${nameOrId}" (ni como fieldName ni como id).`;
|
|
3903
|
+
fieldName = byId.fieldName;
|
|
3904
|
+
}
|
|
3905
|
+
this.fills.set(fieldName, value);
|
|
3906
|
+
return `\u2713 Campo "${fieldName}" \u2190 ${JSON.stringify(value)}`;
|
|
3907
|
+
}
|
|
3908
|
+
/** COMPLETA VARIOS campos de una (por fieldName o id de widget) — UNA sola tool
|
|
3909
|
+
* call en vez de N idas y vueltas con el modelo (clave para forms grandes). */
|
|
3910
|
+
fillFields(entries) {
|
|
3911
|
+
const lines = entries.map((e) => this.fillField(e.name, e.value));
|
|
3912
|
+
return lines.join("\n");
|
|
3913
|
+
}
|
|
3914
|
+
/** Precarga ediciones ya existentes (p. ej. las pendientes del editor UI). */
|
|
3915
|
+
seed(edits = [], imageEdits = []) {
|
|
3916
|
+
for (const e of edits) this.edits.set(e.segmentId, e);
|
|
3917
|
+
for (const e of imageEdits) this.imageEdits.set(e.imageId, e);
|
|
3918
|
+
}
|
|
3919
|
+
/** Ediciones de texto/imagen acumuladas (lo que el editor sabe aplicar a su
|
|
3920
|
+
* estado local; las creaciones/annotations se hornean en el server/CLI). */
|
|
3921
|
+
getEdits() {
|
|
3922
|
+
return { edits: [...this.edits.values()], imageEdits: [...this.imageEdits.values()] };
|
|
3923
|
+
}
|
|
3924
|
+
/** Cantidad total de cambios pendientes. */
|
|
3925
|
+
get count() {
|
|
3926
|
+
return this.edits.size + this.imageEdits.size + this.widgetEdits.size + this.highlightEdits.size + this.linkEdits.size + this.creates.length + this.fills.size;
|
|
3927
|
+
}
|
|
3928
|
+
/** ¿Hay cambios que el editor NO puede reflejar con getEdits() (creaciones,
|
|
3929
|
+
* ediciones de annotations, o llenado de formularios)? El server los hornea +
|
|
3930
|
+
* persiste en vez de devolverlos como seg/img edits. */
|
|
3931
|
+
get hasBakedOps() {
|
|
3932
|
+
return this.creates.length + this.widgetEdits.size + this.highlightEdits.size + this.linkEdits.size + this.fills.size > 0;
|
|
3933
|
+
}
|
|
3934
|
+
summary() {
|
|
3935
|
+
const parts = [];
|
|
3936
|
+
for (const e of this.edits.values()) parts.push(e.remove ? `${e.segmentId}: eliminar` : `${e.segmentId}: editar`);
|
|
3937
|
+
for (const e of this.imageEdits.values()) parts.push(e.remove ? `${e.imageId}: eliminar` : `${e.imageId}: mover/escalar`);
|
|
3938
|
+
for (const e of this.widgetEdits.values()) parts.push(`${e.widgetId}: campo`);
|
|
3939
|
+
for (const e of this.highlightEdits.values()) parts.push(`${e.highlightId}: resaltado`);
|
|
3940
|
+
for (const e of this.linkEdits.values()) parts.push(`${e.linkId}: link`);
|
|
3941
|
+
for (const c of this.creates) parts.push(`+${c.kind}`);
|
|
3942
|
+
for (const [name] of this.fills) parts.push(`${name}: completar`);
|
|
3943
|
+
return parts.join(" \xB7 ") || "(sin cambios)";
|
|
3944
|
+
}
|
|
3945
|
+
/** Aplica una CREACIÓN sobre los bytes ya horneados (bytes→bytes). */
|
|
3946
|
+
async applyCreate(pdf, op) {
|
|
3947
|
+
switch (op.kind) {
|
|
3948
|
+
case "highlightSeg": {
|
|
3949
|
+
const s = this.seg(op.segId);
|
|
3950
|
+
const g = effectiveGeometry(s, this.edits.get(op.segId) ?? null);
|
|
3951
|
+
return (await addHighlight(pdf, { page: s.page, x: g.x, y: g.y, width: g.width, height: g.height, color: op.color })).pdf;
|
|
3952
|
+
}
|
|
3953
|
+
case "linkSeg": {
|
|
3954
|
+
const s = this.seg(op.segId);
|
|
3955
|
+
const g = effectiveGeometry(s, this.edits.get(op.segId) ?? null);
|
|
3956
|
+
return (await addLink(pdf, { page: s.page, x: g.x, y: g.y, width: g.width, height: g.height, url: op.url })).pdf;
|
|
3957
|
+
}
|
|
3958
|
+
case "text":
|
|
3959
|
+
return (await addText(pdf, op)).pdf;
|
|
3960
|
+
case "image": {
|
|
3961
|
+
const bytes = new Uint8Array(await readFile2(op.path));
|
|
3962
|
+
const ext = op.path.split(".").pop()?.toLowerCase() ?? "";
|
|
3963
|
+
const mime = MIME[ext];
|
|
3964
|
+
if (!mime) throw new Error(`imagen no soportada (${op.path}): solo PNG/JPEG`);
|
|
3965
|
+
return (await insertImage(pdf, { page: op.page, x: op.x, y: op.y, bytes, mime, maxWidth: op.maxWidth })).pdf;
|
|
3966
|
+
}
|
|
3967
|
+
case "watermark":
|
|
3968
|
+
return (await addWatermark(pdf, { text: op.text, opacity: op.opacity, color: op.color })).pdf;
|
|
3969
|
+
case "headerFooter":
|
|
3970
|
+
return (await addHeaderFooter(pdf, { header: op.header, footer: op.footer, pageNumbers: op.pageNumbers })).pdf;
|
|
3971
|
+
case "field":
|
|
3972
|
+
return (await addFormField(pdf, { type: op.fieldType, page: op.page, x: op.x, y: op.y, width: op.width, height: op.height, name: op.name })).pdf;
|
|
3973
|
+
}
|
|
3974
|
+
}
|
|
3975
|
+
/** Hornea TODO (ediciones + annotations + creaciones) y devuelve los bytes. */
|
|
3976
|
+
async bake() {
|
|
3977
|
+
const imgs = promoteMovedImages([...this.imageEdits.values()]);
|
|
3978
|
+
const r2 = await bakeSegmentEdits(
|
|
3979
|
+
this.doc.bytes.slice(),
|
|
3980
|
+
[...this.edits.values()],
|
|
3981
|
+
imgs,
|
|
3982
|
+
[...this.widgetEdits.values()],
|
|
3983
|
+
[...this.highlightEdits.values()],
|
|
3984
|
+
[...this.linkEdits.values()]
|
|
3985
|
+
);
|
|
3986
|
+
let pdf = r2.pdf;
|
|
3987
|
+
for (const op of this.creates) {
|
|
3988
|
+
pdf = await this.applyCreate(pdf, op);
|
|
3989
|
+
r2.applied.push(`+${op.kind}`);
|
|
3990
|
+
}
|
|
3991
|
+
if (this.fills.size) {
|
|
3992
|
+
const res = await setFieldValues(pdf, Object.fromEntries(this.fills));
|
|
3993
|
+
pdf = res.pdf;
|
|
3994
|
+
r2.applied.push(...res.applied.map((a2) => `campo ${a2}`));
|
|
3995
|
+
r2.warnings.push(...res.warnings);
|
|
3996
|
+
}
|
|
3997
|
+
return { pdf, applied: r2.applied, warnings: r2.warnings };
|
|
3998
|
+
}
|
|
3999
|
+
/** Hornea y escribe el PDF a `outPath`. */
|
|
4000
|
+
async save(outPath) {
|
|
4001
|
+
const { pdf, applied, warnings } = await this.bake();
|
|
4002
|
+
await writeFile(outPath, pdf);
|
|
4003
|
+
return { applied, warnings };
|
|
4004
|
+
}
|
|
4005
|
+
};
|
|
4006
|
+
|
|
4007
|
+
// ../agent/src/index.ts
|
|
4008
|
+
init_agent();
|
|
4009
|
+
export {
|
|
4010
|
+
EditSession,
|
|
4011
|
+
loadDoc,
|
|
4012
|
+
readFormFields,
|
|
4013
|
+
runTurn,
|
|
4014
|
+
serializeDoc,
|
|
4015
|
+
setFieldValues
|
|
4016
|
+
};
|