autoform-mcp-server 1.2.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.
@@ -0,0 +1,412 @@
1
+ import { PDFDocument, PDFTextField, PDFCheckBox, PDFDropdown, PDFRadioGroup, StandardFonts, rgb } from 'pdf-lib';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ /**
5
+ * PDF field detection and filling service.
6
+ * Uses pdf-lib's AcroForm API for detection + drawText for template-based filling.
7
+ */
8
+ export class PdfService {
9
+ /** Detect AcroForm fields in a PDF */
10
+ static async detectFields(pdfPath) {
11
+ const bytes = fs.readFileSync(pdfPath);
12
+ const pdfDoc = await PDFDocument.load(bytes, { ignoreEncryption: true });
13
+ const fields = [];
14
+ let hasAcroform = false;
15
+ try {
16
+ const form = pdfDoc.getForm();
17
+ const formFields = form.getFields();
18
+ if (formFields.length > 0) {
19
+ hasAcroform = true;
20
+ for (const field of formFields) {
21
+ const name = field.getName();
22
+ const type = field.constructor.name.replace('PDF', '').replace('Field', '').toLowerCase();
23
+ const widgets = field.acroField.getWidgets();
24
+ for (let wi = 0; wi < widgets.length; wi++) {
25
+ const widget = widgets[wi];
26
+ const rect = widget.getRectangle();
27
+ // Determine page number
28
+ const pageRef = widget.P();
29
+ let pageNum = 1;
30
+ if (pageRef) {
31
+ const pages = pdfDoc.getPages();
32
+ for (let pi = 0; pi < pages.length; pi++) {
33
+ if (pages[pi].ref === pageRef) {
34
+ pageNum = pi + 1;
35
+ break;
36
+ }
37
+ }
38
+ }
39
+ let value;
40
+ if (field instanceof PDFTextField) {
41
+ value = field.getText() ?? undefined;
42
+ }
43
+ fields.push({
44
+ name: widgets.length > 1 ? `${name}[${wi}]` : name,
45
+ type,
46
+ page: pageNum,
47
+ x: rect.x,
48
+ y: rect.y,
49
+ width: rect.width,
50
+ height: rect.height,
51
+ value
52
+ });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ catch {
58
+ // No form in this PDF
59
+ }
60
+ return { fields, hasAcroform };
61
+ }
62
+ /** Fill AcroForm fields and save */
63
+ static async fillAcroForm(pdfPath, values, outputPath) {
64
+ const bytes = fs.readFileSync(pdfPath);
65
+ const pdfDoc = await PDFDocument.load(bytes);
66
+ const form = pdfDoc.getForm();
67
+ let filled = 0;
68
+ for (const [name, value] of Object.entries(values)) {
69
+ try {
70
+ const field = form.getField(name);
71
+ if (field instanceof PDFTextField) {
72
+ field.setText(value);
73
+ filled++;
74
+ }
75
+ else if (field instanceof PDFCheckBox) {
76
+ if (['true', '1', 'yes', 'si', 'sí'].includes(value.toLowerCase()))
77
+ field.check();
78
+ else
79
+ field.uncheck();
80
+ filled++;
81
+ }
82
+ else if (field instanceof PDFDropdown) {
83
+ field.select(value);
84
+ filled++;
85
+ }
86
+ else if (field instanceof PDFRadioGroup) {
87
+ field.select(value);
88
+ filled++;
89
+ }
90
+ }
91
+ catch {
92
+ // Field not found or wrong type — skip
93
+ }
94
+ }
95
+ form.flatten();
96
+ const savedBytes = await pdfDoc.save();
97
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
98
+ fs.writeFileSync(outputPath, savedBytes);
99
+ return filled;
100
+ }
101
+ /** Fill PDF using template coordinates (drawText), same logic as web app's pdfGenerator */
102
+ static async fillWithTemplate(pdfPath, fields, values, outputPath) {
103
+ const bytes = fs.readFileSync(pdfPath);
104
+ const basePdf = await PDFDocument.load(bytes);
105
+ const pdfDoc = await PDFDocument.create();
106
+ const pages = await pdfDoc.copyPages(basePdf, basePdf.getPageIndices());
107
+ pages.forEach(p => pdfDoc.addPage(p));
108
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
109
+ let filled = 0;
110
+ for (const field of fields) {
111
+ const value = values[field.fieldName];
112
+ if (!value || value.trim() === '')
113
+ continue;
114
+ const pageIndex = field.pageNumber - 1;
115
+ if (pageIndex < 0 || pageIndex >= pdfDoc.getPageCount())
116
+ continue;
117
+ const page = pdfDoc.getPage(pageIndex);
118
+ this.drawFieldText(page, field, value, font);
119
+ filled++;
120
+ }
121
+ const savedBytes = await pdfDoc.save({ useObjectStreams: true });
122
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
123
+ fs.writeFileSync(outputPath, savedBytes);
124
+ return filled;
125
+ }
126
+ /**
127
+ * Fill PDF at exact coordinates (points, PDF native system — origin bottom-left).
128
+ * Designed for Claude to pass positions it determined visually.
129
+ */
130
+ static async fillAtCoordinates(pdfPath, fields, outputPath) {
131
+ const bytes = fs.readFileSync(pdfPath);
132
+ const basePdf = await PDFDocument.load(bytes, { ignoreEncryption: true });
133
+ const pdfDoc = await PDFDocument.create();
134
+ const pages = await pdfDoc.copyPages(basePdf, basePdf.getPageIndices());
135
+ pages.forEach(p => pdfDoc.addPage(p));
136
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
137
+ let filled = 0;
138
+ for (const field of fields) {
139
+ const sanitized = this.sanitize(field.text);
140
+ if (!sanitized)
141
+ continue;
142
+ const pageIndex = field.page - 1;
143
+ if (pageIndex < 0 || pageIndex >= pdfDoc.getPageCount())
144
+ continue;
145
+ const page = pdfDoc.getPage(pageIndex);
146
+ const pad = 2;
147
+ const maxW = field.width - pad * 2;
148
+ const maxH = field.height - pad * 2;
149
+ const desiredMax = field.fontSize ?? 12;
150
+ const minSize = 6;
151
+ const mode = field.overflowMode ?? 'shrink';
152
+ if (mode === 'wrap') {
153
+ let fs = desiredMax;
154
+ let lines = this.wrapText(sanitized, maxW, font, fs);
155
+ let totalH = lines.length * font.heightAtSize(fs) * 1.15;
156
+ while (totalH > maxH && fs > minSize) {
157
+ fs -= 0.5;
158
+ lines = this.wrapText(sanitized, maxW, font, fs);
159
+ totalH = lines.length * font.heightAtSize(fs) * 1.15;
160
+ }
161
+ if (lines.length === 0)
162
+ continue;
163
+ const lineH = font.heightAtSize(fs) * 1.15;
164
+ const startY = field.y + field.height - pad - font.heightAtSize(fs);
165
+ const vOff = Math.max(0, (maxH - lines.length * lineH) / 2);
166
+ for (let i = 0; i < lines.length; i++) {
167
+ const ly = startY - i * lineH - vOff;
168
+ if (ly < field.y)
169
+ break;
170
+ page.drawText(lines[i], { x: field.x + pad, y: ly, size: fs, font, color: rgb(0, 0, 0) });
171
+ }
172
+ }
173
+ else {
174
+ const fs = this.optimalFontSize(sanitized, maxW, maxH, font, minSize, desiredMax);
175
+ const fitsOneLine = font.widthOfTextAtSize(sanitized, fs) <= maxW;
176
+ if (fitsOneLine) {
177
+ const th = font.heightAtSize(fs);
178
+ page.drawText(sanitized, { x: field.x + pad, y: field.y + (field.height - th) / 2, size: fs, font, color: rgb(0, 0, 0) });
179
+ }
180
+ else {
181
+ const lines = this.wrapText(sanitized, maxW, font, fs);
182
+ const lineH = font.heightAtSize(fs) * 1.15;
183
+ const startY = field.y + field.height - pad - font.heightAtSize(fs);
184
+ const vOff = Math.max(0, (maxH - lines.length * lineH) / 2);
185
+ for (let i = 0; i < lines.length; i++) {
186
+ const ly = startY - i * lineH - vOff;
187
+ if (ly < field.y)
188
+ break;
189
+ page.drawText(lines[i], { x: field.x + pad, y: ly, size: fs, font, color: rgb(0, 0, 0) });
190
+ }
191
+ }
192
+ }
193
+ filled++;
194
+ }
195
+ const savedBytes = await pdfDoc.save({ useObjectStreams: true });
196
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
197
+ fs.writeFileSync(outputPath, savedBytes);
198
+ return filled;
199
+ }
200
+ /**
201
+ * Batch fill: generates multiple PDFs from the same template with different data sets.
202
+ * Each entry in `dataRows` produces one PDF using the same field positions.
203
+ */
204
+ static async batchFillAtCoordinates(pdfPath, fieldDefinitions, dataRows, outputDir, baseName, merge = false) {
205
+ const bytes = fs.readFileSync(pdfPath);
206
+ const outputPaths = [];
207
+ const errors = [];
208
+ const timestamp = new Date().toISOString().slice(0, 10);
209
+ fs.mkdirSync(outputDir, { recursive: true });
210
+ for (let i = 0; i < dataRows.length; i++) {
211
+ try {
212
+ const row = dataRows[i];
213
+ const fields = fieldDefinitions.map(def => ({
214
+ text: row[def.label] || '',
215
+ page: def.page,
216
+ x: def.x,
217
+ y: def.y,
218
+ width: def.width,
219
+ height: def.height,
220
+ fontSize: def.fontSize,
221
+ overflowMode: def.overflowMode
222
+ })).filter(f => f.text.trim() !== '');
223
+ const num = String(i + 1).padStart(3, '0');
224
+ const outPath = path.join(outputDir, `${baseName}_${num}_${timestamp}.pdf`);
225
+ await this.fillAtCoordinates(pdfPath, fields, outPath);
226
+ outputPaths.push(outPath);
227
+ }
228
+ catch (err) {
229
+ errors.push(`Fila ${i + 1}: ${err instanceof Error ? err.message : String(err)}`);
230
+ }
231
+ }
232
+ let mergedPath;
233
+ if (merge && outputPaths.length > 0) {
234
+ mergedPath = path.join(outputDir, `${baseName}_merged_${timestamp}.pdf`);
235
+ await this.mergePdfs(outputPaths, mergedPath);
236
+ }
237
+ return { outputPaths, mergedPath, errors };
238
+ }
239
+ /** Get PDF page dimensions (useful for Claude to calculate coordinates) */
240
+ static async getPageDimensions(pdfPath) {
241
+ const bytes = fs.readFileSync(pdfPath);
242
+ const pdfDoc = await PDFDocument.load(bytes, { ignoreEncryption: true });
243
+ return pdfDoc.getPages().map((p, i) => {
244
+ const { width, height } = p.getSize();
245
+ return { page: i + 1, width: Math.round(width * 100) / 100, height: Math.round(height * 100) / 100 };
246
+ });
247
+ }
248
+ /** Generate a single document with field mappings (for batch generation) */
249
+ static async generateSingleDocument(basePdfBytes, mappings, outputPath) {
250
+ const basePdf = await PDFDocument.load(basePdfBytes);
251
+ const pdfDoc = await PDFDocument.create();
252
+ const pages = await pdfDoc.copyPages(basePdf, basePdf.getPageIndices());
253
+ pages.forEach(p => pdfDoc.addPage(p));
254
+ const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
255
+ for (const { field, value } of mappings) {
256
+ if (!value || String(value).trim() === '')
257
+ continue;
258
+ const pageIndex = field.pageNumber - 1;
259
+ if (pageIndex < 0 || pageIndex >= pdfDoc.getPageCount())
260
+ continue;
261
+ this.drawFieldText(pdfDoc.getPage(pageIndex), field, String(value), font);
262
+ }
263
+ const savedBytes = await pdfDoc.save({ useObjectStreams: true });
264
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
265
+ fs.writeFileSync(outputPath, savedBytes);
266
+ }
267
+ /** Merge multiple PDFs into one */
268
+ static async mergePdfs(pdfPaths, outputPath) {
269
+ const merged = await PDFDocument.create();
270
+ for (const p of pdfPaths) {
271
+ const bytes = fs.readFileSync(p);
272
+ const src = await PDFDocument.load(bytes);
273
+ const pages = await merged.copyPages(src, src.getPageIndices());
274
+ pages.forEach(page => merged.addPage(page));
275
+ }
276
+ const savedBytes = await merged.save({ useObjectStreams: true });
277
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
278
+ fs.writeFileSync(outputPath, savedBytes);
279
+ }
280
+ // ── Internal helpers ──
281
+ static drawFieldText(page, field, text, font) {
282
+ const { width: pageWidth, height: pageHeight } = page.getSize();
283
+ const x = (field.x / 100) * pageWidth;
284
+ const y = pageHeight - ((field.y / 100) * pageHeight) - ((field.height / 100) * pageHeight);
285
+ const w = (field.width / 100) * pageWidth;
286
+ const h = (field.height / 100) * pageHeight;
287
+ const sanitized = this.sanitize(text);
288
+ if (!sanitized)
289
+ return;
290
+ const pad = 2;
291
+ const maxW = w - pad * 2;
292
+ const maxH = h - pad * 2;
293
+ const desiredMax = field.fontSize ?? 14;
294
+ const minSize = 6;
295
+ const overflowMode = field.overflowMode ?? 'shrink';
296
+ if (overflowMode === 'wrap') {
297
+ let fs = desiredMax;
298
+ let lines = this.wrapText(sanitized, maxW, font, fs);
299
+ let totalH = lines.length * font.heightAtSize(fs) * 1.15;
300
+ while (totalH > maxH && fs > minSize) {
301
+ fs -= 0.5;
302
+ lines = this.wrapText(sanitized, maxW, font, fs);
303
+ totalH = lines.length * font.heightAtSize(fs) * 1.15;
304
+ }
305
+ if (lines.length === 0)
306
+ return;
307
+ const lineH = font.heightAtSize(fs) * 1.15;
308
+ const startY = y + h - pad - font.heightAtSize(fs);
309
+ const vOff = Math.max(0, (maxH - lines.length * lineH) / 2);
310
+ for (let i = 0; i < lines.length; i++) {
311
+ const ly = startY - i * lineH - vOff;
312
+ if (ly < y)
313
+ break;
314
+ page.drawText(lines[i], { x: x + pad, y: ly, size: fs, font, color: rgb(0, 0, 0) });
315
+ }
316
+ }
317
+ else {
318
+ const fs = this.optimalFontSize(sanitized, maxW, maxH, font, minSize, desiredMax);
319
+ const fits = font.widthOfTextAtSize(sanitized, fs) <= maxW;
320
+ if (fits) {
321
+ const th = font.heightAtSize(fs);
322
+ page.drawText(sanitized, { x: x + pad, y: y + (h - th) / 2, size: fs, font, color: rgb(0, 0, 0) });
323
+ }
324
+ else {
325
+ // Fallback to wrap
326
+ const lines = this.wrapText(sanitized, maxW, font, fs);
327
+ const lineH = font.heightAtSize(fs) * 1.15;
328
+ const startY = y + h - pad - font.heightAtSize(fs);
329
+ const vOff = Math.max(0, (maxH - lines.length * lineH) / 2);
330
+ for (let i = 0; i < lines.length; i++) {
331
+ const ly = startY - i * lineH - vOff;
332
+ if (ly < y)
333
+ break;
334
+ page.drawText(lines[i], { x: x + pad, y: ly, size: fs, font, color: rgb(0, 0, 0) });
335
+ }
336
+ }
337
+ }
338
+ }
339
+ static sanitize(input) {
340
+ let t = String(input ?? '').trim();
341
+ t = t.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
342
+ t = t.replace(/\s+/g, ' ');
343
+ t = t.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, '');
344
+ if (t.length > 500)
345
+ t = t.substring(0, 500);
346
+ return t;
347
+ }
348
+ static optimalFontSize(text, maxW, maxH, font, min, max) {
349
+ let s = max;
350
+ while (s >= min) {
351
+ if (font.widthOfTextAtSize(text, s) <= maxW && font.heightAtSize(s) <= maxH)
352
+ return s;
353
+ s -= 0.5;
354
+ }
355
+ return min;
356
+ }
357
+ static wrapText(text, maxW, font, fontSize) {
358
+ if (maxW <= 0 || fontSize <= 0)
359
+ return [text];
360
+ const lines = [];
361
+ let remaining = text;
362
+ let safety = 0;
363
+ while (remaining.length > 0 && safety < text.length + 10) {
364
+ safety++;
365
+ try {
366
+ if (font.widthOfTextAtSize(remaining, fontSize) <= maxW) {
367
+ lines.push(remaining);
368
+ break;
369
+ }
370
+ }
371
+ catch {
372
+ break;
373
+ }
374
+ let cut = 0;
375
+ const words = remaining.split(' ');
376
+ let test = '';
377
+ for (let w = 0; w < words.length; w++) {
378
+ const c = w === 0 ? words[w] : test + ' ' + words[w];
379
+ try {
380
+ if (font.widthOfTextAtSize(c, fontSize) <= maxW) {
381
+ test = c;
382
+ cut = c.length;
383
+ }
384
+ else
385
+ break;
386
+ }
387
+ catch {
388
+ break;
389
+ }
390
+ }
391
+ if (cut === 0) {
392
+ for (let c = 1; c <= remaining.length; c++) {
393
+ try {
394
+ if (font.widthOfTextAtSize(remaining.substring(0, c), fontSize) > maxW) {
395
+ cut = Math.max(1, c - 1);
396
+ break;
397
+ }
398
+ cut = c;
399
+ }
400
+ catch {
401
+ break;
402
+ }
403
+ }
404
+ }
405
+ if (cut <= 0)
406
+ cut = 1;
407
+ lines.push(remaining.substring(0, cut));
408
+ remaining = remaining.substring(cut).trimStart();
409
+ }
410
+ return lines;
411
+ }
412
+ }
@@ -0,0 +1,20 @@
1
+ import { SavedTemplate, TemplatePreview } from '../types.js';
2
+ /**
3
+ * Filesystem-based template storage.
4
+ * Templates are stored as individual JSON files in the templates/ directory.
5
+ */
6
+ export declare class TemplateStore {
7
+ private static ensureDir;
8
+ static list(): TemplatePreview[];
9
+ static load(templateName: string): SavedTemplate | null;
10
+ static save(template: SavedTemplate): string;
11
+ /** Get the path to the PDF file for a template */
12
+ static getPdfPath(template: SavedTemplate): string;
13
+ /**
14
+ * Import a template JSON exported from the web app.
15
+ * The web app stores the PDF as base64 in pdfInfo.dataUrl — we extract it to a file.
16
+ */
17
+ static importFromWebApp(jsonFilePath: string): SavedTemplate;
18
+ /** Build a minimal TemplateAnalysis from fields */
19
+ private static buildAnalysis;
20
+ }
@@ -0,0 +1,137 @@
1
+ import * as fs from 'fs';
2
+ import * as path from 'path';
3
+ const TEMPLATES_DIR = path.resolve(import.meta.dirname, '../../templates');
4
+ /**
5
+ * Filesystem-based template storage.
6
+ * Templates are stored as individual JSON files in the templates/ directory.
7
+ */
8
+ export class TemplateStore {
9
+ static ensureDir() {
10
+ fs.mkdirSync(TEMPLATES_DIR, { recursive: true });
11
+ }
12
+ static list() {
13
+ this.ensureDir();
14
+ const files = fs.readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.json'));
15
+ const previews = [];
16
+ for (const file of files) {
17
+ try {
18
+ const content = fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8');
19
+ const template = JSON.parse(content);
20
+ previews.push({
21
+ name: template.name,
22
+ description: template.description,
23
+ createdAt: template.createdAt,
24
+ fieldCount: template.fields.length,
25
+ pageCount: template.pdfInfo.pageCount,
26
+ requiredColumns: template.analysis.requiredColumns
27
+ });
28
+ }
29
+ catch {
30
+ // Skip corrupt files
31
+ }
32
+ }
33
+ return previews;
34
+ }
35
+ static load(templateName) {
36
+ this.ensureDir();
37
+ const safeName = templateName.replace(/[^a-zA-Z0-9_\-]/g, '_');
38
+ const filePath = path.join(TEMPLATES_DIR, `${safeName}.json`);
39
+ if (!fs.existsSync(filePath)) {
40
+ // Try matching by name field in all templates
41
+ const files = fs.readdirSync(TEMPLATES_DIR).filter(f => f.endsWith('.json'));
42
+ for (const file of files) {
43
+ try {
44
+ const content = fs.readFileSync(path.join(TEMPLATES_DIR, file), 'utf-8');
45
+ const t = JSON.parse(content);
46
+ if (t.name.toLowerCase() === templateName.toLowerCase())
47
+ return t;
48
+ }
49
+ catch { /* skip */ }
50
+ }
51
+ return null;
52
+ }
53
+ const content = fs.readFileSync(filePath, 'utf-8');
54
+ return JSON.parse(content);
55
+ }
56
+ static save(template) {
57
+ this.ensureDir();
58
+ const safeName = template.name.replace(/[^a-zA-Z0-9_\-]/g, '_');
59
+ const filePath = path.join(TEMPLATES_DIR, `${safeName}.json`);
60
+ fs.writeFileSync(filePath, JSON.stringify(template, null, 2));
61
+ return filePath;
62
+ }
63
+ /** Get the path to the PDF file for a template */
64
+ static getPdfPath(template) {
65
+ return template.pdfPath;
66
+ }
67
+ /**
68
+ * Import a template JSON exported from the web app.
69
+ * The web app stores the PDF as base64 in pdfInfo.dataUrl — we extract it to a file.
70
+ */
71
+ static importFromWebApp(jsonFilePath) {
72
+ const content = fs.readFileSync(jsonFilePath, 'utf-8');
73
+ const parsed = JSON.parse(content);
74
+ // The web app export wraps in { exportedAt, version, ...templateData }
75
+ const templateData = parsed.id ? parsed : { ...parsed };
76
+ if (!templateData.name || !templateData.fields || !templateData.pdfInfo) {
77
+ throw new Error('Formato de template invalido. Asegurese de exportar desde la app web.');
78
+ }
79
+ // Extract PDF from base64 dataUrl
80
+ const dataUrl = templateData.pdfInfo.dataUrl;
81
+ if (!dataUrl) {
82
+ throw new Error('El template no contiene el PDF embebido (dataUrl)');
83
+ }
84
+ this.ensureDir();
85
+ const safeName = templateData.name.replace(/[^a-zA-Z0-9_\-]/g, '_');
86
+ const pdfFileName = `${safeName}.pdf`;
87
+ const pdfPath = path.join(TEMPLATES_DIR, pdfFileName);
88
+ // Decode base64 and save PDF
89
+ const base64Data = dataUrl.split(',')[1] || dataUrl;
90
+ const pdfBuffer = Buffer.from(base64Data, 'base64');
91
+ fs.writeFileSync(pdfPath, pdfBuffer);
92
+ // Create template without the large dataUrl
93
+ const template = {
94
+ id: `template_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
95
+ name: templateData.name,
96
+ description: templateData.description || '',
97
+ createdAt: new Date().toISOString(),
98
+ updatedAt: new Date().toISOString(),
99
+ version: templateData.version || '1.0.0',
100
+ pdfPath,
101
+ pdfInfo: {
102
+ name: templateData.pdfInfo.name,
103
+ size: pdfBuffer.length,
104
+ pageCount: templateData.pdfInfo.pageCount
105
+ },
106
+ fields: templateData.fields,
107
+ analysis: templateData.analysis || this.buildAnalysis(templateData.fields),
108
+ metadata: { totalUsages: 0 }
109
+ };
110
+ // Save template JSON
111
+ this.save(template);
112
+ return template;
113
+ }
114
+ /** Build a minimal TemplateAnalysis from fields */
115
+ static buildAnalysis(fields) {
116
+ const groups = {};
117
+ for (const f of fields) {
118
+ const name = f.fieldName || f.name;
119
+ if (!groups[name])
120
+ groups[name] = { fieldName: name, count: 0, fields: [], pageDistribution: {} };
121
+ groups[name].count++;
122
+ groups[name].fields.push(f);
123
+ groups[name].pageDistribution[f.pageNumber] = (groups[name].pageDistribution[f.pageNumber] || 0) + 1;
124
+ }
125
+ const fieldGroups = Object.values(groups);
126
+ const counts = fieldGroups.map((g) => g.count);
127
+ return {
128
+ totalFields: fields.length,
129
+ fieldGroups,
130
+ documentCapacity: fieldGroups.reduce((s, g) => s + g.count, 0),
131
+ maxFieldCount: Math.max(...counts, 0),
132
+ minFieldCount: Math.min(...counts, 0),
133
+ requiredColumns: fieldGroups.map((g) => g.fieldName),
134
+ isBalanced: new Set(counts).size <= 1
135
+ };
136
+ }
137
+ }
@@ -0,0 +1,20 @@
1
+ export declare const detectFieldsSchema: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ pdf_path: {
8
+ type: string;
9
+ description: string;
10
+ };
11
+ };
12
+ required: string[];
13
+ };
14
+ };
15
+ export declare function handleDetectFields(args: any): Promise<{
16
+ content: {
17
+ type: "text";
18
+ text: string;
19
+ }[];
20
+ }>;
@@ -0,0 +1,41 @@
1
+ import { PdfService } from '../services/pdfService.js';
2
+ export const detectFieldsSchema = {
3
+ name: 'autoform_detect_fields',
4
+ description: `Detecta campos de formulario AcroForm en un PDF interactivo (creado con Adobe Acrobat, LibreOffice, etc).
5
+
6
+ CUANDO USAR: El PDF es un formulario donde puedes hacer click y escribir. Si el PDF es una imagen/diseño estatico (certificado, diploma), esta tool dira "sin campos" — en ese caso usa autoform_get_pdf_info + analisis visual.
7
+
8
+ DESPUES DE DETECTAR: Usa autoform_fill_pdf para llenar los campos encontrados.`,
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: {
12
+ pdf_path: {
13
+ type: 'string',
14
+ description: 'Ruta absoluta al archivo PDF'
15
+ }
16
+ },
17
+ required: ['pdf_path']
18
+ }
19
+ };
20
+ export async function handleDetectFields(args) {
21
+ const { pdf_path } = args;
22
+ const { fields, hasAcroform } = await PdfService.detectFields(pdf_path);
23
+ let summary;
24
+ if (fields.length === 0) {
25
+ summary = `El PDF "${pdf_path}" no contiene campos AcroForm.\n\nPara usar este PDF, defina los campos manualmente en la app web de AutoForm y exporte la plantilla como JSON. Luego impórtela con autoform_import_template.`;
26
+ }
27
+ else {
28
+ summary = `Encontrados ${fields.length} campos AcroForm:\n\n${fields.map(f => `- ${f.name} (${f.type}) — Pag ${f.page}, pos: ${Math.round(f.x)},${Math.round(f.y)} ${Math.round(f.width)}x${Math.round(f.height)}${f.value ? ` = "${f.value}"` : ''}`).join('\n')}`;
29
+ }
30
+ return {
31
+ content: [{
32
+ type: 'text',
33
+ text: JSON.stringify({
34
+ has_acroform: hasAcroform,
35
+ total_fields: fields.length,
36
+ fields,
37
+ summary
38
+ }, null, 2)
39
+ }]
40
+ };
41
+ }