@larkup/tool-doc-editor 0.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,359 @@
1
+ /**
2
+ * Python code templates for sandbox-based document editing.
3
+ *
4
+ * These scripts run inside the Docker sandbox with:
5
+ * - pypdf: PDF form field filling
6
+ * - python-docx: Word document editing
7
+ * - python-pptx: PowerPoint editing
8
+ * - Pillow: Image processing
9
+ *
10
+ * All scripts expect input files in /sandbox/input/ and output to /sandbox/output/
11
+ */
12
+ /* ------------------------------------------------------------------ */
13
+ /* PDF — Fill form fields using pypdf */
14
+ /* ------------------------------------------------------------------ */
15
+ export function generateFillPDFScript(edits) {
16
+ const editsJson = JSON.stringify(edits);
17
+ return `
18
+ import json
19
+ import sys
20
+ from pypdf import PdfReader, PdfWriter
21
+
22
+ # Load the PDF
23
+ reader = PdfReader("/sandbox/input/document.pdf")
24
+ writer = PdfWriter()
25
+
26
+ # Copy all pages
27
+ for page in reader.pages:
28
+ writer.add_page(page)
29
+
30
+ # Copy existing form fields
31
+ if reader.is_form:
32
+ writer.clone_reader_document_root(reader)
33
+
34
+ # Parse edits
35
+ edits = json.loads('''${editsJson}''')
36
+
37
+ # Build field value map (field_name -> value)
38
+ field_map = {}
39
+ for edit in edits:
40
+ field_map[edit["fieldId"]] = edit["value"]
41
+
42
+ # Apply form field fills
43
+ if reader.is_form:
44
+ fields = reader.get_fields()
45
+ update_dict = {}
46
+ for field_name, field_obj in (fields or {}).items():
47
+ for edit_id, value in field_map.items():
48
+ # Match by name or by detected ID pattern
49
+ if field_name == edit_id or edit_id.endswith(field_name):
50
+ update_dict[field_name] = value
51
+ break
52
+
53
+ if update_dict:
54
+ writer.update_page_form_field_values(writer.pages[0], update_dict)
55
+
56
+ # Save
57
+ with open("/sandbox/output/document.pdf", "wb") as f:
58
+ writer.write(f)
59
+
60
+ print(f"SUCCESS: Updated {len(field_map)} fields")
61
+ `;
62
+ }
63
+ /* ------------------------------------------------------------------ */
64
+ /* PDF — Extract full text using pypdf */
65
+ /* ------------------------------------------------------------------ */
66
+ export function generateExtractPDFTextScript() {
67
+ return `
68
+ import json
69
+ from pypdf import PdfReader
70
+
71
+ reader = PdfReader("/sandbox/input/document.pdf")
72
+ pages = []
73
+ fields = []
74
+
75
+ for i, page in enumerate(reader.pages):
76
+ text = page.extract_text() or ""
77
+ pages.append({
78
+ "index": i,
79
+ "text": text,
80
+ })
81
+
82
+ # Smart signature detection
83
+ lines = text.split('\\n')
84
+ for line in lines:
85
+ lower_line = line.lower()
86
+ if "signature:" in lower_line or "sign here" in lower_line or "date:" in lower_line or "_______" in lower_line:
87
+ if "signature" in lower_line or "sign" in lower_line:
88
+ fields.append({
89
+ "name": "detected_signature_" + str(i),
90
+ "type": "signature_placeholder",
91
+ "value": "",
92
+ "pageIndex": i,
93
+ "context": line.strip()
94
+ })
95
+
96
+ # Extract form fields
97
+ if reader.is_form:
98
+ form_fields = reader.get_fields() or {}
99
+ for name, field in form_fields.items():
100
+ field_type = str(field.get("/FT", ""))
101
+ value = str(field.get("/V", ""))
102
+ fields.append({
103
+ "name": name,
104
+ "type": field_type,
105
+ "value": value,
106
+ "pageIndex": 0 # We might not know the exact page from get_fields easily, default to 0
107
+ })
108
+
109
+ result = {
110
+ "pages": pages,
111
+ "fields": fields,
112
+ "totalPages": len(reader.pages),
113
+ "metadata": dict(reader.metadata or {}),
114
+ }
115
+
116
+ print("__RESULT__:" + json.dumps(result, default=str))
117
+ `;
118
+ }
119
+ /* ------------------------------------------------------------------ */
120
+ /* DOCX — Edit content using python-docx */
121
+ /* ------------------------------------------------------------------ */
122
+ export function generateEditDOCXScript(edits) {
123
+ const editsJson = JSON.stringify(edits);
124
+ return `
125
+ import json
126
+ from docx import Document
127
+
128
+ doc = Document("/sandbox/input/document.docx")
129
+ edits = json.loads('''${editsJson}''')
130
+
131
+ updated = 0
132
+ for edit in edits:
133
+ edit_type = edit.get("type", "replace_text")
134
+
135
+ if edit_type == "replace_text":
136
+ search = edit.get("search", "")
137
+ replacement = edit.get("text", "")
138
+
139
+ for para in doc.paragraphs:
140
+ if search in para.text:
141
+ # Preserve formatting by replacing in runs
142
+ full_text = ""
143
+ for run in para.runs:
144
+ full_text += run.text
145
+
146
+ if search in full_text:
147
+ # Simple replacement — replace in runs
148
+ remaining = search
149
+ for run in para.runs:
150
+ if remaining and remaining in run.text:
151
+ run.text = run.text.replace(remaining, replacement, 1)
152
+ remaining = ""
153
+ updated += 1
154
+ break
155
+ elif remaining and run.text and remaining.startswith(run.text):
156
+ remaining = remaining[len(run.text):]
157
+ run.text = ""
158
+
159
+ # Also check tables
160
+ for table in doc.tables:
161
+ for row in table.rows:
162
+ for cell in row.cells:
163
+ for para in cell.paragraphs:
164
+ if search in para.text:
165
+ for run in para.runs:
166
+ if search in run.text:
167
+ run.text = run.text.replace(search, replacement, 1)
168
+ updated += 1
169
+
170
+ elif edit_type == "insert_text":
171
+ text = edit.get("text", "")
172
+ position = edit.get("position", len(doc.paragraphs))
173
+ if position < len(doc.paragraphs):
174
+ para = doc.paragraphs[position]
175
+ para.insert_paragraph_before(text)
176
+ else:
177
+ doc.add_paragraph(text)
178
+ updated += 1
179
+
180
+ doc.save("/sandbox/output/document.docx")
181
+ print(f"SUCCESS: Applied {updated} edits")
182
+ `;
183
+ }
184
+ /* ------------------------------------------------------------------ */
185
+ /* DOCX — Fill form fields (content controls / placeholder text) */
186
+ /* ------------------------------------------------------------------ */
187
+ export function generateFillDOCXScript(edits) {
188
+ const editsJson = JSON.stringify(edits);
189
+ return `
190
+ import json
191
+ from docx import Document
192
+
193
+ doc = Document("/sandbox/input/document.docx")
194
+ edits = json.loads('''${editsJson}''')
195
+
196
+ # Build lookup
197
+ field_map = {edit["fieldId"]: edit["value"] for edit in edits}
198
+
199
+ updated = 0
200
+
201
+ # Strategy 1: Look for common form patterns like "Label: ___" or "Label: "
202
+ for para in doc.paragraphs:
203
+ for field_id, value in field_map.items():
204
+ # The field_id contains the label name from detection
205
+ label = field_id.replace("detected_", "").strip()
206
+
207
+ # Check if paragraph contains "Label:" pattern
208
+ text = para.text
209
+ for run in para.runs:
210
+ if "___" in run.text or (run.text.strip().endswith(":") and not value):
211
+ # Replace underscores with value
212
+ run.text = run.text.replace("___", value).rstrip("_")
213
+ updated += 1
214
+
215
+ # Strategy 2: Check tables (common in forms)
216
+ for table in doc.tables:
217
+ for row in table.rows:
218
+ cells = row.cells
219
+ for i, cell in enumerate(cells):
220
+ cell_text = cell.text.strip().rstrip(":")
221
+ # If this cell's text matches a field name, fill the next cell
222
+ for field_id, value in field_map.items():
223
+ if cell_text.lower() == field_id.lower() and i + 1 < len(cells):
224
+ next_cell = cells[i + 1]
225
+ for para in next_cell.paragraphs:
226
+ if para.runs:
227
+ para.runs[0].text = value
228
+ else:
229
+ para.text = value
230
+ updated += 1
231
+
232
+ doc.save("/sandbox/output/document.docx")
233
+ print(f"SUCCESS: Filled {updated} fields")
234
+ `;
235
+ }
236
+ /* ------------------------------------------------------------------ */
237
+ /* PPTX — Extract text from slides */
238
+ /* ------------------------------------------------------------------ */
239
+ export function generateExtractPPTXScript() {
240
+ return `
241
+ import json
242
+ from pptx import Presentation
243
+ from pptx.util import Inches, Pt
244
+
245
+ prs = Presentation("/sandbox/input/document.pptx")
246
+ slides = []
247
+
248
+ for i, slide in enumerate(prs.slides):
249
+ texts = []
250
+ fields = []
251
+
252
+ for shape in slide.shapes:
253
+ if shape.has_text_frame:
254
+ for para in shape.text_frame.paragraphs:
255
+ text = para.text.strip()
256
+ if text:
257
+ texts.append(text)
258
+
259
+ # Detect form-like patterns
260
+ if ":" in text and text.endswith(":"):
261
+ fields.append({
262
+ "name": text.rstrip(":").strip(),
263
+ "type": "text",
264
+ "value": "",
265
+ "slideIndex": i,
266
+ })
267
+ elif "___" in text:
268
+ label = text.split("___")[0].strip().rstrip(":")
269
+ if label:
270
+ fields.append({
271
+ "name": label,
272
+ "type": "text",
273
+ "value": "",
274
+ "slideIndex": i,
275
+ })
276
+
277
+ slides.append({
278
+ "index": i,
279
+ "text": "\\n".join(texts),
280
+ "fields": fields,
281
+ })
282
+
283
+ result = {
284
+ "slides": slides,
285
+ "totalSlides": len(prs.slides),
286
+ }
287
+
288
+ print("__RESULT__:" + json.dumps(result, default=str))
289
+ `;
290
+ }
291
+ /* ------------------------------------------------------------------ */
292
+ /* PPTX — Edit slide text */
293
+ /* ------------------------------------------------------------------ */
294
+ export function generateEditPPTXScript(edits) {
295
+ const editsJson = JSON.stringify(edits);
296
+ return `
297
+ import json
298
+ from pptx import Presentation
299
+
300
+ prs = Presentation("/sandbox/input/document.pptx")
301
+ edits = json.loads('''${editsJson}''')
302
+
303
+ updated = 0
304
+ for edit in edits:
305
+ slide_index = edit.get("pageIndex", 0)
306
+ if slide_index >= len(prs.slides):
307
+ continue
308
+
309
+ slide = prs.slides[slide_index]
310
+ edit_type = edit.get("type", "replace_text")
311
+
312
+ if edit_type == "replace_text":
313
+ search = edit.get("search", "")
314
+ replacement = edit.get("text", "")
315
+
316
+ for shape in slide.shapes:
317
+ if shape.has_text_frame:
318
+ for para in shape.text_frame.paragraphs:
319
+ for run in para.runs:
320
+ if search in run.text:
321
+ run.text = run.text.replace(search, replacement)
322
+ updated += 1
323
+
324
+ prs.save("/sandbox/output/document.pptx")
325
+ print(f"SUCCESS: Applied {updated} edits")
326
+ `;
327
+ }
328
+ /* ------------------------------------------------------------------ */
329
+ /* PPTX — Fill form fields in slides */
330
+ /* ------------------------------------------------------------------ */
331
+ export function generateFillPPTXScript(edits) {
332
+ const editsJson = JSON.stringify(edits);
333
+ return `
334
+ import json
335
+ from pptx import Presentation
336
+
337
+ prs = Presentation("/sandbox/input/document.pptx")
338
+ edits = json.loads('''${editsJson}''')
339
+
340
+ field_map = {edit["fieldId"]: edit["value"] for edit in edits}
341
+ updated = 0
342
+
343
+ for slide in prs.slides:
344
+ for shape in slide.shapes:
345
+ if shape.has_text_frame:
346
+ for para in shape.text_frame.paragraphs:
347
+ full_text = para.text
348
+ for field_id, value in field_map.items():
349
+ # Replace "___" blanks after field labels
350
+ if "___" in full_text:
351
+ for run in para.runs:
352
+ if "___" in run.text:
353
+ run.text = run.text.replace("___", value, 1)
354
+ updated += 1
355
+
356
+ prs.save("/sandbox/output/document.pptx")
357
+ print(f"SUCCESS: Filled {updated} fields")
358
+ `;
359
+ }
@@ -0,0 +1,158 @@
1
+ /** Supported document types */
2
+ export type DocumentType = 'pdf' | 'docx' | 'pptx' | 'txt' | 'xlsx';
3
+ /** A single editable field detected in a document */
4
+ export interface DocumentField {
5
+ /** Unique field identifier within the document */
6
+ id: string;
7
+ /** Human-readable field name / label */
8
+ name: string;
9
+ /** Field type */
10
+ type: 'text' | 'textarea' | 'checkbox' | 'radio' | 'date' | 'number' | 'select' | 'signature_placeholder';
11
+ /** Current value (empty if unfilled) */
12
+ value: string;
13
+ /** Page/slide index (0-based) */
14
+ pageIndex: number;
15
+ /** Whether the field is required (if detectable) */
16
+ required?: boolean;
17
+ /** Placeholder or tooltip text */
18
+ placeholder?: string;
19
+ /** Options for select/radio fields */
20
+ options?: string[];
21
+ /** Bounding box for overlay positioning (PDF) — percentages of page */
22
+ bbox?: {
23
+ x: number;
24
+ y: number;
25
+ width: number;
26
+ height: number;
27
+ };
28
+ /** Context around the field (for smart detection) */
29
+ context?: string;
30
+ }
31
+ /** A single page or slide of a parsed document */
32
+ export interface DocumentPage {
33
+ /** 0-based page/slide index */
34
+ index: number;
35
+ /** Extracted text content */
36
+ text: string;
37
+ /** HTML rendering (for DOCX via mammoth) */
38
+ html?: string;
39
+ /** Fields on this page */
40
+ fields: DocumentField[];
41
+ /** Page dimensions in points (PDF) */
42
+ dimensions?: {
43
+ width: number;
44
+ height: number;
45
+ };
46
+ }
47
+ /** Result of parsing a document */
48
+ export interface ParsedDocument {
49
+ /** Session ID for tracking edits */
50
+ sessionId: string;
51
+ /** Original filename */
52
+ fileName: string;
53
+ /** Detected document type */
54
+ type: DocumentType;
55
+ /** MIME type */
56
+ mimeType: string;
57
+ /** File size in bytes */
58
+ fileSize: number;
59
+ /** Pages/slides */
60
+ pages: DocumentPage[];
61
+ /** All fields across all pages (flat) */
62
+ fields: DocumentField[];
63
+ /** Full raw text extraction */
64
+ rawText: string;
65
+ /** Document metadata (author, title, dates, etc.) */
66
+ metadata: Record<string, string>;
67
+ /** Total page/slide count */
68
+ totalPages: number;
69
+ }
70
+ /** An edit operation to apply to a document */
71
+ export interface FieldEdit {
72
+ /** Field ID to update */
73
+ fieldId: string;
74
+ /** New value */
75
+ value: string;
76
+ /** Type of edit */
77
+ type: 'fill' | 'replace' | 'append' | 'clear';
78
+ }
79
+ /** A content-level edit (not field-based) */
80
+ export interface ContentEdit {
81
+ /** Page/slide index */
82
+ pageIndex: number;
83
+ /** Type of content edit */
84
+ type: 'replace_text' | 'insert_text' | 'delete_text';
85
+ /** Search text (for replace) */
86
+ search?: string;
87
+ /** Replacement or insertion text */
88
+ text: string;
89
+ /** Position for insert (paragraph index) */
90
+ position?: number;
91
+ }
92
+ /** Signature details for applying to a document */
93
+ export interface SignatureData {
94
+ /** The mode of the signature */
95
+ mode: 'type' | 'draw' | 'upload';
96
+ /** Base64 image of the signature (for draw/upload) */
97
+ imagePayload?: string;
98
+ /** Typed text for the signature */
99
+ text?: string;
100
+ /** Font family for typed text */
101
+ font?: string;
102
+ /** Color (hex) */
103
+ color?: string;
104
+ /** Optional date to include */
105
+ date?: string;
106
+ /** Optional extra text */
107
+ extraText?: string;
108
+ /** Page index (0-based) where the signature should be placed */
109
+ pageIndex: number;
110
+ /** Approximate X coordinate (if known) */
111
+ x?: number;
112
+ /** Approximate Y coordinate (if known) */
113
+ y?: number;
114
+ /** Signature scale multiplier */
115
+ scale?: number;
116
+ /** Optional base64 to use instead of current session base64 (for undo/replace) */
117
+ base64Override?: string;
118
+ /** The text context detected near the signature line (e.g. "Datum, Unterschrift des Empfängers") */
119
+ detectedContext?: string;
120
+ /** User-provided note about placement (e.g. "bottom right corner") */
121
+ placementNote?: string;
122
+ }
123
+ /** Active editing session */
124
+ export interface EditorSession {
125
+ /** Unique session ID */
126
+ id: string;
127
+ /** Original filename */
128
+ fileName: string;
129
+ /** Document type */
130
+ type: DocumentType;
131
+ /** MIME type */
132
+ mimeType: string;
133
+ /** Original file as base64 */
134
+ originalFileBase64: string;
135
+ /** Current (modified) file as base64 */
136
+ currentFileBase64: string;
137
+ /** Parsed document state */
138
+ parsed: ParsedDocument;
139
+ /** Applied edits history */
140
+ edits: FieldEdit[];
141
+ /** Created timestamp */
142
+ createdAt: string;
143
+ /** Last modified timestamp */
144
+ updatedAt: string;
145
+ }
146
+ /** Result of applying edits */
147
+ export interface EditResult {
148
+ /** Whether the edit succeeded */
149
+ success: boolean;
150
+ /** Updated parsed document */
151
+ parsed: ParsedDocument;
152
+ /** Updated file as base64 */
153
+ fileBase64: string;
154
+ /** Error message if failed */
155
+ error?: string;
156
+ /** Fields that were updated */
157
+ updatedFields: string[];
158
+ }
package/dist/types.js ADDED
@@ -0,0 +1,4 @@
1
+ /* ------------------------------------------------------------------ */
2
+ /* Types for the Document Editor / Form Filling tool */
3
+ /* ------------------------------------------------------------------ */
4
+ export {};
package/package.json ADDED
@@ -0,0 +1,68 @@
1
+ {
2
+ "name": "@larkup/tool-doc-editor",
3
+ "version": "0.2.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "type": "module",
8
+ "description": "Larkup marketplace tool: AI-powered form filling and document editing.",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./dist/index.d.ts",
14
+ "import": "./dist/index.js",
15
+ "default": "./dist/index.js"
16
+ },
17
+ "./types": {
18
+ "types": "./dist/types.d.ts",
19
+ "import": "./dist/types.js",
20
+ "default": "./dist/types.js"
21
+ },
22
+ "./parsers": {
23
+ "types": "./dist/parsers.d.ts",
24
+ "import": "./dist/parsers.js",
25
+ "default": "./dist/parsers.js"
26
+ },
27
+ "./editor": {
28
+ "types": "./dist/editor.d.ts",
29
+ "import": "./dist/editor.js",
30
+ "default": "./dist/editor.js"
31
+ }
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src",
36
+ "tool.manifest.json",
37
+ "README.md"
38
+ ],
39
+ "dependencies": {
40
+ "jszip": "^3.10.1",
41
+ "mammoth": "^1.12.0",
42
+ "pdf-lib": "^1.17.1",
43
+ "@larkup/sandbox": "0.1.1",
44
+ "@larkup/core": "0.2.0"
45
+ },
46
+ "peerDependencies": {
47
+ "@larkup/marketplace": ">=0.1.0"
48
+ },
49
+ "peerDependenciesMeta": {
50
+ "@larkup/marketplace": {
51
+ "optional": true
52
+ }
53
+ },
54
+ "devDependencies": {
55
+ "typescript": "5.7.3",
56
+ "@larkup/marketplace": "0.1.1"
57
+ },
58
+ "license": "Apache-2.0",
59
+ "repository": {
60
+ "type": "git",
61
+ "url": "https://github.com/Larkup-AI/larkup-rag",
62
+ "directory": "packages/tools/doc-editor"
63
+ },
64
+ "scripts": {
65
+ "build": "tsc --outDir dist",
66
+ "type-check": "tsc --noEmit"
67
+ }
68
+ }