diff --git a/.changeset/replace-create-field-with-detect-fields.md b/.changeset/replace-create-field-with-detect-fields.md new file mode 100644 index 0000000..5b69de9 --- /dev/null +++ b/.changeset/replace-create-field-with-detect-fields.md @@ -0,0 +1,15 @@ +--- +"@simplepdf/react-embed-pdf": major +--- + +Replaces `createField` with `detectFields` for automatic form field detection. This is a breaking change: the `createField` action and `CreateFieldOptions` type have been removed. + +If you are not using `actions.createField(...)` or `sendEvent("CREATE_FIELD", ...)`, you can safely update to this new major version. + +```ts +// Before (removed) +await actions.createField({ type: "TEXT", page: 1, x: 100, y: 700, width: 200, height: 30 }); + +// After +await actions.detectFields(); +``` diff --git a/README.md b/README.md index 86e01e6..8e42f4e 100644 --- a/README.md +++ b/README.md @@ -189,7 +189,7 @@ SimplePDF Embed uses a **fully client-side architecture** for PDF processing: | Limitation | Description | Workaround | | --------------------------------- | ----------------------------------------------- | ---------------------------------------------------------- | -| **No server-side PDF generation** | Cannot generate PDFs from templates server-side | Use client-side field creation via `createField()` | +| **No server-side PDF generation** | Cannot generate PDFs from templates server-side | Use client-side field detection via `detectFields()` | | **No bulk processing** | Cannot process multiple PDFs in batch | Process sequentially or use dedicated server-side library | | **No programmatic PDF retrieval** | Cannot get modified PDF as Blob/Base64 in JS | Use webhooks + server storage for programmatic access | | **No persistent storage** | PDFs don't persist without user action | Use `companyIdentifier` for server-side submission storage | @@ -285,7 +285,7 @@ Currently, page manipulation (add/remove/re-arrange/rotate) is only available th | ---------------------- | ---------------------------------------------------------------------------------------------------------- | | `goTo` | Navigate to a specific page | | `selectTool` | Select a tool (`TEXT`, `BOXED_TEXT`, `CHECKBOX`, `PICTURE`, `SIGNATURE`) or `null` for cursor | -| `createField` | Create a field at a specified position | +| `detectFields` | Automatically detect form fields in the document | | `removeFields` | Remove fields by ID, by page, or all fields | | `getDocumentContent` | Extract text content from the document | | `submit` | Submit the document (with optional device download) | diff --git a/documentation/IFRAME.md b/documentation/IFRAME.md index f72c6a2..6566bbc 100644 --- a/documentation/IFRAME.md +++ b/documentation/IFRAME.md @@ -185,16 +185,8 @@ await sendEvent("GO_TO", { page: 3 }); // Select a tool await sendEvent("SELECT_TOOL", { tool: "TEXT" }); // or "CHECKBOX", "SIGNATURE", "PICTURE", "BOXED_TEXT", null -// Create a field -await sendEvent("CREATE_FIELD", { - type: "TEXT", - page: 1, - x: 100, - y: 700, - width: 200, - height: 30, - value: "Hello World", -}); +// Detect fields in the document +await sendEvent("DETECT_FIELDS", {}); // Remove all fields (or specific ones) await sendEvent("REMOVE_FIELDS", {}); // Remove all @@ -290,34 +282,11 @@ Select a drawing tool or return to cursor mode. | ------ | ---------------- | -------- | ---------------------------------------------------------------------------------------- | | `tool` | `string \| null` | Yes | `"TEXT"`, `"BOXED_TEXT"`, `"CHECKBOX"`, `"SIGNATURE"`, `"PICTURE"`, or `null` for cursor | -#### CREATE_FIELD - -Create a new field on the document. +#### DETECT_FIELDS -| Field | Type | Required | Description | -| -------- | -------- | -------- | --------------------------------------------------------------------- | -| `type` | `string` | Yes | `"TEXT"`, `"BOXED_TEXT"`, `"CHECKBOX"`, `"SIGNATURE"`, or `"PICTURE"` | -| `page` | `number` | Yes | Page number (1-indexed) | -| `x` | `number` | Yes | X coordinate (PDF points from left) | -| `y` | `number` | Yes | Y coordinate (PDF points from bottom) | -| `width` | `number` | Yes | Field width in PDF points | -| `height` | `number` | Yes | Field height in PDF points | -| `value` | `string` | No | Initial value (see value formats below) | +Automatically detect form fields in the document. -**Value formats by field type:** - -- `TEXT` / `BOXED_TEXT`: Plain text content -- `CHECKBOX`: `"checked"` or `"unchecked"` -- `PICTURE`: Data URL (base64) -- `SIGNATURE`: Data URL (base64 image) or plain text (generates a typed signature) - -**Response data:** - -```json -{ - "field_id": "f_kj8n2hd9x3m1p" -} -``` +_No data fields required._ #### REMOVE_FIELDS diff --git a/examples/with-playwright-automation/README.md b/examples/with-playwright-automation/README.md index 3d1a709..e05fb3d 100644 --- a/examples/with-playwright-automation/README.md +++ b/examples/with-playwright-automation/README.md @@ -1,244 +1,33 @@ # SimplePDF Editor Automation -Playwright-based CLI tool for programmatically creating and positioning fields in PDF documents using the SimplePDF editor. - -## Features - -- Create TEXT, BOXED_TEXT, CHECKBOX, SIGNATURE, and PICTURE fields -- Position fields using PDF standard coordinates (bottom-left origin) -- Pre-fill field values including typed signatures -- Browser opens for visual inspection after field creation +Playwright-based CLI tool for automatically detecting form fields in PDF documents using the SimplePDF editor. ## Quick Start ```bash npm install -npx tsx src/index.ts example.config.json +npx tsx src/index.ts https://example.com/form.pdf ``` ## Usage ```bash -npx tsx src/index.ts [options] +npx tsx src/index.ts [options] + +Arguments: + document URL or local file path to a PDF Options: --company-identifier Your SimplePDF company identifier (default: embed) --help Show help ``` -### Using Your Company Identifier +### Examples ```bash -npx tsx src/index.ts config.json --company-identifier mycompany -``` - -This connects to `https://mycompany.simplepdf.com`. - -## Configuration - -Create a JSON configuration file: - -```json -{ - "document": "https://example.com/document.pdf", - "fields": [...] -} -``` - -### Document Source - -| Format | Example | -|--------|---------| -| URL | `"https://example.com/doc.pdf"` | -| Local file | `"./documents/form.pdf"` | - -## Field Types - -### TEXT - -Single-line text input. - -```json -{ - "type": "TEXT", - "x": 100, - "y": 700, - "width": 200, - "height": 20, - "page": 1, - "value": "John Doe" -} -``` - -### BOXED_TEXT - -Multi-line text with border. - -```json -{ - "type": "BOXED_TEXT", - "x": 100, - "y": 600, - "width": 300, - "height": 100, - "page": 1, - "value": "Additional notes here..." -} -``` - -### CHECKBOX - -Checkable box. Must be square (equal width/height). - -```json -{ - "type": "CHECKBOX", - "x": 100, - "y": 550, - "width": 12, - "height": 12, - "page": 1, - "value": true -} -``` - -### SIGNATURE - -Signature field with multiple value formats. - -```json -{ - "type": "SIGNATURE", - "x": 100, - "y": 450, - "width": 200, - "height": 60, - "page": 1, - "value": "John Doe" -} -``` - -**Value formats:** - -| Format | Example | Result | -|--------|---------|--------| -| Plain text | `"John Doe"` | Typed signature (cursive font) | -| URL | `"https://example.com/sig.png"` | Drawn signature from image | -| Data URL | `"data:image/png;base64,..."` | Drawn signature from base64 | -| Local file | `"./signatures/john.png"` | Drawn signature from file | - -### PICTURE - -Image field. - -```json -{ - "type": "PICTURE", - "x": 100, - "y": 300, - "width": 150, - "height": 150, - "page": 1, - "value": "https://example.com/photo.jpg" -} -``` - -**Value formats:** URL, data URL, or local file path. - -## Coordinate System - -Uses PDF standard coordinates: - -``` -┌─────────────────────────────┐ -│ │ ↑ -│ │ │ -│ PDF Page │ │ Y increases -│ │ │ -│ │ │ -└─────────────────────────────┘ -(0,0) ───────────────────────→ - X increases -``` - -- **Origin**: Bottom-left corner of page -- **Units**: Points (1/72 inch) -- **Y-axis**: Increases upward - -## Examples - -### Basic Form Fill - -```json -{ - "document": "https://cdn.simplepdf.com/simple-pdf/assets/sample.pdf", - "fields": [ - { - "type": "TEXT", - "x": 72, - "y": 700, - "width": 200, - "height": 14, - "page": 1, - "value": "John" - }, - { - "type": "TEXT", - "x": 320, - "y": 700, - "width": 200, - "height": 14, - "page": 1, - "value": "Doe" - }, - { - "type": "SIGNATURE", - "x": 72, - "y": 100, - "width": 200, - "height": 60, - "page": 1, - "value": "John Doe" - } - ] -} -``` - -### Multi-Page Document - -```json -{ - "document": "./documents/multi-page.pdf", - "fields": [ - { - "type": "TEXT", - "x": 72, - "y": 700, - "width": 200, - "height": 14, - "page": 1, - "value": "Page 1 content" - }, - { - "type": "TEXT", - "x": 72, - "y": 700, - "width": 200, - "height": 14, - "page": 2, - "value": "Page 2 content" - }, - { - "type": "SIGNATURE", - "x": 72, - "y": 100, - "width": 200, - "height": 60, - "page": 3, - "value": "Final Signature" - } - ] -} +npx tsx src/index.ts https://example.com/form.pdf +npx tsx src/index.ts ./documents/form.pdf +npx tsx src/index.ts https://example.com/form.pdf --company-identifier mycompany ``` ## How It Works @@ -247,9 +36,8 @@ The tool uses the SimplePDF editor's iframe postMessage API: 1. Embeds the editor in an iframe 2. Waits for `DOCUMENT_LOADED` event -3. Sends `REMOVE_FIELDS` to remove existing fields -4. Sends `CREATE_FIELD` for each configured field -5. Leaves browser open for inspection +3. Sends `DETECT_FIELDS` to automatically detect form fields +4. Leaves browser open for inspection ## Requirements diff --git a/examples/with-playwright-automation/example.config.json b/examples/with-playwright-automation/example.config.json deleted file mode 100644 index 96f2fc9..0000000 --- a/examples/with-playwright-automation/example.config.json +++ /dev/null @@ -1,163 +0,0 @@ -{ - "document": "https://cdn.simplepdf.com/simple-pdf/assets/sample.pdf", - "fields": [ - { - "type": "TEXT", - "x": 72, - "y": 700, - "width": 200, - "height": 14, - "page": 1, - "value": "John" - }, - { - "type": "TEXT", - "x": 320, - "y": 700, - "width": 200, - "height": 14, - "page": 1, - "value": "Doe" - }, - { - "type": "TEXT", - "x": 72, - "y": 650, - "width": 200, - "height": 14, - "page": 1, - "value": "john.doe@example.com" - }, - { - "type": "TEXT", - "x": 320, - "y": 650, - "width": 200, - "height": 14, - "page": 1, - "value": "+1 555-123-4567" - }, - { - "type": "BOXED_TEXT", - "x": 72, - "y": 600, - "width": 150, - "height": 14, - "page": 1, - "value": "2024-01-15" - }, - { - "type": "BOXED_TEXT", - "x": 320, - "y": 600, - "width": 150, - "height": 14, - "page": 1, - "value": "REF-001" - }, - { - "type": "CHECKBOX", - "x": 72, - "y": 550, - "width": 12, - "height": 12, - "page": 1, - "value": true - }, - { - "type": "CHECKBOX", - "x": 320, - "y": 550, - "width": 12, - "height": 12, - "page": 1, - "value": false - }, - { - "type": "SIGNATURE", - "x": 72, - "y": 450, - "width": 200, - "height": 60, - "page": 1 - }, - { - "type": "PICTURE", - "x": 320, - "y": 450, - "width": 100, - "height": 100, - "page": 1 - }, - { - "type": "TEXT", - "x": 72, - "y": 700, - "width": 200, - "height": 14, - "page": 2, - "value": "Additional Notes" - }, - { - "type": "TEXT", - "x": 320, - "y": 700, - "width": 200, - "height": 14, - "page": 2, - "value": "Section B" - }, - { - "type": "BOXED_TEXT", - "x": 72, - "y": 650, - "width": 200, - "height": 14, - "page": 2, - "value": "ABC-123" - }, - { - "type": "BOXED_TEXT", - "x": 320, - "y": 650, - "width": 200, - "height": 14, - "page": 2, - "value": "XYZ-789" - }, - { - "type": "CHECKBOX", - "x": 72, - "y": 600, - "width": 12, - "height": 12, - "page": 2, - "value": true - }, - { - "type": "CHECKBOX", - "x": 320, - "y": 600, - "width": 12, - "height": 12, - "page": 2, - "value": true - }, - { - "type": "SIGNATURE", - "x": 72, - "y": 450, - "width": 200, - "height": 60, - "page": 2 - }, - { - "type": "PICTURE", - "x": 320, - "y": 450, - "width": 100, - "height": 100, - "page": 2 - } - ] -} diff --git a/examples/with-playwright-automation/package.json b/examples/with-playwright-automation/package.json index 56bbc6e..cd18e23 100644 --- a/examples/with-playwright-automation/package.json +++ b/examples/with-playwright-automation/package.json @@ -13,8 +13,7 @@ "typecheck": "tsc --noEmit" }, "dependencies": { - "playwright": "^1.49.0", - "zod": "^4.3.4" + "playwright": "^1.49.0" }, "devDependencies": { "@types/node": "^22.10.0", diff --git a/examples/with-playwright-automation/src/automation.ts b/examples/with-playwright-automation/src/automation.ts index fca0ac6..56dbf80 100644 --- a/examples/with-playwright-automation/src/automation.ts +++ b/examples/with-playwright-automation/src/automation.ts @@ -1,22 +1,15 @@ import { chromium, Browser, Page } from 'playwright'; import * as fs from 'fs'; import * as path from 'path'; -import type { AutomationConfig } from './schema'; type AutomationErrorCode = - | 'document_load_failed' - | 'field_creation_failed' - | 'remove_fields_failed'; + | 'detect_fields_failed' + | 'document_load_failed'; type AutomationResult = | { success: true; data: null } | { success: false; error: { code: AutomationErrorCode; message: string } }; -type RunAutomationArgs = { - config: AutomationConfig; - baseUrl: string; -}; - type IframeEvent = { type: string; data?: Record; @@ -33,6 +26,7 @@ type RequestResultData = { request_id: string; result: { success: boolean; + data?: { detected_count?: number }; error?: { code: string; message: string }; }; }; @@ -163,39 +157,20 @@ const setupIframePage = async ({ }; }; -const resolveValueToString = ({ value }: { value: string }): string => { - if (value.startsWith('data:') || value.startsWith('http://') || value.startsWith('https://')) { - return value; - } +const isUrl = (value: string): boolean => value.startsWith('http://') || value.startsWith('https://'); - const absolutePath = path.isAbsolute(value) ? value : path.resolve(process.cwd(), value); +const readFileAsDataUrl = ({ filePath }: { filePath: string }): string => { + const absolutePath = path.isAbsolute(filePath) ? filePath : path.resolve(process.cwd(), filePath); if (!fs.existsSync(absolutePath)) { throw new Error(`File not found: ${absolutePath}`); } const buffer = fs.readFileSync(absolutePath); - const ext = path.extname(absolutePath).toLowerCase(); - const mimeType = (() => { - switch (ext) { - case '.jpg': - case '.jpeg': - return 'image/jpeg'; - case '.png': - return 'image/png'; - case '.gif': - return 'image/gif'; - case '.webp': - return 'image/webp'; - default: - return 'image/png'; - } - })(); - - return `data:${mimeType};base64,${buffer.toString('base64')}`; + return `data:application/pdf;base64,${buffer.toString('base64')}`; }; -const runAutomation = async ({ config, baseUrl }: RunAutomationArgs): Promise => { +const runAutomation = async ({ document, baseUrl }: { document: string; baseUrl: string }): Promise => { let browser: Browser | null = null; try { @@ -207,84 +182,45 @@ const runAutomation = async ({ config, baseUrl }: RunAutomationArgs): Promise { - if (field.value === undefined) { - return undefined; - } - - if (typeof field.value === 'boolean') { - return field.value; - } - - if (field.type === 'PICTURE' || field.type === 'SIGNATURE') { - return resolveValueToString({ value: field.value }); - } - - return field.value; - })(); - - const requestId = await sendEvent({ - type: 'CREATE_FIELD', - data: { - type: field.type, - x: field.x, - y: field.y, - width: field.width, - height: field.height, - page: field.page, - ...(fieldValue !== undefined ? { value: fieldValue } : {}), - }, - }); - - const result = await waitForEvent('REQUEST_RESULT', { requestId }); - - if (!isRequestResultData(result.event.data) || !result.event.data.result.success) { - const errorMessage = isRequestResultData(result.event.data) - ? result.event.data.result.error?.message ?? 'Unknown error' - : 'Invalid response'; - return { - success: false, - error: { - code: 'field_creation_failed', - message: `Failed to create ${field.type} field at page ${field.page}: ${errorMessage}`, - }, - }; - } - - console.log(` [${i + 1}/${config.fields.length}] Created ${field.type} on page ${field.page}`); - } - - console.log('All fields created'); await page.pause(); return { success: true, data: null }; @@ -300,18 +236,4 @@ const runAutomation = async ({ config, baseUrl }: RunAutomationArgs): Promise { - if (document.startsWith('http://') || document.startsWith('https://')) { - return `${baseUrl}/editor?open=${encodeURIComponent(document)}`; - } - - const absolutePath = path.isAbsolute(document) ? document : path.resolve(process.cwd(), document); - - if (!fs.existsSync(absolutePath)) { - throw new Error(`File not found: ${absolutePath}`); - } - - return `${baseUrl}/editor?localFile=${encodeURIComponent(absolutePath)}`; -}; - export { runAutomation, AutomationResult }; diff --git a/examples/with-playwright-automation/src/index.ts b/examples/with-playwright-automation/src/index.ts index 41bc0b9..9d74b2a 100644 --- a/examples/with-playwright-automation/src/index.ts +++ b/examples/with-playwright-automation/src/index.ts @@ -1,68 +1,40 @@ -import * as fs from 'fs'; -import * as path from 'path'; -import { AutomationConfig } from './schema'; import { runAutomation } from './automation'; const EXIT_CODES = { SUCCESS: 0, INVALID_ARGS: 1, - FILE_NOT_FOUND: 2, - INVALID_CONFIG: 3, - AUTOMATION_FAILED: 4, + AUTOMATION_FAILED: 2, } as const; const DEFAULT_COMPANY_IDENTIFIER = 'embed'; const printUsage = (): void => { console.log(` -Usage: npx tsx src/index.ts [options] +Usage: npx tsx src/index.ts [options] Arguments: - config.json Path to JSON configuration file + document URL or local file path to a PDF Options: --company-identifier Your SimplePDF company identifier (default: embed) --help Show this help message -Configuration file format: -{ - "document": "https://example.com/document.pdf", - "fields": [ - { - "type": "TEXT", - "x": 100, - "y": 700, - "width": 200, - "height": 20, - "page": 1, - "value": "Hello World" - } - ] -} - -Field types: TEXT, BOXED_TEXT, SIGNATURE, PICTURE, CHECKBOX - -Coordinate System: - Uses PDF standard coordinates: - - Origin at bottom-left corner of page - - Y increases upward - - Units in points (1/72 inch) - Examples: - npx tsx src/index.ts example.config.json - npx tsx src/index.ts example.config.json --company-identifier yourcompany + npx tsx src/index.ts https://example.com/form.pdf + npx tsx src/index.ts ./documents/form.pdf + npx tsx src/index.ts https://example.com/form.pdf --company-identifier yourcompany `); }; type ParsedArgs = { - configPath: string | null; + document: string | null; baseUrl: string; showHelp: boolean; }; const parseArgs = (): ParsedArgs => { const args = process.argv.slice(2); - let configPath: string | null = null; + let document: string | null = null; let companyIdentifier = DEFAULT_COMPANY_IDENTIFIER; let baseUrl: string | null = null; let showHelp = false; @@ -86,58 +58,35 @@ const parseArgs = (): ParsedArgs => { } if (!arg?.startsWith('-')) { - configPath = arg ?? null; + document = arg ?? null; } } const resolvedBaseUrl = baseUrl ?? `https://${companyIdentifier}.simplepdf.com`; - return { configPath, baseUrl: resolvedBaseUrl, showHelp }; -}; - -const loadConfig = ({ configPath }: { configPath: string }): AutomationConfig => { - const absolutePath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath); - - if (!fs.existsSync(absolutePath)) { - throw new Error(`Configuration file not found: ${absolutePath}`); - } - - const content = fs.readFileSync(absolutePath, 'utf-8'); - const parsed = JSON.parse(content) as unknown; - - return parsed as AutomationConfig; + return { document, baseUrl: resolvedBaseUrl, showHelp }; }; const main = async (): Promise => { - const { configPath, baseUrl, showHelp } = parseArgs(); + const { document, baseUrl, showHelp } = parseArgs(); if (showHelp) { printUsage(); process.exit(EXIT_CODES.SUCCESS); } - if (!configPath) { - console.error('Error: Configuration file path is required'); + if (!document) { + console.error('Error: document URL or file path is required'); printUsage(); process.exit(EXIT_CODES.INVALID_ARGS); } - let config: AutomationConfig; - try { - config = loadConfig({ configPath }); - } catch (e) { - const error = e as Error; - console.error(`Error loading configuration: ${error.message}`); - process.exit(EXIT_CODES.FILE_NOT_FOUND); - } - console.log('Starting automation...'); - console.log(`Document: ${config.document}`); - console.log(`Fields: ${config.fields.length}`); + console.log(`Document: ${document}`); console.log(`Editor: ${baseUrl}`); console.log(''); - const result = await runAutomation({ config, baseUrl }); + const result = await runAutomation({ document, baseUrl }); if (!result.success) { console.error(`Automation failed: [${result.error.code}] ${result.error.message}`); diff --git a/examples/with-playwright-automation/src/schema.ts b/examples/with-playwright-automation/src/schema.ts deleted file mode 100644 index f438992..0000000 --- a/examples/with-playwright-automation/src/schema.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { z } from 'zod'; - -const FieldType = z.enum(['TEXT', 'BOXED_TEXT', 'SIGNATURE', 'PICTURE', 'CHECKBOX']); -type FieldType = z.infer; - -const BaseField = z.object({ - x: z.number().describe('X coordinate in PDF points from bottom-left origin'), - y: z.number().describe('Y coordinate in PDF points from bottom-left origin'), - width: z.number().positive().describe('Width in PDF points'), - height: z.number().positive().describe('Height in PDF points'), - page: z.number().int().positive().describe('1-indexed page number'), -}); - -const TextField = BaseField.extend({ - type: z.literal('TEXT'), - value: z.string().optional(), -}); - -const BoxedTextField = BaseField.extend({ - type: z.literal('BOXED_TEXT'), - value: z.string().optional(), -}); - -const CheckboxField = BaseField.extend({ - type: z.literal('CHECKBOX'), - value: z.boolean().optional(), -}); - -const SignatureField = BaseField.extend({ - type: z.literal('SIGNATURE'), - value: z.string().optional().describe('File path, URL, data URL, or plain text (generates typed signature)'), -}); - -const PictureField = BaseField.extend({ - type: z.literal('PICTURE'), - value: z.string().optional().describe('File path, URL, or data URL'), -}); - -const FieldConfig = z.discriminatedUnion('type', [ - TextField, - BoxedTextField, - CheckboxField, - SignatureField, - PictureField, -]); -type FieldConfig = z.infer; - -const AutomationConfig = z.object({ - document: z.string().describe('URL or local file path to PDF'), - fields: z.array(FieldConfig), -}); -type AutomationConfig = z.infer; - -export { FieldType, FieldConfig, AutomationConfig }; diff --git a/react/README.md b/react/README.md index e1e26ad..cf2a666 100644 --- a/react/README.md +++ b/react/README.md @@ -130,7 +130,7 @@ Use `const { embedRef, actions } = useEmbed();` to programmatically control the | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `actions.goTo({ page })` | Navigate to a specific page | | `actions.selectTool(toolType)` | Select a tool: `'TEXT'`, `'BOXED_TEXT'`, `'CHECKBOX'`, `'PICTURE'`, `'SIGNATURE'`, or `null` to deselect (`CURSOR`) | -| `actions.createField(options)` | Create a field at specified position (see below) | +| `actions.detectFields()` | Automatically detect form fields in the document | | `actions.removeFields(options?)` | Remove fields by `fieldIds` or `page`, or all fields if no options | | `actions.getDocumentContent({ extractionMode })` | Extract document content (`extractionMode: 'auto'` or `'ocr'`) | | `actions.submit({ downloadCopyOnDevice })` | Submit the document | @@ -158,18 +158,10 @@ const Editor = () => { } }; - const handleCreateTextField = async () => { - const result = await actions.createField({ - type: 'TEXT', - page: 1, - x: 100, - y: 200, - width: 150, - height: 30, - value: 'Hello World', - }); + const handleDetectFields = async () => { + const result = await actions.detectFields(); if (result.success) { - console.log('Created field:', result.data.field_id); + console.log('Fields detected!'); } }; @@ -177,7 +169,7 @@ const Editor = () => { <> - + { }; ``` -#### `createField` options - -The `createField` action uses a discriminated union based on field type: - -| Type | `value` format | -| --------------------- | ----------------------------------------------------------- | -| `TEXT` / `BOXED_TEXT` | Plain text content | -| `CHECKBOX` | `'checked'` or `'unchecked'` | -| `PICTURE` | Data URL (base64) | -| `SIGNATURE` | Data URL (base64) or plain text (generates typed signature) | - -All field types share these base options: `page`, `x`, `y`, `width`, `height` (coordinates in PDF points, origin at bottom-left). - See [Retrieving PDF Data](../README.md#retrieving-pdf-data) for text extraction, downloading, and server-side storage options. ### Available props diff --git a/react/src/hook.test.ts b/react/src/hook.test.ts index 1d3da97..36d7226 100644 --- a/react/src/hook.test.ts +++ b/react/src/hook.test.ts @@ -247,16 +247,9 @@ describe('useEmbed', () => { expect(actionResult).toEqual(expectedError); }); - it('createField returns error when embedRef not attached', async () => { + it('detectFields returns error when embedRef not attached', async () => { const { result } = renderHook(() => useEmbed()); - const actionResult = await result.current.actions.createField({ - type: 'TEXT', - page: 1, - x: 0, - y: 0, - width: 100, - height: 20, - }); + const actionResult = await result.current.actions.detectFields(); expect(actionResult).toEqual(expectedError); }); @@ -287,7 +280,7 @@ describe('useEmbed', () => { const spies = { goTo: vi.fn().mockResolvedValue({ success: true }), selectTool: vi.fn().mockResolvedValue({ success: true }), - createField: vi.fn().mockResolvedValue({ success: true }), + detectFields: vi.fn().mockResolvedValue({ success: true }), removeFields: vi.fn().mockResolvedValue({ success: true }), getDocumentContent: vi.fn().mockResolvedValue({ success: true }), submit: vi.fn().mockResolvedValue({ success: true }), @@ -321,15 +314,14 @@ describe('useEmbed', () => { expect(actionResult).toEqual({ success: true }); }); - it('createField delegates to ref.createField', async () => { + it('detectFields delegates to ref.detectFields', async () => { const { result } = renderHook(() => useEmbed()); const { ref, spies } = createMockEmbedRef(); (result.current.embedRef as React.MutableRefObject).current = ref; - const fieldOptions = { type: 'TEXT' as const, page: 1, x: 0, y: 0, width: 100, height: 20 }; - const actionResult = await result.current.actions.createField(fieldOptions); + const actionResult = await result.current.actions.detectFields(); - expect(spies.createField).toHaveBeenCalledWith(fieldOptions); + expect(spies.detectFields).toHaveBeenCalled(); expect(actionResult).toEqual({ success: true }); }); @@ -384,40 +376,6 @@ describe('Type assertions', () => { type ExpectedToolType = 'TEXT' | 'BOXED_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; - type ExpectedBaseFieldOptions = { - page: number; - x: number; - y: number; - width: number; - height: number; - }; - - type ExpectedTextFieldOptions = ExpectedBaseFieldOptions & { - type: 'TEXT' | 'BOXED_TEXT'; - value?: string; - }; - - type ExpectedCheckboxFieldOptions = ExpectedBaseFieldOptions & { - type: 'CHECKBOX'; - value?: 'checked' | 'unchecked'; - }; - - type ExpectedPictureFieldOptions = ExpectedBaseFieldOptions & { - type: 'PICTURE'; - value?: string; - }; - - type ExpectedSignatureFieldOptions = ExpectedBaseFieldOptions & { - type: 'SIGNATURE'; - value?: string; - }; - - type ExpectedCreateFieldOptions = - | ExpectedTextFieldOptions - | ExpectedCheckboxFieldOptions - | ExpectedPictureFieldOptions - | ExpectedSignatureFieldOptions; - type ExpectedErrorResult = { success: false; error: { code: string; message: string }; @@ -440,11 +398,9 @@ describe('Type assertions', () => { expectTypeOf().returns.resolves.toExtend(); }); - it('createField accepts CreateFieldOptions and returns ActionResult with field_id', () => { - expectTypeOf().parameter(0).toEqualTypeOf(); - expectTypeOf().returns.resolves.toExtend< - ExpectedActionResult<{ field_id: string }> - >(); + it('detectFields accepts no arguments and returns ActionResult', () => { + expectTypeOf().parameters.toEqualTypeOf<[]>(); + expectTypeOf().returns.resolves.toExtend(); }); it('removeFields accepts optional { fieldIds?, page? } and returns ActionResult with removed_count', () => { @@ -470,82 +426,4 @@ describe('Type assertions', () => { expectTypeOf().returns.resolves.toExtend(); }); }); - - describe('createField discriminated union', () => { - it('TEXT field options are accepted', () => { - const textField = { - type: 'TEXT' as const, - page: 1, - x: 0, - y: 0, - width: 100, - height: 20, - value: 'hello', - }; - expectTypeOf(textField).toExtend(); - }); - - it('BOXED_TEXT field options are accepted', () => { - const boxedTextField = { - type: 'BOXED_TEXT' as const, - page: 1, - x: 0, - y: 0, - width: 100, - height: 20, - value: 'hello', - }; - expectTypeOf(boxedTextField).toExtend(); - }); - - it('CHECKBOX field accepts only checked/unchecked values', () => { - const checkedField = { - type: 'CHECKBOX' as const, - page: 1, - x: 0, - y: 0, - width: 20, - height: 20, - value: 'checked' as const, - }; - expectTypeOf(checkedField).toExtend(); - - const uncheckedField = { - type: 'CHECKBOX' as const, - page: 1, - x: 0, - y: 0, - width: 20, - height: 20, - value: 'unchecked' as const, - }; - expectTypeOf(uncheckedField).toExtend(); - }); - - it('PICTURE field options are accepted', () => { - const pictureField = { - type: 'PICTURE' as const, - page: 1, - x: 0, - y: 0, - width: 100, - height: 100, - value: 'data:image/png;base64,...', - }; - expectTypeOf(pictureField).toExtend(); - }); - - it('SIGNATURE field options are accepted', () => { - const signatureField = { - type: 'SIGNATURE' as const, - page: 1, - x: 0, - y: 0, - width: 150, - height: 50, - value: 'John Doe', - }; - expectTypeOf(signatureField).toExtend(); - }); - }); }); diff --git a/react/src/hook.tsx b/react/src/hook.tsx index 9b9eec8..e005dbe 100644 --- a/react/src/hook.tsx +++ b/react/src/hook.tsx @@ -7,36 +7,6 @@ type ExtractionMode = 'auto' | 'ocr'; type ToolType = 'TEXT' | 'BOXED_TEXT' | 'CHECKBOX' | 'PICTURE' | 'SIGNATURE'; -type BaseFieldOptions = { - page: number; - x: number; - y: number; - width: number; - height: number; -}; - -type TextFieldOptions = BaseFieldOptions & { - type: 'TEXT' | 'BOXED_TEXT'; - value?: string; -}; - -type CheckboxFieldOptions = BaseFieldOptions & { - type: 'CHECKBOX'; - value?: 'checked' | 'unchecked'; -}; - -type PictureFieldOptions = BaseFieldOptions & { - type: 'PICTURE'; - value?: string; // Data URL (base64) -}; - -type SignatureFieldOptions = BaseFieldOptions & { - type: 'SIGNATURE'; - value?: string; // Data URL (base64) or plain text (generates typed signature) -}; - -export type CreateFieldOptions = TextFieldOptions | CheckboxFieldOptions | PictureFieldOptions | SignatureFieldOptions; - type ErrorCodePrefix = 'bad_request' | 'unexpected' | 'forbidden'; type ErrorResult = { @@ -62,16 +32,12 @@ type RemoveFieldsResult = { removed_count: number; }; -type CreateFieldResult = { - field_id: string; -}; - export type EmbedActions = { goTo: (options: { page: number }) => Promise; selectTool: (toolType: ToolType | null) => Promise; - createField: (options: CreateFieldOptions) => Promise>; + detectFields: () => Promise; removeFields: (options?: { fieldIds?: string[]; page?: number }) => Promise>; @@ -177,9 +143,9 @@ export const useEmbed = (): { embedRef: React.RefObject; ac [], ); - const handleCreateField = React.useCallback( - createAction<[CreateFieldOptions], CreateFieldResult>(async (ref, options) => { - return ref.createField(options); + const handleDetectFields = React.useCallback( + createAction(async (ref) => { + return ref.detectFields(); }), [], ); @@ -210,7 +176,7 @@ export const useEmbed = (): { embedRef: React.RefObject; ac actions: { goTo: handleGoTo, selectTool: handleSelectTool, - createField: handleCreateField, + detectFields: handleDetectFields, removeFields: handleRemoveFields, getDocumentContent: handleGetDocumentContent, submit: handleSubmit, diff --git a/react/src/index.test.tsx b/react/src/index.test.tsx index 619c8e8..087bf96 100644 --- a/react/src/index.test.tsx +++ b/react/src/index.test.tsx @@ -311,7 +311,7 @@ describe('EmbedPDF', () => { expect(typeof ref.current?.goTo).toBe('function'); expect(typeof ref.current?.selectTool).toBe('function'); - expect(typeof ref.current?.createField).toBe('function'); + expect(typeof ref.current?.detectFields).toBe('function'); expect(typeof ref.current?.removeFields).toBe('function'); expect(typeof ref.current?.getDocumentContent).toBe('function'); expect(typeof ref.current?.submit).toBe('function'); @@ -321,10 +321,7 @@ describe('EmbedPDF', () => { it.each([ { action: 'goTo' as const, args: { page: 1 } }, { action: 'selectTool' as const, args: 'TEXT' as const }, - { - action: 'createField' as const, - args: { type: 'TEXT' as const, page: 1, x: 0, y: 0, width: 100, height: 20 }, - }, + { action: 'detectFields' as const, args: undefined }, { action: 'removeFields' as const, args: {} }, { action: 'getDocumentContent' as const, args: {} }, { action: 'submit' as const, args: { downloadCopyOnDevice: false } }, diff --git a/react/src/index.tsx b/react/src/index.tsx index 2385134..bb27596 100644 --- a/react/src/index.tsx +++ b/react/src/index.tsx @@ -178,14 +178,14 @@ export const EmbedPDF = React.forwardRef((props, ref) => { }); }, []); - const createField: EmbedActions['createField'] = React.useCallback(async (options) => { + const detectFields: EmbedActions['detectFields'] = React.useCallback(async () => { if (!iframeRef.current) { return { success: false, error: { code: 'unexpected:iframe_not_available', message: 'Iframe not available' } }; } await ensureEditorReady(); return sendEvent(iframeRef.current, { - type: 'CREATE_FIELD', - data: options, + type: 'DETECT_FIELDS', + data: {}, }); }, []); @@ -226,7 +226,7 @@ export const EmbedPDF = React.forwardRef((props, ref) => { loadDocument, goTo, selectTool, - createField, + detectFields, removeFields, getDocumentContent, submit,