@json-to-office/jto 0.28.1 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +151 -37
- package/dist/cli.js.map +1 -1
- package/dist/client/assets/index-DgKS-gEZ.js.map +1 -1
- package/dist/render-server.d.ts +2 -1
- package/dist/render-server.js +149 -36
- package/dist/render-server.js.map +1 -1
- package/package.json +7 -7
package/dist/render-server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
|
-
import { PptxRasterizer } from '@json-to-office/shared';
|
|
2
|
+
import { PptxRasterizer, PptxBatchRasterizer } from '@json-to-office/shared';
|
|
3
3
|
|
|
4
4
|
type ApiAuthMode = 'auto' | 'required' | 'disabled';
|
|
5
5
|
type OutboundSourceMode = 'development' | 'safe';
|
|
@@ -34,6 +34,7 @@ interface RenderServerOptions {
|
|
|
34
34
|
sourcePolicy?: OutboundSourcePolicy;
|
|
35
35
|
fetch?: FetchImplementation;
|
|
36
36
|
getRasterizer?: () => PptxRasterizer;
|
|
37
|
+
getBatchRasterizer?: () => PptxBatchRasterizer;
|
|
37
38
|
}
|
|
38
39
|
declare function createRenderServerApp(options?: RenderServerOptions): Hono;
|
|
39
40
|
|
package/dist/render-server.js
CHANGED
|
@@ -15,9 +15,13 @@ import {
|
|
|
15
15
|
clampVisualDpi,
|
|
16
16
|
DEFAULT_VISUAL_DPI,
|
|
17
17
|
MIN_VISUAL_DPI,
|
|
18
|
-
MAX_VISUAL_DPI
|
|
18
|
+
MAX_VISUAL_DPI,
|
|
19
|
+
MAX_RASTERIZE_BATCH_SLIDES
|
|
19
20
|
} from "@json-to-office/shared";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
createLibreOfficePptxRasterizer,
|
|
23
|
+
createLibreOfficePptxBatchRasterizer
|
|
24
|
+
} from "@json-to-office/jto-cli";
|
|
21
25
|
|
|
22
26
|
// src/server/lib/typebox-validator.ts
|
|
23
27
|
import { Value } from "@sinclair/typebox/value";
|
|
@@ -392,6 +396,24 @@ var RasterizeRequestSchema = Type.Object(
|
|
|
392
396
|
},
|
|
393
397
|
{ additionalProperties: false }
|
|
394
398
|
);
|
|
399
|
+
var RasterizeBatchRequestSchema = Type.Object(
|
|
400
|
+
{
|
|
401
|
+
slides: Type.Array(
|
|
402
|
+
Type.Object(
|
|
403
|
+
{
|
|
404
|
+
presentation: Type.Object({}, { additionalProperties: true }),
|
|
405
|
+
dpi: Type.Optional(
|
|
406
|
+
Type.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
|
|
407
|
+
)
|
|
408
|
+
},
|
|
409
|
+
{ additionalProperties: false }
|
|
410
|
+
),
|
|
411
|
+
{ minItems: 1, maxItems: MAX_RASTERIZE_BATCH_SLIDES }
|
|
412
|
+
),
|
|
413
|
+
baseDir: Type.Optional(Type.String())
|
|
414
|
+
},
|
|
415
|
+
{ additionalProperties: false }
|
|
416
|
+
);
|
|
395
417
|
var sharedRasterizer;
|
|
396
418
|
function getSharedRasterizer() {
|
|
397
419
|
if (!sharedRasterizer) {
|
|
@@ -399,6 +421,13 @@ function getSharedRasterizer() {
|
|
|
399
421
|
}
|
|
400
422
|
return sharedRasterizer;
|
|
401
423
|
}
|
|
424
|
+
var sharedBatchRasterizer;
|
|
425
|
+
function getSharedBatchRasterizer() {
|
|
426
|
+
if (!sharedBatchRasterizer) {
|
|
427
|
+
sharedBatchRasterizer = createLibreOfficePptxBatchRasterizer();
|
|
428
|
+
}
|
|
429
|
+
return sharedBatchRasterizer;
|
|
430
|
+
}
|
|
402
431
|
var jsonOnly = async (c, next) => {
|
|
403
432
|
const contentType = c.req.header("content-type");
|
|
404
433
|
if (!contentType || !contentType.includes("application/json")) {
|
|
@@ -408,10 +437,65 @@ var jsonOnly = async (c, next) => {
|
|
|
408
437
|
}
|
|
409
438
|
await next();
|
|
410
439
|
};
|
|
440
|
+
var DEFAULT_SLIDE_WIDTH_IN = 13.34;
|
|
441
|
+
var DEFAULT_SLIDE_HEIGHT_IN = 7.5;
|
|
442
|
+
var MAX_SLIDE_PIXELS = 64e6;
|
|
443
|
+
var MAX_BATCH_PIXELS = 256e6;
|
|
444
|
+
function estimateSlidePixels(presentation, dpi) {
|
|
445
|
+
const props = presentation?.props;
|
|
446
|
+
const dim = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
447
|
+
const widthIn = dim(props?.slideWidth, DEFAULT_SLIDE_WIDTH_IN);
|
|
448
|
+
const heightIn = dim(props?.slideHeight, DEFAULT_SLIDE_HEIGHT_IN);
|
|
449
|
+
return widthIn * dpi * (heightIn * dpi);
|
|
450
|
+
}
|
|
451
|
+
function assertPixelBudget(slides) {
|
|
452
|
+
let total = 0;
|
|
453
|
+
for (const slide of slides) {
|
|
454
|
+
const pixels = estimateSlidePixels(slide.presentation, slide.dpi);
|
|
455
|
+
if (pixels > MAX_SLIDE_PIXELS) {
|
|
456
|
+
throw new HTTPException2(400, {
|
|
457
|
+
message: "Requested slide dimensions are too large"
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
total += pixels;
|
|
461
|
+
}
|
|
462
|
+
if (total > MAX_BATCH_PIXELS) {
|
|
463
|
+
throw new HTTPException2(400, {
|
|
464
|
+
message: "Requested batch raster size is too large"
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function resolveSafeBaseDir(baseDir) {
|
|
469
|
+
if (baseDir === void 0) return void 0;
|
|
470
|
+
const resolved = path.resolve(baseDir);
|
|
471
|
+
const cwd = process.cwd();
|
|
472
|
+
if (resolved !== cwd && !resolved.startsWith(cwd + path.sep)) {
|
|
473
|
+
throw new HTTPException2(400, {
|
|
474
|
+
message: "baseDir must be inside the server working directory"
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
return resolved;
|
|
478
|
+
}
|
|
479
|
+
function toHttpException(error) {
|
|
480
|
+
if (error instanceof HTTPException2) return error;
|
|
481
|
+
if (error instanceof UnsafeOutboundSourceError) {
|
|
482
|
+
return new HTTPException2(400, { message: error.message });
|
|
483
|
+
}
|
|
484
|
+
const msg = error instanceof Error ? error.message.toLowerCase() : String(error);
|
|
485
|
+
if (msg.includes("not found") || msg.includes("rasterization needs")) {
|
|
486
|
+
return new HTTPException2(503, { message: error.message });
|
|
487
|
+
}
|
|
488
|
+
if (msg.includes("invalid") || msg.includes("validation")) {
|
|
489
|
+
return new HTTPException2(400, { message: error.message });
|
|
490
|
+
}
|
|
491
|
+
return new HTTPException2(500, {
|
|
492
|
+
message: "Internal server error during rasterization"
|
|
493
|
+
});
|
|
494
|
+
}
|
|
411
495
|
function registerRasterizeRoute(router, options = {}) {
|
|
412
496
|
const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
|
|
413
|
-
|
|
414
|
-
|
|
497
|
+
const getBatchRasterizer = options.getBatchRasterizer ?? getSharedBatchRasterizer;
|
|
498
|
+
const shared = [
|
|
415
499
|
...options.preMiddleware ?? [],
|
|
416
500
|
bodyLimit({
|
|
417
501
|
maxSize: 32 * 1024 * 1024,
|
|
@@ -419,22 +503,26 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
419
503
|
throw new HTTPException2(413, { message: "Request body too large" });
|
|
420
504
|
}
|
|
421
505
|
}),
|
|
422
|
-
jsonOnly
|
|
506
|
+
jsonOnly
|
|
507
|
+
];
|
|
508
|
+
const guard = async (run) => {
|
|
509
|
+
try {
|
|
510
|
+
return await run();
|
|
511
|
+
} catch (error) {
|
|
512
|
+
options.onError?.(error);
|
|
513
|
+
throw toHttpException(error);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
router.post(
|
|
517
|
+
"/rasterize",
|
|
518
|
+
...shared,
|
|
423
519
|
tbValidator(RasterizeRequestSchema),
|
|
424
520
|
async (c) => {
|
|
425
521
|
const { presentation, dpi, baseDir } = getValidated(c, "json");
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
if (resolved !== cwd && !resolved.startsWith(cwd + path.sep)) {
|
|
431
|
-
throw new HTTPException2(400, {
|
|
432
|
-
message: "baseDir must be inside the server working directory"
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
safeBaseDir = resolved;
|
|
436
|
-
}
|
|
437
|
-
try {
|
|
522
|
+
const safeBaseDir = resolveSafeBaseDir(baseDir);
|
|
523
|
+
const effectiveDpi = clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI);
|
|
524
|
+
assertPixelBudget([{ presentation, dpi: effectiveDpi }]);
|
|
525
|
+
const result = await guard(async () => {
|
|
438
526
|
if (options.sourcePolicy) {
|
|
439
527
|
assertSafeOutboundSources(
|
|
440
528
|
presentation,
|
|
@@ -442,29 +530,53 @@ function registerRasterizeRoute(router, options = {}) {
|
|
|
442
530
|
"presentation"
|
|
443
531
|
);
|
|
444
532
|
}
|
|
445
|
-
|
|
533
|
+
return getRasterizer()({
|
|
446
534
|
presentation,
|
|
447
|
-
dpi:
|
|
535
|
+
dpi: effectiveDpi,
|
|
448
536
|
baseDir: safeBaseDir
|
|
449
537
|
});
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
538
|
+
});
|
|
539
|
+
return c.json(result);
|
|
540
|
+
}
|
|
541
|
+
);
|
|
542
|
+
router.post(
|
|
543
|
+
"/rasterize/batch",
|
|
544
|
+
...shared,
|
|
545
|
+
tbValidator(RasterizeBatchRequestSchema),
|
|
546
|
+
async (c) => {
|
|
547
|
+
const { slides, baseDir } = getValidated(c, "json");
|
|
548
|
+
const safeBaseDir = resolveSafeBaseDir(baseDir);
|
|
549
|
+
const effectiveSlides = slides.map((slide) => ({
|
|
550
|
+
presentation: slide.presentation,
|
|
551
|
+
dpi: clampVisualDpi(slide.dpi ?? DEFAULT_VISUAL_DPI)
|
|
552
|
+
}));
|
|
553
|
+
assertPixelBudget(effectiveSlides);
|
|
554
|
+
const result = await guard(async () => {
|
|
555
|
+
if (options.sourcePolicy) {
|
|
556
|
+
slides.forEach(
|
|
557
|
+
(slide, index) => assertSafeOutboundSources(
|
|
558
|
+
slide.presentation,
|
|
559
|
+
options.sourcePolicy,
|
|
560
|
+
`slides[${index}].presentation`
|
|
561
|
+
)
|
|
562
|
+
);
|
|
463
563
|
}
|
|
464
|
-
|
|
465
|
-
|
|
564
|
+
return getBatchRasterizer()({
|
|
565
|
+
slides: effectiveSlides,
|
|
566
|
+
baseDir: safeBaseDir
|
|
466
567
|
});
|
|
467
|
-
}
|
|
568
|
+
});
|
|
569
|
+
return c.json({
|
|
570
|
+
results: result.results.map((slide) => {
|
|
571
|
+
if (slide.ok) return slide;
|
|
572
|
+
options.onError?.(new Error(slide.error));
|
|
573
|
+
return slide.stage === "build" ? { ok: false, error: slide.error, stage: slide.stage } : {
|
|
574
|
+
ok: false,
|
|
575
|
+
error: "Slide rasterization failed",
|
|
576
|
+
stage: slide.stage
|
|
577
|
+
};
|
|
578
|
+
})
|
|
579
|
+
});
|
|
468
580
|
}
|
|
469
581
|
);
|
|
470
582
|
}
|
|
@@ -903,6 +1015,7 @@ function createRenderServerApp(options = {}) {
|
|
|
903
1015
|
});
|
|
904
1016
|
registerRasterizeRoute(app, {
|
|
905
1017
|
getRasterizer: options.getRasterizer,
|
|
1018
|
+
getBatchRasterizer: options.getBatchRasterizer,
|
|
906
1019
|
preMiddleware: [
|
|
907
1020
|
rateLimiter({
|
|
908
1021
|
limit: options.rasterizeRateLimit ?? positiveInteger2(
|
|
@@ -1010,7 +1123,7 @@ function createRenderServerApp(options = {}) {
|
|
|
1010
1123
|
return c.body(payload, response.status, headers);
|
|
1011
1124
|
}
|
|
1012
1125
|
);
|
|
1013
|
-
for (const route of ["/export", "/rasterize"]) {
|
|
1126
|
+
for (const route of ["/export", "/rasterize", "/rasterize/batch"]) {
|
|
1014
1127
|
app.all(route, (c) => {
|
|
1015
1128
|
c.header("Allow", "POST");
|
|
1016
1129
|
return c.json({ success: false, error: "Method not allowed" }, 405);
|