@agent-native/core 0.157.8 → 0.157.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/templates/analytics/actions/purge-first-party-analytics-postgres.ts +26 -8
- package/corpus/templates/analytics/scripts/first-party-analytics-postgres-retention-dry-run.sql +261 -0
- package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +3 -1
- package/corpus/templates/analytics/server/lib/first-party-analytics-purge.ts +75 -31
- package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +4 -1
- package/corpus/templates/analytics/server/plugins/db.ts +139 -0
- package/corpus/templates/slides/AGENTS.md +14 -10
- package/corpus/templates/slides/actions/export-pptx.ts +1311 -121
- package/corpus/templates/slides/actions/import-file.ts +28 -1
- package/corpus/templates/slides/actions/import-pptx.ts +69 -42
- package/corpus/templates/slides/app/components/deck/SlideRenderer.tsx +222 -29
- package/corpus/templates/slides/app/components/editor/ExportMenu.tsx +71 -0
- package/corpus/templates/slides/app/global.css +19 -1
- package/corpus/templates/slides/app/lib/export-google-slides-client.ts +44 -5
- package/corpus/templates/slides/app/lib/export-pdf-client.ts +13 -6
- package/corpus/templates/slides/app/lib/export-pptx-client.ts +601 -30
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +18 -1
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +1240 -22
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +329 -64
- package/corpus/templates/slides/server/handlers/import/pptx-assets.ts +24 -0
- package/corpus/templates/slides/server/lib/source-import.ts +10 -1
- package/corpus/templates/slides/shared/aspect-ratios.ts +13 -1
- package/dist/agent/harness/migrations.d.ts +4 -0
- package/dist/agent/harness/migrations.js +60 -0
- package/dist/agent/production-agent.d.ts +2 -0
- package/dist/agent/production-agent.js +15 -0
- package/dist/agent/run-loop-with-resume.d.ts +1 -0
- package/dist/agent/run-loop-with-resume.js +44 -12
- package/dist/agent/run-migrations.d.ts +8 -0
- package/dist/agent/run-migrations.js +61 -0
- package/dist/automations/service.d.ts +6 -0
- package/dist/automations/service.js +35 -0
- package/dist/chat-threads/migrations.d.ts +1 -0
- package/dist/chat-threads/migrations.js +1 -0
- package/dist/chat-threads/schema-migrations.d.ts +8 -0
- package/dist/chat-threads/schema-migrations.js +60 -0
- package/dist/cli/code-agent-connector.d.ts +4 -1
- package/dist/cli/code-agent-connector.js +154 -23
- package/dist/cli/code-agent-executor.js +255 -13
- package/dist/cli/code-agent-runs.d.ts +1 -0
- package/dist/cli/code-agent-runs.js +2 -1
- package/dist/cli/portal-workspace.d.ts +49 -0
- package/dist/cli/portal-workspace.js +320 -0
- package/dist/client/AssistantChat.d.ts +1 -1
- package/dist/client/AssistantChat.js +43 -61
- package/dist/client/ConnectBuilderCard.js +33 -5
- package/dist/client/agent-chat-adapter.js +4 -0
- package/dist/client/analytics-platform.d.ts +2 -0
- package/dist/client/analytics-platform.js +12 -0
- package/dist/client/analytics.d.ts +3 -0
- package/dist/client/analytics.js +10 -1
- package/dist/client/chat/index.d.ts +1 -0
- package/dist/client/chat/index.js +1 -0
- package/dist/client/chat/message-components.d.ts +2 -0
- package/dist/client/chat/message-components.js +11 -1
- package/dist/client/chat/run-recovery.d.ts +2 -1
- package/dist/client/chat/run-recovery.js +2 -2
- package/dist/client/chat-first/apps-rail.d.ts +1 -1
- package/dist/client/chat-first/apps-rail.js +11 -11
- package/dist/client/chat-first/surface-tabs.d.ts +1 -1
- package/dist/client/chat-first/surface-tabs.js +38 -36
- package/dist/client/chat-first/types.d.ts +5 -0
- package/dist/client/chat-first.d.ts +1 -1
- package/dist/client/chat-first.js +4 -4
- package/dist/client/desktop-local-code-change.d.ts +10 -0
- package/dist/client/desktop-local-code-change.js +16 -0
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/observability/ThumbsFeedback.js +53 -26
- package/dist/client/observability/useObservability.d.ts +1 -1
- package/dist/client/observability/useObservability.js +6 -3
- package/dist/client/terminal/AgentTerminal.d.ts +9 -1
- package/dist/client/terminal/AgentTerminal.js +38 -3
- package/dist/client/terminal/index.d.ts +1 -1
- package/dist/client/terminal/index.js +1 -1
- package/dist/client/track.js +9 -1
- package/dist/client/use-action.js +3 -0
- package/dist/collab/awareness.d.ts +2 -2
- package/dist/deploy/build.js +1 -1
- package/dist/feature-flags/a2a-action-route.js +4 -2
- package/dist/feature-flags/permissions.d.ts +2 -0
- package/dist/feature-flags/permissions.js +6 -1
- package/dist/ingestion/pptx.d.ts +92 -1
- package/dist/ingestion/pptx.js +1180 -115
- package/dist/integrations/index.d.ts +3 -3
- package/dist/integrations/index.js +2 -2
- package/dist/integrations/plugin.js +95 -6
- package/dist/integrations/remote-commands-store.d.ts +5 -0
- package/dist/integrations/remote-commands-store.js +63 -22
- package/dist/integrations/remote-devices-store.d.ts +2 -1
- package/dist/integrations/remote-devices-store.js +46 -0
- package/dist/integrations/remote-types.d.ts +20 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +3 -0
- package/dist/jobs/actions/list-recurring-jobs.js +5 -1
- package/dist/jobs/frontmatter.d.ts +16 -0
- package/dist/jobs/frontmatter.js +53 -0
- package/dist/jobs/remote-execution.d.ts +42 -0
- package/dist/jobs/remote-execution.js +287 -0
- package/dist/jobs/scheduler.js +102 -3
- package/dist/jobs/tools.js +50 -3
- package/dist/localization/core-messages/ar-SA.js +4 -0
- package/dist/localization/core-messages/de-DE.js +4 -0
- package/dist/localization/core-messages/en-US.d.ts +4 -0
- package/dist/localization/core-messages/en-US.js +4 -0
- package/dist/localization/core-messages/es-ES.js +4 -0
- package/dist/localization/core-messages/fr-FR.js +4 -0
- package/dist/localization/core-messages/hi-IN.js +4 -0
- package/dist/localization/core-messages/ja-JP.js +4 -0
- package/dist/localization/core-messages/ko-KR.js +4 -0
- package/dist/localization/core-messages/pt-BR.js +4 -0
- package/dist/localization/core-messages/zh-CN.js +4 -0
- package/dist/localization/core-messages/zh-TW.js +4 -0
- package/dist/localization/core-messages.d.ts +4 -0
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/observability/feedback.js +40 -16
- package/dist/observability/routes.d.ts +6 -6
- package/dist/observability/routes.js +15 -3
- package/dist/observability/store.js +2 -0
- package/dist/observability/traces.js +5 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
- package/dist/provider-api/actions/provider-api.d.ts +13 -13
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/action-routes.d.ts +7 -5
- package/dist/server/action-routes.js +13 -6
- package/dist/server/agent-chat-plugin.js +6 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/agent-run-context.d.ts +1 -0
- package/dist/server/agent-run-context.js +9 -0
- package/dist/server/better-auth-instance.js +4 -0
- package/dist/server/core-routes-plugin.js +8 -1
- package/dist/server/realtime-voice.js +2 -1
- package/dist/server/release-migrations.js +16 -0
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/shared/analytics-platform.d.ts +6 -0
- package/dist/shared/analytics-platform.js +14 -0
- package/dist/shared/mcp-embed-headers.d.ts +1 -1
- package/dist/shared/mcp-embed-headers.js +1 -1
- package/dist/styles/agent-native.css +83 -103
- package/dist/terminal/cli-registry.js +5 -0
- package/dist/tracking/registry.js +9 -1
- package/dist/triggers/actions/list-automations.d.ts +3 -0
- package/dist/triggers/actions/list-automations.js +3 -0
- package/dist/triggers/actions/manage-automation.d.ts +9 -0
- package/dist/triggers/actions/manage-automation.js +14 -2
- package/dist/triggers/actions.js +82 -7
- package/dist/usage/migrations.d.ts +4 -0
- package/dist/usage/migrations.js +45 -0
- package/docs/content/template-content-developers.mdx +48 -0
- package/docs/content/toolkit-ui.mdx +57 -0
- package/package.json +3 -3
|
@@ -19,14 +19,16 @@ export interface PdfFidelityPage {
|
|
|
19
19
|
heightEmu: number;
|
|
20
20
|
/** The page's own painted background, when a full-page fill was found; undefined means "plain paper" (render white). */
|
|
21
21
|
backgroundColor: string | undefined;
|
|
22
|
-
/**
|
|
22
|
+
/** Sorted by the PDF's own real paint order, so an image painted over text (or vice versa) keeps its real stacking instead of images always sitting behind. */
|
|
23
23
|
elements: ParsedElement[];
|
|
24
|
+
/** Images detected on this page whose pixel data could not be matched to a paint placement (extraction failed, or no canvas renderer was available) — intentionally size-filtered placements are not counted. */
|
|
25
|
+
imagesSkipped: number;
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
/** An embedded raster image already resolved by `pdf.getImage()`, keyed by its page. */
|
|
27
29
|
export interface PdfPageImage {
|
|
28
30
|
pageNumber: number;
|
|
29
|
-
images: { data: Uint8Array }[];
|
|
31
|
+
images: { data: Uint8Array; name: string }[];
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
interface Rect {
|
|
@@ -36,6 +38,8 @@ interface Rect {
|
|
|
36
38
|
bottom: number;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
type PdfOperatorList = Awaited<ReturnType<PDFPageProxy["getOperatorList"]>>;
|
|
42
|
+
|
|
39
43
|
export function applyPoint(m: Mat, x: number, y: number): [number, number] {
|
|
40
44
|
const p: [number, number] = [x, y];
|
|
41
45
|
Util.applyTransform(p, m);
|
|
@@ -80,6 +84,9 @@ export interface TextRunBox extends Rect {
|
|
|
80
84
|
color: string;
|
|
81
85
|
underline: boolean;
|
|
82
86
|
href: string | undefined;
|
|
87
|
+
fontFamily: string | undefined;
|
|
88
|
+
/** Index in this page's real content-stream paint order, shared with image paint events — lets the final element list be z-ordered by how the PDF actually painted it instead of images-always-first. */
|
|
89
|
+
paintOrder: number;
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
/** A thin filled/stroked rect from the content stream — the usual way PDFs draw an underline (there's no inline "underline" text attribute). */
|
|
@@ -161,6 +168,10 @@ const TEXT_SHOWING_OPS = new Set([
|
|
|
161
168
|
OPS.nextLineSetSpacingShowText,
|
|
162
169
|
]);
|
|
163
170
|
|
|
171
|
+
function isImagePaintOp(fn: number): boolean {
|
|
172
|
+
return fn === OPS.paintImageXObject || fn === OPS.paintInlineImageXObject;
|
|
173
|
+
}
|
|
174
|
+
|
|
164
175
|
const FILL_PAINT_OPS = new Set([
|
|
165
176
|
OPS.fill,
|
|
166
177
|
OPS.eoFill,
|
|
@@ -195,6 +206,30 @@ const BACKGROUND_COVERAGE_RATIO = 0.9;
|
|
|
195
206
|
*/
|
|
196
207
|
const UNTRACKED_COLOR_OPS = new Set([OPS.setFillColorSpace, OPS.shadingFill]);
|
|
197
208
|
|
|
209
|
+
/** A PDF-embedded subset font name is prefixed with a 6-letter tag plus "+" (e.g. "MUFUZY+Poppins-Bold") — that tag and the PostScript style suffix after it are never real CSS font-family values. */
|
|
210
|
+
const SUBSET_TAG_PATTERN = /^[a-z]{6}\+/i;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Maps a PDF's real (subset-tag-stripped) font name onto the nearest
|
|
214
|
+
* websafe CSS family by a simple keyword match, since the raw PostScript
|
|
215
|
+
* name (e.g. "TimesNewRomanPSMT") usually isn't installed or resolvable as
|
|
216
|
+
* a browser font-family at all. Returns undefined for an unresolved font id
|
|
217
|
+
* so the caller falls back to the deck's default font instead of rendering
|
|
218
|
+
* a literal empty family.
|
|
219
|
+
*/
|
|
220
|
+
function toWebSafeFontFamily(fontName: string): string | undefined {
|
|
221
|
+
const base = fontName.replace(SUBSET_TAG_PATTERN, "").split(/[-,]/)[0].trim();
|
|
222
|
+
if (!base) return undefined;
|
|
223
|
+
const normalized = base.toLowerCase();
|
|
224
|
+
if (/times|georgia|garamond|cambria|palatino|serif/.test(normalized)) {
|
|
225
|
+
return "Georgia";
|
|
226
|
+
}
|
|
227
|
+
if (/courier|mono|consolas/.test(normalized)) {
|
|
228
|
+
return "Courier New";
|
|
229
|
+
}
|
|
230
|
+
return base;
|
|
231
|
+
}
|
|
232
|
+
|
|
198
233
|
/**
|
|
199
234
|
* `getTextContent()` gives baseline position + font size per run but no
|
|
200
235
|
* paragraph structure — PDFs don't have one. Each run's placed box is
|
|
@@ -244,6 +279,11 @@ export function textItemToBox(
|
|
|
244
279
|
color,
|
|
245
280
|
underline: false,
|
|
246
281
|
href: undefined,
|
|
282
|
+
fontFamily: toWebSafeFontFamily(fontName),
|
|
283
|
+
// Real paint order is only known once this box is sliced from its
|
|
284
|
+
// originating content-stream op in `sliceTextItemBySegments` — callers
|
|
285
|
+
// overwrite this with the real value; 0 is just a type-satisfying default.
|
|
286
|
+
paintOrder: 0,
|
|
247
287
|
};
|
|
248
288
|
}
|
|
249
289
|
|
|
@@ -279,6 +319,8 @@ export function mergeLine(items: TextRunBox[]): TextRunBox {
|
|
|
279
319
|
color: sorted[0].color,
|
|
280
320
|
underline: sorted.some((s) => s.underline),
|
|
281
321
|
href: sorted.find((s) => s.href)?.href,
|
|
322
|
+
fontFamily: sorted[0].fontFamily,
|
|
323
|
+
paintOrder: Math.min(...sorted.map((s) => s.paintOrder)),
|
|
282
324
|
};
|
|
283
325
|
}
|
|
284
326
|
|
|
@@ -331,20 +373,112 @@ export function annotateLineDecorations(
|
|
|
331
373
|
|
|
332
374
|
/** Group same-baseline runs into lines, in the order pdf.js emitted them (reading order for typical single-column pages). */
|
|
333
375
|
export function groupIntoLines(items: TextRunBox[]): TextRunBox[] {
|
|
334
|
-
return groupByBaseline(items).map(mergeLine);
|
|
376
|
+
return groupByBaseline(items, detectColumnGutter(items)).map(mergeLine);
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** Buckets scanned across a page's text-item x-span when looking for a recurring empty column gutter. */
|
|
380
|
+
const COLUMN_GUTTER_BUCKET_COUNT = 40;
|
|
381
|
+
/** A candidate gutter must span at least this fraction of the page's text width — an ordinary word/paragraph gap never gets close. */
|
|
382
|
+
const MIN_COLUMN_GUTTER_WIDTH_FRACTION = 0.06;
|
|
383
|
+
/** Each side of a candidate gutter needs at least this many items, or it's a stray gap rather than a second column. */
|
|
384
|
+
const MIN_ITEMS_PER_COLUMN = 3;
|
|
385
|
+
|
|
386
|
+
interface ColumnGutter {
|
|
387
|
+
start: number;
|
|
388
|
+
end: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Looks for an x-band that no text item's [left, right] range ever crosses,
|
|
393
|
+
* strictly inside the page's overall text span (not a page margin). A band
|
|
394
|
+
* that wide and that consistently empty across every line on the page — not
|
|
395
|
+
* just one line's word gap, which some other line would fill in — is a real
|
|
396
|
+
* column boundary.
|
|
397
|
+
*
|
|
398
|
+
* ponytail: one global gutter per page, not per text-region — a page with
|
|
399
|
+
* more than two columns, or columns that don't share a single vertical
|
|
400
|
+
* gutter, won't be detected. Upgrade to a real column-layout pass if that
|
|
401
|
+
* shows up in practice.
|
|
402
|
+
*/
|
|
403
|
+
function detectColumnGutter(items: Rect[]): ColumnGutter | undefined {
|
|
404
|
+
if (items.length < MIN_ITEMS_PER_COLUMN * 2) return undefined;
|
|
405
|
+
const minX = Math.min(...items.map((i) => i.left));
|
|
406
|
+
const maxX = Math.max(...items.map((i) => i.right));
|
|
407
|
+
const span = maxX - minX;
|
|
408
|
+
if (span <= 0) return undefined;
|
|
409
|
+
const bucketWidth = span / COLUMN_GUTTER_BUCKET_COUNT;
|
|
410
|
+
const covered = new Array<boolean>(COLUMN_GUTTER_BUCKET_COUNT).fill(false);
|
|
411
|
+
for (const item of items) {
|
|
412
|
+
const startBucket = Math.max(
|
|
413
|
+
0,
|
|
414
|
+
Math.floor((item.left - minX) / bucketWidth),
|
|
415
|
+
);
|
|
416
|
+
const endBucket = Math.min(
|
|
417
|
+
COLUMN_GUTTER_BUCKET_COUNT - 1,
|
|
418
|
+
Math.floor((item.right - minX) / bucketWidth),
|
|
419
|
+
);
|
|
420
|
+
for (let b = startBucket; b <= endBucket; b++) covered[b] = true;
|
|
421
|
+
}
|
|
422
|
+
let bestStart = -1;
|
|
423
|
+
let bestLength = 0;
|
|
424
|
+
let runStart = -1;
|
|
425
|
+
// Buckets 0 and the last one are page margins, not a gutter between columns.
|
|
426
|
+
for (let b = 1; b < COLUMN_GUTTER_BUCKET_COUNT - 1; b++) {
|
|
427
|
+
if (covered[b]) {
|
|
428
|
+
runStart = -1;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
if (runStart === -1) runStart = b;
|
|
432
|
+
const length = b - runStart + 1;
|
|
433
|
+
if (length > bestLength) {
|
|
434
|
+
bestLength = length;
|
|
435
|
+
bestStart = runStart;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const minBuckets = Math.ceil(
|
|
439
|
+
COLUMN_GUTTER_BUCKET_COUNT * MIN_COLUMN_GUTTER_WIDTH_FRACTION,
|
|
440
|
+
);
|
|
441
|
+
if (bestStart === -1 || bestLength < minBuckets) return undefined;
|
|
442
|
+
const start = minX + bestStart * bucketWidth;
|
|
443
|
+
const end = minX + (bestStart + bestLength) * bucketWidth;
|
|
444
|
+
const leftCount = items.filter((i) => i.right <= start).length;
|
|
445
|
+
const rightCount = items.filter((i) => i.left >= end).length;
|
|
446
|
+
if (leftCount < MIN_ITEMS_PER_COLUMN || rightCount < MIN_ITEMS_PER_COLUMN) {
|
|
447
|
+
return undefined;
|
|
448
|
+
}
|
|
449
|
+
return { start, end };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/** Which side of a detected column gutter an item falls on; undefined when there's no gutter or the item straddles it. */
|
|
453
|
+
function columnSideOf(
|
|
454
|
+
item: Rect,
|
|
455
|
+
gutter: ColumnGutter | undefined,
|
|
456
|
+
): 0 | 1 | undefined {
|
|
457
|
+
if (!gutter) return undefined;
|
|
458
|
+
if (item.right <= gutter.start) return 0;
|
|
459
|
+
if (item.left >= gutter.end) return 1;
|
|
460
|
+
return undefined;
|
|
335
461
|
}
|
|
336
462
|
|
|
337
|
-
/** Shared by `groupIntoLines` and `groupIntoStyledLines`: cluster items whose baselines are close enough to read as the same physical line. */
|
|
338
|
-
function groupByBaseline(
|
|
463
|
+
/** Shared by `groupIntoLines` and `groupIntoStyledLines`: cluster items whose baselines are close enough to read as the same physical line, but never across a detected column gutter even when two columns' baselines coincide. */
|
|
464
|
+
function groupByBaseline(
|
|
465
|
+
items: TextRunBox[],
|
|
466
|
+
columnGutter?: ColumnGutter,
|
|
467
|
+
): TextRunBox[][] {
|
|
339
468
|
const lines: TextRunBox[][] = [];
|
|
340
469
|
let current: TextRunBox[] = [];
|
|
341
470
|
for (const item of items) {
|
|
342
471
|
const prev = current[current.length - 1];
|
|
343
|
-
|
|
344
|
-
prev &&
|
|
472
|
+
const differentBaseline =
|
|
473
|
+
prev !== undefined &&
|
|
345
474
|
Math.abs(item.top - prev.top) >
|
|
346
|
-
Math.max(item.fontSize, prev.fontSize) * 0.4
|
|
347
|
-
|
|
475
|
+
Math.max(item.fontSize, prev.fontSize) * 0.4;
|
|
476
|
+
const differentColumn =
|
|
477
|
+
prev !== undefined &&
|
|
478
|
+
columnSideOf(prev, columnGutter) !== undefined &&
|
|
479
|
+
columnSideOf(item, columnGutter) !== undefined &&
|
|
480
|
+
columnSideOf(prev, columnGutter) !== columnSideOf(item, columnGutter);
|
|
481
|
+
if (prev && (differentBaseline || differentColumn)) {
|
|
348
482
|
lines.push(current);
|
|
349
483
|
current = [item];
|
|
350
484
|
} else {
|
|
@@ -379,7 +513,8 @@ export function mergeLineRuns(items: TextRunBox[]): TextRunBox[] {
|
|
|
379
513
|
prev.bold === item.bold &&
|
|
380
514
|
prev.italic === item.italic &&
|
|
381
515
|
prev.underline === item.underline &&
|
|
382
|
-
prev.href === item.href
|
|
516
|
+
prev.href === item.href &&
|
|
517
|
+
prev.fontFamily === item.fontFamily;
|
|
383
518
|
const needsSpace =
|
|
384
519
|
prev !== undefined && item.left - prev.right > item.fontSize * 0.25;
|
|
385
520
|
if (prev && sameStyle) {
|
|
@@ -387,6 +522,7 @@ export function mergeLineRuns(items: TextRunBox[]): TextRunBox[] {
|
|
|
387
522
|
prev.right = Math.max(prev.right, item.right);
|
|
388
523
|
prev.top = Math.min(prev.top, item.top);
|
|
389
524
|
prev.bottom = Math.max(prev.bottom, item.bottom);
|
|
525
|
+
prev.paintOrder = Math.min(prev.paintOrder, item.paintOrder);
|
|
390
526
|
} else {
|
|
391
527
|
// A word-sized gap has to survive a style change too. Only the
|
|
392
528
|
// same-style branch used to re-add it, so a heading whose colour
|
|
@@ -404,7 +540,8 @@ export function mergeLineRuns(items: TextRunBox[]): TextRunBox[] {
|
|
|
404
540
|
|
|
405
541
|
/** Like `groupIntoLines`, but keeps each line's distinct-styled runs separate — needed once a line can mix colors/weights within itself. */
|
|
406
542
|
export function groupIntoStyledLines(items: TextRunBox[]): TextLine[] {
|
|
407
|
-
|
|
543
|
+
const columnGutter = detectColumnGutter(items);
|
|
544
|
+
return groupByBaseline(items, columnGutter).map((lineItems) => ({
|
|
408
545
|
...rectFromCorners(
|
|
409
546
|
lineItems.flatMap(
|
|
410
547
|
(s) =>
|
|
@@ -468,6 +605,49 @@ export function lineSpacingBeforePt(
|
|
|
468
605
|
return previousLine ? Math.max(0, line.top - previousLine.bottom) : 0;
|
|
469
606
|
}
|
|
470
607
|
|
|
608
|
+
/** How close a line's own midpoint/edge has to sit to the block's for the whole block to read as centered/right-aligned rather than left. */
|
|
609
|
+
const TEXT_ALIGNMENT_TOLERANCE_PT = 3;
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* A block's lines already carry their exact `left`/`right` geometry — rather
|
|
613
|
+
* than always rendering as left-aligned, compare each line against the
|
|
614
|
+
* block's own bounding box to recover which alignment actually produced
|
|
615
|
+
* that geometry. A single line can't disambiguate center from left (every
|
|
616
|
+
* line trivially "centers" on itself), so it's left as-is.
|
|
617
|
+
*/
|
|
618
|
+
export function detectBlockAlignment(
|
|
619
|
+
blockLines: Rect[],
|
|
620
|
+
blockLeft: number,
|
|
621
|
+
blockRight: number,
|
|
622
|
+
): "left" | "center" | "right" {
|
|
623
|
+
if (blockLines.length < 2) return "left";
|
|
624
|
+
// The block bounds come from these same lines. Equal-width lines therefore
|
|
625
|
+
// have identical left edges, right edges, and midpoints whether they were
|
|
626
|
+
// left-aligned or centered; keep that ambiguous case left-aligned rather
|
|
627
|
+
// than moving ordinary paragraphs to the center.
|
|
628
|
+
const widths = blockLines.map((line) => line.right - line.left);
|
|
629
|
+
const minWidth = Math.min(...widths);
|
|
630
|
+
const maxWidth = Math.max(...widths);
|
|
631
|
+
const sameBounds = blockLines.every(
|
|
632
|
+
(line) => line.left === blockLeft && line.right === blockRight,
|
|
633
|
+
);
|
|
634
|
+
if (maxWidth - minWidth <= TEXT_ALIGNMENT_TOLERANCE_PT && sameBounds) {
|
|
635
|
+
return "left";
|
|
636
|
+
}
|
|
637
|
+
const midpoint = (blockLeft + blockRight) / 2;
|
|
638
|
+
const centered = blockLines.every(
|
|
639
|
+
(line) =>
|
|
640
|
+
Math.abs((line.left + line.right) / 2 - midpoint) <=
|
|
641
|
+
TEXT_ALIGNMENT_TOLERANCE_PT,
|
|
642
|
+
);
|
|
643
|
+
if (centered) return "center";
|
|
644
|
+
const rightAligned = blockLines.every(
|
|
645
|
+
(line) => Math.abs(line.right - blockRight) <= TEXT_ALIGNMENT_TOLERANCE_PT,
|
|
646
|
+
);
|
|
647
|
+
if (rightAligned) return "right";
|
|
648
|
+
return "left";
|
|
649
|
+
}
|
|
650
|
+
|
|
471
651
|
/** A run needs to overlap most of a candidate background image to inherit its "assume dark" contrast — a run merely near a small inset photo shouldn't be treated as sitting on top of it. */
|
|
472
652
|
const BACKGROUND_IMAGE_OVERLAP_RATIO = 0.5;
|
|
473
653
|
|
|
@@ -517,7 +697,13 @@ function sliceTextItemBySegments(
|
|
|
517
697
|
if (segments.length <= 1) {
|
|
518
698
|
const color = segments[0]?.color;
|
|
519
699
|
const box = textItemToBox(item, viewportTransform, color);
|
|
520
|
-
|
|
700
|
+
if (!box) return [];
|
|
701
|
+
return [
|
|
702
|
+
{
|
|
703
|
+
box: { ...box, paintOrder: segments[0]?.paintOrder ?? 0 },
|
|
704
|
+
colorKnown: color !== undefined,
|
|
705
|
+
},
|
|
706
|
+
];
|
|
521
707
|
}
|
|
522
708
|
const fullBox = textItemToBox(item, viewportTransform, DEFAULT_TEXT_COLOR);
|
|
523
709
|
if (!fullBox) return [];
|
|
@@ -537,6 +723,7 @@ function sliceTextItemBySegments(
|
|
|
537
723
|
right: fullBox.left + spanX * endFraction,
|
|
538
724
|
text,
|
|
539
725
|
color: segment.color ?? DEFAULT_TEXT_COLOR,
|
|
726
|
+
paintOrder: segment.paintOrder,
|
|
540
727
|
},
|
|
541
728
|
colorKnown: segment.color !== undefined,
|
|
542
729
|
});
|
|
@@ -555,7 +742,7 @@ async function buildTextElements(
|
|
|
555
742
|
backgroundImageRect: Rect | undefined,
|
|
556
743
|
underlineRects: UnderlineRect[],
|
|
557
744
|
linkRects: LinkRect[],
|
|
558
|
-
): Promise<ParsedElement[]> {
|
|
745
|
+
): Promise<{ element: ParsedElement; paintOrder: number }[]> {
|
|
559
746
|
const content = await page.getTextContent();
|
|
560
747
|
const rawItems = content.items.filter(
|
|
561
748
|
(item): item is TextItem => "str" in item && item.str.trim().length > 0,
|
|
@@ -589,15 +776,25 @@ async function buildTextElements(
|
|
|
589
776
|
// ligature collapsing multiple glyphs into fewer output
|
|
590
777
|
// characters). Split what fits and leave the remainder queued for
|
|
591
778
|
// the next item instead of corrupting this item's text.
|
|
592
|
-
itemSegments.push({
|
|
779
|
+
itemSegments.push({
|
|
780
|
+
length: remaining,
|
|
781
|
+
color: segment.color,
|
|
782
|
+
paintOrder: segment.paintOrder,
|
|
783
|
+
});
|
|
593
784
|
segmentQueue[0] = { ...segment, length: segment.length - remaining };
|
|
594
785
|
consumed = item.str.length;
|
|
595
786
|
}
|
|
596
787
|
}
|
|
597
788
|
if (consumed < item.str.length) {
|
|
789
|
+
// Ran out of tracked color/paint-order segments before this item's
|
|
790
|
+
// text did — reuse the last known paint order (falling back to the
|
|
791
|
+
// very start of the page) rather than leaving it unset.
|
|
792
|
+
const lastPaintOrder =
|
|
793
|
+
itemSegments[itemSegments.length - 1]?.paintOrder ?? 0;
|
|
598
794
|
itemSegments.push({
|
|
599
795
|
length: item.str.length - consumed,
|
|
600
796
|
color: undefined,
|
|
797
|
+
paintOrder: lastPaintOrder,
|
|
601
798
|
});
|
|
602
799
|
}
|
|
603
800
|
return sliceTextItemBySegments(
|
|
@@ -642,6 +839,7 @@ async function buildTextElements(
|
|
|
642
839
|
const top = Math.min(...blockLines.map((l) => l.top));
|
|
643
840
|
const right = Math.max(...blockLines.map((l) => l.right));
|
|
644
841
|
const bottom = Math.max(...blockLines.map((l) => l.bottom));
|
|
842
|
+
const alignment = detectBlockAlignment(blockLines, left, right);
|
|
645
843
|
const paragraphs: ParsedParagraph[] = blockLines.map((line, lineIndex) => {
|
|
646
844
|
const spaceBeforePt = lineSpacingBeforePt(
|
|
647
845
|
line,
|
|
@@ -656,19 +854,26 @@ async function buildTextElements(
|
|
|
656
854
|
italic: run.italic,
|
|
657
855
|
underline: run.underline,
|
|
658
856
|
href: run.href,
|
|
857
|
+
fontFamily: run.fontFamily,
|
|
659
858
|
})),
|
|
660
|
-
alignment
|
|
859
|
+
alignment,
|
|
661
860
|
...(spaceBeforePt > 0 ? { spaceBeforePt } : {}),
|
|
662
861
|
};
|
|
663
862
|
});
|
|
863
|
+
const paintOrder = Math.min(
|
|
864
|
+
...blockLines.flatMap((line) => line.runs.map((run) => run.paintOrder)),
|
|
865
|
+
);
|
|
664
866
|
return {
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
867
|
+
element: {
|
|
868
|
+
id: `pdf-text-${pageNumber}-${index}`,
|
|
869
|
+
kind: "text",
|
|
870
|
+
x: left * EMU_PER_POINT,
|
|
871
|
+
y: top * EMU_PER_POINT,
|
|
872
|
+
width: Math.max(1, right - left) * EMU_PER_POINT,
|
|
873
|
+
height: Math.max(1, bottom - top) * EMU_PER_POINT,
|
|
874
|
+
paragraphs,
|
|
875
|
+
},
|
|
876
|
+
paintOrder,
|
|
672
877
|
};
|
|
673
878
|
});
|
|
674
879
|
}
|
|
@@ -677,10 +882,19 @@ async function buildTextElements(
|
|
|
677
882
|
export interface TextColorRun {
|
|
678
883
|
length: number;
|
|
679
884
|
color: string | undefined;
|
|
885
|
+
/** Index in this page's real content-stream paint order, shared with image paint events. */
|
|
886
|
+
paintOrder: number;
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/** An image's placed rect plus its index in this page's real paint order, shared with `TextColorRun.paintOrder` so images and text can be z-ordered by how the PDF actually painted them instead of images always going first. */
|
|
890
|
+
export interface PaintedImageRect extends Rect {
|
|
891
|
+
paintOrder: number;
|
|
892
|
+
/** The PDF resource name shared with the corresponding `pdf.getImage()` result. */
|
|
893
|
+
imageName: string | undefined;
|
|
680
894
|
}
|
|
681
895
|
|
|
682
896
|
interface PageGraphics {
|
|
683
|
-
imageRects:
|
|
897
|
+
imageRects: PaintedImageRect[];
|
|
684
898
|
/** The earliest fill covering most of the page — almost always the deck's background. */
|
|
685
899
|
backgroundColor: string | undefined;
|
|
686
900
|
/** Fill color + character length of each real text-showing op, in operator-list order; `color` is undefined when it wasn't recoverable. */
|
|
@@ -702,12 +916,16 @@ interface PageGraphics {
|
|
|
702
916
|
export async function walkPageGraphics(
|
|
703
917
|
page: PDFPageProxy,
|
|
704
918
|
viewport: { transform: Mat; width: number; height: number },
|
|
919
|
+
operatorList?: PdfOperatorList,
|
|
705
920
|
): Promise<PageGraphics> {
|
|
706
921
|
const viewportTransform = viewport.transform;
|
|
707
|
-
const opList = await page.getOperatorList();
|
|
708
|
-
const imageRects:
|
|
922
|
+
const opList = operatorList ?? (await page.getOperatorList());
|
|
923
|
+
const imageRects: PaintedImageRect[] = [];
|
|
709
924
|
const textRuns: TextColorRun[] = [];
|
|
710
925
|
const underlineRects: UnderlineRect[] = [];
|
|
926
|
+
// Shared by images and text so both can be sorted back into one real
|
|
927
|
+
// paint order afterward, instead of images always sitting behind text.
|
|
928
|
+
let paintOrder = 0;
|
|
711
929
|
let backgroundColor: string | undefined;
|
|
712
930
|
let fillColor = DEFAULT_TEXT_COLOR;
|
|
713
931
|
// Starts true: the PDF spec's initial nonstroking color IS black, so text
|
|
@@ -735,10 +953,7 @@ export async function walkPageGraphics(
|
|
|
735
953
|
}
|
|
736
954
|
} else if (fn === OPS.transform) {
|
|
737
955
|
ctm = Util.transform(ctm, args as Mat) as Mat;
|
|
738
|
-
} else if (
|
|
739
|
-
fn === OPS.paintImageXObject ||
|
|
740
|
-
fn === OPS.paintInlineImageXObject
|
|
741
|
-
) {
|
|
956
|
+
} else if (isImagePaintOp(fn)) {
|
|
742
957
|
const unitCorners: [number, number][] = [
|
|
743
958
|
[0, 0],
|
|
744
959
|
[1, 0],
|
|
@@ -749,7 +964,11 @@ export async function walkPageGraphics(
|
|
|
749
964
|
const [ux, uy] = applyPoint(ctm, x, y);
|
|
750
965
|
return applyPoint(viewportTransform, ux, uy);
|
|
751
966
|
});
|
|
752
|
-
imageRects.push(
|
|
967
|
+
imageRects.push({
|
|
968
|
+
...rectFromCorners(deviceCorners),
|
|
969
|
+
paintOrder: paintOrder++,
|
|
970
|
+
imageName: typeof args[0] === "string" ? args[0] : undefined,
|
|
971
|
+
});
|
|
753
972
|
} else if (TEXT_SHOWING_OPS.has(fn)) {
|
|
754
973
|
// Count only the glyph descriptors, not the raw kerning-adjustment
|
|
755
974
|
// numbers a `TJ` array can interleave between them, so this length
|
|
@@ -767,6 +986,7 @@ export async function walkPageGraphics(
|
|
|
767
986
|
textRuns.push({
|
|
768
987
|
length,
|
|
769
988
|
color: fillColorKnown ? fillColor : undefined,
|
|
989
|
+
paintOrder: paintOrder++,
|
|
770
990
|
});
|
|
771
991
|
}
|
|
772
992
|
} else if (fn === OPS.constructPath) {
|
|
@@ -879,26 +1099,30 @@ export async function parsePdfFidelity(
|
|
|
879
1099
|
doc: PDFDocumentProxy,
|
|
880
1100
|
imagesByPage: PdfPageImage[],
|
|
881
1101
|
): Promise<PdfFidelityPage[]> {
|
|
882
|
-
const imageBytesByPage = new Map<number,
|
|
1102
|
+
const imageBytesByPage = new Map<number, PdfPageImage["images"]>();
|
|
883
1103
|
for (const entry of imagesByPage) {
|
|
884
|
-
imageBytesByPage.set(
|
|
885
|
-
entry.pageNumber,
|
|
886
|
-
entry.images.map((img) => img.data),
|
|
887
|
-
);
|
|
1104
|
+
imageBytesByPage.set(entry.pageNumber, entry.images);
|
|
888
1105
|
}
|
|
889
1106
|
|
|
890
1107
|
const pages: PdfFidelityPage[] = [];
|
|
891
1108
|
for (let pageNumber = 1; pageNumber <= doc.numPages; pageNumber++) {
|
|
1109
|
+
let imagePaintCount = 0;
|
|
892
1110
|
try {
|
|
893
1111
|
const page = await doc.getPage(pageNumber);
|
|
894
1112
|
const viewport = page.getViewport({ scale: 1, rotation: page.rotate });
|
|
895
1113
|
const viewportTransform = viewport.transform as Mat;
|
|
1114
|
+
const operatorList = await page.getOperatorList();
|
|
1115
|
+
imagePaintCount = operatorList.fnArray.filter(isImagePaintOp).length;
|
|
896
1116
|
|
|
897
|
-
const graphics = await walkPageGraphics(
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1117
|
+
const graphics = await walkPageGraphics(
|
|
1118
|
+
page,
|
|
1119
|
+
{
|
|
1120
|
+
transform: viewportTransform,
|
|
1121
|
+
width: viewport.width,
|
|
1122
|
+
height: viewport.height,
|
|
1123
|
+
},
|
|
1124
|
+
operatorList,
|
|
1125
|
+
);
|
|
902
1126
|
const linkRects = await collectLinkRects(page, viewportTransform);
|
|
903
1127
|
const imageRects = graphics.imageRects;
|
|
904
1128
|
// The largest image that covers a substantial share of the page is
|
|
@@ -911,29 +1135,61 @@ export async function parsePdfFidelity(
|
|
|
911
1135
|
.filter((rect) => rectArea(rect) >= pageArea * 0.4)
|
|
912
1136
|
.sort((a, b) => rectArea(b) - rectArea(a))[0];
|
|
913
1137
|
const imageBytes = imageBytesByPage.get(pageNumber) ?? [];
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1138
|
+
const rectsByImageName = new Map<string, PaintedImageRect[]>();
|
|
1139
|
+
for (const rect of imageRects) {
|
|
1140
|
+
if (rect.imageName === undefined) continue;
|
|
1141
|
+
const rects = rectsByImageName.get(rect.imageName) ?? [];
|
|
1142
|
+
rects.push(rect);
|
|
1143
|
+
rectsByImageName.set(rect.imageName, rects);
|
|
1144
|
+
}
|
|
1145
|
+
const bytesByImageName = new Map<string, PdfPageImage["images"]>();
|
|
1146
|
+
for (const image of imageBytes) {
|
|
1147
|
+
const images = bytesByImageName.get(image.name) ?? [];
|
|
1148
|
+
images.push(image);
|
|
1149
|
+
bytesByImageName.set(image.name, images);
|
|
1150
|
+
}
|
|
1151
|
+
const imageDataByRect = new Map<PaintedImageRect, Uint8Array>();
|
|
1152
|
+
for (const [imageName, rects] of rectsByImageName) {
|
|
1153
|
+
const images = bytesByImageName.get(imageName);
|
|
1154
|
+
// A resource can be painted more than once. If extraction returned a
|
|
1155
|
+
// different number of occurrences, there is no safe way to know which
|
|
1156
|
+
// placement is missing, so omit all of them rather than misplacing one.
|
|
1157
|
+
if (!images || images.length !== rects.length) continue;
|
|
1158
|
+
for (let index = 0; index < rects.length; index++) {
|
|
1159
|
+
const data = images[index].data;
|
|
1160
|
+
if (data.byteLength > 0) imageDataByRect.set(rects[index], data);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
const imageResults: { element: ParsedElement; paintOrder: number }[] =
|
|
1164
|
+
imageRects
|
|
1165
|
+
.map((rect) => ({ rect, data: imageDataByRect.get(rect) }))
|
|
1166
|
+
.filter(
|
|
1167
|
+
({ rect, data }) =>
|
|
1168
|
+
data &&
|
|
1169
|
+
rect.right - rect.left >= MIN_IMAGE_POINTS &&
|
|
1170
|
+
rect.bottom - rect.top >= MIN_IMAGE_POINTS,
|
|
1171
|
+
)
|
|
1172
|
+
.map(({ rect, data }, index) => ({
|
|
1173
|
+
element: {
|
|
1174
|
+
id: `pdf-img-${pageNumber}-${index}`,
|
|
1175
|
+
kind: "image" as const,
|
|
1176
|
+
x: rect.left * EMU_PER_POINT,
|
|
1177
|
+
y: rect.top * EMU_PER_POINT,
|
|
1178
|
+
width: Math.max(1, rect.right - rect.left) * EMU_PER_POINT,
|
|
1179
|
+
height: Math.max(1, rect.bottom - rect.top) * EMU_PER_POINT,
|
|
1180
|
+
image: {
|
|
1181
|
+
data: data as Uint8Array,
|
|
1182
|
+
mimeType: "image/png",
|
|
1183
|
+
name: `image-${index}`,
|
|
1184
|
+
},
|
|
1185
|
+
},
|
|
1186
|
+
paintOrder: rect.paintOrder,
|
|
1187
|
+
}));
|
|
1188
|
+
const imagesSkipped = imageRects.filter(
|
|
1189
|
+
(rect) => !imageDataByRect.has(rect),
|
|
1190
|
+
).length;
|
|
1191
|
+
|
|
1192
|
+
const textResults = await buildTextElements(
|
|
937
1193
|
page,
|
|
938
1194
|
viewportTransform,
|
|
939
1195
|
pageNumber,
|
|
@@ -944,12 +1200,17 @@ export async function parsePdfFidelity(
|
|
|
944
1200
|
linkRects,
|
|
945
1201
|
);
|
|
946
1202
|
|
|
1203
|
+
const elements = [...imageResults, ...textResults]
|
|
1204
|
+
.sort((a, b) => a.paintOrder - b.paintOrder)
|
|
1205
|
+
.map((result) => result.element);
|
|
1206
|
+
|
|
947
1207
|
pages.push({
|
|
948
1208
|
pageNumber,
|
|
949
1209
|
widthEmu: viewport.width * EMU_PER_POINT,
|
|
950
1210
|
heightEmu: viewport.height * EMU_PER_POINT,
|
|
951
1211
|
backgroundColor: graphics.backgroundColor,
|
|
952
|
-
elements
|
|
1212
|
+
elements,
|
|
1213
|
+
imagesSkipped,
|
|
953
1214
|
});
|
|
954
1215
|
} catch (err) {
|
|
955
1216
|
console.warn(
|
|
@@ -962,6 +1223,10 @@ export async function parsePdfFidelity(
|
|
|
962
1223
|
heightEmu: 0,
|
|
963
1224
|
backgroundColor: undefined,
|
|
964
1225
|
elements: [],
|
|
1226
|
+
// A parse failure after operator-list inspection must remain visibly
|
|
1227
|
+
// partial. If inspection itself failed, keep the failure loud rather
|
|
1228
|
+
// than allowing a text-only fallback to look source-faithful.
|
|
1229
|
+
imagesSkipped: Math.max(imagePaintCount, 1),
|
|
965
1230
|
});
|
|
966
1231
|
}
|
|
967
1232
|
}
|
|
@@ -12,6 +12,30 @@ const BROWSER_RENDERABLE_IMAGE_MIME_TYPES = new Set([
|
|
|
12
12
|
"image/bmp",
|
|
13
13
|
]);
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Validate every image before any upload starts. PPTX imports reject partial
|
|
17
|
+
* fidelity, so an unsupported image must not leave earlier slide uploads
|
|
18
|
+
* orphaned when the action eventually throws.
|
|
19
|
+
*/
|
|
20
|
+
export function assertPptxImagesRenderable(
|
|
21
|
+
slides: readonly ParsedSlide[],
|
|
22
|
+
): void {
|
|
23
|
+
const unsupported = new Set<string>();
|
|
24
|
+
for (const slide of slides) {
|
|
25
|
+
for (const element of slide.elements ?? []) {
|
|
26
|
+
if (element.kind !== "image" || !element.image) continue;
|
|
27
|
+
if (!BROWSER_RENDERABLE_IMAGE_MIME_TYPES.has(element.image.mimeType)) {
|
|
28
|
+
unsupported.add(element.image.mimeType || "unknown");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (unsupported.size > 0) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Source-faithful PPTX import cannot preserve unsupported image type(s): ${[...unsupported].join(", ")}. No images were uploaded. Re-export the deck with browser-renderable images or use a PDF export for page-faithful preservation.`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
15
39
|
export async function uploadPptxSlideImages(args: {
|
|
16
40
|
slide: ParsedSlide;
|
|
17
41
|
slideIndex: number;
|