@bernatch22/aldus 0.1.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 +59 -0
- package/dist/cli.js +3250 -0
- package/dist/index.js +3070 -0
- package/package.json +29 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,3250 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// ../agent/src/tools.ts
|
|
13
|
+
import { tool, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
function buildToolServer(session) {
|
|
16
|
+
const ok = (t) => ({ content: [{ type: "text", text: t }] });
|
|
17
|
+
const tools = TOOL_DEFS.map(
|
|
18
|
+
(d) => tool(d.name, d.description, d.shape, async (args) => ok(d.run(session, args)))
|
|
19
|
+
);
|
|
20
|
+
return createSdkMcpServer({ name: "aldus", version: "0.0.1", tools });
|
|
21
|
+
}
|
|
22
|
+
function openaiTools() {
|
|
23
|
+
return TOOL_DEFS.map((d) => ({
|
|
24
|
+
type: "function",
|
|
25
|
+
function: { name: d.name, description: d.description, parameters: z.toJSONSchema(z.object(d.shape)) }
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
function runTool(session, name, args) {
|
|
29
|
+
const d = TOOL_DEFS.find((x) => x.name === name);
|
|
30
|
+
if (!d) return `\u26A0\uFE0F tool desconocida: ${name}`;
|
|
31
|
+
try {
|
|
32
|
+
return d.run(session, args);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
return `\u26A0\uFE0F ${name}: ${err instanceof Error ? err.message : "error"}`;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
var FIELD_TYPES, a, TOOL_DEFS, TOOL_NAMES;
|
|
38
|
+
var init_tools = __esm({
|
|
39
|
+
"../agent/src/tools.ts"() {
|
|
40
|
+
FIELD_TYPES = ["text", "checkbox", "radio", "select", "list", "button", "signature"];
|
|
41
|
+
a = (o) => o;
|
|
42
|
+
TOOL_DEFS = [
|
|
43
|
+
// ── texto existente ──
|
|
44
|
+
{
|
|
45
|
+
name: "edit_text",
|
|
46
|
+
description: "Reemplaza el CONTENIDO de un nodo de texto (conserva su estilo). Us\xE1 el id exacto del grafo.",
|
|
47
|
+
shape: { id: z.string().describe("id del nodo de texto, p. ej. p1-y708-x72"), text: z.string().describe("texto nuevo") },
|
|
48
|
+
run: (s, { id, text }) => s.editText(id, text)
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "move_text",
|
|
52
|
+
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.",
|
|
53
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional() },
|
|
54
|
+
run: (s, { id, x, y }) => s.moveText(id, x, y)
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
name: "set_text_color",
|
|
58
|
+
description: "Cambia el color de un nodo de texto (hex #rrggbb).",
|
|
59
|
+
shape: { id: z.string(), color: z.string().describe("#rrggbb") },
|
|
60
|
+
run: (s, { id, color }) => s.colorText(id, color)
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "set_text_size",
|
|
64
|
+
description: "Cambia el tama\xF1o de fuente de un nodo de texto (puntos).",
|
|
65
|
+
shape: { id: z.string(), size: z.number().positive() },
|
|
66
|
+
run: (s, { id, size }) => s.resizeText(id, size)
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "delete_text",
|
|
70
|
+
description: "Elimina un nodo de texto del documento.",
|
|
71
|
+
shape: { id: z.string() },
|
|
72
|
+
run: (s, { id }) => s.deleteText(id)
|
|
73
|
+
},
|
|
74
|
+
// ── imagen existente ──
|
|
75
|
+
{
|
|
76
|
+
name: "move_image",
|
|
77
|
+
description: "Mueve y/o escala una imagen. Coordenadas/tama\xF1o en puntos PDF (origen abajo-izquierda). Omit\xED lo que no cambia.",
|
|
78
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional(), width: z.number().positive().optional(), height: z.number().positive().optional() },
|
|
79
|
+
run: (s, { id, x, y, width, height }) => s.moveImage(id, a({ x, y, width, height }))
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "delete_image",
|
|
83
|
+
description: "Elimina una imagen del documento.",
|
|
84
|
+
shape: { id: z.string() },
|
|
85
|
+
run: (s, { id }) => s.deleteImage(id)
|
|
86
|
+
},
|
|
87
|
+
// ── resaltados (/Annots) ──
|
|
88
|
+
{
|
|
89
|
+
name: "highlight_text",
|
|
90
|
+
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).",
|
|
91
|
+
shape: { id: z.string().describe("id del nodo de texto a resaltar"), color: z.string().optional().describe("#rrggbb") },
|
|
92
|
+
run: (s, { id, color }) => s.highlightText(id, color)
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "set_highlight_color",
|
|
96
|
+
description: "Cambia el color de un resaltado YA EXISTENTE (id de highlight del grafo, p. ej. p1-hl0). Hex #rrggbb.",
|
|
97
|
+
shape: { id: z.string(), color: z.string().describe("#rrggbb") },
|
|
98
|
+
run: (s, { id, color }) => s.recolorHighlight(id, color)
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "delete_highlight",
|
|
102
|
+
description: "Elimina un resaltado existente (id de highlight del grafo).",
|
|
103
|
+
shape: { id: z.string() },
|
|
104
|
+
run: (s, { id }) => s.deleteHighlight(id)
|
|
105
|
+
},
|
|
106
|
+
// ── links (/Annots) ──
|
|
107
|
+
{
|
|
108
|
+
name: "add_link",
|
|
109
|
+
description: "Pone un LINK clickeable sobre un nodo de texto EXISTENTE (por su id) hacia una URL.",
|
|
110
|
+
shape: { id: z.string().describe("id del nodo de texto"), url: z.string().describe("URL destino") },
|
|
111
|
+
run: (s, { id, url }) => s.linkText(id, url)
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: "delete_link",
|
|
115
|
+
description: "Elimina un link existente (id de link del grafo, p. ej. p1-link0).",
|
|
116
|
+
shape: { id: z.string() },
|
|
117
|
+
run: (s, { id }) => s.deleteLink(id)
|
|
118
|
+
},
|
|
119
|
+
// ── creación de contenido nuevo ──
|
|
120
|
+
{
|
|
121
|
+
name: "add_text",
|
|
122
|
+
description: "Agrega un p\xE1rrafo de texto NUEVO. (x,y) = esquina superior-izquierda en puntos PDF (origen abajo-izq). Se re-extrae como un nodo editable.",
|
|
123
|
+
shape: {
|
|
124
|
+
page: z.number().int().min(1),
|
|
125
|
+
x: z.number(),
|
|
126
|
+
y: z.number(),
|
|
127
|
+
text: z.string(),
|
|
128
|
+
size: z.number().positive().optional(),
|
|
129
|
+
bold: z.boolean().optional(),
|
|
130
|
+
italic: z.boolean().optional(),
|
|
131
|
+
color: z.string().optional().describe("#rrggbb")
|
|
132
|
+
},
|
|
133
|
+
run: (s, { page, x, y, text, size, bold, italic, color }) => s.addTextNode(a({ page, x, y, text, size, bold, italic, color }))
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
name: "insert_image",
|
|
137
|
+
description: "Inserta una imagen (PNG/JPEG) desde una RUTA de archivo local. (x,y) = esquina superior-izquierda en puntos PDF.",
|
|
138
|
+
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() },
|
|
139
|
+
run: (s, { page, x, y, path: path2, maxWidth }) => s.insertImageFile(page, x, y, path2, maxWidth)
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "add_watermark",
|
|
143
|
+
description: "Marca de agua de texto diagonal en TODAS las p\xE1ginas.",
|
|
144
|
+
shape: { text: z.string(), color: z.string().optional().describe("#rrggbb"), opacity: z.number().min(0).max(1).optional() },
|
|
145
|
+
run: (s, { text, color, opacity }) => s.watermark(text, color, opacity)
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
name: "add_header_footer",
|
|
149
|
+
description: "Agrega encabezado y/o pie de p\xE1gina (texto) y opcionalmente n\xFAmeros de p\xE1gina, en todas las p\xE1ginas.",
|
|
150
|
+
shape: { header: z.string().optional(), footer: z.string().optional(), pageNumbers: z.boolean().optional() },
|
|
151
|
+
run: (s, { header, footer, pageNumbers }) => s.headerFooter(a({ header, footer, pageNumbers }))
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: "add_form_field",
|
|
155
|
+
description: "Crea un campo de formulario nuevo (text/checkbox/radio/select/list/button/signature). (x,y) = esquina inferior-izquierda en puntos PDF.",
|
|
156
|
+
shape: {
|
|
157
|
+
type: z.enum(FIELD_TYPES),
|
|
158
|
+
page: z.number().int().min(1),
|
|
159
|
+
x: z.number(),
|
|
160
|
+
y: z.number(),
|
|
161
|
+
width: z.number().positive().optional(),
|
|
162
|
+
height: z.number().positive().optional(),
|
|
163
|
+
name: z.string().optional()
|
|
164
|
+
},
|
|
165
|
+
run: (s, { type, page, x, y, width, height, name }) => s.addField(type, page, x, y, width, height, name)
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "fill_field",
|
|
169
|
+
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.',
|
|
170
|
+
shape: { name: z.string().describe("fieldName o id de widget (p1-w3)"), value: z.string().describe('valor (para checkbox: "true"/"false")') },
|
|
171
|
+
run: (s, { name, value }) => s.fillField(name, value === "true" ? true : value === "false" ? false : value)
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "fill_fields",
|
|
175
|
+
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".',
|
|
176
|
+
shape: { fields: z.array(z.object({ name: z.string(), value: z.string() })).describe("lista de campos a completar") },
|
|
177
|
+
run: (s, { fields }) => s.fillFields(
|
|
178
|
+
fields.map((f) => ({ name: f.name, value: f.value === "true" ? true : f.value === "false" ? false : f.value }))
|
|
179
|
+
)
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: "move_field",
|
|
183
|
+
description: "Mueve un campo de formulario EXISTENTE (id de widget del grafo). Coordenadas en puntos PDF; omit\xED lo que no cambia.",
|
|
184
|
+
shape: { id: z.string(), x: z.number().optional(), y: z.number().optional() },
|
|
185
|
+
run: (s, { id, x, y }) => s.moveField(id, x, y)
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
name: "delete_field",
|
|
189
|
+
description: "Elimina un campo de formulario existente (id de widget del grafo).",
|
|
190
|
+
shape: { id: z.string() },
|
|
191
|
+
run: (s, { id }) => s.deleteField(id)
|
|
192
|
+
}
|
|
193
|
+
];
|
|
194
|
+
TOOL_NAMES = TOOL_DEFS.map((d) => `mcp__aldus__${d.name}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// ../agent/src/serialize.ts
|
|
199
|
+
function styleOf(s) {
|
|
200
|
+
const run = s.runs.reduce((a2, b) => b.fontSize > a2.fontSize ? b : a2, s.runs[0]);
|
|
201
|
+
if (!run) return "";
|
|
202
|
+
const bits = [];
|
|
203
|
+
if (run.font.bold) bits.push("bold");
|
|
204
|
+
if (run.font.italic) bits.push("italic");
|
|
205
|
+
bits.push(run.font.postScriptName || run.font.bucket);
|
|
206
|
+
if (run.color) bits.push(run.color);
|
|
207
|
+
return bits.join(" ");
|
|
208
|
+
}
|
|
209
|
+
function nearestLabel(w, segs) {
|
|
210
|
+
const cy = w.y + w.height / 2;
|
|
211
|
+
let best = null;
|
|
212
|
+
for (const s of segs) {
|
|
213
|
+
const sy = s.baseline;
|
|
214
|
+
const dyLine = Math.abs(sy - cy);
|
|
215
|
+
const leftGap = w.x - (s.x + s.width);
|
|
216
|
+
const aboveGap = sy - (w.y + w.height);
|
|
217
|
+
let score;
|
|
218
|
+
const belowGap = w.y - sy;
|
|
219
|
+
const hOverlap = s.x < w.x + w.width && s.x + s.width > w.x - 40;
|
|
220
|
+
if (dyLine <= w.height + 6 && leftGap >= -2 && leftGap < 220) score = leftGap;
|
|
221
|
+
else if (aboveGap >= -2 && aboveGap < 26 && hOverlap) score = 300 + aboveGap;
|
|
222
|
+
else if (belowGap >= -2 && belowGap < 20 && hOverlap) score = 600 + belowGap;
|
|
223
|
+
else continue;
|
|
224
|
+
if (!best || score < best.score) best = { s, score };
|
|
225
|
+
}
|
|
226
|
+
if (!best) return void 0;
|
|
227
|
+
const t = best.s.text.replace(/\s+/g, " ").trim();
|
|
228
|
+
return t.length > 40 ? t.slice(0, 40) + "\u2026" : t;
|
|
229
|
+
}
|
|
230
|
+
function readingView(p) {
|
|
231
|
+
const items = [];
|
|
232
|
+
const placed = /* @__PURE__ */ new Set();
|
|
233
|
+
for (const s of p.segments) {
|
|
234
|
+
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);
|
|
235
|
+
const runs = [];
|
|
236
|
+
for (const m of s.text.matchAll(/_{2,}/g)) {
|
|
237
|
+
const start = m.index;
|
|
238
|
+
const end = start + m[0].length;
|
|
239
|
+
runs.push({
|
|
240
|
+
start,
|
|
241
|
+
end,
|
|
242
|
+
x0: s.x + s.width * start / s.text.length,
|
|
243
|
+
x1: s.x + s.width * end / s.text.length,
|
|
244
|
+
ids: []
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
for (const w of inline) {
|
|
248
|
+
if (!runs.length) break;
|
|
249
|
+
const cx = w.x + w.width / 2;
|
|
250
|
+
const best = runs.reduce((a2, b) => Math.abs(cx - (a2.x0 + a2.x1) / 2) <= Math.abs(cx - (b.x0 + b.x1) / 2) ? a2 : b);
|
|
251
|
+
if (cx > best.x0 - 35 && cx < best.x1 + 35) {
|
|
252
|
+
best.ids.push(w.id);
|
|
253
|
+
placed.add(w.id);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
let text = "";
|
|
257
|
+
let cursor = 0;
|
|
258
|
+
for (const run of runs) {
|
|
259
|
+
text += s.text.slice(cursor, run.start);
|
|
260
|
+
text += run.ids.length ? run.ids.map((id) => `[[${id}]]`).join(" ") : " ";
|
|
261
|
+
cursor = run.end;
|
|
262
|
+
}
|
|
263
|
+
text += s.text.slice(cursor);
|
|
264
|
+
if (text.trim()) items.push({ x: s.x, y: s.baseline, text });
|
|
265
|
+
}
|
|
266
|
+
for (const w of p.widgets) {
|
|
267
|
+
if (!placed.has(w.id)) items.push({ x: w.x, y: w.y + w.height / 2 - 4, text: `[[${w.id}]]` });
|
|
268
|
+
}
|
|
269
|
+
items.sort((a2, b) => b.y - a2.y || a2.x - b.x);
|
|
270
|
+
const lines = [];
|
|
271
|
+
for (const it of items) {
|
|
272
|
+
const line = lines[lines.length - 1];
|
|
273
|
+
if (line && Math.abs(line[0].y - it.y) <= 8) line.push(it);
|
|
274
|
+
else lines.push([it]);
|
|
275
|
+
}
|
|
276
|
+
const out = [];
|
|
277
|
+
for (const line of lines) {
|
|
278
|
+
const txt = line.sort((a2, b) => a2.x - b.x).map((i) => i.text).join(" ").replace(/\s+/g, " ").trim();
|
|
279
|
+
if (txt) out.push(txt);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
function serializeDoc(doc) {
|
|
284
|
+
const out = [];
|
|
285
|
+
for (const p of doc.pages) {
|
|
286
|
+
out.push(`## P\xE1gina ${p.page} \u2014 ${r(p.width)}\xD7${r(p.height)} pt`);
|
|
287
|
+
if (p.widgets.length) {
|
|
288
|
+
out.push("### Lectura (texto con los campos [[id]] intercalados donde caen \u2014 as\xED se entiende qu\xE9 va en cada uno)");
|
|
289
|
+
out.push(...readingView(p));
|
|
290
|
+
}
|
|
291
|
+
if (p.segments.length) {
|
|
292
|
+
out.push('### Texto (id @(x,baseline) ancho\xD7alto tama\xF1o estilo: "contenido")');
|
|
293
|
+
const segs = [...p.segments].sort((a2, b) => b.baseline - a2.baseline || a2.x - b.x);
|
|
294
|
+
for (const s of segs) {
|
|
295
|
+
const t = s.text.replace(/\n/g, "\\n");
|
|
296
|
+
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)}`);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
if (p.images.length) {
|
|
300
|
+
out.push("### Im\xE1genes (id @(x,y) ancho\xD7alto)");
|
|
301
|
+
for (const im of p.images) {
|
|
302
|
+
out.push(`- ${im.id} @(${r(im.x)},${r(im.y)}) ${r(im.width)}\xD7${r(im.height)}`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (p.widgets.length) {
|
|
306
|
+
out.push('### Campos de formulario (nombre tipo [valor] near "label" @(x,y) ancho\xD7alto)');
|
|
307
|
+
out.push(' (los nombres suelen ser opacos \u2192 us\xE1 "near" (el texto pegado al campo) para saber QU\xC9 va en cada uno)');
|
|
308
|
+
for (const w of p.widgets) {
|
|
309
|
+
const val = w.value != null ? ` = ${JSON.stringify(Array.isArray(w.value) ? w.value.join(", ") : w.value)}` : " (vac\xEDo)";
|
|
310
|
+
const opts = w.options?.length ? ` opciones:[${w.options.join(", ")}]` : "";
|
|
311
|
+
const label = nearestLabel(w, p.segments);
|
|
312
|
+
const near = label ? ` near ${JSON.stringify(label)}` : "";
|
|
313
|
+
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" : ""}`);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
if (p.highlights.length) {
|
|
317
|
+
out.push("### Resaltados (id color @(x,y) ancho\xD7alto)");
|
|
318
|
+
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)}`);
|
|
319
|
+
}
|
|
320
|
+
if (p.links.length) {
|
|
321
|
+
out.push("### Links (id \u2192 url @(x,y) ancho\xD7alto)");
|
|
322
|
+
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)}`);
|
|
323
|
+
}
|
|
324
|
+
out.push("");
|
|
325
|
+
}
|
|
326
|
+
return out.join("\n").trimEnd();
|
|
327
|
+
}
|
|
328
|
+
var r;
|
|
329
|
+
var init_serialize = __esm({
|
|
330
|
+
"../agent/src/serialize.ts"() {
|
|
331
|
+
r = (n) => Math.round(n);
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ../agent/src/config.ts
|
|
336
|
+
var provider, config;
|
|
337
|
+
var init_config = __esm({
|
|
338
|
+
"../agent/src/config.ts"() {
|
|
339
|
+
provider = process.env.ALDUS_PROVIDER === "openrouter" ? "openrouter" : "subscription";
|
|
340
|
+
config = {
|
|
341
|
+
provider,
|
|
342
|
+
model: process.env.ALDUS_MODEL || "claude-sonnet-5",
|
|
343
|
+
maxTurns: Number(process.env.ALDUS_MAX_TURNS || 24),
|
|
344
|
+
openrouter: {
|
|
345
|
+
key: process.env.OPENROUTER_API_KEY || "",
|
|
346
|
+
baseUrl: (process.env.OPENROUTER_BASE_URL || "https://openrouter.ai/api/v1").replace(/\/$/, ""),
|
|
347
|
+
model: process.env.ALDUS_OPENROUTER_MODEL || "anthropic/claude-3.5-sonnet"
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
// ../agent/src/openrouter.ts
|
|
354
|
+
var openrouter_exports = {};
|
|
355
|
+
__export(openrouter_exports, {
|
|
356
|
+
runTurnOpenRouter: () => runTurnOpenRouter
|
|
357
|
+
});
|
|
358
|
+
async function streamCompletion(messages, onEvent) {
|
|
359
|
+
const res = await fetch(`${config.openrouter.baseUrl}/chat/completions`, {
|
|
360
|
+
method: "POST",
|
|
361
|
+
headers: {
|
|
362
|
+
authorization: `Bearer ${config.openrouter.key}`,
|
|
363
|
+
"content-type": "application/json",
|
|
364
|
+
"http-referer": "https://bernardocastro.dev",
|
|
365
|
+
"x-title": "Aldus PDF Agent"
|
|
366
|
+
},
|
|
367
|
+
body: JSON.stringify({
|
|
368
|
+
model: config.openrouter.model,
|
|
369
|
+
messages,
|
|
370
|
+
tools: openaiTools(),
|
|
371
|
+
tool_choice: "auto",
|
|
372
|
+
stream: true
|
|
373
|
+
})
|
|
374
|
+
});
|
|
375
|
+
if (!res.ok || !res.body) {
|
|
376
|
+
const body = await res.text().catch(() => "");
|
|
377
|
+
throw new Error(`OpenRouter ${res.status}: ${body.slice(0, 300)}`);
|
|
378
|
+
}
|
|
379
|
+
let content = "";
|
|
380
|
+
const calls = [];
|
|
381
|
+
const reader = res.body.getReader();
|
|
382
|
+
const dec = new TextDecoder();
|
|
383
|
+
let buf = "";
|
|
384
|
+
for (; ; ) {
|
|
385
|
+
const { done, value } = await reader.read();
|
|
386
|
+
if (done) break;
|
|
387
|
+
buf += dec.decode(value, { stream: true });
|
|
388
|
+
const lines = buf.split("\n");
|
|
389
|
+
buf = lines.pop() ?? "";
|
|
390
|
+
for (const line of lines) {
|
|
391
|
+
const t = line.trim();
|
|
392
|
+
if (!t.startsWith("data:")) continue;
|
|
393
|
+
const data = t.slice(5).trim();
|
|
394
|
+
if (!data || data === "[DONE]") continue;
|
|
395
|
+
let json;
|
|
396
|
+
try {
|
|
397
|
+
json = JSON.parse(data);
|
|
398
|
+
} catch {
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
const delta = json.choices?.[0]?.delta;
|
|
402
|
+
if (!delta) continue;
|
|
403
|
+
if (delta.content) {
|
|
404
|
+
content += delta.content;
|
|
405
|
+
onEvent?.({ type: "text", delta: delta.content });
|
|
406
|
+
}
|
|
407
|
+
for (const tc of delta.tool_calls ?? []) {
|
|
408
|
+
const i = tc.index ?? 0;
|
|
409
|
+
calls[i] ??= { id: "", type: "function", function: { name: "", arguments: "" } };
|
|
410
|
+
if (tc.id) calls[i].id = tc.id;
|
|
411
|
+
if (tc.function?.name) calls[i].function.name += tc.function.name;
|
|
412
|
+
if (tc.function?.arguments) calls[i].function.arguments += tc.function.arguments;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return { content, toolCalls: calls.filter(Boolean) };
|
|
417
|
+
}
|
|
418
|
+
async function runTurnOpenRouter(opts) {
|
|
419
|
+
if (!config.openrouter.key) throw new Error("falta OPENROUTER_API_KEY (o un token de sesi\xF3n del llm-proxy)");
|
|
420
|
+
const messages = [
|
|
421
|
+
{ role: "system", content: systemPrompt(opts.doc) },
|
|
422
|
+
{ role: "user", content: opts.prompt }
|
|
423
|
+
];
|
|
424
|
+
let text = "";
|
|
425
|
+
let toolCalls = 0;
|
|
426
|
+
for (let turn = 0; turn < config.maxTurns; turn++) {
|
|
427
|
+
const { content, toolCalls: calls } = await streamCompletion(messages, opts.onEvent);
|
|
428
|
+
text += content;
|
|
429
|
+
if (!calls.length) break;
|
|
430
|
+
messages.push({ role: "assistant", content: content || null, tool_calls: calls });
|
|
431
|
+
for (const tc of calls) {
|
|
432
|
+
toolCalls++;
|
|
433
|
+
opts.onEvent?.({ type: "tool", name: `mcp__aldus__${tc.function.name}` });
|
|
434
|
+
let args = {};
|
|
435
|
+
try {
|
|
436
|
+
args = JSON.parse(tc.function.arguments || "{}");
|
|
437
|
+
} catch {
|
|
438
|
+
}
|
|
439
|
+
const result = runTool(opts.session, tc.function.name, args);
|
|
440
|
+
messages.push({ role: "tool", tool_call_id: tc.id, content: result });
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return { text, toolCalls };
|
|
444
|
+
}
|
|
445
|
+
var init_openrouter = __esm({
|
|
446
|
+
"../agent/src/openrouter.ts"() {
|
|
447
|
+
init_config();
|
|
448
|
+
init_agent();
|
|
449
|
+
init_tools();
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
// ../agent/src/agent.ts
|
|
454
|
+
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
455
|
+
function systemPrompt(doc) {
|
|
456
|
+
const pages = doc.pages.length;
|
|
457
|
+
return [
|
|
458
|
+
"Sos Aldus, un agente experto en documentos PDF. Ten\xE9s EMBEBIDO abajo el",
|
|
459
|
+
"contenido completo del documento como un grafo. Sos CONSCIENTE de TODO:",
|
|
460
|
+
"de cada nodo de texto conoc\xE9s su `id`, posici\xF3n (x, baseline), ancho\xD7alto,",
|
|
461
|
+
"tama\xF1o de fuente, negrita/it\xE1lica y familia (y color si figura); de cada",
|
|
462
|
+
"imagen, campo, resaltado y link su `id`, rect y datos. Us\xE1 esa geometr\xEDa y",
|
|
463
|
+
"ese estilo para ubicar y emparejar lo que hagas (p. ej. escribir alineado a",
|
|
464
|
+
"un label, o crear un campo del tama\xF1o justo).",
|
|
465
|
+
"",
|
|
466
|
+
"C\xF3mo trabaj\xE1s:",
|
|
467
|
+
"- PREGUNTAS sobre el contenido \u2192 respond\xE9 directo leyendo el grafo. NO hay",
|
|
468
|
+
" tool de lectura: ya ten\xE9s todo el documento ac\xE1. Si el usuario pide los",
|
|
469
|
+
" datos en un formato (JSON, tabla, lista), devolvelos EXACTAMENTE as\xED.",
|
|
470
|
+
"- CAMBIOS \u2192 us\xE1 las tools referenciando los `id` EXACTOS del grafo. Pod\xE9s",
|
|
471
|
+
" encadenar varias. No inventes ids. Ten\xE9s las MISMAS capacidades que un",
|
|
472
|
+
" humano en el editor:",
|
|
473
|
+
" \xB7 Texto existente: edit_text, move_text, set_text_color, set_text_size, delete_text.",
|
|
474
|
+
" \xB7 Imagen existente: move_image, delete_image.",
|
|
475
|
+
" \xB7 Resaltar: highlight_text (sobre un id de texto). Sobre resaltados que ya",
|
|
476
|
+
" existen: set_highlight_color, delete_highlight.",
|
|
477
|
+
" \xB7 Links: add_link (sobre un id de texto \u2192 URL), delete_link.",
|
|
478
|
+
" \xB7 Crear: add_text, insert_image (desde una ruta local), add_watermark,",
|
|
479
|
+
" add_header_footer, add_form_field (type = text/checkbox/radio/select/",
|
|
480
|
+
" list/button/signature \u2014 pod\xE9s poner inputs NUEVOS: firmas, radios, checks\u2026).",
|
|
481
|
+
' \xB7 Formularios: las p\xE1ginas con campos traen una secci\xF3n "Lectura" \u2014 el',
|
|
482
|
+
" texto en orden con cada campo [[id]] intercalado DONDE CAE. Esa lectura",
|
|
483
|
+
" es LA fuente de verdad para saber qu\xE9 va en cada campo (le\xE9 la oraci\xF3n",
|
|
484
|
+
" alrededor del [[id]], como un humano). Cada campo muestra su VALOR",
|
|
485
|
+
' actual (o "(vac\xEDo)") \u2014 para "extraer"/leer un form respond\xE9 desde el',
|
|
486
|
+
" grafo. Para COMPLETAR VARIOS campos us\xE1 fill_fields (UNA sola llamada con",
|
|
487
|
+
" la lista {name,value}) \u2014 mucho m\xE1s r\xE1pido que fill_field N veces; us\xE1",
|
|
488
|
+
" fill_field solo para uno. name = fieldName o el [[id]] de la Lectura.",
|
|
489
|
+
" Campos existentes: move_field, delete_field.",
|
|
490
|
+
" Un PDF PLANO (sin campos, con l\xEDneas/labels) se puede volver fillable:",
|
|
491
|
+
" add_form_field en cada hueco (mir\xE1 los labels y su geometr\xEDa) y opcionalmente",
|
|
492
|
+
" fill_field. O simplemente escribir la respuesta con add_text al lado del label.",
|
|
493
|
+
"",
|
|
494
|
+
"Coordenadas: puntos PDF, origen ABAJO-IZQUIERDA, x\u2192derecha, y\u2192arriba. Para el",
|
|
495
|
+
"texto la `y` es la baseline. El tama\xF1o de cada p\xE1gina est\xE1 en su encabezado.",
|
|
496
|
+
"Para NO perder contenido, no coloques nada fuera de los l\xEDmites de la p\xE1gina.",
|
|
497
|
+
"",
|
|
498
|
+
"Respond\xE9 en el idioma del usuario, conciso. Si una edici\xF3n es ambigua o el id",
|
|
499
|
+
"no existe, decilo en vez de adivinar.",
|
|
500
|
+
"",
|
|
501
|
+
`=== DOCUMENTO: ${doc.path} (${pages} ${pages === 1 ? "p\xE1gina" : "p\xE1ginas"}) ===`,
|
|
502
|
+
serializeDoc(doc)
|
|
503
|
+
].join("\n");
|
|
504
|
+
}
|
|
505
|
+
async function runTurn(opts) {
|
|
506
|
+
if (config.provider === "openrouter") {
|
|
507
|
+
const { runTurnOpenRouter: runTurnOpenRouter2 } = await Promise.resolve().then(() => (init_openrouter(), openrouter_exports));
|
|
508
|
+
return runTurnOpenRouter2(opts);
|
|
509
|
+
}
|
|
510
|
+
const server = buildToolServer(opts.session);
|
|
511
|
+
let text = "";
|
|
512
|
+
let sessionId;
|
|
513
|
+
let toolCalls = 0;
|
|
514
|
+
for await (const message of query({
|
|
515
|
+
prompt: opts.prompt,
|
|
516
|
+
options: {
|
|
517
|
+
model: config.model,
|
|
518
|
+
systemPrompt: systemPrompt(opts.doc),
|
|
519
|
+
mcpServers: { aldus: server },
|
|
520
|
+
// Deltas token a token → el panel muestra la respuesta escribiéndose y las
|
|
521
|
+
// tools ejecutándose, en vez de quedarse mudo 20-40s en "Pensando".
|
|
522
|
+
includePartialMessages: true,
|
|
523
|
+
// En headless no hay prompt de permisos interactivo: `canUseTool` es el
|
|
524
|
+
// ÚNICO gate — auto-aprueba las tools de Aldus y niega cualquier otra (sin
|
|
525
|
+
// `allowedTools`, que las auto-aprobaría antes y shadowearía este callback).
|
|
526
|
+
canUseTool: async (name, input2) => name.startsWith("mcp__aldus__") ? { behavior: "allow", updatedInput: input2 } : { behavior: "deny", message: "Aldus solo permite sus propias tools de edici\xF3n." },
|
|
527
|
+
maxTurns: config.maxTurns,
|
|
528
|
+
...opts.resume ? { resume: opts.resume } : {}
|
|
529
|
+
}
|
|
530
|
+
})) {
|
|
531
|
+
if (message.type === "stream_event") {
|
|
532
|
+
const ev = message.event;
|
|
533
|
+
if (ev.type === "content_block_delta" && ev.delta?.type === "text_delta" && ev.delta.text) {
|
|
534
|
+
text += ev.delta.text;
|
|
535
|
+
opts.onEvent?.({ type: "text", delta: ev.delta.text });
|
|
536
|
+
} else if (ev.type === "content_block_start" && ev.content_block?.type === "tool_use") {
|
|
537
|
+
const name = ev.content_block.name ?? "tool";
|
|
538
|
+
if (name.startsWith("mcp__aldus__")) {
|
|
539
|
+
toolCalls++;
|
|
540
|
+
opts.onEvent?.({ type: "tool", name });
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
} else if (message.type === "result") {
|
|
544
|
+
sessionId = message.session_id;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return { text, sessionId, toolCalls };
|
|
548
|
+
}
|
|
549
|
+
var init_agent = __esm({
|
|
550
|
+
"../agent/src/agent.ts"() {
|
|
551
|
+
init_tools();
|
|
552
|
+
init_serialize();
|
|
553
|
+
init_config();
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
|
|
557
|
+
// ../agent/src/cli.ts
|
|
558
|
+
import { parseArgs } from "node:util";
|
|
559
|
+
import { createInterface } from "node:readline/promises";
|
|
560
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
561
|
+
import { spawn } from "node:child_process";
|
|
562
|
+
import { readFile as readFile3, writeFile as writeFile2 } from "node:fs/promises";
|
|
563
|
+
import path from "node:path";
|
|
564
|
+
|
|
565
|
+
// ../core/src/bake/bake.ts
|
|
566
|
+
import { PDFDocument as PDFDocument8 } from "pdf-lib";
|
|
567
|
+
|
|
568
|
+
// ../core/src/bake/tokenizer.ts
|
|
569
|
+
var isWs = (c) => c === 0 || c === 9 || c === 10 || c === 12 || c === 13 || c === 32;
|
|
570
|
+
var isDelim = (c) => c === 40 || c === 41 || c === 60 || c === 62 || c === 91 || c === 93 || c === 123 || c === 125 || c === 47 || c === 37;
|
|
571
|
+
var latin1 = (bytes, a2, b) => {
|
|
572
|
+
let s = "";
|
|
573
|
+
for (let i = a2; i < b; i++) s += String.fromCharCode(bytes[i]);
|
|
574
|
+
return s;
|
|
575
|
+
};
|
|
576
|
+
function tokenizeContentStream(src) {
|
|
577
|
+
const ops = [];
|
|
578
|
+
let operands = [];
|
|
579
|
+
let i = 0;
|
|
580
|
+
const n = src.length;
|
|
581
|
+
const skipWs = () => {
|
|
582
|
+
while (i < n) {
|
|
583
|
+
if (isWs(src[i])) {
|
|
584
|
+
i++;
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
if (src[i] === 37) {
|
|
588
|
+
while (i < n && src[i] !== 10 && src[i] !== 13) i++;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
const parseString = () => {
|
|
595
|
+
const start = i;
|
|
596
|
+
i++;
|
|
597
|
+
const out = [];
|
|
598
|
+
let depth = 1;
|
|
599
|
+
while (i < n && depth > 0) {
|
|
600
|
+
const c = src[i];
|
|
601
|
+
if (c === 92) {
|
|
602
|
+
const e = src[i + 1];
|
|
603
|
+
i += 2;
|
|
604
|
+
if (e === 110) out.push(10);
|
|
605
|
+
else if (e === 114) out.push(13);
|
|
606
|
+
else if (e === 116) out.push(9);
|
|
607
|
+
else if (e === 98) out.push(8);
|
|
608
|
+
else if (e === 102) out.push(12);
|
|
609
|
+
else if (e === 40 || e === 41 || e === 92) out.push(e);
|
|
610
|
+
else if (e >= 48 && e <= 55) {
|
|
611
|
+
let v = e - 48;
|
|
612
|
+
for (let k = 0; k < 2 && src[i] >= 48 && src[i] <= 55; k++) {
|
|
613
|
+
v = v * 8 + (src[i] - 48);
|
|
614
|
+
i++;
|
|
615
|
+
}
|
|
616
|
+
out.push(v & 255);
|
|
617
|
+
} else if (e === 10) {
|
|
618
|
+
} else if (e === 13) {
|
|
619
|
+
if (src[i] === 10) i++;
|
|
620
|
+
} else if (e !== void 0) out.push(e);
|
|
621
|
+
} else {
|
|
622
|
+
if (c === 40) depth++;
|
|
623
|
+
else if (c === 41) {
|
|
624
|
+
depth--;
|
|
625
|
+
if (depth === 0) {
|
|
626
|
+
i++;
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
out.push(c);
|
|
631
|
+
i++;
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return { kind: "str", raw: latin1(src, start, i), bytes: Uint8Array.from(out), start, end: i };
|
|
636
|
+
};
|
|
637
|
+
const parseHex = () => {
|
|
638
|
+
const start = i;
|
|
639
|
+
i++;
|
|
640
|
+
const digits = [];
|
|
641
|
+
while (i < n && src[i] !== 62) {
|
|
642
|
+
const c = src[i];
|
|
643
|
+
if (!isWs(c)) digits.push(c);
|
|
644
|
+
i++;
|
|
645
|
+
}
|
|
646
|
+
i++;
|
|
647
|
+
let hex = digits.map((c) => String.fromCharCode(c)).join("");
|
|
648
|
+
if (hex.length % 2) hex += "0";
|
|
649
|
+
const out = new Uint8Array(hex.length / 2);
|
|
650
|
+
for (let k = 0; k < out.length; k++) out[k] = parseInt(hex.slice(k * 2, k * 2 + 2), 16);
|
|
651
|
+
return { kind: "hex", raw: latin1(src, start, i), bytes: out, start, end: i };
|
|
652
|
+
};
|
|
653
|
+
const parseName = () => {
|
|
654
|
+
const start = i;
|
|
655
|
+
i++;
|
|
656
|
+
let name = "";
|
|
657
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) {
|
|
658
|
+
if (src[i] === 35 && i + 2 < n) {
|
|
659
|
+
name += String.fromCharCode(parseInt(latin1(src, i + 1, i + 3), 16));
|
|
660
|
+
i += 3;
|
|
661
|
+
} else {
|
|
662
|
+
name += String.fromCharCode(src[i]);
|
|
663
|
+
i++;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return { kind: "name", value: name, raw: latin1(src, start, i), start, end: i };
|
|
667
|
+
};
|
|
668
|
+
const parseNumber = () => {
|
|
669
|
+
const start = i;
|
|
670
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) i++;
|
|
671
|
+
const raw = latin1(src, start, i);
|
|
672
|
+
return { kind: "num", value: parseFloat(raw), raw, start, end: i };
|
|
673
|
+
};
|
|
674
|
+
const parseDict = () => {
|
|
675
|
+
const start = i;
|
|
676
|
+
i += 2;
|
|
677
|
+
let depth = 1;
|
|
678
|
+
while (i < n && depth > 0) {
|
|
679
|
+
if (src[i] === 60 && src[i + 1] === 60) {
|
|
680
|
+
depth++;
|
|
681
|
+
i += 2;
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (src[i] === 62 && src[i + 1] === 62) {
|
|
685
|
+
depth--;
|
|
686
|
+
i += 2;
|
|
687
|
+
continue;
|
|
688
|
+
}
|
|
689
|
+
if (src[i] === 40) {
|
|
690
|
+
parseString();
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
i++;
|
|
694
|
+
}
|
|
695
|
+
return { kind: "dict", raw: latin1(src, start, i), start, end: i };
|
|
696
|
+
};
|
|
697
|
+
const parseArray = () => {
|
|
698
|
+
const start = i;
|
|
699
|
+
i++;
|
|
700
|
+
const items = [];
|
|
701
|
+
while (i < n) {
|
|
702
|
+
skipWs();
|
|
703
|
+
if (src[i] === 93) {
|
|
704
|
+
i++;
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
707
|
+
items.push(parseOne());
|
|
708
|
+
}
|
|
709
|
+
return { kind: "arr", items, raw: latin1(src, start, i), start, end: i };
|
|
710
|
+
};
|
|
711
|
+
const parseKeyword = () => {
|
|
712
|
+
const start = i;
|
|
713
|
+
while (i < n && !isWs(src[i]) && !isDelim(src[i])) i++;
|
|
714
|
+
const kw = latin1(src, start, i);
|
|
715
|
+
return { kind: "kw", value: kw, raw: kw, start, end: i };
|
|
716
|
+
};
|
|
717
|
+
const parseOne = () => {
|
|
718
|
+
const c = src[i];
|
|
719
|
+
if (c === 40) return parseString();
|
|
720
|
+
if (c === 60) return src[i + 1] === 60 ? parseDict() : parseHex();
|
|
721
|
+
if (c === 47) return parseName();
|
|
722
|
+
if (c === 91) return parseArray();
|
|
723
|
+
if (c >= 48 && c <= 57 || c === 43 || c === 45 || c === 46) return parseNumber();
|
|
724
|
+
return parseKeyword();
|
|
725
|
+
};
|
|
726
|
+
while (i < n) {
|
|
727
|
+
skipWs();
|
|
728
|
+
if (i >= n) break;
|
|
729
|
+
const tok = parseOne();
|
|
730
|
+
if (tok.kind === "kw") {
|
|
731
|
+
const op = tok.value;
|
|
732
|
+
if (op === "BI") {
|
|
733
|
+
while (i < n) {
|
|
734
|
+
if (isWs(src[i - 1]) && src[i] === 69 && src[i + 1] === 73 && (i + 2 >= n || isWs(src[i + 2]) || isDelim(src[i + 2]))) {
|
|
735
|
+
i += 2;
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
i++;
|
|
739
|
+
}
|
|
740
|
+
operands = [];
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
ops.push({
|
|
744
|
+
op,
|
|
745
|
+
operands,
|
|
746
|
+
start: operands.length ? operands[0].start : tok.start,
|
|
747
|
+
end: tok.end
|
|
748
|
+
});
|
|
749
|
+
operands = [];
|
|
750
|
+
} else {
|
|
751
|
+
operands.push(tok);
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return ops;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// ../core/src/bake/splice.ts
|
|
758
|
+
var fmt = (v) => {
|
|
759
|
+
const r2 = Math.round(v * 1e4) / 1e4;
|
|
760
|
+
return Object.is(r2, -0) ? "0" : String(r2);
|
|
761
|
+
};
|
|
762
|
+
var latin12 = (bytes, a2, b) => {
|
|
763
|
+
let s = "";
|
|
764
|
+
for (let i = a2; i < b; i++) s += String.fromCharCode(bytes[i]);
|
|
765
|
+
return s;
|
|
766
|
+
};
|
|
767
|
+
var toBytes = (s) => {
|
|
768
|
+
const out = new Uint8Array(s.length);
|
|
769
|
+
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i) & 255;
|
|
770
|
+
return out;
|
|
771
|
+
};
|
|
772
|
+
var hexString = (bytes) => `<${[...bytes].map((b) => b.toString(16).padStart(2, "0")).join("")}>`;
|
|
773
|
+
function rebuild(src, splices, prepend = "", append = "") {
|
|
774
|
+
const sorted = [...splices].sort((a2, b) => a2.start - b.start || a2.end - a2.start - (b.end - b.start));
|
|
775
|
+
let out = prepend ? `${prepend}
|
|
776
|
+
` : "";
|
|
777
|
+
let pos = 0;
|
|
778
|
+
for (const s of sorted) {
|
|
779
|
+
if (s.start < pos) continue;
|
|
780
|
+
out += latin12(src, pos, s.start);
|
|
781
|
+
if (s.text) out += `
|
|
782
|
+
${s.text}
|
|
783
|
+
`;
|
|
784
|
+
pos = s.end;
|
|
785
|
+
}
|
|
786
|
+
out += latin12(src, pos, src.length);
|
|
787
|
+
if (append) out += `
|
|
788
|
+
${append}
|
|
789
|
+
`;
|
|
790
|
+
return toBytes(out);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
// ../core/src/bake/color.ts
|
|
794
|
+
function hexToRg(hex) {
|
|
795
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
796
|
+
if (!m) return void 0;
|
|
797
|
+
const v = parseInt(m[1], 16);
|
|
798
|
+
const c = (n) => fmt(n / 255);
|
|
799
|
+
return `${c(v >> 16 & 255)} ${c(v >> 8 & 255)} ${c(v & 255)} rg`;
|
|
800
|
+
}
|
|
801
|
+
function rawFillToRgb(raw) {
|
|
802
|
+
if (!raw) return null;
|
|
803
|
+
const nums = (raw.match(/-?\d*\.?\d+/g) ?? []).map(Number);
|
|
804
|
+
if (/\brg\b/.test(raw) && nums.length >= 3) return { r: nums[0], g: nums[1], b: nums[2] };
|
|
805
|
+
if (/\bg\b/.test(raw) && !/\brg\b/.test(raw) && nums.length >= 1) return { r: nums[0], g: nums[0], b: nums[0] };
|
|
806
|
+
if (/\bk\b/.test(raw) && nums.length >= 4) {
|
|
807
|
+
const [c, m, y, kk] = nums;
|
|
808
|
+
return { r: (1 - c) * (1 - kk), g: (1 - m) * (1 - kk), b: (1 - y) * (1 - kk) };
|
|
809
|
+
}
|
|
810
|
+
if (nums.length >= 3) return { r: nums[nums.length - 3], g: nums[nums.length - 2], b: nums[nums.length - 1] };
|
|
811
|
+
if (nums.length === 1) return { r: nums[0], g: nums[0], b: nums[0] };
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
var hexToRgbObj = (hex) => {
|
|
815
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
816
|
+
const v = m ? parseInt(m[1], 16) : 0;
|
|
817
|
+
return { r: (v >> 16 & 255) / 255, g: (v >> 8 & 255) / 255, b: (v & 255) / 255 };
|
|
818
|
+
};
|
|
819
|
+
var rgbToHex = (c) => {
|
|
820
|
+
const h = (v) => Math.max(0, Math.min(255, Math.round(v * 255))).toString(16).padStart(2, "0");
|
|
821
|
+
return `#${h(c.r)}${h(c.g)}${h(c.b)}`;
|
|
822
|
+
};
|
|
823
|
+
function isWhiteFill(rawFill) {
|
|
824
|
+
const toks = rawFill.trim().split(/\s+/);
|
|
825
|
+
if (toks.length < 2) return false;
|
|
826
|
+
const nums = toks.filter((t) => /^[-+.\d]/.test(t)).map(Number).filter(Number.isFinite);
|
|
827
|
+
const op = toks[toks.length - 1];
|
|
828
|
+
if (op === "g" && nums.length >= 1) return nums[nums.length - 1] >= 0.99;
|
|
829
|
+
if (op === "rg" && nums.length >= 3) return nums.slice(-3).every((v) => v >= 0.99);
|
|
830
|
+
if (op === "k" && nums.length >= 4) return nums.slice(-4).every((v) => v <= 0.01);
|
|
831
|
+
if ((op === "sc" || op === "scn") && nums.length >= 1) {
|
|
832
|
+
const vals = nums.slice(-Math.min(nums.length, 3));
|
|
833
|
+
return vals.every((v) => v >= 0.99);
|
|
834
|
+
}
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
// ../core/src/bake/textWalk.ts
|
|
839
|
+
var IDENTITY = [1, 0, 0, 1, 0, 0];
|
|
840
|
+
function mul(m, n) {
|
|
841
|
+
return [
|
|
842
|
+
m[0] * n[0] + m[1] * n[2],
|
|
843
|
+
m[0] * n[1] + m[1] * n[3],
|
|
844
|
+
m[2] * n[0] + m[3] * n[2],
|
|
845
|
+
m[2] * n[1] + m[3] * n[3],
|
|
846
|
+
m[4] * n[0] + m[5] * n[2] + n[4],
|
|
847
|
+
m[4] * n[1] + m[5] * n[3] + n[5]
|
|
848
|
+
];
|
|
849
|
+
}
|
|
850
|
+
function invert(m) {
|
|
851
|
+
const det = m[0] * m[3] - m[1] * m[2];
|
|
852
|
+
if (!det) return null;
|
|
853
|
+
const ia = m[3] / det;
|
|
854
|
+
const ib = -m[1] / det;
|
|
855
|
+
const ic = -m[2] / det;
|
|
856
|
+
const id = m[0] / det;
|
|
857
|
+
return [ia, ib, ic, id, -(m[4] * ia + m[5] * ic), -(m[4] * ib + m[5] * id)];
|
|
858
|
+
}
|
|
859
|
+
function walkContent(src) {
|
|
860
|
+
const ops = tokenizeContentStream(src);
|
|
861
|
+
const shows = [];
|
|
862
|
+
const xobjects = [];
|
|
863
|
+
const fillRects = [];
|
|
864
|
+
let simple = null;
|
|
865
|
+
const isRectPoly = (pts) => {
|
|
866
|
+
if (pts.length !== 4) return false;
|
|
867
|
+
for (let i = 0; i < 4; i++) {
|
|
868
|
+
const [x1, y1] = pts[i];
|
|
869
|
+
const [x2, y2] = pts[(i + 1) % 4];
|
|
870
|
+
if (Math.abs(y2 - y1) > 0.01 && Math.abs(x2 - x1) > 0.01) return false;
|
|
871
|
+
}
|
|
872
|
+
return true;
|
|
873
|
+
};
|
|
874
|
+
let ctm = IDENTITY;
|
|
875
|
+
let clip = null;
|
|
876
|
+
let clipPending = false;
|
|
877
|
+
const stack = [];
|
|
878
|
+
let tm = IDENTITY;
|
|
879
|
+
let tlm = IDENTITY;
|
|
880
|
+
let fontName = "";
|
|
881
|
+
let fontSize = 0;
|
|
882
|
+
let charSpacing = 0;
|
|
883
|
+
let wordSpacing = 0;
|
|
884
|
+
let hScale = 100;
|
|
885
|
+
let leading = 0;
|
|
886
|
+
let stale = false;
|
|
887
|
+
let fillColorRaw = "";
|
|
888
|
+
let csRaw = "";
|
|
889
|
+
let backstop = null;
|
|
890
|
+
let pathStart = null;
|
|
891
|
+
const markContent = (at) => {
|
|
892
|
+
if (!backstop) backstop = at;
|
|
893
|
+
};
|
|
894
|
+
const raw = (rec) => {
|
|
895
|
+
let s = "";
|
|
896
|
+
for (let k = rec.start; k < rec.end; k++) s += String.fromCharCode(src[k]);
|
|
897
|
+
return s;
|
|
898
|
+
};
|
|
899
|
+
const num = (rec, idx) => {
|
|
900
|
+
const t = rec.operands[idx];
|
|
901
|
+
return t && t.kind === "num" ? t.value : 0;
|
|
902
|
+
};
|
|
903
|
+
const setTd = (tx, ty) => {
|
|
904
|
+
tlm = mul([1, 0, 0, 1, tx, ty], tlm);
|
|
905
|
+
tm = tlm;
|
|
906
|
+
stale = false;
|
|
907
|
+
};
|
|
908
|
+
const record = (rec, op) => {
|
|
909
|
+
const m = mul(tm, ctm);
|
|
910
|
+
shows.push({
|
|
911
|
+
record: rec,
|
|
912
|
+
op,
|
|
913
|
+
x: m[4],
|
|
914
|
+
y: m[5],
|
|
915
|
+
matrix: m,
|
|
916
|
+
ctm,
|
|
917
|
+
fontName,
|
|
918
|
+
fontSize,
|
|
919
|
+
charSpacing,
|
|
920
|
+
wordSpacing,
|
|
921
|
+
hScale,
|
|
922
|
+
fillColorRaw,
|
|
923
|
+
stale,
|
|
924
|
+
clip
|
|
925
|
+
});
|
|
926
|
+
stale = true;
|
|
927
|
+
};
|
|
928
|
+
const absRectOf = (p) => {
|
|
929
|
+
const m = p.ctm;
|
|
930
|
+
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 : [];
|
|
931
|
+
if (!corners.length || Math.abs(m[1]) > 0.01 || Math.abs(m[2]) > 0.01) return null;
|
|
932
|
+
const xs = corners.map(([px]) => m[0] * px + m[4]);
|
|
933
|
+
const ys = corners.map(([, py]) => m[3] * py + m[5]);
|
|
934
|
+
const x = Math.min(...xs);
|
|
935
|
+
const y = Math.min(...ys);
|
|
936
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y };
|
|
937
|
+
};
|
|
938
|
+
const consumeClip = () => {
|
|
939
|
+
if (!clipPending) return;
|
|
940
|
+
clipPending = false;
|
|
941
|
+
const r2 = simple?.valid ? absRectOf(simple) : null;
|
|
942
|
+
if (!r2) return;
|
|
943
|
+
if (!clip) {
|
|
944
|
+
clip = r2;
|
|
945
|
+
return;
|
|
946
|
+
}
|
|
947
|
+
const x = Math.max(clip.x, r2.x);
|
|
948
|
+
const y = Math.max(clip.y, r2.y);
|
|
949
|
+
clip = {
|
|
950
|
+
x,
|
|
951
|
+
y,
|
|
952
|
+
width: Math.max(0, Math.min(clip.x + clip.width, r2.x + r2.width) - x),
|
|
953
|
+
height: Math.max(0, Math.min(clip.y + clip.height, r2.y + r2.height) - y)
|
|
954
|
+
};
|
|
955
|
+
};
|
|
956
|
+
for (const rec of ops) {
|
|
957
|
+
switch (rec.op) {
|
|
958
|
+
case "q":
|
|
959
|
+
stack.push({ ctm, clip });
|
|
960
|
+
break;
|
|
961
|
+
case "Q": {
|
|
962
|
+
const s = stack.pop();
|
|
963
|
+
ctm = s?.ctm ?? IDENTITY;
|
|
964
|
+
clip = s?.clip ?? null;
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
case "cm":
|
|
968
|
+
ctm = mul([num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3), num(rec, 4), num(rec, 5)], ctm);
|
|
969
|
+
break;
|
|
970
|
+
// ── backstop: construcción y pintado de paths ──
|
|
971
|
+
case "m":
|
|
972
|
+
if (!pathStart) pathStart = { offset: rec.start, ctm };
|
|
973
|
+
if (simple) simple.valid = false;
|
|
974
|
+
else simple = { start: rec.start, ctm, fill: fillColorRaw, pts: [[num(rec, 0), num(rec, 1)]], valid: true };
|
|
975
|
+
break;
|
|
976
|
+
case "l":
|
|
977
|
+
if (simple && simple.valid && !simple.rect && simple.pts.length < 5) simple.pts.push([num(rec, 0), num(rec, 1)]);
|
|
978
|
+
else if (simple) simple.valid = false;
|
|
979
|
+
break;
|
|
980
|
+
case "c":
|
|
981
|
+
case "v":
|
|
982
|
+
case "y":
|
|
983
|
+
if (simple) simple.valid = false;
|
|
984
|
+
break;
|
|
985
|
+
case "h":
|
|
986
|
+
break;
|
|
987
|
+
// cerrar el subpath no cambia el bbox
|
|
988
|
+
case "re":
|
|
989
|
+
if (!pathStart) pathStart = { offset: rec.start, ctm };
|
|
990
|
+
if (simple) simple.valid = false;
|
|
991
|
+
else simple = { start: rec.start, ctm, fill: fillColorRaw, rect: [num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3)], pts: [], valid: true };
|
|
992
|
+
break;
|
|
993
|
+
case "W":
|
|
994
|
+
case "W*":
|
|
995
|
+
clipPending = true;
|
|
996
|
+
break;
|
|
997
|
+
case "n":
|
|
998
|
+
consumeClip();
|
|
999
|
+
pathStart = null;
|
|
1000
|
+
simple = null;
|
|
1001
|
+
break;
|
|
1002
|
+
// solo clip — no es contenido
|
|
1003
|
+
case "f":
|
|
1004
|
+
case "F":
|
|
1005
|
+
case "f*":
|
|
1006
|
+
case "b":
|
|
1007
|
+
case "b*":
|
|
1008
|
+
case "B":
|
|
1009
|
+
case "B*": {
|
|
1010
|
+
consumeClip();
|
|
1011
|
+
if (!isWhiteFill(fillColorRaw)) markContent(pathStart ?? { offset: rec.start, ctm });
|
|
1012
|
+
if (simple?.valid) {
|
|
1013
|
+
const r2 = absRectOf(simple);
|
|
1014
|
+
if (r2) fillRects.push({ start: simple.start, end: rec.end, ...r2, fillColorRaw: simple.fill });
|
|
1015
|
+
}
|
|
1016
|
+
pathStart = null;
|
|
1017
|
+
simple = null;
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
case "S":
|
|
1021
|
+
case "s":
|
|
1022
|
+
consumeClip();
|
|
1023
|
+
markContent(pathStart ?? { offset: rec.start, ctm });
|
|
1024
|
+
pathStart = null;
|
|
1025
|
+
simple = null;
|
|
1026
|
+
break;
|
|
1027
|
+
case "sh":
|
|
1028
|
+
markContent({ offset: rec.start, ctm });
|
|
1029
|
+
break;
|
|
1030
|
+
case "BI":
|
|
1031
|
+
markContent({ offset: rec.start, ctm });
|
|
1032
|
+
break;
|
|
1033
|
+
case "BT":
|
|
1034
|
+
markContent({ offset: rec.start, ctm });
|
|
1035
|
+
tm = IDENTITY;
|
|
1036
|
+
tlm = IDENTITY;
|
|
1037
|
+
stale = false;
|
|
1038
|
+
break;
|
|
1039
|
+
case "ET":
|
|
1040
|
+
break;
|
|
1041
|
+
case "Tf": {
|
|
1042
|
+
const t = rec.operands[0];
|
|
1043
|
+
fontName = t && t.kind === "name" ? t.value : fontName;
|
|
1044
|
+
fontSize = num(rec, 1);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
case "Tc":
|
|
1048
|
+
charSpacing = num(rec, 0);
|
|
1049
|
+
break;
|
|
1050
|
+
case "Tw":
|
|
1051
|
+
wordSpacing = num(rec, 0);
|
|
1052
|
+
break;
|
|
1053
|
+
case "Tz":
|
|
1054
|
+
hScale = num(rec, 0);
|
|
1055
|
+
break;
|
|
1056
|
+
case "TL":
|
|
1057
|
+
leading = num(rec, 0);
|
|
1058
|
+
break;
|
|
1059
|
+
case "Td":
|
|
1060
|
+
setTd(num(rec, 0), num(rec, 1));
|
|
1061
|
+
break;
|
|
1062
|
+
case "TD":
|
|
1063
|
+
leading = -num(rec, 1);
|
|
1064
|
+
setTd(num(rec, 0), num(rec, 1));
|
|
1065
|
+
break;
|
|
1066
|
+
case "Tm":
|
|
1067
|
+
tlm = [num(rec, 0), num(rec, 1), num(rec, 2), num(rec, 3), num(rec, 4), num(rec, 5)];
|
|
1068
|
+
tm = tlm;
|
|
1069
|
+
stale = false;
|
|
1070
|
+
break;
|
|
1071
|
+
case "T*":
|
|
1072
|
+
setTd(0, -leading);
|
|
1073
|
+
break;
|
|
1074
|
+
case "Tj":
|
|
1075
|
+
record(rec, "Tj");
|
|
1076
|
+
break;
|
|
1077
|
+
case "TJ":
|
|
1078
|
+
record(rec, "TJ");
|
|
1079
|
+
break;
|
|
1080
|
+
case "'":
|
|
1081
|
+
setTd(0, -leading);
|
|
1082
|
+
record(rec, "'");
|
|
1083
|
+
break;
|
|
1084
|
+
case '"':
|
|
1085
|
+
wordSpacing = num(rec, 0);
|
|
1086
|
+
charSpacing = num(rec, 1);
|
|
1087
|
+
setTd(0, -leading);
|
|
1088
|
+
record(rec, '"');
|
|
1089
|
+
break;
|
|
1090
|
+
case "g":
|
|
1091
|
+
case "rg":
|
|
1092
|
+
case "k":
|
|
1093
|
+
fillColorRaw = raw(rec);
|
|
1094
|
+
break;
|
|
1095
|
+
case "cs":
|
|
1096
|
+
csRaw = raw(rec);
|
|
1097
|
+
break;
|
|
1098
|
+
case "sc":
|
|
1099
|
+
case "scn":
|
|
1100
|
+
fillColorRaw = csRaw ? `${csRaw} ${raw(rec)}` : raw(rec);
|
|
1101
|
+
break;
|
|
1102
|
+
case "Do": {
|
|
1103
|
+
const t = rec.operands[0];
|
|
1104
|
+
if (t && t.kind === "name") xobjects.push({ record: rec, name: t.value, matrix: ctm });
|
|
1105
|
+
markContent({ offset: rec.start, ctm });
|
|
1106
|
+
break;
|
|
1107
|
+
}
|
|
1108
|
+
default:
|
|
1109
|
+
break;
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
return { shows, xobjects, fillRects, backstop: backstop ?? { offset: 0, ctm: IDENTITY } };
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// ../core/src/bake/pageContent.ts
|
|
1116
|
+
import {
|
|
1117
|
+
PDFArray,
|
|
1118
|
+
PDFName,
|
|
1119
|
+
PDFRawStream,
|
|
1120
|
+
PDFRef,
|
|
1121
|
+
decodePDFRawStream
|
|
1122
|
+
} from "pdf-lib";
|
|
1123
|
+
function pageContentBytes(doc, page) {
|
|
1124
|
+
const ctx = doc.context;
|
|
1125
|
+
const resolve = (o) => o instanceof PDFRef ? ctx.lookup(o) : o;
|
|
1126
|
+
const contents = resolve(page.node.get(PDFName.of("Contents")));
|
|
1127
|
+
const streams = [];
|
|
1128
|
+
if (contents instanceof PDFArray) {
|
|
1129
|
+
for (let i = 0; i < contents.size(); i++) {
|
|
1130
|
+
const s = resolve(contents.get(i));
|
|
1131
|
+
if (s instanceof PDFRawStream) streams.push(s);
|
|
1132
|
+
else throw new Error("Content stream no soportado (no es PDFRawStream).");
|
|
1133
|
+
}
|
|
1134
|
+
} else if (contents instanceof PDFRawStream) {
|
|
1135
|
+
streams.push(contents);
|
|
1136
|
+
} else if (contents != null) {
|
|
1137
|
+
throw new Error("Content stream no soportado.");
|
|
1138
|
+
}
|
|
1139
|
+
const parts = streams.map((s) => decodePDFRawStream(s).decode());
|
|
1140
|
+
const total = parts.reduce((a2, p) => a2 + p.length + 1, 0);
|
|
1141
|
+
const out = new Uint8Array(total);
|
|
1142
|
+
let off = 0;
|
|
1143
|
+
for (const p of parts) {
|
|
1144
|
+
out.set(p, off);
|
|
1145
|
+
off += p.length;
|
|
1146
|
+
out[off++] = 10;
|
|
1147
|
+
}
|
|
1148
|
+
return out;
|
|
1149
|
+
}
|
|
1150
|
+
function setPageContents(doc, page, bytes) {
|
|
1151
|
+
const stream = doc.context.stream(bytes);
|
|
1152
|
+
const ref = doc.context.register(stream);
|
|
1153
|
+
page.node.set(PDFName.of("Contents"), ref);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ../core/src/bake/locate.ts
|
|
1157
|
+
import { PDFDict, PDFName as PDFName2, PDFRawStream as PDFRawStream2, PDFRef as PDFRef2 } from "pdf-lib";
|
|
1158
|
+
var Y_TOL = 1.8;
|
|
1159
|
+
var X_TOL = 1.8;
|
|
1160
|
+
function xobjectRect(m) {
|
|
1161
|
+
const [a2, b, c, d, e, f] = m;
|
|
1162
|
+
const xs = [e, a2 + e, c + e, a2 + c + e];
|
|
1163
|
+
const ys = [f, b + f, d + f, b + d + f];
|
|
1164
|
+
const x = Math.min(...xs);
|
|
1165
|
+
const y = Math.min(...ys);
|
|
1166
|
+
return { x, y, width: Math.max(...xs) - x, height: Math.max(...ys) - y, rotated: Math.abs(b) > 0.01 || Math.abs(c) > 0.01 };
|
|
1167
|
+
}
|
|
1168
|
+
function imageResourceNames(doc, page) {
|
|
1169
|
+
const out = /* @__PURE__ */ new Set();
|
|
1170
|
+
try {
|
|
1171
|
+
const res = page.node.Resources();
|
|
1172
|
+
const xo = res?.lookup(PDFName2.of("XObject"));
|
|
1173
|
+
if (!(xo instanceof PDFDict)) return out;
|
|
1174
|
+
for (const [key, val] of xo.entries()) {
|
|
1175
|
+
const obj = val instanceof PDFRef2 ? doc.context.lookup(val) : val;
|
|
1176
|
+
const dict = obj instanceof PDFRawStream2 ? obj.dict : obj instanceof PDFDict ? obj : null;
|
|
1177
|
+
if (dict?.get(PDFName2.of("Subtype")) === PDFName2.of("Image")) {
|
|
1178
|
+
out.add(key.toString().replace(/^\//, ""));
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
} catch {
|
|
1182
|
+
}
|
|
1183
|
+
return out;
|
|
1184
|
+
}
|
|
1185
|
+
function matchImage(xobjects, orig) {
|
|
1186
|
+
const tol = Math.max(2, orig.width * 0.02, orig.height * 0.02);
|
|
1187
|
+
return xobjects.find((o) => {
|
|
1188
|
+
const r2 = xobjectRect(o.matrix);
|
|
1189
|
+
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;
|
|
1190
|
+
}) ?? null;
|
|
1191
|
+
}
|
|
1192
|
+
function matchOps(shows, orig) {
|
|
1193
|
+
const lines = orig.baselines?.length ? orig.baselines : [orig.baseline];
|
|
1194
|
+
const inLine = shows.filter((s) => lines.some((b) => Math.abs(s.y - b) <= Y_TOL));
|
|
1195
|
+
if (inLine.some((s) => s.stale)) {
|
|
1196
|
+
return { ops: [], conflict: "la l\xEDnea tiene shows encadenados sin reposicionar (x desconocida sin widths)" };
|
|
1197
|
+
}
|
|
1198
|
+
const inside = inLine.filter((s) => s.x >= orig.x - X_TOL && s.x <= orig.x + orig.width + X_TOL);
|
|
1199
|
+
if (!inside.length) {
|
|
1200
|
+
return { ops: [], conflict: "ning\xFAn operador de texto arranca dentro del segmento (\xBFun TJ de otra columna lo contiene?)" };
|
|
1201
|
+
}
|
|
1202
|
+
return { ops: inside, conflict: null };
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// ../core/src/bake/images.ts
|
|
1206
|
+
function applyImageEditsToPage(args) {
|
|
1207
|
+
const { doc, page, pageImgEdits, xobjects, backstop, splices, appendBlocks, report } = args;
|
|
1208
|
+
if (!pageImgEdits.length) return;
|
|
1209
|
+
const imgNames = imageResourceNames(doc, page);
|
|
1210
|
+
const imageOps = xobjects.filter((o) => imgNames.has(o.name));
|
|
1211
|
+
for (const edit of pageImgEdits) {
|
|
1212
|
+
const op = matchImage(imageOps, edit.original);
|
|
1213
|
+
if (!op) {
|
|
1214
|
+
report.warn(`${edit.imageId}: no se encontr\xF3 el XObject en la posici\xF3n original \u2014 sin cambios`);
|
|
1215
|
+
continue;
|
|
1216
|
+
}
|
|
1217
|
+
if (edit.remove) {
|
|
1218
|
+
splices.push({ start: op.record.start, end: op.record.end, text: "" });
|
|
1219
|
+
report.apply(`${edit.imageId}: eliminada`);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const r2 = xobjectRect(op.matrix);
|
|
1223
|
+
if (r2.rotated) {
|
|
1224
|
+
report.warn(`${edit.imageId}: la imagen tiene rotaci\xF3n \u2014 mover/escalar no soportado a\xFAn, queda intacta`);
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
const [a2, , , d] = op.matrix;
|
|
1228
|
+
const newW = edit.width ?? r2.width;
|
|
1229
|
+
const newH = edit.height ?? r2.height;
|
|
1230
|
+
const newX = edit.x ?? r2.x;
|
|
1231
|
+
const newY = edit.y ?? r2.y;
|
|
1232
|
+
const na = a2 * (newW / r2.width);
|
|
1233
|
+
const nd = d * (newH / r2.height);
|
|
1234
|
+
const ne = newX - Math.min(0, na);
|
|
1235
|
+
const nf = newY - Math.min(0, nd);
|
|
1236
|
+
const abs = [na, 0, 0, nd, ne, nf];
|
|
1237
|
+
if (edit.zOrder) {
|
|
1238
|
+
splices.push({ start: op.record.start, end: op.record.end, text: "" });
|
|
1239
|
+
if (edit.zOrder === "back") {
|
|
1240
|
+
const binv = invert(backstop.ctm);
|
|
1241
|
+
const m = binv ? mul(abs, binv) : abs;
|
|
1242
|
+
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`;
|
|
1243
|
+
splices.push({ start: backstop.offset, end: backstop.offset, text: block });
|
|
1244
|
+
} else {
|
|
1245
|
+
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`);
|
|
1246
|
+
}
|
|
1247
|
+
report.apply(`${edit.imageId}: enviada ${edit.zOrder === "back" ? "al fondo" : "al frente"}`);
|
|
1248
|
+
continue;
|
|
1249
|
+
}
|
|
1250
|
+
const inv = invert(op.matrix);
|
|
1251
|
+
if (!inv) {
|
|
1252
|
+
report.warn(`${edit.imageId}: matriz degenerada \u2014 sin cambios`);
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
const rel = mul(abs, inv);
|
|
1256
|
+
splices.push({
|
|
1257
|
+
start: op.record.start,
|
|
1258
|
+
end: op.record.end,
|
|
1259
|
+
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`
|
|
1260
|
+
});
|
|
1261
|
+
report.apply(`${edit.imageId}: reubicada/escalada`);
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
// ../core/src/bake/fonts.ts
|
|
1266
|
+
import { PDFDict as PDFDict2, PDFName as PDFName3, PDFRawStream as PDFRawStream3, StandardFonts, decodePDFRawStream as decodePDFRawStream2 } from "pdf-lib";
|
|
1267
|
+
|
|
1268
|
+
// ../core/src/bake/toUnicode.ts
|
|
1269
|
+
var hexToBytes = (hex) => {
|
|
1270
|
+
const out = [];
|
|
1271
|
+
for (let i = 0; i + 1 < hex.length; i += 2) out.push(parseInt(hex.slice(i, i + 2), 16));
|
|
1272
|
+
return out;
|
|
1273
|
+
};
|
|
1274
|
+
var bytesToUnicode = (bytes) => {
|
|
1275
|
+
const units = [];
|
|
1276
|
+
for (let i = 0; i + 1 < bytes.length; i += 2) units.push(bytes[i] << 8 | bytes[i + 1]);
|
|
1277
|
+
return String.fromCharCode(...units);
|
|
1278
|
+
};
|
|
1279
|
+
function parseToUnicode(cmapText) {
|
|
1280
|
+
const map = /* @__PURE__ */ new Map();
|
|
1281
|
+
const hexRe = /<([0-9a-fA-F]+)>/g;
|
|
1282
|
+
const takeHexes = (chunk) => {
|
|
1283
|
+
const out = [];
|
|
1284
|
+
let m2;
|
|
1285
|
+
hexRe.lastIndex = 0;
|
|
1286
|
+
while (m2 = hexRe.exec(chunk)) out.push(m2[1]);
|
|
1287
|
+
return out;
|
|
1288
|
+
};
|
|
1289
|
+
const bfcharRe = /beginbfchar([\s\S]*?)endbfchar/g;
|
|
1290
|
+
let m;
|
|
1291
|
+
while (m = bfcharRe.exec(cmapText)) {
|
|
1292
|
+
const hexes = takeHexes(m[1]);
|
|
1293
|
+
for (let i = 0; i + 1 < hexes.length; i += 2) {
|
|
1294
|
+
const uni = bytesToUnicode(hexToBytes(hexes[i + 1]));
|
|
1295
|
+
if (uni && !map.has(uni)) map.set(uni, hexToBytes(hexes[i]));
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
const bfrangeRe = /beginbfrange([\s\S]*?)endbfrange/g;
|
|
1299
|
+
while (m = bfrangeRe.exec(cmapText)) {
|
|
1300
|
+
const body = m[1];
|
|
1301
|
+
const lineRe = /<([0-9a-fA-F]+)>\s*<([0-9a-fA-F]+)>\s*(<[0-9a-fA-F]+>|\[[\s\S]*?\])/g;
|
|
1302
|
+
let lm;
|
|
1303
|
+
while (lm = lineRe.exec(body)) {
|
|
1304
|
+
const lo = parseInt(lm[1], 16);
|
|
1305
|
+
const hi = parseInt(lm[2], 16);
|
|
1306
|
+
const codeLen = Math.ceil(lm[1].length / 2);
|
|
1307
|
+
const dst = lm[3];
|
|
1308
|
+
const codeBytes = (code) => {
|
|
1309
|
+
const out = [];
|
|
1310
|
+
for (let k = codeLen - 1; k >= 0; k--) out.push(code >> 8 * k & 255);
|
|
1311
|
+
return out;
|
|
1312
|
+
};
|
|
1313
|
+
if (dst.startsWith("[")) {
|
|
1314
|
+
const dsts = takeHexes(dst);
|
|
1315
|
+
for (let c = lo, idx = 0; c <= hi && idx < dsts.length; c++, idx++) {
|
|
1316
|
+
const uni = bytesToUnicode(hexToBytes(dsts[idx]));
|
|
1317
|
+
if (uni && !map.has(uni)) map.set(uni, codeBytes(c));
|
|
1318
|
+
}
|
|
1319
|
+
} else {
|
|
1320
|
+
const startBytes = hexToBytes(dst.replace(/[<>]/g, ""));
|
|
1321
|
+
const startUnits = [];
|
|
1322
|
+
for (let i = 0; i + 1 < startBytes.length; i += 2) startUnits.push(startBytes[i] << 8 | startBytes[i + 1]);
|
|
1323
|
+
for (let c = lo; c <= hi && c - lo < 65536; c++) {
|
|
1324
|
+
const units = [...startUnits];
|
|
1325
|
+
units[units.length - 1] += c - lo;
|
|
1326
|
+
const uni = String.fromCharCode(...units);
|
|
1327
|
+
if (uni && !map.has(uni)) map.set(uni, codeBytes(c));
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
return {
|
|
1333
|
+
encode(text) {
|
|
1334
|
+
const out = [];
|
|
1335
|
+
for (const ch of text) {
|
|
1336
|
+
const bytes = map.get(ch);
|
|
1337
|
+
if (!bytes) return null;
|
|
1338
|
+
out.push(...bytes);
|
|
1339
|
+
}
|
|
1340
|
+
return Uint8Array.from(out);
|
|
1341
|
+
}
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
// ../core/src/bake/fonts.ts
|
|
1346
|
+
var STD_FONTS = {
|
|
1347
|
+
sans: [StandardFonts.Helvetica, StandardFonts.HelveticaBold, StandardFonts.HelveticaOblique, StandardFonts.HelveticaBoldOblique],
|
|
1348
|
+
serif: [StandardFonts.TimesRoman, StandardFonts.TimesRomanBold, StandardFonts.TimesRomanItalic, StandardFonts.TimesRomanBoldItalic],
|
|
1349
|
+
mono: [StandardFonts.Courier, StandardFonts.CourierBold, StandardFonts.CourierOblique, StandardFonts.CourierBoldOblique]
|
|
1350
|
+
};
|
|
1351
|
+
function stdFontFor(bucket, bold, italic) {
|
|
1352
|
+
return STD_FONTS[bucket][(bold ? 1 : 0) + (italic ? 2 : 0)];
|
|
1353
|
+
}
|
|
1354
|
+
function encoderForFont(doc, page, fontName, cache) {
|
|
1355
|
+
const hit = cache.get(fontName);
|
|
1356
|
+
if (hit !== void 0) return hit;
|
|
1357
|
+
let enc = null;
|
|
1358
|
+
try {
|
|
1359
|
+
const res = page.node.Resources();
|
|
1360
|
+
const fonts = res?.lookup(PDFName3.of("Font"));
|
|
1361
|
+
const fdict = fonts instanceof PDFDict2 ? fonts.lookup(PDFName3.of(fontName)) : null;
|
|
1362
|
+
const tu = fdict instanceof PDFDict2 ? fdict.lookup(PDFName3.of("ToUnicode")) : null;
|
|
1363
|
+
if (tu instanceof PDFRawStream3) {
|
|
1364
|
+
const decoded = decodePDFRawStream2(tu).decode();
|
|
1365
|
+
enc = parseToUnicode(latin12(decoded, 0, decoded.length));
|
|
1366
|
+
}
|
|
1367
|
+
} catch {
|
|
1368
|
+
enc = null;
|
|
1369
|
+
}
|
|
1370
|
+
cache.set(fontName, enc);
|
|
1371
|
+
return enc;
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
// ../core/src/bake/textEmit.ts
|
|
1375
|
+
function relTm(o, ratio, x, y) {
|
|
1376
|
+
const m = o.matrix;
|
|
1377
|
+
const abs = [m[0] * ratio, m[1] * ratio, m[2] * ratio, m[3] * ratio, x, y];
|
|
1378
|
+
const inv = invert(o.ctm);
|
|
1379
|
+
return inv ? mul(abs, inv) : null;
|
|
1380
|
+
}
|
|
1381
|
+
function reemitBlock(o, src, ratio, x, y, ov = {}) {
|
|
1382
|
+
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`;
|
|
1383
|
+
const t = relTm(o, ratio, x, y);
|
|
1384
|
+
if (!t) return null;
|
|
1385
|
+
const colorRaw = ov.colorRaw ?? o.fillColorRaw;
|
|
1386
|
+
const color = colorRaw ? `${colorRaw} ` : "";
|
|
1387
|
+
const tc = ov.charSpacing ?? o.charSpacing * ratio;
|
|
1388
|
+
const tz = ov.hScale ?? o.hScale;
|
|
1389
|
+
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`;
|
|
1390
|
+
}
|
|
1391
|
+
function newTextBlock(o, ratio, x, y, bytes, ov = {}) {
|
|
1392
|
+
const t = relTm(o, ratio, x, y);
|
|
1393
|
+
if (!t) return null;
|
|
1394
|
+
const colorRaw = ov.colorRaw ?? o.fillColorRaw;
|
|
1395
|
+
const color = colorRaw ? `${colorRaw} ` : "";
|
|
1396
|
+
const tc = ov.charSpacing ?? 0;
|
|
1397
|
+
const tz = ov.hScale ?? o.hScale;
|
|
1398
|
+
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`;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// ../core/src/bake/text.ts
|
|
1402
|
+
var underlineRectsFor = (edit, fillRects) => {
|
|
1403
|
+
const size = edit.original.fontSize;
|
|
1404
|
+
const lines = edit.original.baselines?.length ? edit.original.baselines : [edit.original.baseline];
|
|
1405
|
+
const x0 = edit.original.x - 2;
|
|
1406
|
+
const x1 = edit.original.x + edit.original.width + 2;
|
|
1407
|
+
return fillRects.filter(
|
|
1408
|
+
(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)
|
|
1409
|
+
);
|
|
1410
|
+
};
|
|
1411
|
+
var escapesClip = (o, x, y) => {
|
|
1412
|
+
if (!o.clip) return false;
|
|
1413
|
+
const m = 1;
|
|
1414
|
+
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;
|
|
1415
|
+
};
|
|
1416
|
+
var atStreamEnd = (o) => ({ ...o, ctm: IDENTITY });
|
|
1417
|
+
var editBasics = (edit) => ({
|
|
1418
|
+
ratio: (edit.fontSize ?? edit.original.fontSize) / edit.original.fontSize,
|
|
1419
|
+
newX: edit.x ?? edit.original.x,
|
|
1420
|
+
newBaseline: edit.baseline ?? edit.original.baseline,
|
|
1421
|
+
styleOv: {
|
|
1422
|
+
charSpacing: edit.charSpacing,
|
|
1423
|
+
hScale: edit.hScale,
|
|
1424
|
+
colorRaw: edit.color ? hexToRg(edit.color) : void 0
|
|
1425
|
+
}
|
|
1426
|
+
});
|
|
1427
|
+
var VerbatimReemit = class {
|
|
1428
|
+
canHandle(edit) {
|
|
1429
|
+
return edit.text === edit.original.text && edit.font === void 0 && !edit.runs;
|
|
1430
|
+
}
|
|
1431
|
+
emit({ edit, ops, src, fillRects, splices, appendBlocks, report }) {
|
|
1432
|
+
const { ratio, newX, newBaseline, styleOv } = editBasics(edit);
|
|
1433
|
+
const editSplices = [];
|
|
1434
|
+
const editAppends = [];
|
|
1435
|
+
for (const o of ops) {
|
|
1436
|
+
const nx = newX + (o.x - edit.original.x) * ratio;
|
|
1437
|
+
const ny = newBaseline + (o.y - edit.original.baseline) * ratio;
|
|
1438
|
+
const escaped = escapesClip(o, nx, ny);
|
|
1439
|
+
const block = reemitBlock(escaped ? atStreamEnd(o) : o, src, ratio, nx, ny, styleOv);
|
|
1440
|
+
if (!block) {
|
|
1441
|
+
report.warn(`${edit.segmentId}: matriz degenerada \u2014 sin cambios`);
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
if (escaped) {
|
|
1445
|
+
editSplices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
1446
|
+
editAppends.push(block);
|
|
1447
|
+
} else {
|
|
1448
|
+
editSplices.push({ start: o.record.start, end: o.record.end, text: block });
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
splices.push(...editSplices);
|
|
1452
|
+
appendBlocks.push(...editAppends);
|
|
1453
|
+
for (const r2 of underlineRectsFor(edit, fillRects)) {
|
|
1454
|
+
splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
1455
|
+
const nx = newX + (r2.x - edit.original.x) * ratio;
|
|
1456
|
+
const ny = newBaseline + (r2.y - edit.original.baseline) * ratio;
|
|
1457
|
+
const fill = r2.fillColorRaw || "0 0 0 rg";
|
|
1458
|
+
appendBlocks.push(`q ${fill} ${fmt(nx)} ${fmt(ny)} ${fmt(r2.width * ratio)} ${fmt(r2.height * ratio)} re f Q`);
|
|
1459
|
+
}
|
|
1460
|
+
report.apply(`${edit.segmentId}: reubicado/escalado (${ops.length} op${ops.length > 1 ? "s" : ""})`);
|
|
1461
|
+
}
|
|
1462
|
+
};
|
|
1463
|
+
var StyledRunsReemit = class {
|
|
1464
|
+
canHandle() {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
emit(ctx) {
|
|
1468
|
+
const { edit, ops, doc, page, pageNum, encCache, splices, appendBlocks, fallbackDraws, report } = ctx;
|
|
1469
|
+
const { ratio, newX, newBaseline, styleOv } = editBasics(edit);
|
|
1470
|
+
const familyChanged = edit.font !== void 0;
|
|
1471
|
+
for (const o of ops.slice(1)) splices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
1472
|
+
for (const r2 of underlineRectsFor(edit, ctx.fillRects)) splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
1473
|
+
const firstOp = ops[0];
|
|
1474
|
+
const inlineBlocks = [];
|
|
1475
|
+
const runsToEmit = edit.runs ?? [{
|
|
1476
|
+
text: edit.text,
|
|
1477
|
+
bold: edit.original.bold ?? false,
|
|
1478
|
+
italic: edit.original.italic ?? false,
|
|
1479
|
+
dx: 0
|
|
1480
|
+
}];
|
|
1481
|
+
const fontForStyle = /* @__PURE__ */ new Map();
|
|
1482
|
+
for (const or of edit.original.runs ?? []) {
|
|
1483
|
+
const op = ops.find((o) => Math.abs(o.x - or.x) <= 2.5);
|
|
1484
|
+
const key = `${or.bold}|${or.italic}`;
|
|
1485
|
+
if (op && !fontForStyle.has(key)) fontForStyle.set(key, op.fontName);
|
|
1486
|
+
}
|
|
1487
|
+
const lineRuns = [[]];
|
|
1488
|
+
for (const sr of runsToEmit) {
|
|
1489
|
+
const parts = sr.text.split("\n");
|
|
1490
|
+
parts.forEach((p, i) => {
|
|
1491
|
+
if (i > 0) lineRuns.push([]);
|
|
1492
|
+
if (p) lineRuns[lineRuns.length - 1].push({ ...sr, text: p });
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
const leading = (edit.fontSize ?? edit.original.fontSize) * 1.2;
|
|
1496
|
+
const escaped = lineRuns.some((_, li) => escapesClip(firstOp, newX, newBaseline - li * leading));
|
|
1497
|
+
let substituted = 0;
|
|
1498
|
+
for (let li = 0; li < lineRuns.length; li++) {
|
|
1499
|
+
const lineBase = newBaseline - li * leading;
|
|
1500
|
+
for (const sr of lineRuns[li]) {
|
|
1501
|
+
if (!sr.text) continue;
|
|
1502
|
+
const x = newX + sr.dx * ratio;
|
|
1503
|
+
const fontName = familyChanged ? void 0 : fontForStyle.get(`${sr.bold}|${sr.italic}`);
|
|
1504
|
+
const bytes = fontName ? encoderForFont(doc, page, fontName, encCache)?.encode(sr.text) ?? null : null;
|
|
1505
|
+
const runOv = {
|
|
1506
|
+
...styleOv,
|
|
1507
|
+
colorRaw: sr.color ? hexToRg(sr.color) : styleOv.colorRaw
|
|
1508
|
+
};
|
|
1509
|
+
const srcForBlock = ops.find((o) => o.fontName === fontName) ?? firstOp;
|
|
1510
|
+
const inlineBlock = fontName && bytes ? newTextBlock(escaped ? atStreamEnd(srcForBlock) : srcForBlock, ratio, x, lineBase, bytes, runOv) : null;
|
|
1511
|
+
if (inlineBlock) {
|
|
1512
|
+
inlineBlocks.push(inlineBlock);
|
|
1513
|
+
if (sr.underline && sr.w) {
|
|
1514
|
+
const size = edit.fontSize ?? edit.original.fontSize;
|
|
1515
|
+
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 };
|
|
1516
|
+
appendBlocks.push(
|
|
1517
|
+
`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`
|
|
1518
|
+
);
|
|
1519
|
+
}
|
|
1520
|
+
} else {
|
|
1521
|
+
if (!/^\s+$/.test(sr.text)) {
|
|
1522
|
+
const srcOp = ops.find((o) => o.fontName === fontName) ?? firstOp;
|
|
1523
|
+
const color = sr.color ? hexToRgbObj(sr.color) : edit.color ? hexToRgbObj(edit.color) : rawFillToRgb(srcOp?.fillColorRaw) ?? void 0;
|
|
1524
|
+
fallbackDraws.push({
|
|
1525
|
+
page: pageNum,
|
|
1526
|
+
text: sr.text,
|
|
1527
|
+
x,
|
|
1528
|
+
y: lineBase,
|
|
1529
|
+
size: edit.fontSize ?? edit.original.fontSize,
|
|
1530
|
+
bucket: edit.font ?? edit.original.bucket ?? "sans",
|
|
1531
|
+
bold: sr.bold,
|
|
1532
|
+
italic: sr.italic,
|
|
1533
|
+
color: color ?? void 0,
|
|
1534
|
+
underline: sr.underline
|
|
1535
|
+
});
|
|
1536
|
+
}
|
|
1537
|
+
substituted++;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
if (escaped) {
|
|
1542
|
+
splices.push({ start: firstOp.record.start, end: firstOp.record.end, text: "" });
|
|
1543
|
+
if (inlineBlocks.length) appendBlocks.push(inlineBlocks.join("\n"));
|
|
1544
|
+
} else {
|
|
1545
|
+
splices.push({ start: firstOp.record.start, end: firstOp.record.end, text: inlineBlocks.join("\n") });
|
|
1546
|
+
}
|
|
1547
|
+
if (substituted && !familyChanged) {
|
|
1548
|
+
report.warn(`${edit.segmentId}: ${substituted} tramo${substituted > 1 ? "s" : ""} sin fuente original disponible (estilo nuevo o subset insuficiente) \u2014 sustituido por est\xE1ndar`);
|
|
1549
|
+
}
|
|
1550
|
+
report.apply(familyChanged ? `${edit.segmentId}: redibujado con fuente est\xE1ndar (cambio de familia)` : `${edit.segmentId}: reescrito por tramos (${runsToEmit.length})`);
|
|
1551
|
+
}
|
|
1552
|
+
};
|
|
1553
|
+
var textEmitStrategies = [
|
|
1554
|
+
new VerbatimReemit(),
|
|
1555
|
+
new StyledRunsReemit()
|
|
1556
|
+
];
|
|
1557
|
+
function applySegmentEditsToPage(args) {
|
|
1558
|
+
const { doc, page, pageNum, pageEdits, shows, fillRects, src, splices, appendBlocks, fallbackDraws, report } = args;
|
|
1559
|
+
const encCache = /* @__PURE__ */ new Map();
|
|
1560
|
+
for (const edit of pageEdits) {
|
|
1561
|
+
const { ops, conflict } = matchOps(shows, edit.original);
|
|
1562
|
+
if (conflict) {
|
|
1563
|
+
report.warn(`${edit.segmentId}: ${conflict} \u2014 sin cambios`);
|
|
1564
|
+
continue;
|
|
1565
|
+
}
|
|
1566
|
+
const colorOp = ops.find((o) => rawFillToRgb(o.fillColorRaw));
|
|
1567
|
+
if (colorOp) {
|
|
1568
|
+
const c = rawFillToRgb(colorOp.fillColorRaw);
|
|
1569
|
+
if (c) report.color(edit.segmentId, rgbToHex(c));
|
|
1570
|
+
}
|
|
1571
|
+
if (edit.remove) {
|
|
1572
|
+
for (const o of ops) splices.push({ start: o.record.start, end: o.record.end, text: "" });
|
|
1573
|
+
for (const r2 of underlineRectsFor(edit, fillRects)) splices.push({ start: r2.start, end: r2.end, text: "" });
|
|
1574
|
+
report.apply(`${edit.segmentId}: eliminado`);
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
const ctx = { doc, page, pageNum, edit, ops, src, encCache, fillRects, splices, appendBlocks, fallbackDraws, report };
|
|
1578
|
+
for (const strategy of textEmitStrategies) {
|
|
1579
|
+
if (strategy.canHandle(edit)) {
|
|
1580
|
+
strategy.emit(ctx);
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// ../core/src/bake/widgets.ts
|
|
1588
|
+
function applyWidgetEdits(doc, edits, report) {
|
|
1589
|
+
if (!edits.length) return;
|
|
1590
|
+
let form;
|
|
1591
|
+
try {
|
|
1592
|
+
form = doc.getForm();
|
|
1593
|
+
} catch {
|
|
1594
|
+
report.warn("el documento no tiene AcroForm \u2014 ediciones de campos saltadas");
|
|
1595
|
+
return;
|
|
1596
|
+
}
|
|
1597
|
+
let touched = false;
|
|
1598
|
+
for (const edit of edits) {
|
|
1599
|
+
const tol = 2.5;
|
|
1600
|
+
let matchedField = null;
|
|
1601
|
+
let matchedWidget = null;
|
|
1602
|
+
for (const field of form.getFields()) {
|
|
1603
|
+
if (field.getName() !== edit.original.fieldName) continue;
|
|
1604
|
+
for (const widget of field.acroField.getWidgets()) {
|
|
1605
|
+
const r2 = widget.getRectangle();
|
|
1606
|
+
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) {
|
|
1607
|
+
matchedField = field;
|
|
1608
|
+
matchedWidget = widget;
|
|
1609
|
+
break;
|
|
1610
|
+
}
|
|
1611
|
+
}
|
|
1612
|
+
if (matchedField) break;
|
|
1613
|
+
}
|
|
1614
|
+
if (!matchedField || !matchedWidget) {
|
|
1615
|
+
report.warn(`${edit.widgetId}: campo "${edit.original.fieldName}" no encontrado en su rect \u2014 sin cambios`);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
if (edit.remove) {
|
|
1619
|
+
try {
|
|
1620
|
+
form.removeField(matchedField);
|
|
1621
|
+
report.apply(`${edit.widgetId}: campo "${edit.original.fieldName}" eliminado`);
|
|
1622
|
+
touched = true;
|
|
1623
|
+
} catch (err) {
|
|
1624
|
+
report.warn(`${edit.widgetId}: no se pudo eliminar (${err instanceof Error ? err.message : "error"})`);
|
|
1625
|
+
}
|
|
1626
|
+
continue;
|
|
1627
|
+
}
|
|
1628
|
+
matchedWidget.setRectangle({
|
|
1629
|
+
x: edit.x ?? edit.original.x,
|
|
1630
|
+
y: edit.y ?? edit.original.y,
|
|
1631
|
+
width: edit.width ?? edit.original.width,
|
|
1632
|
+
height: edit.height ?? edit.original.height
|
|
1633
|
+
});
|
|
1634
|
+
report.apply(`${edit.widgetId}: campo "${edit.original.fieldName}" reubicado/escalado`);
|
|
1635
|
+
touched = true;
|
|
1636
|
+
}
|
|
1637
|
+
if (touched) {
|
|
1638
|
+
try {
|
|
1639
|
+
form.updateFieldAppearances();
|
|
1640
|
+
} catch {
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// ../core/src/bake/highlights.ts
|
|
1646
|
+
import { PDFArray as PDFArray4, PDFDict as PDFDict5, PDFDocument as PDFDocument6, PDFName as PDFName6, PDFNumber, PDFRef as PDFRef5 } from "pdf-lib";
|
|
1647
|
+
|
|
1648
|
+
// ../core/src/bake/annotEdits.ts
|
|
1649
|
+
import { PDFArray as PDFArray2, PDFDict as PDFDict3, PDFName as PDFName4, PDFRef as PDFRef3 } from "pdf-lib";
|
|
1650
|
+
var rectNums = (rect) => {
|
|
1651
|
+
if (rect.size() !== 4) return null;
|
|
1652
|
+
const nums = [0, 1, 2, 3].map((k) => Number(rect.get(k).asNumber?.() ?? NaN));
|
|
1653
|
+
return nums.some(Number.isNaN) ? null : nums;
|
|
1654
|
+
};
|
|
1655
|
+
function applyAnnotRectEdits(doc, subtype, label, edits, report, onRect) {
|
|
1656
|
+
if (!edits.length) return;
|
|
1657
|
+
const tol = 2;
|
|
1658
|
+
for (const edit of edits) {
|
|
1659
|
+
const page = doc.getPages()[edit.page - 1];
|
|
1660
|
+
if (!page) {
|
|
1661
|
+
report.warn(`${edit.id}: p\xE1gina ${edit.page} fuera de rango \u2014 sin cambios`);
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
const annots = page.node.lookupMaybe(PDFName4.of("Annots"), PDFArray2);
|
|
1665
|
+
if (!annots) {
|
|
1666
|
+
report.warn(`${edit.id}: la p\xE1gina no tiene /Annots \u2014 sin cambios`);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
let done = false;
|
|
1670
|
+
for (let i = 0; i < annots.size(); i++) {
|
|
1671
|
+
const raw = annots.get(i);
|
|
1672
|
+
const dict = raw instanceof PDFRef3 ? doc.context.lookup(raw) : raw;
|
|
1673
|
+
if (!(dict instanceof PDFDict3)) continue;
|
|
1674
|
+
if (dict.get(PDFName4.of("Subtype")) !== PDFName4.of(subtype)) continue;
|
|
1675
|
+
const rect = dict.lookupMaybe(PDFName4.of("Rect"), PDFArray2);
|
|
1676
|
+
const nums = rect ? rectNums(rect) : null;
|
|
1677
|
+
if (!nums) continue;
|
|
1678
|
+
const [ax, ay, bx, by] = nums;
|
|
1679
|
+
const rx = Math.min(ax, bx), ry = Math.min(ay, by), rw = Math.abs(bx - ax), rh = Math.abs(by - ay);
|
|
1680
|
+
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) {
|
|
1681
|
+
if (edit.remove) {
|
|
1682
|
+
annots.remove(i);
|
|
1683
|
+
report.apply(`${edit.id}: ${label} eliminado`);
|
|
1684
|
+
} else {
|
|
1685
|
+
const nx = edit.x ?? rx, ny = edit.y ?? ry, nw = edit.width ?? rw, nh = edit.height ?? rh;
|
|
1686
|
+
dict.set(PDFName4.of("Rect"), doc.context.obj([nx, ny, nx + nw, ny + nh]));
|
|
1687
|
+
onRect?.(dict, nx, ny, nw, nh, edit);
|
|
1688
|
+
report.apply(`${edit.id}: ${label} ${edit.color ? "recoloreado" : "reubicado/escalado"}`);
|
|
1689
|
+
}
|
|
1690
|
+
done = true;
|
|
1691
|
+
break;
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
if (!done) report.warn(`${edit.id}: no se encontr\xF3 la anotaci\xF3n en su rect original \u2014 sin cambios`);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
// ../core/src/bake/createNodes.ts
|
|
1699
|
+
import {
|
|
1700
|
+
PDFArray as PDFArray3,
|
|
1701
|
+
PDFDict as PDFDict4,
|
|
1702
|
+
PDFDocument as PDFDocument5,
|
|
1703
|
+
PDFName as PDFName5,
|
|
1704
|
+
PDFRef as PDFRef4,
|
|
1705
|
+
PDFString,
|
|
1706
|
+
degrees,
|
|
1707
|
+
rgb
|
|
1708
|
+
} from "pdf-lib";
|
|
1709
|
+
|
|
1710
|
+
// ../core/src/model.ts
|
|
1711
|
+
var FIELD_DEFAULT_SIZE = {
|
|
1712
|
+
text: { width: 160, height: 20 },
|
|
1713
|
+
checkbox: { width: 14, height: 14 },
|
|
1714
|
+
radio: { width: 14, height: 14 },
|
|
1715
|
+
select: { width: 140, height: 20 },
|
|
1716
|
+
list: { width: 140, height: 60 },
|
|
1717
|
+
button: { width: 90, height: 24 },
|
|
1718
|
+
signature: { width: 200, height: 50 }
|
|
1719
|
+
};
|
|
1720
|
+
|
|
1721
|
+
// ../core/src/bake/createNodes.ts
|
|
1722
|
+
var hexToRgb = (hex) => {
|
|
1723
|
+
const m = /^#?([0-9a-f]{6})$/i.exec(hex.trim());
|
|
1724
|
+
const v = m ? parseInt(m[1], 16) : 0;
|
|
1725
|
+
return rgb((v >> 16 & 255) / 255, (v >> 8 & 255) / 255, (v & 255) / 255);
|
|
1726
|
+
};
|
|
1727
|
+
var FIELD_BASE_NAME = {
|
|
1728
|
+
text: "texto",
|
|
1729
|
+
checkbox: "check",
|
|
1730
|
+
radio: "radio",
|
|
1731
|
+
select: "select",
|
|
1732
|
+
list: "lista",
|
|
1733
|
+
button: "boton",
|
|
1734
|
+
signature: "firma"
|
|
1735
|
+
};
|
|
1736
|
+
function uniqueName(existing, base) {
|
|
1737
|
+
let n = 1;
|
|
1738
|
+
while (existing.has(`${base}_${n}`)) n++;
|
|
1739
|
+
return `${base}_${n}`;
|
|
1740
|
+
}
|
|
1741
|
+
var MODERN_WIDGET = {
|
|
1742
|
+
borderWidth: 1,
|
|
1743
|
+
borderColor: rgb(0.72, 0.77, 0.85),
|
|
1744
|
+
backgroundColor: rgb(0.955, 0.965, 0.985)
|
|
1745
|
+
};
|
|
1746
|
+
function addSignatureField(doc, page, name, rect) {
|
|
1747
|
+
const ctx = doc.context;
|
|
1748
|
+
const dict = ctx.obj({
|
|
1749
|
+
Type: "Annot",
|
|
1750
|
+
Subtype: "Widget",
|
|
1751
|
+
FT: "Sig",
|
|
1752
|
+
T: PDFString.of(name),
|
|
1753
|
+
// string, no name — pdf.js lee el fieldName de acá
|
|
1754
|
+
Rect: rect,
|
|
1755
|
+
F: 4,
|
|
1756
|
+
// print
|
|
1757
|
+
P: page.ref
|
|
1758
|
+
});
|
|
1759
|
+
const ref = ctx.register(dict);
|
|
1760
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray3) ?? ctx.obj([]);
|
|
1761
|
+
annots.push(ref);
|
|
1762
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
1763
|
+
const form = doc.getForm();
|
|
1764
|
+
form.acroForm.addField(ref);
|
|
1765
|
+
}
|
|
1766
|
+
async function addFormField(pdfBytes, spec) {
|
|
1767
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1768
|
+
const page = doc.getPages()[spec.page - 1];
|
|
1769
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
1770
|
+
const form = doc.getForm();
|
|
1771
|
+
const existing = new Set(form.getFields().map((f) => f.getName()));
|
|
1772
|
+
const name = spec.name && !existing.has(spec.name) ? spec.name : uniqueName(existing, FIELD_BASE_NAME[spec.type]);
|
|
1773
|
+
const size = FIELD_DEFAULT_SIZE[spec.type];
|
|
1774
|
+
const width = spec.width ?? size.width;
|
|
1775
|
+
const height = spec.height ?? size.height;
|
|
1776
|
+
const rect = { x: spec.x, y: spec.y, width, height };
|
|
1777
|
+
const styled = { ...rect, ...MODERN_WIDGET };
|
|
1778
|
+
switch (spec.type) {
|
|
1779
|
+
case "text": {
|
|
1780
|
+
const f = form.createTextField(name);
|
|
1781
|
+
f.addToPage(page, styled);
|
|
1782
|
+
try {
|
|
1783
|
+
f.setFontSize(10);
|
|
1784
|
+
} catch {
|
|
1785
|
+
}
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
case "checkbox": {
|
|
1789
|
+
const f = form.createCheckBox(name);
|
|
1790
|
+
f.addToPage(page, styled);
|
|
1791
|
+
break;
|
|
1792
|
+
}
|
|
1793
|
+
case "radio": {
|
|
1794
|
+
const f = form.createRadioGroup(name);
|
|
1795
|
+
f.addOptionToPage("opcion_1", page, styled);
|
|
1796
|
+
break;
|
|
1797
|
+
}
|
|
1798
|
+
case "select": {
|
|
1799
|
+
const f = form.createDropdown(name);
|
|
1800
|
+
f.addOptions(["Opci\xF3n 1"]);
|
|
1801
|
+
f.addToPage(page, styled);
|
|
1802
|
+
break;
|
|
1803
|
+
}
|
|
1804
|
+
case "list": {
|
|
1805
|
+
const f = form.createOptionList(name);
|
|
1806
|
+
f.addOptions(["Opci\xF3n 1"]);
|
|
1807
|
+
f.addToPage(page, styled);
|
|
1808
|
+
break;
|
|
1809
|
+
}
|
|
1810
|
+
case "button": {
|
|
1811
|
+
const f = form.createButton(name);
|
|
1812
|
+
f.addToPage(name, page, styled);
|
|
1813
|
+
break;
|
|
1814
|
+
}
|
|
1815
|
+
case "signature": {
|
|
1816
|
+
addSignatureField(doc, page, name, [spec.x, spec.y, spec.x + width, spec.y + height]);
|
|
1817
|
+
break;
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
try {
|
|
1821
|
+
form.updateFieldAppearances();
|
|
1822
|
+
} catch {
|
|
1823
|
+
}
|
|
1824
|
+
return { pdf: await doc.save(), name };
|
|
1825
|
+
}
|
|
1826
|
+
async function addText(pdfBytes, spec) {
|
|
1827
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1828
|
+
const page = doc.getPages()[spec.page - 1];
|
|
1829
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
1830
|
+
const size = spec.size ?? 11;
|
|
1831
|
+
const font = await doc.embedFont(stdFontFor(spec.bucket ?? "sans", spec.bold ?? false, spec.italic ?? false));
|
|
1832
|
+
page.drawText(spec.text, {
|
|
1833
|
+
x: spec.x,
|
|
1834
|
+
y: spec.y - size,
|
|
1835
|
+
size,
|
|
1836
|
+
font,
|
|
1837
|
+
color: spec.color ? hexToRgb(spec.color) : rgb(0, 0, 0),
|
|
1838
|
+
lineHeight: size * 1.35,
|
|
1839
|
+
maxWidth: Math.max(80, page.getWidth() - spec.x - 40)
|
|
1840
|
+
});
|
|
1841
|
+
return { pdf: await doc.save() };
|
|
1842
|
+
}
|
|
1843
|
+
async function addWatermark(pdfBytes, spec) {
|
|
1844
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1845
|
+
const font = await doc.embedFont(stdFontFor("sans", true, false));
|
|
1846
|
+
for (const page of doc.getPages()) {
|
|
1847
|
+
const w = page.getWidth();
|
|
1848
|
+
const h = page.getHeight();
|
|
1849
|
+
const size = Math.min(84, w * 1.1 / Math.max(4, spec.text.length) / 0.55);
|
|
1850
|
+
page.drawText(spec.text, {
|
|
1851
|
+
x: w * 0.14,
|
|
1852
|
+
y: h * 0.28,
|
|
1853
|
+
size,
|
|
1854
|
+
font,
|
|
1855
|
+
rotate: degrees(38),
|
|
1856
|
+
opacity: spec.opacity ?? 0.14,
|
|
1857
|
+
color: spec.color ? hexToRgb(spec.color) : rgb(0.4, 0.4, 0.45)
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
return { pdf: await doc.save() };
|
|
1861
|
+
}
|
|
1862
|
+
async function addHeaderFooter(pdfBytes, spec) {
|
|
1863
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1864
|
+
const font = await doc.embedFont(stdFontFor("sans", false, false));
|
|
1865
|
+
const pages = doc.getPages();
|
|
1866
|
+
const gray = rgb(0.35, 0.35, 0.4);
|
|
1867
|
+
pages.forEach((page, i) => {
|
|
1868
|
+
const w = page.getWidth();
|
|
1869
|
+
const h = page.getHeight();
|
|
1870
|
+
if (spec.header) page.drawText(spec.header, { x: 40, y: h - 28, size: 9, font, color: gray });
|
|
1871
|
+
if (spec.footer) page.drawText(spec.footer, { x: 40, y: 18, size: 9, font, color: gray });
|
|
1872
|
+
if (spec.pageNumbers) {
|
|
1873
|
+
const label = `P\xE1gina ${i + 1} de ${pages.length}`;
|
|
1874
|
+
const lw = font.widthOfTextAtSize(label, 9);
|
|
1875
|
+
page.drawText(label, { x: w - 40 - lw, y: 18, size: 9, font, color: gray });
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
return { pdf: await doc.save() };
|
|
1879
|
+
}
|
|
1880
|
+
function highlightAppearance(ctx, colorHex, w, h) {
|
|
1881
|
+
const c = colorHex ? hexToRgb(colorHex) : rgb(1, 0.84, 0);
|
|
1882
|
+
const gsRef = ctx.register(ctx.obj({ Type: "ExtGState", BM: "Multiply", ca: 0.55, CA: 0.55 }));
|
|
1883
|
+
const ap = ctx.stream(`/GS gs ${fmt(c.red)} ${fmt(c.green)} ${fmt(c.blue)} rg 0 0 ${fmt(w)} ${fmt(h)} re f`, {
|
|
1884
|
+
Type: "XObject",
|
|
1885
|
+
Subtype: "Form",
|
|
1886
|
+
FormType: 1,
|
|
1887
|
+
BBox: [0, 0, w, h],
|
|
1888
|
+
Resources: ctx.obj({ ExtGState: ctx.obj({ GS: gsRef }) })
|
|
1889
|
+
});
|
|
1890
|
+
return { apRef: ctx.register(ap), color: [c.red, c.green, c.blue] };
|
|
1891
|
+
}
|
|
1892
|
+
async function addHighlight(pdfBytes, spec) {
|
|
1893
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1894
|
+
const page = doc.getPages()[spec.page - 1];
|
|
1895
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
1896
|
+
const ctx = doc.context;
|
|
1897
|
+
const x = spec.x - 1, y = spec.y - 1, w = spec.width + 2, h = spec.height + 2;
|
|
1898
|
+
const { apRef, color } = highlightAppearance(ctx, spec.color, w, h);
|
|
1899
|
+
const dict = ctx.obj({
|
|
1900
|
+
Type: "Annot",
|
|
1901
|
+
Subtype: "Highlight",
|
|
1902
|
+
Rect: [x, y, x + w, y + h],
|
|
1903
|
+
// QuadPoints ISO 32000: UL UR LL LR (y crece hacia arriba).
|
|
1904
|
+
QuadPoints: [x, y + h, x + w, y + h, x, y, x + w, y],
|
|
1905
|
+
C: color,
|
|
1906
|
+
CA: 0.55,
|
|
1907
|
+
AP: ctx.obj({ N: apRef })
|
|
1908
|
+
});
|
|
1909
|
+
const ref = ctx.register(dict);
|
|
1910
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray3) ?? ctx.obj([]);
|
|
1911
|
+
annots.push(ref);
|
|
1912
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
1913
|
+
return { pdf: await doc.save() };
|
|
1914
|
+
}
|
|
1915
|
+
async function addLink(pdfBytes, spec) {
|
|
1916
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1917
|
+
const page = doc.getPages()[spec.page - 1];
|
|
1918
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
1919
|
+
const ctx = doc.context;
|
|
1920
|
+
const dict = ctx.obj({
|
|
1921
|
+
Type: "Annot",
|
|
1922
|
+
Subtype: "Link",
|
|
1923
|
+
Rect: [spec.x, spec.y, spec.x + spec.width, spec.y + spec.height],
|
|
1924
|
+
Border: [0, 0, 0],
|
|
1925
|
+
A: ctx.obj({ Type: "Action", S: "URI", URI: PDFString.of(spec.url) })
|
|
1926
|
+
});
|
|
1927
|
+
const ref = ctx.register(dict);
|
|
1928
|
+
const annots = page.node.lookupMaybe(PDFName5.of("Annots"), PDFArray3) ?? ctx.obj([]);
|
|
1929
|
+
annots.push(ref);
|
|
1930
|
+
page.node.set(PDFName5.of("Annots"), annots);
|
|
1931
|
+
return { pdf: await doc.save() };
|
|
1932
|
+
}
|
|
1933
|
+
async function insertImage(pdfBytes, spec) {
|
|
1934
|
+
const doc = await PDFDocument5.load(pdfBytes, { ignoreEncryption: true });
|
|
1935
|
+
const page = doc.getPages()[spec.page - 1];
|
|
1936
|
+
if (!page) throw new Error(`p\xE1gina ${spec.page} fuera de rango`);
|
|
1937
|
+
const image = /png$/i.test(spec.mime) ? await doc.embedPng(spec.bytes) : await doc.embedJpg(spec.bytes);
|
|
1938
|
+
const maxW = spec.maxWidth ?? 240;
|
|
1939
|
+
const ratio = image.width > maxW ? maxW / image.width : 1;
|
|
1940
|
+
const width = image.width * ratio;
|
|
1941
|
+
const height = image.height * ratio;
|
|
1942
|
+
const rect = { x: spec.x, y: spec.y - height, width, height };
|
|
1943
|
+
page.drawImage(image, rect);
|
|
1944
|
+
return { pdf: await doc.save(), rect };
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// ../core/src/bake/highlights.ts
|
|
1948
|
+
function applyHighlightEdits(doc, edits, report) {
|
|
1949
|
+
applyAnnotRectEdits(
|
|
1950
|
+
doc,
|
|
1951
|
+
"Highlight",
|
|
1952
|
+
"resaltado",
|
|
1953
|
+
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 })),
|
|
1954
|
+
report,
|
|
1955
|
+
(dict, nx, ny, nw, nh, edit) => {
|
|
1956
|
+
dict.set(PDFName6.of("QuadPoints"), doc.context.obj([nx, ny + nh, nx + nw, ny + nh, nx, ny, nx + nw, ny]));
|
|
1957
|
+
if (edit.color) {
|
|
1958
|
+
const { apRef, color } = highlightAppearance(doc.context, edit.color, nw, nh);
|
|
1959
|
+
dict.set(PDFName6.of("C"), doc.context.obj(color));
|
|
1960
|
+
dict.set(PDFName6.of("AP"), doc.context.obj({ N: apRef }));
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
// ../core/src/bake/links.ts
|
|
1967
|
+
function applyLinkEdits(doc, edits, report) {
|
|
1968
|
+
applyAnnotRectEdits(
|
|
1969
|
+
doc,
|
|
1970
|
+
"Link",
|
|
1971
|
+
"link",
|
|
1972
|
+
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 })),
|
|
1973
|
+
report
|
|
1974
|
+
);
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
// ../core/src/bake/fallback.ts
|
|
1978
|
+
import { rgb as rgb2 } from "pdf-lib";
|
|
1979
|
+
async function drawFallbackTexts(doc, draws, report) {
|
|
1980
|
+
if (!draws.length) return;
|
|
1981
|
+
const pages = doc.getPages();
|
|
1982
|
+
const fontCache = /* @__PURE__ */ new Map();
|
|
1983
|
+
for (const d of draws) {
|
|
1984
|
+
const key = `${d.bucket}|${d.bold}|${d.italic}`;
|
|
1985
|
+
let font = fontCache.get(key);
|
|
1986
|
+
if (!font) {
|
|
1987
|
+
font = await doc.embedFont(stdFontFor(d.bucket, d.bold, d.italic));
|
|
1988
|
+
fontCache.set(key, font);
|
|
1989
|
+
}
|
|
1990
|
+
const page = pages[d.page - 1];
|
|
1991
|
+
const color = d.color ? rgb2(d.color.r, d.color.g, d.color.b) : rgb2(0, 0, 0);
|
|
1992
|
+
const drawUnderline = () => {
|
|
1993
|
+
if (!d.underline || !font) return;
|
|
1994
|
+
const w = font.widthOfTextAtSize(d.text, d.size);
|
|
1995
|
+
page.drawRectangle({ x: d.x, y: d.y - d.size * 0.11, width: w, height: d.size * 0.055, color });
|
|
1996
|
+
};
|
|
1997
|
+
try {
|
|
1998
|
+
page.drawText(d.text, { x: d.x, y: d.y, size: d.size, font, color });
|
|
1999
|
+
drawUnderline();
|
|
2000
|
+
} catch {
|
|
2001
|
+
const clean = [...d.text].filter((c) => c.charCodeAt(0) <= 255).join("");
|
|
2002
|
+
try {
|
|
2003
|
+
page.drawText(clean, { x: d.x, y: d.y, size: d.size, font, color });
|
|
2004
|
+
report.warn(`p${d.page}: caracteres no representables descartados en "${d.text.slice(0, 24)}\u2026"`);
|
|
2005
|
+
} catch {
|
|
2006
|
+
report.warn(`p${d.page}: no se pudo dibujar el reemplazo "${d.text.slice(0, 24)}\u2026"`);
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
// ../core/src/bake/report.ts
|
|
2013
|
+
var BakeReport = class {
|
|
2014
|
+
applied = [];
|
|
2015
|
+
warnings = [];
|
|
2016
|
+
colors = {};
|
|
2017
|
+
apply(message) {
|
|
2018
|
+
this.applied.push(message);
|
|
2019
|
+
}
|
|
2020
|
+
warn(message) {
|
|
2021
|
+
this.warnings.push(message);
|
|
2022
|
+
}
|
|
2023
|
+
color(segmentId, hex) {
|
|
2024
|
+
this.colors[segmentId] = hex;
|
|
2025
|
+
}
|
|
2026
|
+
finish(pdf) {
|
|
2027
|
+
return { pdf, applied: this.applied, warnings: this.warnings, colors: this.colors };
|
|
2028
|
+
}
|
|
2029
|
+
};
|
|
2030
|
+
|
|
2031
|
+
// ../core/src/bake/bake.ts
|
|
2032
|
+
var groupByPage = (items) => {
|
|
2033
|
+
const out = /* @__PURE__ */ new Map();
|
|
2034
|
+
for (const item of items) out.set(item.page, [...out.get(item.page) ?? [], item]);
|
|
2035
|
+
return out;
|
|
2036
|
+
};
|
|
2037
|
+
async function bakeSegmentEdits(pdfBytes, edits, imageEdits = [], widgetEdits = [], highlightEdits = [], linkEdits = []) {
|
|
2038
|
+
const doc = await PDFDocument8.load(pdfBytes, { ignoreEncryption: true });
|
|
2039
|
+
const pages = doc.getPages();
|
|
2040
|
+
const report = new BakeReport();
|
|
2041
|
+
const fallbackDraws = [];
|
|
2042
|
+
applyWidgetEdits(doc, widgetEdits, report);
|
|
2043
|
+
applyHighlightEdits(doc, highlightEdits, report);
|
|
2044
|
+
applyLinkEdits(doc, linkEdits, report);
|
|
2045
|
+
const byPage = groupByPage(edits);
|
|
2046
|
+
const imgByPage = groupByPage(imageEdits);
|
|
2047
|
+
const allPages = /* @__PURE__ */ new Set([...byPage.keys(), ...imgByPage.keys()]);
|
|
2048
|
+
for (const pageNum of allPages) {
|
|
2049
|
+
const pageEdits = byPage.get(pageNum) ?? [];
|
|
2050
|
+
const pageImgEdits = imgByPage.get(pageNum) ?? [];
|
|
2051
|
+
const page = pages[pageNum - 1];
|
|
2052
|
+
if (!page) {
|
|
2053
|
+
report.warn(`p\xE1gina ${pageNum} fuera de rango \u2014 ediciones saltadas`);
|
|
2054
|
+
continue;
|
|
2055
|
+
}
|
|
2056
|
+
let src;
|
|
2057
|
+
try {
|
|
2058
|
+
src = pageContentBytes(doc, page);
|
|
2059
|
+
} catch (err) {
|
|
2060
|
+
report.warn(`p\xE1gina ${pageNum}: ${err instanceof Error ? err.message : "stream ilegible"}`);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
const { shows, xobjects, fillRects, backstop } = walkContent(src);
|
|
2064
|
+
const splices = [];
|
|
2065
|
+
const appendBlocks = [];
|
|
2066
|
+
applyImageEditsToPage({ doc, page, pageImgEdits, xobjects, backstop, splices, appendBlocks, report });
|
|
2067
|
+
applySegmentEditsToPage({ doc, page, pageNum, pageEdits, shows, fillRects, src, splices, appendBlocks, fallbackDraws, report });
|
|
2068
|
+
if (splices.length || appendBlocks.length) {
|
|
2069
|
+
setPageContents(doc, page, rebuild(src, splices, "", appendBlocks.join("\n")));
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
await drawFallbackTexts(doc, fallbackDraws, report);
|
|
2073
|
+
return report.finish(await doc.save());
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// ../core/src/bake/forms.ts
|
|
2077
|
+
import {
|
|
2078
|
+
PDFArray as PDFArray5,
|
|
2079
|
+
PDFDocument as PDFDocument9,
|
|
2080
|
+
PDFName as PDFName7,
|
|
2081
|
+
PDFRef as PDFRef6,
|
|
2082
|
+
PDFTextField,
|
|
2083
|
+
PDFCheckBox,
|
|
2084
|
+
PDFRadioGroup,
|
|
2085
|
+
PDFDropdown,
|
|
2086
|
+
PDFOptionList,
|
|
2087
|
+
PDFButton
|
|
2088
|
+
} from "pdf-lib";
|
|
2089
|
+
var nz = (s) => s ? s : void 0;
|
|
2090
|
+
function widgetPages(doc) {
|
|
2091
|
+
const map = /* @__PURE__ */ new Map();
|
|
2092
|
+
doc.getPages().forEach((pg, i) => {
|
|
2093
|
+
const annots = pg.node.lookupMaybe(PDFName7.of("Annots"), PDFArray5);
|
|
2094
|
+
if (!annots) return;
|
|
2095
|
+
for (let k = 0; k < annots.size(); k++) {
|
|
2096
|
+
const ref = annots.get(k);
|
|
2097
|
+
map.set(ref instanceof PDFRef6 ? doc.context.lookup(ref) : ref, i + 1);
|
|
2098
|
+
}
|
|
2099
|
+
});
|
|
2100
|
+
return map;
|
|
2101
|
+
}
|
|
2102
|
+
function rectsOf(field, pageBy) {
|
|
2103
|
+
const out = [];
|
|
2104
|
+
for (const w of field.acroField.getWidgets()) {
|
|
2105
|
+
try {
|
|
2106
|
+
const r2 = w.getRectangle();
|
|
2107
|
+
const as = w.dict.get(PDFName7.of("AS"));
|
|
2108
|
+
out.push({
|
|
2109
|
+
page: pageBy.get(w.dict) ?? 1,
|
|
2110
|
+
x: Math.round(r2.x * 10) / 10,
|
|
2111
|
+
y: Math.round(r2.y * 10) / 10,
|
|
2112
|
+
width: Math.round(r2.width * 10) / 10,
|
|
2113
|
+
height: Math.round(r2.height * 10) / 10,
|
|
2114
|
+
export: as instanceof PDFName7 && as.asString() !== "/Off" ? as.asString().slice(1) : void 0
|
|
2115
|
+
});
|
|
2116
|
+
} catch {
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
return out;
|
|
2120
|
+
}
|
|
2121
|
+
async function readFormFields(pdfBytes) {
|
|
2122
|
+
const doc = await PDFDocument9.load(pdfBytes, { ignoreEncryption: true });
|
|
2123
|
+
let form;
|
|
2124
|
+
try {
|
|
2125
|
+
form = doc.getForm();
|
|
2126
|
+
} catch {
|
|
2127
|
+
return [];
|
|
2128
|
+
}
|
|
2129
|
+
const pageBy = widgetPages(doc);
|
|
2130
|
+
const out = [];
|
|
2131
|
+
for (const f of form.getFields()) {
|
|
2132
|
+
const name = f.getName();
|
|
2133
|
+
const readOnly = f.isReadOnly();
|
|
2134
|
+
const rects = rectsOf(f, pageBy);
|
|
2135
|
+
if (f instanceof PDFTextField) out.push({ name, type: "text", value: nz(f.getText() ?? void 0), readOnly, rects });
|
|
2136
|
+
else if (f instanceof PDFCheckBox) out.push({ name, type: "checkbox", value: f.isChecked() ? "On" : void 0, readOnly, rects });
|
|
2137
|
+
else if (f instanceof PDFRadioGroup) out.push({ name, type: "radio", value: nz(f.getSelected() ?? void 0), options: f.getOptions(), readOnly, rects });
|
|
2138
|
+
else if (f instanceof PDFDropdown) out.push({ name, type: "select", value: nz(f.getSelected()[0]), options: f.getOptions(), readOnly, rects });
|
|
2139
|
+
else if (f instanceof PDFOptionList) out.push({ name, type: "list", value: f.getSelected().length ? f.getSelected() : void 0, options: f.getOptions(), readOnly, rects });
|
|
2140
|
+
else if (f instanceof PDFButton) out.push({ name, type: "button", readOnly, rects });
|
|
2141
|
+
else out.push({ name, type: "signature", readOnly, rects });
|
|
2142
|
+
}
|
|
2143
|
+
return out;
|
|
2144
|
+
}
|
|
2145
|
+
var isChecked = (v) => v === true || typeof v === "string" && ["on", "true", "yes", "si", "s\xED", "1", "x", "checked"].includes(v.trim().toLowerCase());
|
|
2146
|
+
async function setFieldValues(pdfBytes, values) {
|
|
2147
|
+
const doc = await PDFDocument9.load(pdfBytes, { ignoreEncryption: true });
|
|
2148
|
+
const applied = [];
|
|
2149
|
+
const warnings = [];
|
|
2150
|
+
let form;
|
|
2151
|
+
try {
|
|
2152
|
+
form = doc.getForm();
|
|
2153
|
+
} catch {
|
|
2154
|
+
return { pdf: pdfBytes, applied, warnings: ["el documento no tiene formulario (AcroForm)"] };
|
|
2155
|
+
}
|
|
2156
|
+
const byName = new Map(form.getFields().map((f) => [f.getName(), f]));
|
|
2157
|
+
for (const [name, value] of Object.entries(values)) {
|
|
2158
|
+
const field = byName.get(name);
|
|
2159
|
+
if (!field) {
|
|
2160
|
+
warnings.push(`campo "${name}" no existe`);
|
|
2161
|
+
continue;
|
|
2162
|
+
}
|
|
2163
|
+
if (field.isReadOnly()) {
|
|
2164
|
+
warnings.push(`campo "${name}" es read-only \u2014 saltado`);
|
|
2165
|
+
continue;
|
|
2166
|
+
}
|
|
2167
|
+
const badOption = (opts) => {
|
|
2168
|
+
const want = Array.isArray(value) ? value.map(String) : [String(value)];
|
|
2169
|
+
const invalid = want.filter((v) => !opts.includes(v));
|
|
2170
|
+
return invalid.length ? `opci\xF3n(es) inv\xE1lida(s) [${invalid.join(", ")}] \u2014 v\xE1lidas: [${opts.join(", ")}]` : null;
|
|
2171
|
+
};
|
|
2172
|
+
try {
|
|
2173
|
+
if (field instanceof PDFTextField) field.setText(String(value));
|
|
2174
|
+
else if (field instanceof PDFCheckBox) {
|
|
2175
|
+
if (isChecked(value)) field.check();
|
|
2176
|
+
else field.uncheck();
|
|
2177
|
+
} else if (field instanceof PDFRadioGroup) {
|
|
2178
|
+
const b = badOption(field.getOptions());
|
|
2179
|
+
if (b) {
|
|
2180
|
+
warnings.push(`${name}: ${b}`);
|
|
2181
|
+
continue;
|
|
2182
|
+
}
|
|
2183
|
+
field.select(String(value));
|
|
2184
|
+
} else if (field instanceof PDFDropdown) {
|
|
2185
|
+
const b = badOption(field.getOptions());
|
|
2186
|
+
if (b) {
|
|
2187
|
+
warnings.push(`${name}: ${b}`);
|
|
2188
|
+
continue;
|
|
2189
|
+
}
|
|
2190
|
+
field.select(String(value));
|
|
2191
|
+
} else if (field instanceof PDFOptionList) {
|
|
2192
|
+
const b = badOption(field.getOptions());
|
|
2193
|
+
if (b) {
|
|
2194
|
+
warnings.push(`${name}: ${b}`);
|
|
2195
|
+
continue;
|
|
2196
|
+
}
|
|
2197
|
+
field.select(Array.isArray(value) ? value : String(value));
|
|
2198
|
+
} else {
|
|
2199
|
+
warnings.push(`campo "${name}" (${field.constructor.name}) no admite valor`);
|
|
2200
|
+
continue;
|
|
2201
|
+
}
|
|
2202
|
+
applied.push(`${name} = ${JSON.stringify(value)}`);
|
|
2203
|
+
} catch (err) {
|
|
2204
|
+
warnings.push(`${name}: ${err instanceof Error ? err.message : "no se pudo completar"}`);
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
if (applied.length) {
|
|
2208
|
+
try {
|
|
2209
|
+
form.updateFieldAppearances();
|
|
2210
|
+
} catch {
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
return { pdf: await doc.save(), applied, warnings };
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2216
|
+
// ../agent/src/graph.ts
|
|
2217
|
+
import { readFile } from "node:fs/promises";
|
|
2218
|
+
|
|
2219
|
+
// ../core/src/tokens.ts
|
|
2220
|
+
function avgCharWidth(run) {
|
|
2221
|
+
const w = run.width / Math.max(1, run.text.length);
|
|
2222
|
+
return Math.max(w, run.fontSize * 0.2);
|
|
2223
|
+
}
|
|
2224
|
+
function classifyGap(gap, prev, next) {
|
|
2225
|
+
if (gap <= 0) return "none";
|
|
2226
|
+
const ref = (avgCharWidth(prev) + avgCharWidth(next)) / 2;
|
|
2227
|
+
if (gap > 2 * ref) return "column";
|
|
2228
|
+
if (gap > 0.5 * ref) return "space";
|
|
2229
|
+
return "none";
|
|
2230
|
+
}
|
|
2231
|
+
function splitSegments(runsSorted) {
|
|
2232
|
+
const segments = [];
|
|
2233
|
+
let current = [];
|
|
2234
|
+
for (const run of runsSorted) {
|
|
2235
|
+
if (current.length === 0) {
|
|
2236
|
+
current.push(run);
|
|
2237
|
+
continue;
|
|
2238
|
+
}
|
|
2239
|
+
const prev = current[current.length - 1];
|
|
2240
|
+
const gap = run.x - (prev.x + prev.width);
|
|
2241
|
+
if (classifyGap(gap, prev, run) === "column") {
|
|
2242
|
+
segments.push(current);
|
|
2243
|
+
current = [run];
|
|
2244
|
+
} else {
|
|
2245
|
+
current.push(run);
|
|
2246
|
+
}
|
|
2247
|
+
}
|
|
2248
|
+
if (current.length) segments.push(current);
|
|
2249
|
+
return segments;
|
|
2250
|
+
}
|
|
2251
|
+
function segmentText(runsSorted) {
|
|
2252
|
+
let text = "";
|
|
2253
|
+
for (let i = 0; i < runsSorted.length; i++) {
|
|
2254
|
+
const run = runsSorted[i];
|
|
2255
|
+
if (i > 0) {
|
|
2256
|
+
const prev = runsSorted[i - 1];
|
|
2257
|
+
const gap = run.x - (prev.x + prev.width);
|
|
2258
|
+
if (classifyGap(gap, prev, run) === "space" && !text.endsWith(" ") && !run.text.startsWith(" ")) {
|
|
2259
|
+
text += " ";
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
text += run.text;
|
|
2263
|
+
}
|
|
2264
|
+
return text;
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
// ../core/src/extractGraph.ts
|
|
2268
|
+
function mergeBlockSegments(lines) {
|
|
2269
|
+
const out = [];
|
|
2270
|
+
let i = 0;
|
|
2271
|
+
while (i < lines.length) {
|
|
2272
|
+
const line = lines[i];
|
|
2273
|
+
if (line.segments.length !== 1) {
|
|
2274
|
+
out.push(...line.segments);
|
|
2275
|
+
i++;
|
|
2276
|
+
continue;
|
|
2277
|
+
}
|
|
2278
|
+
const chain = [line.segments[0]];
|
|
2279
|
+
let j = i + 1;
|
|
2280
|
+
while (j < lines.length) {
|
|
2281
|
+
const prev = chain[chain.length - 1];
|
|
2282
|
+
const next = lines[j].segments.length === 1 ? lines[j].segments[0] : null;
|
|
2283
|
+
if (!next) break;
|
|
2284
|
+
const step = prev.baseline - next.baseline;
|
|
2285
|
+
const lead = prev.fontSize * 1.2;
|
|
2286
|
+
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;
|
|
2287
|
+
if (!match) break;
|
|
2288
|
+
chain.push(next);
|
|
2289
|
+
j++;
|
|
2290
|
+
}
|
|
2291
|
+
if (chain.length === 1) {
|
|
2292
|
+
out.push(chain[0]);
|
|
2293
|
+
} else {
|
|
2294
|
+
const first = chain[0];
|
|
2295
|
+
const last = chain[chain.length - 1];
|
|
2296
|
+
out.push({
|
|
2297
|
+
...first,
|
|
2298
|
+
text: chain.map((s) => s.text).join("\n"),
|
|
2299
|
+
runs: chain.flatMap((s) => s.runs),
|
|
2300
|
+
width: Math.max(...chain.map((s) => s.width)),
|
|
2301
|
+
y: last.y,
|
|
2302
|
+
height: first.y + first.height - last.y
|
|
2303
|
+
});
|
|
2304
|
+
}
|
|
2305
|
+
i = j;
|
|
2306
|
+
}
|
|
2307
|
+
return out;
|
|
2308
|
+
}
|
|
2309
|
+
function extractLinks(annots, page, x0, y0) {
|
|
2310
|
+
const out = [];
|
|
2311
|
+
for (const raw of annots) {
|
|
2312
|
+
if (raw?.subtype !== "Link" || !Array.isArray(raw.rect)) continue;
|
|
2313
|
+
const url = raw.url ?? raw.unsafeUrl;
|
|
2314
|
+
if (!url) continue;
|
|
2315
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
2316
|
+
out.push({
|
|
2317
|
+
id: `p${page}-link${out.length}`,
|
|
2318
|
+
kind: "link",
|
|
2319
|
+
page,
|
|
2320
|
+
url,
|
|
2321
|
+
x: Math.min(ax, bx) - x0,
|
|
2322
|
+
y: Math.min(ay, by) - y0,
|
|
2323
|
+
width: Math.abs(bx - ax),
|
|
2324
|
+
height: Math.abs(by - ay)
|
|
2325
|
+
});
|
|
2326
|
+
}
|
|
2327
|
+
return out;
|
|
2328
|
+
}
|
|
2329
|
+
function extractHighlights(annots, page, x0, y0) {
|
|
2330
|
+
const out = [];
|
|
2331
|
+
const hx = (v) => Math.max(0, Math.min(255, Math.round(v <= 1 ? v * 255 : v))).toString(16).padStart(2, "0");
|
|
2332
|
+
for (const raw of annots) {
|
|
2333
|
+
if (raw?.subtype !== "Highlight" || !Array.isArray(raw.rect)) continue;
|
|
2334
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
2335
|
+
const c = raw.color && raw.color.length >= 3 ? raw.color : null;
|
|
2336
|
+
out.push({
|
|
2337
|
+
id: `p${page}-hl${out.length}`,
|
|
2338
|
+
kind: "highlight",
|
|
2339
|
+
page,
|
|
2340
|
+
x: Math.min(ax, bx) - x0,
|
|
2341
|
+
y: Math.min(ay, by) - y0,
|
|
2342
|
+
width: Math.abs(bx - ax),
|
|
2343
|
+
height: Math.abs(by - ay),
|
|
2344
|
+
color: c ? `#${hx(c[0])}${hx(c[1])}${hx(c[2])}` : "#ffd400"
|
|
2345
|
+
});
|
|
2346
|
+
}
|
|
2347
|
+
return out;
|
|
2348
|
+
}
|
|
2349
|
+
function widgetKindOf(a2) {
|
|
2350
|
+
if (a2.fieldType === "Tx") return "text";
|
|
2351
|
+
if (a2.fieldType === "Sig") return "signature";
|
|
2352
|
+
if (a2.fieldType === "Ch") return a2.combo ? "select" : "list";
|
|
2353
|
+
if (a2.fieldType === "Btn") return a2.checkBox ? "checkbox" : a2.radioButton ? "radio" : "button";
|
|
2354
|
+
return "text";
|
|
2355
|
+
}
|
|
2356
|
+
function extractWidgets(annots, page, x0, y0) {
|
|
2357
|
+
const out = [];
|
|
2358
|
+
for (const raw of annots) {
|
|
2359
|
+
if (raw?.subtype !== "Widget" || !Array.isArray(raw.rect) || raw.hidden) continue;
|
|
2360
|
+
const [ax, ay, bx, by] = raw.rect;
|
|
2361
|
+
const kind = widgetKindOf(raw);
|
|
2362
|
+
out.push({
|
|
2363
|
+
id: `p${page}-w${out.length}`,
|
|
2364
|
+
kind: "widget",
|
|
2365
|
+
page,
|
|
2366
|
+
fieldName: raw.fieldName ?? "",
|
|
2367
|
+
widgetType: kind,
|
|
2368
|
+
readOnly: raw.readOnly === true,
|
|
2369
|
+
options: (kind === "select" || kind === "list") && Array.isArray(raw.options) ? raw.options.map((o) => o.displayValue || o.exportValue || "").filter(Boolean) : void 0,
|
|
2370
|
+
// Valor actual (/V). pdf.js entrega '' para vacío y 'Off' para un
|
|
2371
|
+
// checkbox/radio sin marcar → los normalizamos a "ausente".
|
|
2372
|
+
value: (() => {
|
|
2373
|
+
const v = raw.fieldValue;
|
|
2374
|
+
if (Array.isArray(v)) return v.length ? v : void 0;
|
|
2375
|
+
if (typeof v === "string" && v && v !== "Off") return v;
|
|
2376
|
+
return void 0;
|
|
2377
|
+
})(),
|
|
2378
|
+
x: Math.min(ax, bx) - x0,
|
|
2379
|
+
y: Math.min(ay, by) - y0,
|
|
2380
|
+
width: Math.abs(bx - ax),
|
|
2381
|
+
height: Math.abs(by - ay)
|
|
2382
|
+
});
|
|
2383
|
+
}
|
|
2384
|
+
return out;
|
|
2385
|
+
}
|
|
2386
|
+
var OP_SAVE = 10;
|
|
2387
|
+
var OP_RESTORE = 11;
|
|
2388
|
+
var OP_TRANSFORM = 12;
|
|
2389
|
+
var OP_PAINT_IMAGE = 85;
|
|
2390
|
+
var OP_PAINT_INLINE_IMAGE = 86;
|
|
2391
|
+
var OP_PAINT_IMAGE_MASK = 83;
|
|
2392
|
+
var OP_PAINT_IMAGE_REPEAT = 88;
|
|
2393
|
+
var PAINT_OPS = /* @__PURE__ */ new Set([OP_PAINT_IMAGE, OP_PAINT_INLINE_IMAGE, OP_PAINT_IMAGE_MASK, OP_PAINT_IMAGE_REPEAT]);
|
|
2394
|
+
var mulMat = (m, n) => [
|
|
2395
|
+
m[0] * n[0] + m[1] * n[2],
|
|
2396
|
+
m[0] * n[1] + m[1] * n[3],
|
|
2397
|
+
m[2] * n[0] + m[3] * n[2],
|
|
2398
|
+
m[2] * n[1] + m[3] * n[3],
|
|
2399
|
+
m[4] * n[0] + m[5] * n[2] + n[4],
|
|
2400
|
+
m[4] * n[1] + m[5] * n[3] + n[5]
|
|
2401
|
+
];
|
|
2402
|
+
function extractImages(fnArray, argsArray, page, x0, y0) {
|
|
2403
|
+
const images = [];
|
|
2404
|
+
let ctm = [1, 0, 0, 1, 0, 0];
|
|
2405
|
+
const stack = [];
|
|
2406
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2407
|
+
for (let i = 0; i < fnArray.length; i++) {
|
|
2408
|
+
const fn = fnArray[i];
|
|
2409
|
+
if (fn === OP_SAVE) stack.push(ctm);
|
|
2410
|
+
else if (fn === OP_RESTORE) ctm = stack.pop() ?? [1, 0, 0, 1, 0, 0];
|
|
2411
|
+
else if (fn === OP_TRANSFORM) {
|
|
2412
|
+
const a2 = argsArray[i];
|
|
2413
|
+
ctm = mulMat([a2[0], a2[1], a2[2], a2[3], a2[4], a2[5]], ctm);
|
|
2414
|
+
} else if (PAINT_OPS.has(fn)) {
|
|
2415
|
+
const [a2, b, c, d, e, f] = ctm;
|
|
2416
|
+
const xs = [e, a2 + e, c + e, a2 + c + e];
|
|
2417
|
+
const ys = [f, b + f, d + f, b + d + f];
|
|
2418
|
+
const minX = Math.min(...xs);
|
|
2419
|
+
const minY = Math.min(...ys);
|
|
2420
|
+
const arg0 = argsArray[i][0];
|
|
2421
|
+
const objId = (fn === OP_PAINT_IMAGE || fn === OP_PAINT_IMAGE_REPEAT) && typeof arg0 === "string" ? arg0 : void 0;
|
|
2422
|
+
let id;
|
|
2423
|
+
if (objId) {
|
|
2424
|
+
const n = seen.get(objId) ?? 0;
|
|
2425
|
+
seen.set(objId, n + 1);
|
|
2426
|
+
id = n === 0 ? `p${page}-${objId}` : `p${page}-${objId}#${n}`;
|
|
2427
|
+
} else {
|
|
2428
|
+
id = `p${page}-img${images.length}`;
|
|
2429
|
+
}
|
|
2430
|
+
images.push({
|
|
2431
|
+
id,
|
|
2432
|
+
kind: "image",
|
|
2433
|
+
page,
|
|
2434
|
+
x: minX - x0,
|
|
2435
|
+
y: minY - y0,
|
|
2436
|
+
width: Math.max(...xs) - minX,
|
|
2437
|
+
height: Math.max(...ys) - minY,
|
|
2438
|
+
rotated: Math.abs(b) > 0.01 || Math.abs(c) > 0.01,
|
|
2439
|
+
objId
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
return images;
|
|
2444
|
+
}
|
|
2445
|
+
function styleFromName(name) {
|
|
2446
|
+
const n = name.toLowerCase();
|
|
2447
|
+
const bold = /bold|black|heavy|semibold|demibold|extrabold|-bd\b|,bd\b/.test(n);
|
|
2448
|
+
const italic = /italic|oblique/.test(n);
|
|
2449
|
+
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";
|
|
2450
|
+
return { bold, italic, bucket };
|
|
2451
|
+
}
|
|
2452
|
+
function fontInfoFor(page, loadedName, cache) {
|
|
2453
|
+
const hit = cache.get(loadedName);
|
|
2454
|
+
if (hit) return hit;
|
|
2455
|
+
let raw = null;
|
|
2456
|
+
try {
|
|
2457
|
+
raw = page.commonObjs.get(loadedName);
|
|
2458
|
+
} catch {
|
|
2459
|
+
raw = null;
|
|
2460
|
+
}
|
|
2461
|
+
const ps = raw?.name || raw?.fallbackName || loadedName;
|
|
2462
|
+
const style = styleFromName(ps);
|
|
2463
|
+
const info = {
|
|
2464
|
+
loadedName,
|
|
2465
|
+
postScriptName: ps,
|
|
2466
|
+
bold: raw?.bold ?? style.bold,
|
|
2467
|
+
italic: raw?.italic ?? style.italic,
|
|
2468
|
+
bucket: style.bucket,
|
|
2469
|
+
ascent: typeof raw?.ascent === "number" && raw.ascent > 0 ? raw.ascent : 0.8,
|
|
2470
|
+
descent: typeof raw?.descent === "number" && raw.descent < 0 ? raw.descent : -0.2,
|
|
2471
|
+
embedded: raw != null && raw.missingFile !== true
|
|
2472
|
+
};
|
|
2473
|
+
cache.set(loadedName, info);
|
|
2474
|
+
return info;
|
|
2475
|
+
}
|
|
2476
|
+
async function extractPageGraph(page) {
|
|
2477
|
+
const opList = await page.getOperatorList();
|
|
2478
|
+
const tc = await page.getTextContent({ disableNormalization: true });
|
|
2479
|
+
const annots = await page.getAnnotations().catch(() => []);
|
|
2480
|
+
const [x0, y0, x1, y1] = page.view;
|
|
2481
|
+
const fontCache = /* @__PURE__ */ new Map();
|
|
2482
|
+
const runs = [];
|
|
2483
|
+
let i = 0;
|
|
2484
|
+
for (const item of tc.items) {
|
|
2485
|
+
if (typeof item.str !== "string" || item.str.trim().length === 0) continue;
|
|
2486
|
+
const [a2, b, c, d, e, f] = item.transform;
|
|
2487
|
+
runs.push({
|
|
2488
|
+
id: `p${page.pageNumber}-r${i++}`,
|
|
2489
|
+
kind: "text",
|
|
2490
|
+
page: page.pageNumber,
|
|
2491
|
+
text: item.str,
|
|
2492
|
+
x: e - x0,
|
|
2493
|
+
baseline: f - y0,
|
|
2494
|
+
width: item.width,
|
|
2495
|
+
fontSize: Math.hypot(c, d),
|
|
2496
|
+
angle: Math.atan2(b, a2),
|
|
2497
|
+
font: fontInfoFor(page, item.fontName, fontCache)
|
|
2498
|
+
});
|
|
2499
|
+
}
|
|
2500
|
+
const lines = groupIntoLines(runs, page.pageNumber);
|
|
2501
|
+
return {
|
|
2502
|
+
page: page.pageNumber,
|
|
2503
|
+
width: x1 - x0,
|
|
2504
|
+
height: y1 - y0,
|
|
2505
|
+
runs,
|
|
2506
|
+
lines,
|
|
2507
|
+
segments: mergeBlockSegments(lines),
|
|
2508
|
+
images: extractImages(opList.fnArray, opList.argsArray, page.pageNumber, x0, y0),
|
|
2509
|
+
widgets: extractWidgets(annots, page.pageNumber, x0, y0),
|
|
2510
|
+
links: extractLinks(annots, page.pageNumber, x0, y0),
|
|
2511
|
+
highlights: extractHighlights(annots, page.pageNumber, x0, y0)
|
|
2512
|
+
};
|
|
2513
|
+
}
|
|
2514
|
+
function groupIntoLines(runs, page) {
|
|
2515
|
+
const horizontal = runs.filter((r2) => Math.abs(r2.angle) < 0.01);
|
|
2516
|
+
const rotated = runs.filter((r2) => Math.abs(r2.angle) >= 0.01);
|
|
2517
|
+
const sorted = [...horizontal].sort((p, q) => q.baseline - p.baseline || p.x - q.x);
|
|
2518
|
+
const groups = [];
|
|
2519
|
+
for (const r2 of sorted) {
|
|
2520
|
+
const current = groups[groups.length - 1];
|
|
2521
|
+
const tol = Math.max(1, r2.fontSize * 0.35);
|
|
2522
|
+
if (current && Math.abs(current[0].baseline - r2.baseline) <= tol) current.push(r2);
|
|
2523
|
+
else groups.push([r2]);
|
|
2524
|
+
}
|
|
2525
|
+
for (const r2 of rotated) groups.push([r2]);
|
|
2526
|
+
return groups.map((g, i) => lineFromRuns(g, page, i));
|
|
2527
|
+
}
|
|
2528
|
+
function bboxOf(runs) {
|
|
2529
|
+
const x = runs[0].x;
|
|
2530
|
+
const right = Math.max(...runs.map((r2) => r2.x + r2.width));
|
|
2531
|
+
const baseline = runs[0].baseline;
|
|
2532
|
+
const fontSize = Math.max(...runs.map((r2) => r2.fontSize));
|
|
2533
|
+
const ascent = Math.max(...runs.map((r2) => r2.font.ascent * r2.fontSize));
|
|
2534
|
+
const descent = Math.min(...runs.map((r2) => r2.font.descent * r2.fontSize));
|
|
2535
|
+
return { x, baseline, width: right - x, y: baseline + descent, height: ascent - descent, fontSize };
|
|
2536
|
+
}
|
|
2537
|
+
function lineFromRuns(group, page, index) {
|
|
2538
|
+
const runs = [...group].sort((a2, b) => a2.x - b.x);
|
|
2539
|
+
void index;
|
|
2540
|
+
const lineId = `p${page}-y${Math.round(runs[0].baseline)}`;
|
|
2541
|
+
const segments = splitSegments(runs).map((segRuns) => ({
|
|
2542
|
+
id: `${lineId}-x${Math.round(segRuns[0].x)}`,
|
|
2543
|
+
kind: "segment",
|
|
2544
|
+
page,
|
|
2545
|
+
text: segmentText(segRuns),
|
|
2546
|
+
runs: segRuns,
|
|
2547
|
+
...bboxOf(segRuns)
|
|
2548
|
+
}));
|
|
2549
|
+
return {
|
|
2550
|
+
id: lineId,
|
|
2551
|
+
kind: "line",
|
|
2552
|
+
page,
|
|
2553
|
+
text: segments.map((s) => s.text).join(" "),
|
|
2554
|
+
segments,
|
|
2555
|
+
...bboxOf(runs)
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
// ../core/src/edits.ts
|
|
2560
|
+
var OVERRIDE_KEYS = ["fontSize", "font", "x", "baseline", "remove", "charSpacing", "hScale", "color", "align"];
|
|
2561
|
+
function originalStyledRuns(seg) {
|
|
2562
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
2563
|
+
for (const r2 of seg.runs) {
|
|
2564
|
+
const key = Math.round(r2.baseline * 10) / 10;
|
|
2565
|
+
byLine.set(key, [...byLine.get(key) ?? [], r2]);
|
|
2566
|
+
}
|
|
2567
|
+
const lineKeys = [...byLine.keys()].sort((a2, b) => b - a2);
|
|
2568
|
+
const out = [];
|
|
2569
|
+
for (let li = 0; li < lineKeys.length; li++) {
|
|
2570
|
+
const runs = [...byLine.get(lineKeys[li]) ?? []].sort((a2, b) => a2.x - b.x);
|
|
2571
|
+
if (li > 0 && out.length) out[out.length - 1].text += "\n";
|
|
2572
|
+
for (let i = 0; i < runs.length; i++) {
|
|
2573
|
+
const r2 = runs[i];
|
|
2574
|
+
let space = "";
|
|
2575
|
+
if (i > 0) {
|
|
2576
|
+
const prev = runs[i - 1];
|
|
2577
|
+
const gap = r2.x - (prev.x + prev.width);
|
|
2578
|
+
if (classifyGap(gap, prev, r2) === "space" && !out[out.length - 1]?.text.endsWith(" ") && !r2.text.startsWith(" ")) {
|
|
2579
|
+
space = " ";
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
const last = out[out.length - 1];
|
|
2583
|
+
if (last && last.bold === r2.font.bold && last.italic === r2.font.italic && last.color === r2.color) {
|
|
2584
|
+
last.text += space + r2.text;
|
|
2585
|
+
} else {
|
|
2586
|
+
if (last && space) last.text += space;
|
|
2587
|
+
out.push({ text: r2.text, bold: r2.font.bold, italic: r2.font.italic, color: r2.color, dx: r2.x - seg.x });
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return out;
|
|
2592
|
+
}
|
|
2593
|
+
function styledRunsEqual(a2, b) {
|
|
2594
|
+
if (a2.length !== b.length) return false;
|
|
2595
|
+
for (let i = 0; i < a2.length; i++) {
|
|
2596
|
+
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;
|
|
2597
|
+
}
|
|
2598
|
+
return true;
|
|
2599
|
+
}
|
|
2600
|
+
var styledText = (runs) => runs.map((r2) => r2.text).join("");
|
|
2601
|
+
function applyTextDiff(runs, newText) {
|
|
2602
|
+
const oldText = styledText(runs);
|
|
2603
|
+
if (oldText === newText) return runs;
|
|
2604
|
+
if (!runs.length) return newText ? [{ text: newText, bold: false, italic: false, dx: 0 }] : runs;
|
|
2605
|
+
let p = 0;
|
|
2606
|
+
while (p < oldText.length && p < newText.length && oldText[p] === newText[p]) p++;
|
|
2607
|
+
let s = 0;
|
|
2608
|
+
while (s < oldText.length - p && s < newText.length - p && oldText[oldText.length - 1 - s] === newText[newText.length - 1 - s]) s++;
|
|
2609
|
+
const inserted = newText.slice(p, newText.length - s);
|
|
2610
|
+
let styleSrc = runs[runs.length - 1];
|
|
2611
|
+
let pos = 0;
|
|
2612
|
+
for (const r2 of runs) {
|
|
2613
|
+
if (p < pos + r2.text.length || p === pos + r2.text.length && r2 === runs[runs.length - 1]) {
|
|
2614
|
+
styleSrc = r2;
|
|
2615
|
+
break;
|
|
2616
|
+
}
|
|
2617
|
+
pos += r2.text.length;
|
|
2618
|
+
}
|
|
2619
|
+
const out = [];
|
|
2620
|
+
let cursor = 0;
|
|
2621
|
+
const pushPiece = (piece) => {
|
|
2622
|
+
const last = out[out.length - 1];
|
|
2623
|
+
if (last && sameStyle(last, piece)) last.text += piece.text;
|
|
2624
|
+
else out.push({ ...piece });
|
|
2625
|
+
};
|
|
2626
|
+
for (const r2 of runs) {
|
|
2627
|
+
if (cursor >= p) break;
|
|
2628
|
+
const take = Math.min(r2.text.length, p - cursor);
|
|
2629
|
+
if (take > 0) pushPiece({ ...r2, text: r2.text.slice(0, take) });
|
|
2630
|
+
cursor += r2.text.length;
|
|
2631
|
+
}
|
|
2632
|
+
if (inserted) pushPiece({ ...styleSrc, text: inserted });
|
|
2633
|
+
const sufStart = oldText.length - s;
|
|
2634
|
+
cursor = 0;
|
|
2635
|
+
for (const r2 of runs) {
|
|
2636
|
+
const end = cursor + r2.text.length;
|
|
2637
|
+
if (end > sufStart) {
|
|
2638
|
+
const from = Math.max(0, sufStart - cursor);
|
|
2639
|
+
pushPiece({ ...r2, text: r2.text.slice(from) });
|
|
2640
|
+
}
|
|
2641
|
+
cursor = end;
|
|
2642
|
+
}
|
|
2643
|
+
return out.filter((r2) => r2.text.length > 0);
|
|
2644
|
+
}
|
|
2645
|
+
var sameStyle = (a2, b) => a2.bold === b.bold && a2.italic === b.italic && a2.color === b.color && !!a2.underline === !!b.underline;
|
|
2646
|
+
function segmentOriginal(seg) {
|
|
2647
|
+
const dom = seg.runs.reduce((a2, b) => b.width > a2.width ? b : a2);
|
|
2648
|
+
const baselines = [...new Set(seg.runs.map((r2) => Math.round(r2.baseline * 10) / 10))].sort((a2, b) => b - a2);
|
|
2649
|
+
return {
|
|
2650
|
+
text: seg.text,
|
|
2651
|
+
x: seg.x,
|
|
2652
|
+
baseline: seg.baseline,
|
|
2653
|
+
width: seg.width,
|
|
2654
|
+
fontSize: seg.fontSize,
|
|
2655
|
+
bucket: dom.font.bucket,
|
|
2656
|
+
bold: dom.font.bold,
|
|
2657
|
+
italic: dom.font.italic,
|
|
2658
|
+
runs: [...seg.runs].sort((a2, b) => a2.x - b.x).map((r2) => ({ x: r2.x, bold: r2.font.bold, italic: r2.font.italic })),
|
|
2659
|
+
...baselines.length > 1 ? { baselines } : {}
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
function mergeSegmentEdit(seg, prev, patch) {
|
|
2663
|
+
const next = prev ? { ...prev } : {
|
|
2664
|
+
segmentId: seg.id,
|
|
2665
|
+
page: seg.page,
|
|
2666
|
+
text: seg.text,
|
|
2667
|
+
original: segmentOriginal(seg)
|
|
2668
|
+
};
|
|
2669
|
+
if (patch.runs !== void 0) {
|
|
2670
|
+
if (patch.runs === null) delete next.runs;
|
|
2671
|
+
else next.runs = patch.runs;
|
|
2672
|
+
}
|
|
2673
|
+
if (patch.text !== void 0) next.text = patch.text;
|
|
2674
|
+
if (next.runs) {
|
|
2675
|
+
if (styledRunsEqual(next.runs, originalStyledRuns(seg))) delete next.runs;
|
|
2676
|
+
else next.text = styledText(next.runs);
|
|
2677
|
+
}
|
|
2678
|
+
for (const key of OVERRIDE_KEYS) {
|
|
2679
|
+
const value = patch[key];
|
|
2680
|
+
if (value === void 0) continue;
|
|
2681
|
+
if (value === null) delete next[key];
|
|
2682
|
+
else next[key] = value;
|
|
2683
|
+
}
|
|
2684
|
+
const noop = next.text === seg.text && next.runs === void 0 && OVERRIDE_KEYS.every((k) => next[k] === void 0);
|
|
2685
|
+
return noop ? null : next;
|
|
2686
|
+
}
|
|
2687
|
+
var IMAGE_KEYS = ["x", "y", "width", "height", "remove", "zOrder"];
|
|
2688
|
+
function mergeImageEdit(img, prev, patch) {
|
|
2689
|
+
const next = prev ? { ...prev } : {
|
|
2690
|
+
imageId: img.id,
|
|
2691
|
+
page: img.page,
|
|
2692
|
+
original: { x: img.x, y: img.y, width: img.width, height: img.height }
|
|
2693
|
+
};
|
|
2694
|
+
for (const key of IMAGE_KEYS) {
|
|
2695
|
+
const value = patch[key];
|
|
2696
|
+
if (value === void 0) continue;
|
|
2697
|
+
if (value === null) delete next[key];
|
|
2698
|
+
else next[key] = value;
|
|
2699
|
+
}
|
|
2700
|
+
return IMAGE_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
2701
|
+
}
|
|
2702
|
+
function promoteMovedImages(edits) {
|
|
2703
|
+
return edits.map((e) => !e.remove && !e.zOrder && (e.x != null || e.y != null || e.width != null || e.height != null) ? { ...e, zOrder: "front" } : e);
|
|
2704
|
+
}
|
|
2705
|
+
var WIDGET_KEYS = ["x", "y", "width", "height", "remove"];
|
|
2706
|
+
function mergeWidgetEdit(w, prev, patch) {
|
|
2707
|
+
const next = prev ? { ...prev } : {
|
|
2708
|
+
widgetId: w.id,
|
|
2709
|
+
page: w.page,
|
|
2710
|
+
original: { fieldName: w.fieldName, x: w.x, y: w.y, width: w.width, height: w.height }
|
|
2711
|
+
};
|
|
2712
|
+
for (const key of WIDGET_KEYS) {
|
|
2713
|
+
const value = patch[key];
|
|
2714
|
+
if (value === void 0) continue;
|
|
2715
|
+
if (value === null) delete next[key];
|
|
2716
|
+
else next[key] = value;
|
|
2717
|
+
}
|
|
2718
|
+
return WIDGET_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
2719
|
+
}
|
|
2720
|
+
var HIGHLIGHT_KEYS = ["x", "y", "width", "height", "color", "remove"];
|
|
2721
|
+
function mergeHighlightEdit(h, prev, patch) {
|
|
2722
|
+
const next = prev ? { ...prev } : {
|
|
2723
|
+
highlightId: h.id,
|
|
2724
|
+
page: h.page,
|
|
2725
|
+
original: { x: h.x, y: h.y, width: h.width, height: h.height, color: h.color }
|
|
2726
|
+
};
|
|
2727
|
+
for (const key of HIGHLIGHT_KEYS) {
|
|
2728
|
+
const value = patch[key];
|
|
2729
|
+
if (value === void 0) continue;
|
|
2730
|
+
if (value === null) delete next[key];
|
|
2731
|
+
else next[key] = value;
|
|
2732
|
+
}
|
|
2733
|
+
return HIGHLIGHT_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
2734
|
+
}
|
|
2735
|
+
var LINK_KEYS = ["x", "y", "width", "height", "remove"];
|
|
2736
|
+
function mergeLinkEdit(l, prev, patch) {
|
|
2737
|
+
const next = prev ? { ...prev } : {
|
|
2738
|
+
linkId: l.id,
|
|
2739
|
+
page: l.page,
|
|
2740
|
+
original: { url: l.url, x: l.x, y: l.y, width: l.width, height: l.height }
|
|
2741
|
+
};
|
|
2742
|
+
for (const key of LINK_KEYS) {
|
|
2743
|
+
const value = patch[key];
|
|
2744
|
+
if (value === void 0) continue;
|
|
2745
|
+
if (value === null) delete next[key];
|
|
2746
|
+
else next[key] = value;
|
|
2747
|
+
}
|
|
2748
|
+
return LINK_KEYS.every((k) => next[k] === void 0) ? null : next;
|
|
2749
|
+
}
|
|
2750
|
+
function effectiveGeometry(seg, edit) {
|
|
2751
|
+
const fontSize = edit?.fontSize ?? seg.fontSize;
|
|
2752
|
+
const ratio = fontSize / seg.fontSize;
|
|
2753
|
+
const x = edit?.x ?? seg.x;
|
|
2754
|
+
const baseline = edit?.baseline ?? seg.baseline;
|
|
2755
|
+
return {
|
|
2756
|
+
x,
|
|
2757
|
+
baseline,
|
|
2758
|
+
fontSize,
|
|
2759
|
+
y: baseline + (seg.y - seg.baseline) * ratio,
|
|
2760
|
+
height: seg.height * ratio,
|
|
2761
|
+
width: seg.width,
|
|
2762
|
+
moved: edit?.x !== void 0 || edit?.baseline !== void 0
|
|
2763
|
+
};
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
// ../agent/src/graph.ts
|
|
2767
|
+
async function graphFromBytes(bytes, path2 = "(memoria)") {
|
|
2768
|
+
const { getDocument } = await import("pdfjs-dist/legacy/build/pdf.mjs");
|
|
2769
|
+
const doc = await getDocument({ data: bytes.slice(), verbosity: 0 }).promise;
|
|
2770
|
+
const pages = [];
|
|
2771
|
+
for (let n = 1; n <= doc.numPages; n++) {
|
|
2772
|
+
const page = await doc.getPage(n);
|
|
2773
|
+
pages.push(await extractPageGraph(page));
|
|
2774
|
+
}
|
|
2775
|
+
await doc.destroy();
|
|
2776
|
+
return { path: path2, bytes, pages };
|
|
2777
|
+
}
|
|
2778
|
+
async function loadDoc(path2) {
|
|
2779
|
+
return graphFromBytes(new Uint8Array(await readFile(path2)), path2);
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// ../agent/src/session.ts
|
|
2783
|
+
import { readFile as readFile2, writeFile } from "node:fs/promises";
|
|
2784
|
+
var MIME = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg" };
|
|
2785
|
+
var EditSession = class {
|
|
2786
|
+
constructor(doc) {
|
|
2787
|
+
this.doc = doc;
|
|
2788
|
+
}
|
|
2789
|
+
edits = /* @__PURE__ */ new Map();
|
|
2790
|
+
imageEdits = /* @__PURE__ */ new Map();
|
|
2791
|
+
widgetEdits = /* @__PURE__ */ new Map();
|
|
2792
|
+
highlightEdits = /* @__PURE__ */ new Map();
|
|
2793
|
+
linkEdits = /* @__PURE__ */ new Map();
|
|
2794
|
+
creates = [];
|
|
2795
|
+
/** Valores de formulario a COMPLETAR, por nombre de campo (setFieldValues). */
|
|
2796
|
+
fills = /* @__PURE__ */ new Map();
|
|
2797
|
+
seg(id) {
|
|
2798
|
+
for (const p of this.doc.pages) {
|
|
2799
|
+
const s = p.segments.find((x) => x.id === id);
|
|
2800
|
+
if (s) return s;
|
|
2801
|
+
}
|
|
2802
|
+
return void 0;
|
|
2803
|
+
}
|
|
2804
|
+
img(id) {
|
|
2805
|
+
for (const p of this.doc.pages) {
|
|
2806
|
+
const i = p.images.find((x) => x.id === id);
|
|
2807
|
+
if (i) return i;
|
|
2808
|
+
}
|
|
2809
|
+
return void 0;
|
|
2810
|
+
}
|
|
2811
|
+
widget(id) {
|
|
2812
|
+
for (const p of this.doc.pages) {
|
|
2813
|
+
const w = p.widgets.find((x) => x.id === id);
|
|
2814
|
+
if (w) return w;
|
|
2815
|
+
}
|
|
2816
|
+
return void 0;
|
|
2817
|
+
}
|
|
2818
|
+
hlNode(id) {
|
|
2819
|
+
for (const p of this.doc.pages) {
|
|
2820
|
+
const h = p.highlights.find((x) => x.id === id);
|
|
2821
|
+
if (h) return h;
|
|
2822
|
+
}
|
|
2823
|
+
return void 0;
|
|
2824
|
+
}
|
|
2825
|
+
linkNode(id) {
|
|
2826
|
+
for (const p of this.doc.pages) {
|
|
2827
|
+
const l = p.links.find((x) => x.id === id);
|
|
2828
|
+
if (l) return l;
|
|
2829
|
+
}
|
|
2830
|
+
return void 0;
|
|
2831
|
+
}
|
|
2832
|
+
putSeg(seg, patch) {
|
|
2833
|
+
const m = mergeSegmentEdit(seg, this.edits.get(seg.id) ?? null, patch);
|
|
2834
|
+
if (m) this.edits.set(seg.id, m);
|
|
2835
|
+
else this.edits.delete(seg.id);
|
|
2836
|
+
}
|
|
2837
|
+
putImg(img, patch) {
|
|
2838
|
+
const m = mergeImageEdit(img, this.imageEdits.get(img.id) ?? null, patch);
|
|
2839
|
+
if (m) this.imageEdits.set(img.id, m);
|
|
2840
|
+
else this.imageEdits.delete(img.id);
|
|
2841
|
+
}
|
|
2842
|
+
// ── EDICIONES de texto (nodos existentes) ──
|
|
2843
|
+
editText(id, text) {
|
|
2844
|
+
const s = this.seg(id);
|
|
2845
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
2846
|
+
this.putSeg(s, { runs: applyTextDiff(originalStyledRuns(s), text) });
|
|
2847
|
+
return `\u2713 Texto ${id}: ${JSON.stringify(s.text)} \u2192 ${JSON.stringify(text)}`;
|
|
2848
|
+
}
|
|
2849
|
+
moveText(id, x, y) {
|
|
2850
|
+
const s = this.seg(id);
|
|
2851
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
2852
|
+
this.putSeg(s, { x, baseline: y });
|
|
2853
|
+
return `\u2713 Texto ${id} movido a @(${x ?? Math.round(s.x)},${y ?? Math.round(s.baseline)})`;
|
|
2854
|
+
}
|
|
2855
|
+
colorText(id, color) {
|
|
2856
|
+
const s = this.seg(id);
|
|
2857
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
2858
|
+
this.putSeg(s, { color });
|
|
2859
|
+
return `\u2713 Texto ${id} \u2192 color ${color}`;
|
|
2860
|
+
}
|
|
2861
|
+
resizeText(id, fontSize) {
|
|
2862
|
+
const s = this.seg(id);
|
|
2863
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
2864
|
+
this.putSeg(s, { fontSize });
|
|
2865
|
+
return `\u2713 Texto ${id} \u2192 ${fontSize}pt`;
|
|
2866
|
+
}
|
|
2867
|
+
deleteText(id) {
|
|
2868
|
+
const s = this.seg(id);
|
|
2869
|
+
if (!s) return `\u26A0\uFE0F No existe el nodo de texto "${id}".`;
|
|
2870
|
+
this.putSeg(s, { remove: true });
|
|
2871
|
+
return `\u2713 Texto ${id} eliminado`;
|
|
2872
|
+
}
|
|
2873
|
+
// ── EDICIONES de imagen (nodos existentes) ──
|
|
2874
|
+
moveImage(id, patch) {
|
|
2875
|
+
const im = this.img(id);
|
|
2876
|
+
if (!im) return `\u26A0\uFE0F No existe la imagen "${id}".`;
|
|
2877
|
+
this.putImg(im, patch);
|
|
2878
|
+
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)}`;
|
|
2879
|
+
}
|
|
2880
|
+
deleteImage(id) {
|
|
2881
|
+
const im = this.img(id);
|
|
2882
|
+
if (!im) return `\u26A0\uFE0F No existe la imagen "${id}".`;
|
|
2883
|
+
this.putImg(im, { remove: true });
|
|
2884
|
+
return `\u2713 Imagen ${id} eliminada`;
|
|
2885
|
+
}
|
|
2886
|
+
// ── EDICIONES de campo / highlight / link existentes ──
|
|
2887
|
+
moveField(id, x, y) {
|
|
2888
|
+
const w = this.widget(id);
|
|
2889
|
+
if (!w) return `\u26A0\uFE0F No existe el campo "${id}".`;
|
|
2890
|
+
const m = mergeWidgetEdit(w, this.widgetEdits.get(id) ?? null, { x, y });
|
|
2891
|
+
if (m) this.widgetEdits.set(id, m);
|
|
2892
|
+
else this.widgetEdits.delete(id);
|
|
2893
|
+
return `\u2713 Campo ${id} movido a @(${x ?? Math.round(w.x)},${y ?? Math.round(w.y)})`;
|
|
2894
|
+
}
|
|
2895
|
+
deleteField(id) {
|
|
2896
|
+
const w = this.widget(id);
|
|
2897
|
+
if (!w) return `\u26A0\uFE0F No existe el campo "${id}".`;
|
|
2898
|
+
const m = mergeWidgetEdit(w, this.widgetEdits.get(id) ?? null, { remove: true });
|
|
2899
|
+
if (m) this.widgetEdits.set(id, m);
|
|
2900
|
+
return `\u2713 Campo ${id} eliminado`;
|
|
2901
|
+
}
|
|
2902
|
+
recolorHighlight(id, color) {
|
|
2903
|
+
const h = this.hlNode(id);
|
|
2904
|
+
if (!h) return `\u26A0\uFE0F No existe el resaltado "${id}".`;
|
|
2905
|
+
const m = mergeHighlightEdit(h, this.highlightEdits.get(id) ?? null, { color });
|
|
2906
|
+
if (m) this.highlightEdits.set(id, m);
|
|
2907
|
+
else this.highlightEdits.delete(id);
|
|
2908
|
+
return `\u2713 Resaltado ${id} \u2192 color ${color}`;
|
|
2909
|
+
}
|
|
2910
|
+
deleteHighlight(id) {
|
|
2911
|
+
const h = this.hlNode(id);
|
|
2912
|
+
if (!h) return `\u26A0\uFE0F No existe el resaltado "${id}".`;
|
|
2913
|
+
const m = mergeHighlightEdit(h, this.highlightEdits.get(id) ?? null, { remove: true });
|
|
2914
|
+
if (m) this.highlightEdits.set(id, m);
|
|
2915
|
+
return `\u2713 Resaltado ${id} eliminado`;
|
|
2916
|
+
}
|
|
2917
|
+
deleteLink(id) {
|
|
2918
|
+
const l = this.linkNode(id);
|
|
2919
|
+
if (!l) return `\u26A0\uFE0F No existe el link "${id}".`;
|
|
2920
|
+
const m = mergeLinkEdit(l, this.linkEdits.get(id) ?? null, { remove: true });
|
|
2921
|
+
if (m) this.linkEdits.set(id, m);
|
|
2922
|
+
return `\u2713 Link ${id} eliminado`;
|
|
2923
|
+
}
|
|
2924
|
+
// ── CREACIONES (nodos nuevos — cola aplicada post-bake) ──
|
|
2925
|
+
highlightText(segId, color) {
|
|
2926
|
+
if (!this.seg(segId)) return `\u26A0\uFE0F No existe el nodo de texto "${segId}".`;
|
|
2927
|
+
this.creates.push({ kind: "highlightSeg", segId, color });
|
|
2928
|
+
return `\u2713 Resaltado sobre ${segId}${color ? ` (${color})` : ""}`;
|
|
2929
|
+
}
|
|
2930
|
+
linkText(segId, url) {
|
|
2931
|
+
if (!this.seg(segId)) return `\u26A0\uFE0F No existe el nodo de texto "${segId}".`;
|
|
2932
|
+
this.creates.push({ kind: "linkSeg", segId, url });
|
|
2933
|
+
return `\u2713 Link sobre ${segId} \u2192 ${url}`;
|
|
2934
|
+
}
|
|
2935
|
+
addTextNode(op) {
|
|
2936
|
+
this.creates.push({ kind: "text", ...op });
|
|
2937
|
+
return `\u2713 Texto nuevo en p${op.page} @(${op.x},${op.y}): ${JSON.stringify(op.text.slice(0, 40))}`;
|
|
2938
|
+
}
|
|
2939
|
+
insertImageFile(page, x, y, path2, maxWidth) {
|
|
2940
|
+
this.creates.push({ kind: "image", page, x, y, path: path2, maxWidth });
|
|
2941
|
+
return `\u2713 Imagen "${path2}" en p${page} @(${x},${y})`;
|
|
2942
|
+
}
|
|
2943
|
+
watermark(text, color, opacity) {
|
|
2944
|
+
this.creates.push({ kind: "watermark", text, color, opacity });
|
|
2945
|
+
return `\u2713 Marca de agua: ${JSON.stringify(text)}`;
|
|
2946
|
+
}
|
|
2947
|
+
headerFooter(op) {
|
|
2948
|
+
this.creates.push({ kind: "headerFooter", ...op });
|
|
2949
|
+
return `\u2713 Encabezado/pie aplicado`;
|
|
2950
|
+
}
|
|
2951
|
+
addField(fieldType, page, x, y, width, height, name) {
|
|
2952
|
+
this.creates.push({ kind: "field", fieldType, page, x, y, width, height, name });
|
|
2953
|
+
return `\u2713 Campo ${fieldType} en p${page} @(${x},${y})`;
|
|
2954
|
+
}
|
|
2955
|
+
/** COMPLETA un campo de formulario por su NOMBRE o por su id de widget
|
|
2956
|
+
* ([[p1-w3]] de la vista de Lectura — se resuelve al fieldName). Valor:
|
|
2957
|
+
* texto para text/select/radio, true/false para checkbox. Determinístico. */
|
|
2958
|
+
fillField(nameOrId, value) {
|
|
2959
|
+
let fieldName = nameOrId;
|
|
2960
|
+
if (!this.doc.pages.some((p) => p.widgets.some((w) => w.fieldName === fieldName))) {
|
|
2961
|
+
const byId = this.widget(nameOrId.replace(/^\[\[|\]\]$/g, ""));
|
|
2962
|
+
if (!byId) return `\u26A0\uFE0F No existe un campo llamado "${nameOrId}" (ni como fieldName ni como id).`;
|
|
2963
|
+
fieldName = byId.fieldName;
|
|
2964
|
+
}
|
|
2965
|
+
this.fills.set(fieldName, value);
|
|
2966
|
+
return `\u2713 Campo "${fieldName}" \u2190 ${JSON.stringify(value)}`;
|
|
2967
|
+
}
|
|
2968
|
+
/** COMPLETA VARIOS campos de una (por fieldName o id de widget) — UNA sola tool
|
|
2969
|
+
* call en vez de N idas y vueltas con el modelo (clave para forms grandes). */
|
|
2970
|
+
fillFields(entries) {
|
|
2971
|
+
const lines = entries.map((e) => this.fillField(e.name, e.value));
|
|
2972
|
+
return lines.join("\n");
|
|
2973
|
+
}
|
|
2974
|
+
/** Precarga ediciones ya existentes (p. ej. las pendientes del editor UI). */
|
|
2975
|
+
seed(edits = [], imageEdits = []) {
|
|
2976
|
+
for (const e of edits) this.edits.set(e.segmentId, e);
|
|
2977
|
+
for (const e of imageEdits) this.imageEdits.set(e.imageId, e);
|
|
2978
|
+
}
|
|
2979
|
+
/** Ediciones de texto/imagen acumuladas (lo que el editor sabe aplicar a su
|
|
2980
|
+
* estado local; las creaciones/annotations se hornean en el server/CLI). */
|
|
2981
|
+
getEdits() {
|
|
2982
|
+
return { edits: [...this.edits.values()], imageEdits: [...this.imageEdits.values()] };
|
|
2983
|
+
}
|
|
2984
|
+
/** Cantidad total de cambios pendientes. */
|
|
2985
|
+
get count() {
|
|
2986
|
+
return this.edits.size + this.imageEdits.size + this.widgetEdits.size + this.highlightEdits.size + this.linkEdits.size + this.creates.length + this.fills.size;
|
|
2987
|
+
}
|
|
2988
|
+
/** ¿Hay cambios que el editor NO puede reflejar con getEdits() (creaciones,
|
|
2989
|
+
* ediciones de annotations, o llenado de formularios)? El server los hornea +
|
|
2990
|
+
* persiste en vez de devolverlos como seg/img edits. */
|
|
2991
|
+
get hasBakedOps() {
|
|
2992
|
+
return this.creates.length + this.widgetEdits.size + this.highlightEdits.size + this.linkEdits.size + this.fills.size > 0;
|
|
2993
|
+
}
|
|
2994
|
+
summary() {
|
|
2995
|
+
const parts = [];
|
|
2996
|
+
for (const e of this.edits.values()) parts.push(e.remove ? `${e.segmentId}: eliminar` : `${e.segmentId}: editar`);
|
|
2997
|
+
for (const e of this.imageEdits.values()) parts.push(e.remove ? `${e.imageId}: eliminar` : `${e.imageId}: mover/escalar`);
|
|
2998
|
+
for (const e of this.widgetEdits.values()) parts.push(`${e.widgetId}: campo`);
|
|
2999
|
+
for (const e of this.highlightEdits.values()) parts.push(`${e.highlightId}: resaltado`);
|
|
3000
|
+
for (const e of this.linkEdits.values()) parts.push(`${e.linkId}: link`);
|
|
3001
|
+
for (const c of this.creates) parts.push(`+${c.kind}`);
|
|
3002
|
+
for (const [name] of this.fills) parts.push(`${name}: completar`);
|
|
3003
|
+
return parts.join(" \xB7 ") || "(sin cambios)";
|
|
3004
|
+
}
|
|
3005
|
+
/** Aplica una CREACIÓN sobre los bytes ya horneados (bytes→bytes). */
|
|
3006
|
+
async applyCreate(pdf, op) {
|
|
3007
|
+
switch (op.kind) {
|
|
3008
|
+
case "highlightSeg": {
|
|
3009
|
+
const s = this.seg(op.segId);
|
|
3010
|
+
const g = effectiveGeometry(s, this.edits.get(op.segId) ?? null);
|
|
3011
|
+
return (await addHighlight(pdf, { page: s.page, x: g.x, y: g.y, width: g.width, height: g.height, color: op.color })).pdf;
|
|
3012
|
+
}
|
|
3013
|
+
case "linkSeg": {
|
|
3014
|
+
const s = this.seg(op.segId);
|
|
3015
|
+
const g = effectiveGeometry(s, this.edits.get(op.segId) ?? null);
|
|
3016
|
+
return (await addLink(pdf, { page: s.page, x: g.x, y: g.y, width: g.width, height: g.height, url: op.url })).pdf;
|
|
3017
|
+
}
|
|
3018
|
+
case "text":
|
|
3019
|
+
return (await addText(pdf, op)).pdf;
|
|
3020
|
+
case "image": {
|
|
3021
|
+
const bytes = new Uint8Array(await readFile2(op.path));
|
|
3022
|
+
const ext = op.path.split(".").pop()?.toLowerCase() ?? "";
|
|
3023
|
+
const mime = MIME[ext];
|
|
3024
|
+
if (!mime) throw new Error(`imagen no soportada (${op.path}): solo PNG/JPEG`);
|
|
3025
|
+
return (await insertImage(pdf, { page: op.page, x: op.x, y: op.y, bytes, mime, maxWidth: op.maxWidth })).pdf;
|
|
3026
|
+
}
|
|
3027
|
+
case "watermark":
|
|
3028
|
+
return (await addWatermark(pdf, { text: op.text, opacity: op.opacity, color: op.color })).pdf;
|
|
3029
|
+
case "headerFooter":
|
|
3030
|
+
return (await addHeaderFooter(pdf, { header: op.header, footer: op.footer, pageNumbers: op.pageNumbers })).pdf;
|
|
3031
|
+
case "field":
|
|
3032
|
+
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;
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
/** Hornea TODO (ediciones + annotations + creaciones) y devuelve los bytes. */
|
|
3036
|
+
async bake() {
|
|
3037
|
+
const imgs = promoteMovedImages([...this.imageEdits.values()]);
|
|
3038
|
+
const r2 = await bakeSegmentEdits(
|
|
3039
|
+
this.doc.bytes.slice(),
|
|
3040
|
+
[...this.edits.values()],
|
|
3041
|
+
imgs,
|
|
3042
|
+
[...this.widgetEdits.values()],
|
|
3043
|
+
[...this.highlightEdits.values()],
|
|
3044
|
+
[...this.linkEdits.values()]
|
|
3045
|
+
);
|
|
3046
|
+
let pdf = r2.pdf;
|
|
3047
|
+
for (const op of this.creates) {
|
|
3048
|
+
pdf = await this.applyCreate(pdf, op);
|
|
3049
|
+
r2.applied.push(`+${op.kind}`);
|
|
3050
|
+
}
|
|
3051
|
+
if (this.fills.size) {
|
|
3052
|
+
const res = await setFieldValues(pdf, Object.fromEntries(this.fills));
|
|
3053
|
+
pdf = res.pdf;
|
|
3054
|
+
r2.applied.push(...res.applied.map((a2) => `campo ${a2}`));
|
|
3055
|
+
r2.warnings.push(...res.warnings);
|
|
3056
|
+
}
|
|
3057
|
+
return { pdf, applied: r2.applied, warnings: r2.warnings };
|
|
3058
|
+
}
|
|
3059
|
+
/** Hornea y escribe el PDF a `outPath`. */
|
|
3060
|
+
async save(outPath) {
|
|
3061
|
+
const { pdf, applied, warnings } = await this.bake();
|
|
3062
|
+
await writeFile(outPath, pdf);
|
|
3063
|
+
return { applied, warnings };
|
|
3064
|
+
}
|
|
3065
|
+
};
|
|
3066
|
+
|
|
3067
|
+
// ../agent/src/cli.ts
|
|
3068
|
+
init_agent();
|
|
3069
|
+
var TOOL_LABEL = {
|
|
3070
|
+
edit_text: "editando texto",
|
|
3071
|
+
move_text: "moviendo texto",
|
|
3072
|
+
set_text_color: "coloreando texto",
|
|
3073
|
+
set_text_size: "cambiando tama\xF1o",
|
|
3074
|
+
delete_text: "eliminando texto",
|
|
3075
|
+
move_image: "moviendo imagen",
|
|
3076
|
+
delete_image: "eliminando imagen",
|
|
3077
|
+
highlight_text: "resaltando",
|
|
3078
|
+
set_highlight_color: "recoloreando resaltado",
|
|
3079
|
+
delete_highlight: "quitando resaltado",
|
|
3080
|
+
add_link: "agregando link",
|
|
3081
|
+
delete_link: "quitando link",
|
|
3082
|
+
add_text: "agregando texto",
|
|
3083
|
+
insert_image: "insertando imagen",
|
|
3084
|
+
add_watermark: "marca de agua",
|
|
3085
|
+
add_header_footer: "encabezado/pie",
|
|
3086
|
+
add_form_field: "creando campo",
|
|
3087
|
+
fill_field: "completando campo",
|
|
3088
|
+
move_field: "moviendo campo",
|
|
3089
|
+
delete_field: "quitando campo"
|
|
3090
|
+
};
|
|
3091
|
+
function openFile(file) {
|
|
3092
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
3093
|
+
spawn(cmd, [file], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
|
|
3094
|
+
}
|
|
3095
|
+
function streamToStdout(ev) {
|
|
3096
|
+
if (ev.type === "text") process.stdout.write(ev.delta);
|
|
3097
|
+
else if (ev.type === "tool") process.stdout.write(`
|
|
3098
|
+
\xB7 ${TOOL_LABEL[ev.name.replace("mcp__aldus__", "")] ?? ev.name}\u2026
|
|
3099
|
+
`);
|
|
3100
|
+
}
|
|
3101
|
+
function startSpinner() {
|
|
3102
|
+
if (!process.stdout.isTTY) return { stop() {
|
|
3103
|
+
} };
|
|
3104
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3105
|
+
const t0 = Date.now();
|
|
3106
|
+
let i = 0;
|
|
3107
|
+
const id = setInterval(() => {
|
|
3108
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} pensando\u2026 ${Math.round((Date.now() - t0) / 1e3)}s`);
|
|
3109
|
+
}, 120);
|
|
3110
|
+
return { stop() {
|
|
3111
|
+
clearInterval(id);
|
|
3112
|
+
process.stdout.write("\r\x1B[K");
|
|
3113
|
+
} };
|
|
3114
|
+
}
|
|
3115
|
+
async function runTurnLive(opts) {
|
|
3116
|
+
const spin = startSpinner();
|
|
3117
|
+
let stopped = false;
|
|
3118
|
+
const stop = () => {
|
|
3119
|
+
if (!stopped) {
|
|
3120
|
+
spin.stop();
|
|
3121
|
+
stopped = true;
|
|
3122
|
+
}
|
|
3123
|
+
};
|
|
3124
|
+
try {
|
|
3125
|
+
return await runTurn({ ...opts, onEvent: (ev) => {
|
|
3126
|
+
stop();
|
|
3127
|
+
opts.onEvent?.(ev);
|
|
3128
|
+
} });
|
|
3129
|
+
} finally {
|
|
3130
|
+
stop();
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
function usage() {
|
|
3134
|
+
console.error(`aldus \u2014 agente sobre el grafo de un PDF
|
|
3135
|
+
|
|
3136
|
+
aldus <archivo.pdf> "<prompt>" [-o salida.pdf] [--open] one-shot (agente LLM)
|
|
3137
|
+
aldus <archivo.pdf> -p "<prompt>" (equivalente, con flag)
|
|
3138
|
+
aldus <archivo.pdf> chat interactivo
|
|
3139
|
+
aldus <archivo.pdf> --fields volcar campos+valores (JSON)
|
|
3140
|
+
aldus <archivo.pdf> --fill datos.json [-o out.pdf] completar el form (sin LLM)
|
|
3141
|
+
|
|
3142
|
+
Ejemplos:
|
|
3143
|
+
aldus contrato.pdf "Describ\xED el contenido"
|
|
3144
|
+
aldus contrato.pdf "Resalt\xE1 los montos y pon\xE9 el t\xEDtulo en may\xFAsculas" --open
|
|
3145
|
+
aldus formulario.pdf --fields
|
|
3146
|
+
aldus formulario.pdf --fill '{"nombre":"Juan","acepta":"true"}' --open
|
|
3147
|
+
|
|
3148
|
+
--open abre el PDF resultante (o el original si no hubo cambios).
|
|
3149
|
+
--fields / --fill formularios de forma DETERMIN\xCDSTICA (no usan el LLM).
|
|
3150
|
+
Chat: pregunt\xE1 o ped\xED cambios; /save [ruta] guarda, /edits lista, /exit sale.
|
|
3151
|
+
Auth: suscripci\xF3n de Claude Code (corr\xE9 sin ANTHROPIC_API_KEY).`);
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
3154
|
+
function defaultOut(src) {
|
|
3155
|
+
const ext = path.extname(src);
|
|
3156
|
+
return path.join(path.dirname(src), `${path.basename(src, ext)}-edited${ext || ".pdf"}`);
|
|
3157
|
+
}
|
|
3158
|
+
async function save(session, outPath) {
|
|
3159
|
+
if (session.count === 0) {
|
|
3160
|
+
console.log("(no hay ediciones para guardar)");
|
|
3161
|
+
return;
|
|
3162
|
+
}
|
|
3163
|
+
const { applied, warnings } = await session.save(outPath);
|
|
3164
|
+
console.log(`\u{1F4BE} Guardado ${outPath} \u2014 ${applied.length} aplicada(s)${warnings.length ? `, ${warnings.length} aviso(s)` : ""}`);
|
|
3165
|
+
for (const w of warnings) console.log(` \u26A0\uFE0F ${w}`);
|
|
3166
|
+
}
|
|
3167
|
+
async function main() {
|
|
3168
|
+
const { values, positionals } = parseArgs({
|
|
3169
|
+
allowPositionals: true,
|
|
3170
|
+
options: {
|
|
3171
|
+
prompt: { type: "string", short: "p" },
|
|
3172
|
+
out: { type: "string", short: "o" },
|
|
3173
|
+
open: { type: "boolean" },
|
|
3174
|
+
fields: { type: "boolean" },
|
|
3175
|
+
fill: { type: "string" },
|
|
3176
|
+
help: { type: "boolean", short: "h" }
|
|
3177
|
+
}
|
|
3178
|
+
});
|
|
3179
|
+
const file = positionals[0];
|
|
3180
|
+
if (!file || values.help) usage();
|
|
3181
|
+
const prompt = values.prompt ?? positionals[1];
|
|
3182
|
+
if (values.fields) {
|
|
3183
|
+
const fields = await readFormFields(new Uint8Array(await readFile3(file)));
|
|
3184
|
+
process.stdout.write(JSON.stringify(fields, null, 2) + "\n");
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
if (values.fill) {
|
|
3188
|
+
const raw = values.fill.trim().startsWith("{") ? values.fill : await readFile3(values.fill, "utf8");
|
|
3189
|
+
const data = JSON.parse(raw);
|
|
3190
|
+
const out = values.out || defaultOut(file);
|
|
3191
|
+
const { pdf, applied, warnings } = await setFieldValues(new Uint8Array(await readFile3(file)), data);
|
|
3192
|
+
await writeFile2(out, pdf);
|
|
3193
|
+
console.log(`\u{1F4BE} ${out} \u2014 ${applied.length} campo(s) completado(s)${warnings.length ? `, ${warnings.length} aviso(s)` : ""}`);
|
|
3194
|
+
for (const a2 of applied) console.log(` \u2713 ${a2}`);
|
|
3195
|
+
for (const w of warnings) console.log(` \u26A0\uFE0F ${w}`);
|
|
3196
|
+
if (values.open) openFile(out);
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
process.stdout.write(`\u{1F4C4} Cargando ${file} \u2026
|
|
3200
|
+
`);
|
|
3201
|
+
const doc = await loadDoc(file);
|
|
3202
|
+
const totalSegs = doc.pages.reduce((n, p) => n + p.segments.length, 0);
|
|
3203
|
+
console.log(` ${doc.pages.length} p\xE1gina(s), ${totalSegs} nodo(s) de texto. Grafo embebido en el agente.
|
|
3204
|
+
`);
|
|
3205
|
+
const session = new EditSession(doc);
|
|
3206
|
+
if (prompt) {
|
|
3207
|
+
await runTurnLive({ doc, session, prompt, onEvent: streamToStdout });
|
|
3208
|
+
process.stdout.write("\n");
|
|
3209
|
+
const out = values.out || defaultOut(file);
|
|
3210
|
+
if (session.count > 0) await save(session, out);
|
|
3211
|
+
if (values.open) openFile(session.count > 0 ? out : file);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
console.log("Chat con Aldus. Pregunt\xE1 o ped\xED cambios. /save [ruta] \xB7 /edits \xB7 /exit\n");
|
|
3215
|
+
const rl = createInterface({ input, output });
|
|
3216
|
+
const outPath = values.out || defaultOut(file);
|
|
3217
|
+
let resume;
|
|
3218
|
+
try {
|
|
3219
|
+
while (true) {
|
|
3220
|
+
const line = (await rl.question("\u203A ")).trim();
|
|
3221
|
+
if (!line) continue;
|
|
3222
|
+
if (line === "/exit" || line === "/quit") break;
|
|
3223
|
+
if (line === "/edits") {
|
|
3224
|
+
console.log(session.summary());
|
|
3225
|
+
continue;
|
|
3226
|
+
}
|
|
3227
|
+
if (line.startsWith("/save")) {
|
|
3228
|
+
const arg = line.slice(5).trim();
|
|
3229
|
+
await save(session, arg || outPath);
|
|
3230
|
+
continue;
|
|
3231
|
+
}
|
|
3232
|
+
process.stdout.write("\n");
|
|
3233
|
+
const { sessionId } = await runTurnLive({ doc, session, prompt: line, resume, onEvent: streamToStdout });
|
|
3234
|
+
resume = sessionId ?? resume;
|
|
3235
|
+
process.stdout.write("\n");
|
|
3236
|
+
if (session.count > 0) console.log(` \xB7 ${session.count} edici\xF3n(es) pendiente(s) \u2014 /save para hornear`);
|
|
3237
|
+
process.stdout.write("\n");
|
|
3238
|
+
}
|
|
3239
|
+
if (session.count > 0) {
|
|
3240
|
+
const ans = (await rl.question(`Ten\xE9s ${session.count} edici\xF3n(es) sin guardar. \xBFGuardar en ${outPath}? [s/N] `)).trim().toLowerCase();
|
|
3241
|
+
if (ans === "s" || ans === "si" || ans === "y") await save(session, outPath);
|
|
3242
|
+
}
|
|
3243
|
+
} finally {
|
|
3244
|
+
rl.close();
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3247
|
+
main().catch((err) => {
|
|
3248
|
+
console.error("\u2717", err instanceof Error ? err.message : err);
|
|
3249
|
+
process.exit(1);
|
|
3250
|
+
});
|