@maravilla-labs/platform 0.3.11 → 0.4.1

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,300 @@
1
+ /**
2
+ * @fileoverview Media transforms — async ffmpeg / image / OCR jobs.
3
+ *
4
+ * Mirrors the Rust `platform.media.transforms` surface. All mutating
5
+ * methods (transcode/thumbnail/resize/ocr) return a {@link JobHandle}
6
+ * whose `outputKey` is content-addressed via {@link keyFor} and known
7
+ * up front — clients can render placeholder UI for the derived asset
8
+ * before the worker even starts the job.
9
+ *
10
+ * The runtime injects the live JS object as `platform.media.transforms`
11
+ * (see `crates/runtime/src/ops/platform/js_bindings/platform_js.rs`).
12
+ * The static helper {@link keyFor} is exported separately and runs
13
+ * client-side — it must produce byte-for-byte identical output to the
14
+ * Rust `derive_key`. The shared golden-vector fixture
15
+ * `crates/platform/tests/derive_key_vectors.json` is consumed from both
16
+ * the Rust integration test (`crates/platform/tests/derive_key_golden.rs`)
17
+ * and the TS test (`packages/platform/tests/derive-key.test.ts`) to keep
18
+ * the two in lockstep.
19
+ */
20
+ /** Video container targets supported by v1. */
21
+ type VideoFormat = 'mp4' | 'webm';
22
+ /** Image output formats supported by v1. */
23
+ type ImageFormat = 'jpg' | 'png' | 'webp';
24
+ /** Lifecycle state of a transform job. */
25
+ type JobStatus = 'pending' | 'running' | 'complete' | 'failed';
26
+ /** Options for `transforms.transcode`. */
27
+ interface TranscodeOpts {
28
+ format: VideoFormat;
29
+ codec?: string;
30
+ max_width?: number;
31
+ max_height?: number;
32
+ audio_codec?: string;
33
+ bitrate_kbps?: number;
34
+ }
35
+ /** Options for `transforms.thumbnail` — extract a single video frame. */
36
+ interface ThumbnailOpts {
37
+ /** Offset into the source. Accepts `"00:00:01"`, `"1s"`, or a plain number-as-string. */
38
+ at: string;
39
+ width?: number;
40
+ height?: number;
41
+ /** Defaults to `"jpg"` server-side when omitted. */
42
+ format?: ImageFormat;
43
+ quality?: number;
44
+ }
45
+ /** Options for `transforms.resize`. */
46
+ interface ResizeOpts {
47
+ width?: number;
48
+ height?: number;
49
+ format: ImageFormat;
50
+ quality?: number;
51
+ strip_metadata?: boolean;
52
+ }
53
+ /** Options for `transforms.ocr`. */
54
+ interface OcrOpts {
55
+ /** Tesseract language code(s). Defaults to `"eng"` server-side when omitted. */
56
+ lang?: string;
57
+ }
58
+ /**
59
+ * LibreOffice-supported output container for `doc_convert`. Each value
60
+ * maps to a `--convert-to` filter and a derived-key extension.
61
+ */
62
+ type DocFormat = 'pdf' | 'docx' | 'odt' | 'xlsx' | 'html' | 'txt' | 'rtf';
63
+ /** Options for `transforms.docToPdf`. */
64
+ interface DocToPdfOpts {
65
+ }
66
+ /** Options for `transforms.docThumbnail` — single-page raster of a document. */
67
+ interface DocThumbnailOpts {
68
+ width?: number;
69
+ height?: number;
70
+ /** Defaults to `"png"` server-side when omitted. */
71
+ format?: ImageFormat;
72
+ /** 1-indexed page. Defaults to `1` server-side when omitted. */
73
+ page?: number;
74
+ quality?: number;
75
+ }
76
+ /** Options for `transforms.docConvert` — generic LibreOffice conversion. */
77
+ interface DocConvertOpts {
78
+ to: DocFormat;
79
+ }
80
+ /** Options for `transforms.docToMarkdown` — RAG / LLM-friendly extraction. */
81
+ interface DocToMarkdownOpts {
82
+ /** Keep GFM tables in the output. Defaults to `true` server-side when omitted. */
83
+ preserve_tables?: boolean;
84
+ }
85
+ /**
86
+ * Options for `transforms.docToHtml` — single-file HTML with images
87
+ * inlined as base64 `data:` URIs and CSS inlined as `<style>` blocks.
88
+ * The output is a self-contained `.html` the caller can email, embed
89
+ * in an `<iframe>`, or hand to an LLM as one string.
90
+ */
91
+ interface DocToHtmlOpts {
92
+ /** Inline `<img>` references as base64 `data:` URIs. Defaults `true` server-side. */
93
+ inline_images?: boolean;
94
+ /** Inline CSS as a single `<style>` block. Defaults `true` server-side. */
95
+ inline_styles?: boolean;
96
+ }
97
+ /** Reference to a replacement image — its key in tenant `STORAGE`. */
98
+ interface ImageRef {
99
+ src_key: string;
100
+ }
101
+ /**
102
+ * Options for `transforms.docReplaceImages` — substitute placeholder
103
+ * text-tags and/or named drawing objects with caller-supplied images.
104
+ *
105
+ * Keys in `placeholders` are the literal text the user types in their
106
+ * template (e.g. `"{{USER_LOGO}}"`). Keys in `named_objects` are
107
+ * LibreOffice draw-object Name properties (set in Word/Writer via
108
+ * Format → Anchor → Properties → Name) — named-object swap preserves
109
+ * the template's exact frame size, border, anchor, and wrap.
110
+ */
111
+ interface DocReplaceImagesOpts {
112
+ /** Optional output format. When unset, the worker keeps the input format. */
113
+ output_format?: DocFormat;
114
+ /** `{{TAG}}` → image. The text is matched verbatim; missing tags are silently skipped. */
115
+ placeholders?: Record<string, ImageRef>;
116
+ /** `objectName` → image. */
117
+ named_objects?: Record<string, ImageRef>;
118
+ /** Pre-resize each image to the target frame's dimensions. Defaults `true` server-side. */
119
+ auto_resize?: boolean;
120
+ }
121
+ /** A single QR code to embed. Exactly one of `placeholder` or `named_object` must be set. */
122
+ interface QrCodeSpec {
123
+ placeholder?: string;
124
+ named_object?: string;
125
+ /** Payload to encode — typically a URL. ≤ 1500 bytes; the server rejects larger. */
126
+ payload: string;
127
+ /** Output PNG width in pixels. QR codes are square. Defaults to 256 server-side. */
128
+ size?: number;
129
+ }
130
+ /**
131
+ * Options for `transforms.docInsertQrCode` — generate QR PNG(s)
132
+ * server-side and inject them via the same pipeline as
133
+ * `docReplaceImages`.
134
+ */
135
+ interface DocInsertQrCodeOpts {
136
+ output_format?: DocFormat;
137
+ codes: QrCodeSpec[];
138
+ }
139
+ /** Inline QR-code spec for `DocTemplateMergeOpts.qr_codes`. The placeholder
140
+ * is the map key, so we only carry payload + size here. */
141
+ interface QrPayload {
142
+ payload: string;
143
+ /** Output PNG width in pixels (square). Defaults to 256 server-side. */
144
+ size?: number;
145
+ }
146
+ /**
147
+ * Options for `transforms.docTemplateMerge` — the unified single-call
148
+ * template-fill: text + images + QR codes in one render. The headline
149
+ * templating API; one call → one server-side render → one round-trip.
150
+ *
151
+ * `data` runs first (string find-and-replace), then `images` (text-tag →
152
+ * image insertion), then `named_objects` (preserves frame size/border),
153
+ * then `qr_codes` (server generates each PNG and injects via the same
154
+ * placeholder pipeline as `images`). All inside one document load.
155
+ */
156
+ interface DocTemplateMergeOpts {
157
+ /** Output format. Defaults to keeping the input format. */
158
+ output_format?: DocFormat;
159
+ /** Text-tag → string. e.g. `{ '{{NAME}}': 'Senol Aktas' }`. */
160
+ data?: Record<string, string>;
161
+ /** Text-tag → image. Same semantics as `DocReplaceImagesOpts.placeholders`. */
162
+ images?: Record<string, ImageRef>;
163
+ /** Object-name → image. Preserves the template's frame size/border/wrap. */
164
+ named_objects?: Record<string, ImageRef>;
165
+ /** Text-tag → QR code spec. Server generates the PNG; ≤ 1500-byte payload. */
166
+ qr_codes?: Record<string, QrPayload>;
167
+ /** Pre-resize images / QR PNGs to anchor frames. Defaults `true`. */
168
+ auto_resize?: boolean;
169
+ }
170
+ /** Tagged union of all transform requests — matches the Rust `TransformSpec`. */
171
+ type TransformSpec = ({
172
+ kind: 'transcode';
173
+ } & TranscodeOpts) | ({
174
+ kind: 'thumbnail';
175
+ } & ThumbnailOpts) | ({
176
+ kind: 'resize';
177
+ } & ResizeOpts) | ({
178
+ kind: 'ocr';
179
+ } & OcrOpts) | ({
180
+ kind: 'doc_to_pdf';
181
+ } & DocToPdfOpts) | ({
182
+ kind: 'doc_thumbnail';
183
+ } & DocThumbnailOpts) | ({
184
+ kind: 'doc_convert';
185
+ } & DocConvertOpts) | ({
186
+ kind: 'doc_to_markdown';
187
+ } & DocToMarkdownOpts) | ({
188
+ kind: 'doc_to_html';
189
+ } & DocToHtmlOpts) | ({
190
+ kind: 'doc_replace_images';
191
+ } & DocReplaceImagesOpts) | ({
192
+ kind: 'doc_insert_qr_code';
193
+ } & DocInsertQrCodeOpts) | ({
194
+ kind: 'doc_template_merge';
195
+ } & DocTemplateMergeOpts);
196
+ /** Probe output. All fields optional — ffprobe doesn't fill every one for every input. */
197
+ interface MediaInfo {
198
+ duration_secs?: number;
199
+ width?: number;
200
+ height?: number;
201
+ video_codec?: string;
202
+ audio_codec?: string;
203
+ bitrate_bps?: number;
204
+ container?: string;
205
+ }
206
+ /** Returned by every mutating transforms method. */
207
+ interface JobHandle {
208
+ id: string;
209
+ src_key: string;
210
+ output_key: string;
211
+ status: JobStatus;
212
+ }
213
+ /** Returned by `transforms.job(id)`. */
214
+ interface JobStatusResponse {
215
+ id: string;
216
+ status: JobStatus;
217
+ }
218
+ interface TransformsService {
219
+ transcode(srcKey: string, opts: TranscodeOpts): Promise<JobHandle>;
220
+ thumbnail(srcKey: string, opts: ThumbnailOpts): Promise<JobHandle>;
221
+ resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
222
+ probe(srcKey: string): Promise<MediaInfo>;
223
+ ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
224
+ /** Convert a LibreOffice-supported document to PDF. */
225
+ docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
226
+ /** Render a single page of a document as a raster thumbnail. */
227
+ docThumbnail(srcKey: string, opts: DocThumbnailOpts): Promise<JobHandle>;
228
+ /** Generic LibreOffice format conversion. */
229
+ docConvert(srcKey: string, opts: DocConvertOpts): Promise<JobHandle>;
230
+ /** Extract Markdown from a document (HTML → pandoc pipeline). */
231
+ docToMarkdown(srcKey: string, opts?: DocToMarkdownOpts | null): Promise<JobHandle>;
232
+ /** Convert to single-file HTML with base64-inlined images and inlined CSS. */
233
+ docToHtml(srcKey: string, opts?: DocToHtmlOpts | null): Promise<JobHandle>;
234
+ /** Swap placeholder text-tags and/or named drawing objects with caller-supplied images. */
235
+ docReplaceImages(srcKey: string, opts: DocReplaceImagesOpts): Promise<JobHandle>;
236
+ /** Generate QR codes server-side and inject them via the image-replace pipeline. */
237
+ docInsertQrCode(srcKey: string, opts: DocInsertQrCodeOpts): Promise<JobHandle>;
238
+ /** Unified template fill: text + images + QR codes in one render. The headline templating API. */
239
+ docTemplateMerge(srcKey: string, opts: DocTemplateMergeOpts): Promise<JobHandle>;
240
+ job(id: string): Promise<JobStatusResponse>;
241
+ }
242
+ /**
243
+ * Compute the canonical derived-asset key for `(srcKey, spec)`.
244
+ *
245
+ * Shape: `__derived/<srcHash>/<variantHash>.<ext>` where each hash is
246
+ * the first 16 hex chars (8 bytes / 64 bits) of `SHA-256(...)`.
247
+ *
248
+ * **MUST** match Rust `platform::media::transforms::derive_key` byte-for-byte.
249
+ * Cross-language golden vectors live at
250
+ * `crates/platform/tests/derive_key_vectors.json`.
251
+ */
252
+ declare function keyFor(srcKey: string, spec: TransformSpec): string;
253
+ /** Convenience namespace mirroring the Rust `transforms::keyFor` re-export. */
254
+ declare const transforms: {
255
+ keyFor: typeof keyFor;
256
+ };
257
+ /**
258
+ * Per-pattern transforms declaration used inside the `transforms` block of
259
+ * `maravilla.config.ts`. Each field accepts a single opts object or an array.
260
+ *
261
+ * `variants` is sugar for `resize` arrays — convenient for the common
262
+ * "image → multiple resized renditions" case.
263
+ */
264
+ interface TransformsPatternSpec {
265
+ transcode?: TranscodeOpts | TranscodeOpts[];
266
+ thumbnail?: ThumbnailOpts | ThumbnailOpts[];
267
+ resize?: ResizeOpts | ResizeOpts[];
268
+ /** Sugar for `resize` arrays — same shape, same semantics. */
269
+ variants?: ResizeOpts[];
270
+ ocr?: OcrOpts | OcrOpts[];
271
+ }
272
+ /**
273
+ * Top-level `transforms` block. Keys are glob path patterns matched against
274
+ * the storage key of every uploaded object. The adapter compiles each entry
275
+ * into a synthetic `storage.put` event handler that fans out the declared
276
+ * transforms via `Promise.all`.
277
+ *
278
+ * @example
279
+ * ```ts
280
+ * import { defineConfig } from '@maravilla-labs/platform/config';
281
+ *
282
+ * export default defineConfig({
283
+ * transforms: {
284
+ * 'uploads/videos/**': {
285
+ * transcode: [{ format: 'mp4' }, { format: 'webm' }],
286
+ * thumbnail: { at: '1s', width: 640, format: 'jpg' },
287
+ * },
288
+ * 'uploads/photos/**': {
289
+ * variants: [
290
+ * { width: 1600, format: 'webp', quality: 85 },
291
+ * { width: 400, format: 'webp', quality: 80 },
292
+ * ],
293
+ * },
294
+ * },
295
+ * });
296
+ * ```
297
+ */
298
+ type TransformsConfig = Record<string, TransformsPatternSpec>;
299
+
300
+ export { type DocConvertOpts as D, type ImageFormat as I, type JobHandle as J, type MediaInfo as M, type OcrOpts as O, type QrCodeSpec as Q, type ResizeOpts as R, type TransformsConfig as T, type VideoFormat as V, type TransformsPatternSpec as a, type DocFormat as b, type DocInsertQrCodeOpts as c, type DocReplaceImagesOpts as d, type DocTemplateMergeOpts as e, type DocThumbnailOpts as f, type DocToHtmlOpts as g, type DocToMarkdownOpts as h, type DocToPdfOpts as i, type ImageRef as j, type JobStatus as k, type JobStatusResponse as l, type QrPayload as m, type ThumbnailOpts as n, type TranscodeOpts as o, type TransformSpec as p, type TransformsService as q, keyFor as r, transforms as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maravilla-labs/platform",
3
- "version": "0.3.11",
3
+ "version": "0.4.1",
4
4
  "description": "Universal platform client for Maravilla runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -1,5 +1,5 @@
1
1
  import type { KvNamespace, KvListResult, Database, DbFindOptions, Storage, RealtimeService, PresenceService, AuthService, AuthCaller, AuthUser, AuthSession, AuthField, RegisterOptions, LoginOptions, UserListFilter, UserListResponse, UpdateUserOptions, PolicyService, VectorIndexSpec, VectorIndexDescriptor, VectorQueryWithFilter, VectorSearchHit, IndexSpec, IndexDescriptor, Workflows, WorkflowHandle, WorkflowRun, WorkflowStepRecord } from './types.js';
2
- import type { TransformsService, TranscodeOpts, ThumbnailOpts, ResizeOpts, OcrOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
2
+ import type { TransformsService, TranscodeOpts, ThumbnailOpts, ResizeOpts, OcrOpts, DocToPdfOpts, DocThumbnailOpts, DocConvertOpts, DocToMarkdownOpts, DocToHtmlOpts, DocReplaceImagesOpts, DocInsertQrCodeOpts, DocTemplateMergeOpts, JobHandle, JobStatusResponse, MediaInfo } from './transforms.js';
3
3
  import { RemoteMediaService } from './media.js';
4
4
  import { getRequestAuthHeader } from './request-scope.js';
5
5
 
@@ -1110,6 +1110,38 @@ class RemoteTransformsService implements TransformsService {
1110
1110
  return this.post<JobHandle>('/ocr', { srcKey, opts: opts ?? {} });
1111
1111
  }
1112
1112
 
1113
+ docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle> {
1114
+ return this.post<JobHandle>('/doc_to_pdf', { srcKey, opts: opts ?? {} });
1115
+ }
1116
+
1117
+ docThumbnail(srcKey: string, opts: DocThumbnailOpts): Promise<JobHandle> {
1118
+ return this.post<JobHandle>('/doc_thumbnail', { srcKey, opts });
1119
+ }
1120
+
1121
+ docConvert(srcKey: string, opts: DocConvertOpts): Promise<JobHandle> {
1122
+ return this.post<JobHandle>('/doc_convert', { srcKey, opts });
1123
+ }
1124
+
1125
+ docToMarkdown(srcKey: string, opts?: DocToMarkdownOpts | null): Promise<JobHandle> {
1126
+ return this.post<JobHandle>('/doc_to_markdown', { srcKey, opts: opts ?? {} });
1127
+ }
1128
+
1129
+ docToHtml(srcKey: string, opts?: DocToHtmlOpts | null): Promise<JobHandle> {
1130
+ return this.post<JobHandle>('/doc_to_html', { srcKey, opts: opts ?? {} });
1131
+ }
1132
+
1133
+ docReplaceImages(srcKey: string, opts: DocReplaceImagesOpts): Promise<JobHandle> {
1134
+ return this.post<JobHandle>('/doc_replace_images', { srcKey, opts });
1135
+ }
1136
+
1137
+ docInsertQrCode(srcKey: string, opts: DocInsertQrCodeOpts): Promise<JobHandle> {
1138
+ return this.post<JobHandle>('/doc_insert_qr_code', { srcKey, opts });
1139
+ }
1140
+
1141
+ docTemplateMerge(srcKey: string, opts: DocTemplateMergeOpts): Promise<JobHandle> {
1142
+ return this.post<JobHandle>('/doc_template_merge', { srcKey, opts });
1143
+ }
1144
+
1113
1145
  probe(srcKey: string): Promise<MediaInfo> {
1114
1146
  return this.post<MediaInfo>('/probe', { srcKey });
1115
1147
  }
package/src/transforms.ts CHANGED
@@ -71,12 +71,144 @@ export interface OcrOpts {
71
71
  lang?: string;
72
72
  }
73
73
 
74
+ /**
75
+ * LibreOffice-supported output container for `doc_convert`. Each value
76
+ * maps to a `--convert-to` filter and a derived-key extension.
77
+ */
78
+ export type DocFormat = 'pdf' | 'docx' | 'odt' | 'xlsx' | 'html' | 'txt' | 'rtf';
79
+
80
+ /** Options for `transforms.docToPdf`. */
81
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
82
+ export interface DocToPdfOpts {}
83
+
84
+ /** Options for `transforms.docThumbnail` — single-page raster of a document. */
85
+ export interface DocThumbnailOpts {
86
+ width?: number;
87
+ height?: number;
88
+ /** Defaults to `"png"` server-side when omitted. */
89
+ format?: ImageFormat;
90
+ /** 1-indexed page. Defaults to `1` server-side when omitted. */
91
+ page?: number;
92
+ quality?: number;
93
+ }
94
+
95
+ /** Options for `transforms.docConvert` — generic LibreOffice conversion. */
96
+ export interface DocConvertOpts {
97
+ to: DocFormat;
98
+ }
99
+
100
+ /** Options for `transforms.docToMarkdown` — RAG / LLM-friendly extraction. */
101
+ export interface DocToMarkdownOpts {
102
+ /** Keep GFM tables in the output. Defaults to `true` server-side when omitted. */
103
+ preserve_tables?: boolean;
104
+ }
105
+
106
+ /**
107
+ * Options for `transforms.docToHtml` — single-file HTML with images
108
+ * inlined as base64 `data:` URIs and CSS inlined as `<style>` blocks.
109
+ * The output is a self-contained `.html` the caller can email, embed
110
+ * in an `<iframe>`, or hand to an LLM as one string.
111
+ */
112
+ export interface DocToHtmlOpts {
113
+ /** Inline `<img>` references as base64 `data:` URIs. Defaults `true` server-side. */
114
+ inline_images?: boolean;
115
+ /** Inline CSS as a single `<style>` block. Defaults `true` server-side. */
116
+ inline_styles?: boolean;
117
+ }
118
+
119
+ /** Reference to a replacement image — its key in tenant `STORAGE`. */
120
+ export interface ImageRef {
121
+ src_key: string;
122
+ }
123
+
124
+ /**
125
+ * Options for `transforms.docReplaceImages` — substitute placeholder
126
+ * text-tags and/or named drawing objects with caller-supplied images.
127
+ *
128
+ * Keys in `placeholders` are the literal text the user types in their
129
+ * template (e.g. `"{{USER_LOGO}}"`). Keys in `named_objects` are
130
+ * LibreOffice draw-object Name properties (set in Word/Writer via
131
+ * Format → Anchor → Properties → Name) — named-object swap preserves
132
+ * the template's exact frame size, border, anchor, and wrap.
133
+ */
134
+ export interface DocReplaceImagesOpts {
135
+ /** Optional output format. When unset, the worker keeps the input format. */
136
+ output_format?: DocFormat;
137
+ /** `{{TAG}}` → image. The text is matched verbatim; missing tags are silently skipped. */
138
+ placeholders?: Record<string, ImageRef>;
139
+ /** `objectName` → image. */
140
+ named_objects?: Record<string, ImageRef>;
141
+ /** Pre-resize each image to the target frame's dimensions. Defaults `true` server-side. */
142
+ auto_resize?: boolean;
143
+ }
144
+
145
+ /** A single QR code to embed. Exactly one of `placeholder` or `named_object` must be set. */
146
+ export interface QrCodeSpec {
147
+ placeholder?: string;
148
+ named_object?: string;
149
+ /** Payload to encode — typically a URL. ≤ 1500 bytes; the server rejects larger. */
150
+ payload: string;
151
+ /** Output PNG width in pixels. QR codes are square. Defaults to 256 server-side. */
152
+ size?: number;
153
+ }
154
+
155
+ /**
156
+ * Options for `transforms.docInsertQrCode` — generate QR PNG(s)
157
+ * server-side and inject them via the same pipeline as
158
+ * `docReplaceImages`.
159
+ */
160
+ export interface DocInsertQrCodeOpts {
161
+ output_format?: DocFormat;
162
+ codes: QrCodeSpec[];
163
+ }
164
+
165
+ /** Inline QR-code spec for `DocTemplateMergeOpts.qr_codes`. The placeholder
166
+ * is the map key, so we only carry payload + size here. */
167
+ export interface QrPayload {
168
+ payload: string;
169
+ /** Output PNG width in pixels (square). Defaults to 256 server-side. */
170
+ size?: number;
171
+ }
172
+
173
+ /**
174
+ * Options for `transforms.docTemplateMerge` — the unified single-call
175
+ * template-fill: text + images + QR codes in one render. The headline
176
+ * templating API; one call → one server-side render → one round-trip.
177
+ *
178
+ * `data` runs first (string find-and-replace), then `images` (text-tag →
179
+ * image insertion), then `named_objects` (preserves frame size/border),
180
+ * then `qr_codes` (server generates each PNG and injects via the same
181
+ * placeholder pipeline as `images`). All inside one document load.
182
+ */
183
+ export interface DocTemplateMergeOpts {
184
+ /** Output format. Defaults to keeping the input format. */
185
+ output_format?: DocFormat;
186
+ /** Text-tag → string. e.g. `{ '{{NAME}}': 'Senol Aktas' }`. */
187
+ data?: Record<string, string>;
188
+ /** Text-tag → image. Same semantics as `DocReplaceImagesOpts.placeholders`. */
189
+ images?: Record<string, ImageRef>;
190
+ /** Object-name → image. Preserves the template's frame size/border/wrap. */
191
+ named_objects?: Record<string, ImageRef>;
192
+ /** Text-tag → QR code spec. Server generates the PNG; ≤ 1500-byte payload. */
193
+ qr_codes?: Record<string, QrPayload>;
194
+ /** Pre-resize images / QR PNGs to anchor frames. Defaults `true`. */
195
+ auto_resize?: boolean;
196
+ }
197
+
74
198
  /** Tagged union of all transform requests — matches the Rust `TransformSpec`. */
75
199
  export type TransformSpec =
76
200
  | ({ kind: 'transcode' } & TranscodeOpts)
77
201
  | ({ kind: 'thumbnail' } & ThumbnailOpts)
78
202
  | ({ kind: 'resize' } & ResizeOpts)
79
- | ({ kind: 'ocr' } & OcrOpts);
203
+ | ({ kind: 'ocr' } & OcrOpts)
204
+ | ({ kind: 'doc_to_pdf' } & DocToPdfOpts)
205
+ | ({ kind: 'doc_thumbnail' } & DocThumbnailOpts)
206
+ | ({ kind: 'doc_convert' } & DocConvertOpts)
207
+ | ({ kind: 'doc_to_markdown' } & DocToMarkdownOpts)
208
+ | ({ kind: 'doc_to_html' } & DocToHtmlOpts)
209
+ | ({ kind: 'doc_replace_images' } & DocReplaceImagesOpts)
210
+ | ({ kind: 'doc_insert_qr_code' } & DocInsertQrCodeOpts)
211
+ | ({ kind: 'doc_template_merge' } & DocTemplateMergeOpts);
80
212
 
81
213
  /** Probe output. All fields optional — ffprobe doesn't fill every one for every input. */
82
214
  export interface MediaInfo {
@@ -111,6 +243,22 @@ export interface TransformsService {
111
243
  resize(srcKey: string, opts: ResizeOpts): Promise<JobHandle>;
112
244
  probe(srcKey: string): Promise<MediaInfo>;
113
245
  ocr(srcKey: string, opts?: OcrOpts | null): Promise<JobHandle>;
246
+ /** Convert a LibreOffice-supported document to PDF. */
247
+ docToPdf(srcKey: string, opts?: DocToPdfOpts | null): Promise<JobHandle>;
248
+ /** Render a single page of a document as a raster thumbnail. */
249
+ docThumbnail(srcKey: string, opts: DocThumbnailOpts): Promise<JobHandle>;
250
+ /** Generic LibreOffice format conversion. */
251
+ docConvert(srcKey: string, opts: DocConvertOpts): Promise<JobHandle>;
252
+ /** Extract Markdown from a document (HTML → pandoc pipeline). */
253
+ docToMarkdown(srcKey: string, opts?: DocToMarkdownOpts | null): Promise<JobHandle>;
254
+ /** Convert to single-file HTML with base64-inlined images and inlined CSS. */
255
+ docToHtml(srcKey: string, opts?: DocToHtmlOpts | null): Promise<JobHandle>;
256
+ /** Swap placeholder text-tags and/or named drawing objects with caller-supplied images. */
257
+ docReplaceImages(srcKey: string, opts: DocReplaceImagesOpts): Promise<JobHandle>;
258
+ /** Generate QR codes server-side and inject them via the image-replace pipeline. */
259
+ docInsertQrCode(srcKey: string, opts: DocInsertQrCodeOpts): Promise<JobHandle>;
260
+ /** Unified template fill: text + images + QR codes in one render. The headline templating API. */
261
+ docTemplateMerge(srcKey: string, opts: DocTemplateMergeOpts): Promise<JobHandle>;
114
262
  job(id: string): Promise<JobStatusResponse>;
115
263
  }
116
264
 
@@ -164,6 +312,29 @@ function outputExtension(spec: TransformSpec): string {
164
312
  }
165
313
  case 'ocr':
166
314
  return 'txt';
315
+ case 'doc_to_pdf':
316
+ return 'pdf';
317
+ case 'doc_thumbnail': {
318
+ // `format` defaults to png server-side — mirror so the canonical
319
+ // JSON the Rust impl hashes matches the JS one for default-shape
320
+ // requests.
321
+ const fmt = (spec as { format?: ImageFormat }).format ?? 'png';
322
+ return fmt;
323
+ }
324
+ case 'doc_convert':
325
+ return spec.to;
326
+ case 'doc_to_markdown':
327
+ return 'md';
328
+ case 'doc_to_html':
329
+ return 'html';
330
+ case 'doc_replace_images':
331
+ case 'doc_insert_qr_code':
332
+ case 'doc_template_merge':
333
+ // Mirror the Rust fallback: when output_format is omitted the
334
+ // result preserves the input format, which we don't know without
335
+ // inspecting bytes — fall back to docx (the dominant template
336
+ // format).
337
+ return spec.output_format ?? 'docx';
167
338
  default: {
168
339
  const _exhaust: never = spec;
169
340
  throw new Error(`unknown transform kind: ${(_exhaust as { kind: string }).kind}`);