@agent-native/core 0.146.1 → 0.146.2
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/slides/actions/import-file.ts +144 -99
- package/corpus/templates/slides/package.json +1 -0
- package/corpus/templates/slides/server/handlers/import/html-converter.ts +9 -0
- package/corpus/templates/slides/server/handlers/import/pdf-fidelity-parser.ts +960 -0
- package/dist/ingestion/pptx.d.ts +1 -0
- package/dist/ingestion/pptx.js +353 -28
- package/dist/notifications/routes.d.ts +3 -3
- package/dist/progress/routes.d.ts +1 -1
- package/dist/secrets/routes.d.ts +6 -6
- package/package.json +2 -2
package/corpus/README.md
CHANGED
|
@@ -2,7 +2,6 @@ import path from "path";
|
|
|
2
2
|
|
|
3
3
|
import { defineAction } from "@agent-native/core";
|
|
4
4
|
import { writeAppState } from "@agent-native/core/application-state";
|
|
5
|
-
import { uploadFile } from "@agent-native/core/file-upload";
|
|
6
5
|
import { startBuilderDesignSystemIndex } from "@agent-native/core/server";
|
|
7
6
|
import {
|
|
8
7
|
getRequestOrgId,
|
|
@@ -273,21 +272,16 @@ export default defineAction({
|
|
|
273
272
|
const { PDFParse, canvasFactory } = await setupPdfParse();
|
|
274
273
|
const title = titleFromPath(filename);
|
|
275
274
|
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
275
|
+
// Reconstruct each page's real layout — positioned text blocks at
|
|
276
|
+
// their actual sizes plus every embedded image at its actual
|
|
277
|
+
// placement — instead of flattening the page to one guessed
|
|
278
|
+
// background photo and a canned text template. Image placement needs
|
|
279
|
+
// the optional canvas renderer; text positioning does not, so this
|
|
280
|
+
// still beats the old bullet-text fallback even when canvasFactory is
|
|
281
|
+
// unavailable in this runtime.
|
|
283
282
|
if (importIntoDeck) {
|
|
284
|
-
if (!canvasFactory) {
|
|
285
|
-
throw new Error(
|
|
286
|
-
"Source-faithful PDF import is unavailable in this runtime because the page renderer is not configured. Retry after connecting file storage or use a PDF export with a supported Slides deployment.",
|
|
287
|
-
);
|
|
288
|
-
}
|
|
289
283
|
if (!deckId) throw new Error("deckId is required to import into deck");
|
|
290
|
-
return
|
|
284
|
+
return importPdfPagesWithFidelity({
|
|
291
285
|
fileBuffer,
|
|
292
286
|
title,
|
|
293
287
|
deckId,
|
|
@@ -351,60 +345,61 @@ function nearestAspectRatio(width: number, height: number): AspectRatio {
|
|
|
351
345
|
return best;
|
|
352
346
|
}
|
|
353
347
|
|
|
354
|
-
async function
|
|
348
|
+
async function importPdfPagesWithFidelity(args: {
|
|
355
349
|
fileBuffer: Buffer;
|
|
356
350
|
title: string;
|
|
357
351
|
deckId: string;
|
|
358
352
|
PDFParse: Awaited<ReturnType<typeof setupPdfParse>>["PDFParse"];
|
|
359
|
-
canvasFactory: object;
|
|
353
|
+
canvasFactory: object | undefined;
|
|
360
354
|
}) {
|
|
361
355
|
const { fileBuffer, title, deckId, PDFParse, canvasFactory } = args;
|
|
362
|
-
const {
|
|
356
|
+
const { convertToSlideHtml, convertSectionsToSlides } =
|
|
363
357
|
await import("../server/handlers/import/html-converter.js");
|
|
358
|
+
const { parsePdfFidelity } =
|
|
359
|
+
await import("../server/handlers/import/pdf-fidelity-parser.js");
|
|
364
360
|
|
|
365
361
|
const pdf = new PDFParse({
|
|
366
362
|
data: new Uint8Array(fileBuffer),
|
|
367
363
|
CanvasFactory: canvasFactory,
|
|
368
364
|
});
|
|
369
365
|
let pages: { num: number; text: string }[];
|
|
370
|
-
let
|
|
371
|
-
number,
|
|
372
|
-
{ data: Uint8Array; width: number; height: number }
|
|
373
|
-
>;
|
|
366
|
+
let fidelityPages: Awaited<ReturnType<typeof parsePdfFidelity>>;
|
|
374
367
|
try {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
368
|
+
pages = normalizePdfPages(await pdf.getText());
|
|
369
|
+
// Image placement needs the optional canvas renderer to decode pixel
|
|
370
|
+
// data; skip it (text still gets real positions/sizes) when the native
|
|
371
|
+
// canvas binding isn't available in this runtime.
|
|
372
|
+
const imageResult = canvasFactory
|
|
373
|
+
? await pdf
|
|
374
|
+
.getImage({
|
|
375
|
+
imageBuffer: true,
|
|
376
|
+
imageDataUrl: false,
|
|
377
|
+
imageThreshold: 0,
|
|
378
|
+
})
|
|
379
|
+
.catch((err) => {
|
|
380
|
+
console.warn(
|
|
381
|
+
"[import-file] PDF image extraction failed, importing text-only fidelity:",
|
|
382
|
+
err instanceof Error ? err.message : String(err),
|
|
383
|
+
);
|
|
384
|
+
return undefined;
|
|
385
|
+
})
|
|
386
|
+
: undefined;
|
|
387
|
+
|
|
388
|
+
// `pdf-parse` memoizes the loaded pdfjs document behind `load()` (a
|
|
389
|
+
// TS-only `private` method — a real, callable runtime property).
|
|
390
|
+
// Reaching into it reuses the exact document `getText`/`getImage` above
|
|
391
|
+
// already parsed instead of parsing the file a second time.
|
|
392
|
+
const doc = await (
|
|
393
|
+
pdf as unknown as {
|
|
394
|
+
load(): Promise<
|
|
395
|
+
import("pdfjs-dist/legacy/build/pdf.mjs").PDFDocumentProxy
|
|
396
|
+
>;
|
|
397
|
+
}
|
|
398
|
+
).load();
|
|
399
|
+
// coercion-ok: undefined here means either canvasFactory was absent or
|
|
400
|
+
// getImage() already failed and logged a warning above — text-only
|
|
401
|
+
// fidelity is the intended degrade, not a swallowed failure.
|
|
402
|
+
fidelityPages = await parsePdfFidelity(doc, imageResult?.pages ?? []);
|
|
408
403
|
} finally {
|
|
409
404
|
await pdf.destroy();
|
|
410
405
|
}
|
|
@@ -413,60 +408,110 @@ async function importPdfPagesAsFullBleedSlides(args: {
|
|
|
413
408
|
throw new Error("The PDF renderer returned no importable pages.");
|
|
414
409
|
}
|
|
415
410
|
|
|
411
|
+
// A page with neither extracted text nor a fidelity element is only ever
|
|
412
|
+
// produced when nothing on it could be recovered (e.g. a scanned/image
|
|
413
|
+
// page and canvas rendering was unavailable or failed) — if that's true of
|
|
414
|
+
// every page, importing anyway would silently create a deck of blank
|
|
415
|
+
// placeholder slides and report success, matching the earlier
|
|
416
|
+
// text-extraction path's "needs OCR" failure keeps that lossy import from
|
|
417
|
+
// going unnoticed.
|
|
418
|
+
const hasRecoverableContent = pages.some((page) => {
|
|
419
|
+
const fidelity = fidelityPages.find((p) => p.pageNumber === page.num);
|
|
420
|
+
return page.text.trim().length > 0 || (fidelity?.elements.length ?? 0) > 0;
|
|
421
|
+
});
|
|
422
|
+
if (!hasRecoverableContent) {
|
|
423
|
+
throw new Error(
|
|
424
|
+
"No importable text or images found in this PDF. Scanned PDFs need OCR first.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
416
428
|
// Source decks (e.g. Instagram carousel exports) are commonly portrait or
|
|
417
429
|
// square, not the deck editor's 16:9 default — match the canvas to the
|
|
418
|
-
//
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
.
|
|
422
|
-
const aspectRatio = firstScreenshot
|
|
423
|
-
? nearestAspectRatio(firstScreenshot.width, firstScreenshot.height)
|
|
430
|
+
// PDF's own real page proportions instead of stretching/cropping it.
|
|
431
|
+
const firstSizedPage = fidelityPages.find((p) => p.widthEmu > 0);
|
|
432
|
+
const aspectRatio = firstSizedPage
|
|
433
|
+
? nearestAspectRatio(firstSizedPage.widthEmu, firstSizedPage.heightEmu)
|
|
424
434
|
: undefined;
|
|
425
435
|
|
|
426
436
|
const ownerEmail = getRequestUserEmail();
|
|
437
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
438
|
+
|
|
439
|
+
// Bounded the same way as the PPTX upload path below — an unbounded
|
|
440
|
+
// `Promise.all` here would fire one image-upload batch per page at once,
|
|
441
|
+
// and a large deck can be dozens of pages.
|
|
442
|
+
const uploadLimit = pLimit(4);
|
|
427
443
|
const imported = await Promise.all(
|
|
428
|
-
pages.map(
|
|
429
|
-
|
|
444
|
+
pages.map((page, index) =>
|
|
445
|
+
uploadLimit(async () => {
|
|
446
|
+
const fidelity = fidelityPages.find((p) => p.pageNumber === page.num);
|
|
447
|
+
|
|
448
|
+
if (!fidelity || fidelity.elements.length === 0) {
|
|
449
|
+
// Fidelity parsing failed or found nothing placeable on this page
|
|
450
|
+
// (e.g. a fully blank page) — fall back to plain extracted text
|
|
451
|
+
// instead of producing a silently blank slide.
|
|
452
|
+
const firstLine = page.text.split(/\r?\n/)[0]?.trim();
|
|
453
|
+
const [content] = convertSectionsToSlides([
|
|
454
|
+
{ heading: firstLine || `Page ${page.num}`, content: page.text },
|
|
455
|
+
]);
|
|
456
|
+
const id = newSlideId();
|
|
457
|
+
return {
|
|
458
|
+
slide: {
|
|
459
|
+
id,
|
|
460
|
+
content: content ?? '<div class="fmd-slide"></div>',
|
|
461
|
+
layout: "content",
|
|
462
|
+
notes: page.text,
|
|
463
|
+
},
|
|
464
|
+
snapshot: {
|
|
465
|
+
id,
|
|
466
|
+
text: page.text,
|
|
467
|
+
notes: page.text,
|
|
468
|
+
imageUrls: [],
|
|
469
|
+
editableText: true,
|
|
470
|
+
} satisfies SourceImportSlideSnapshot,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
430
473
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
474
|
+
const slideForUpload = {
|
|
475
|
+
texts: [],
|
|
476
|
+
images: [],
|
|
477
|
+
elements: fidelity.elements,
|
|
478
|
+
};
|
|
479
|
+
const uploaded = await uploadPptxSlideImages({
|
|
480
|
+
slide: slideForUpload,
|
|
481
|
+
slideIndex: index,
|
|
482
|
+
ownerEmail,
|
|
483
|
+
});
|
|
484
|
+
const content = convertToSlideHtml(
|
|
485
|
+
{
|
|
486
|
+
texts: [],
|
|
487
|
+
images: [],
|
|
488
|
+
elements: fidelity.elements,
|
|
489
|
+
widthEmu: fidelity.widthEmu,
|
|
490
|
+
heightEmu: fidelity.heightEmu,
|
|
491
|
+
// A page with no detected full-page fill is plain paper.
|
|
492
|
+
backgroundColor: fidelity.backgroundColor ?? "#ffffff", // guard:allow-raw-color - fallback plain-paper background, not a design-system token
|
|
493
|
+
},
|
|
494
|
+
uploaded.urls,
|
|
434
495
|
);
|
|
435
|
-
}
|
|
436
496
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
screenshot.width,
|
|
456
|
-
screenshot.height,
|
|
457
|
-
),
|
|
458
|
-
layout: "full-image",
|
|
459
|
-
notes: page.text,
|
|
460
|
-
},
|
|
461
|
-
snapshot: {
|
|
462
|
-
id,
|
|
463
|
-
text: page.text,
|
|
464
|
-
notes: page.text,
|
|
465
|
-
imageUrls: [uploadResult.url],
|
|
466
|
-
editableText: false,
|
|
467
|
-
} satisfies SourceImportSlideSnapshot,
|
|
468
|
-
};
|
|
469
|
-
}),
|
|
497
|
+
const id = newSlideId();
|
|
498
|
+
return {
|
|
499
|
+
slide: {
|
|
500
|
+
id,
|
|
501
|
+
content,
|
|
502
|
+
layout: "content",
|
|
503
|
+
notes: page.text,
|
|
504
|
+
},
|
|
505
|
+
snapshot: {
|
|
506
|
+
id,
|
|
507
|
+
text: page.text,
|
|
508
|
+
notes: page.text,
|
|
509
|
+
imageUrls: Object.values(uploaded.urls),
|
|
510
|
+
editableText: true,
|
|
511
|
+
} satisfies SourceImportSlideSnapshot,
|
|
512
|
+
};
|
|
513
|
+
}),
|
|
514
|
+
),
|
|
470
515
|
);
|
|
471
516
|
const slides = imported.map((entry) => entry.slide);
|
|
472
517
|
const sourceImport = buildSourceImportMetadata({
|
|
@@ -334,9 +334,18 @@ function formatFidelityRun(
|
|
|
334
334
|
`font-style:${run.italic ? "italic" : "normal"}`,
|
|
335
335
|
`text-decoration:${run.underline ? "underline" : "none"}`,
|
|
336
336
|
].join(";");
|
|
337
|
+
const href = run.href && isSafeLinkHref(run.href) ? run.href : undefined;
|
|
338
|
+
if (href) {
|
|
339
|
+
return `<a href="${esc(href)}" target="_blank" rel="noopener noreferrer" style="${styles};">${esc(run.content)}</a>`;
|
|
340
|
+
}
|
|
337
341
|
return `<span style="${styles};">${esc(run.content)}</span>`;
|
|
338
342
|
}
|
|
339
343
|
|
|
344
|
+
/** A source PDF/PPTX link annotation is untrusted input — only render schemes a browser treats as navigation, never `javascript:`/`data:`/etc. */
|
|
345
|
+
function isSafeLinkHref(href: string): boolean {
|
|
346
|
+
return /^(https?:|mailto:)/i.test(href);
|
|
347
|
+
}
|
|
348
|
+
|
|
340
349
|
function fontWeightForFamily(
|
|
341
350
|
fontFamily: string | undefined,
|
|
342
351
|
fallback: number,
|