@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 CHANGED
@@ -1910,19 +1910,80 @@ import {
1910
1910
  clampVisualDpi,
1911
1911
  DEFAULT_VISUAL_DPI,
1912
1912
  MIN_VISUAL_DPI,
1913
- MAX_VISUAL_DPI
1913
+ MAX_VISUAL_DPI,
1914
+ MAX_RASTERIZE_BATCH_SLIDES
1914
1915
  } from "@json-to-office/shared";
1915
- import { createLibreOfficePptxRasterizer } from "@json-to-office/jto-cli";
1916
+ import {
1917
+ createLibreOfficePptxRasterizer,
1918
+ createLibreOfficePptxBatchRasterizer
1919
+ } from "@json-to-office/jto-cli";
1916
1920
  function getSharedRasterizer() {
1917
1921
  if (!sharedRasterizer) {
1918
1922
  sharedRasterizer = createLibreOfficePptxRasterizer();
1919
1923
  }
1920
1924
  return sharedRasterizer;
1921
1925
  }
1926
+ function getSharedBatchRasterizer() {
1927
+ if (!sharedBatchRasterizer) {
1928
+ sharedBatchRasterizer = createLibreOfficePptxBatchRasterizer();
1929
+ }
1930
+ return sharedBatchRasterizer;
1931
+ }
1932
+ function estimateSlidePixels(presentation, dpi) {
1933
+ const props = presentation?.props;
1934
+ const dim = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
1935
+ const widthIn = dim(props?.slideWidth, DEFAULT_SLIDE_WIDTH_IN);
1936
+ const heightIn = dim(props?.slideHeight, DEFAULT_SLIDE_HEIGHT_IN);
1937
+ return widthIn * dpi * (heightIn * dpi);
1938
+ }
1939
+ function assertPixelBudget(slides) {
1940
+ let total = 0;
1941
+ for (const slide of slides) {
1942
+ const pixels = estimateSlidePixels(slide.presentation, slide.dpi);
1943
+ if (pixels > MAX_SLIDE_PIXELS) {
1944
+ throw new HTTPException3(400, {
1945
+ message: "Requested slide dimensions are too large"
1946
+ });
1947
+ }
1948
+ total += pixels;
1949
+ }
1950
+ if (total > MAX_BATCH_PIXELS) {
1951
+ throw new HTTPException3(400, {
1952
+ message: "Requested batch raster size is too large"
1953
+ });
1954
+ }
1955
+ }
1956
+ function resolveSafeBaseDir(baseDir) {
1957
+ if (baseDir === void 0) return void 0;
1958
+ const resolved = path6.resolve(baseDir);
1959
+ const cwd = process.cwd();
1960
+ if (resolved !== cwd && !resolved.startsWith(cwd + path6.sep)) {
1961
+ throw new HTTPException3(400, {
1962
+ message: "baseDir must be inside the server working directory"
1963
+ });
1964
+ }
1965
+ return resolved;
1966
+ }
1967
+ function toHttpException(error) {
1968
+ if (error instanceof HTTPException3) return error;
1969
+ if (error instanceof UnsafeOutboundSourceError) {
1970
+ return new HTTPException3(400, { message: error.message });
1971
+ }
1972
+ const msg = error instanceof Error ? error.message.toLowerCase() : String(error);
1973
+ if (msg.includes("not found") || msg.includes("rasterization needs")) {
1974
+ return new HTTPException3(503, { message: error.message });
1975
+ }
1976
+ if (msg.includes("invalid") || msg.includes("validation")) {
1977
+ return new HTTPException3(400, { message: error.message });
1978
+ }
1979
+ return new HTTPException3(500, {
1980
+ message: "Internal server error during rasterization"
1981
+ });
1982
+ }
1922
1983
  function registerRasterizeRoute(router, options = {}) {
1923
1984
  const getRasterizer = options.getRasterizer ?? getSharedRasterizer;
1924
- router.post(
1925
- "/rasterize",
1985
+ const getBatchRasterizer = options.getBatchRasterizer ?? getSharedBatchRasterizer;
1986
+ const shared = [
1926
1987
  ...options.preMiddleware ?? [],
1927
1988
  bodyLimit({
1928
1989
  maxSize: 32 * 1024 * 1024,
@@ -1930,22 +1991,26 @@ function registerRasterizeRoute(router, options = {}) {
1930
1991
  throw new HTTPException3(413, { message: "Request body too large" });
1931
1992
  }
1932
1993
  }),
1933
- jsonOnly,
1994
+ jsonOnly
1995
+ ];
1996
+ const guard = async (run) => {
1997
+ try {
1998
+ return await run();
1999
+ } catch (error) {
2000
+ options.onError?.(error);
2001
+ throw toHttpException(error);
2002
+ }
2003
+ };
2004
+ router.post(
2005
+ "/rasterize",
2006
+ ...shared,
1934
2007
  tbValidator(RasterizeRequestSchema),
1935
2008
  async (c) => {
1936
2009
  const { presentation, dpi, baseDir } = getValidated(c, "json");
1937
- let safeBaseDir;
1938
- if (baseDir !== void 0) {
1939
- const resolved = path6.resolve(baseDir);
1940
- const cwd = process.cwd();
1941
- if (resolved !== cwd && !resolved.startsWith(cwd + path6.sep)) {
1942
- throw new HTTPException3(400, {
1943
- message: "baseDir must be inside the server working directory"
1944
- });
1945
- }
1946
- safeBaseDir = resolved;
1947
- }
1948
- try {
2010
+ const safeBaseDir = resolveSafeBaseDir(baseDir);
2011
+ const effectiveDpi = clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI);
2012
+ assertPixelBudget([{ presentation, dpi: effectiveDpi }]);
2013
+ const result = await guard(async () => {
1949
2014
  if (options.sourcePolicy) {
1950
2015
  assertSafeOutboundSources(
1951
2016
  presentation,
@@ -1953,33 +2018,57 @@ function registerRasterizeRoute(router, options = {}) {
1953
2018
  "presentation"
1954
2019
  );
1955
2020
  }
1956
- const result = await getRasterizer()({
2021
+ return getRasterizer()({
1957
2022
  presentation,
1958
- dpi: clampVisualDpi(dpi ?? DEFAULT_VISUAL_DPI),
2023
+ dpi: effectiveDpi,
1959
2024
  baseDir: safeBaseDir
1960
2025
  });
1961
- return c.json(result);
1962
- } catch (error) {
1963
- options.onError?.(error);
1964
- if (error instanceof HTTPException3) throw error;
1965
- if (error instanceof UnsafeOutboundSourceError) {
1966
- throw new HTTPException3(400, { message: error.message });
1967
- }
1968
- const msg = error instanceof Error ? error.message.toLowerCase() : String(error);
1969
- if (msg.includes("not found") || msg.includes("rasterization needs")) {
1970
- throw new HTTPException3(503, { message: error.message });
1971
- }
1972
- if (msg.includes("invalid") || msg.includes("validation")) {
1973
- throw new HTTPException3(400, { message: error.message });
2026
+ });
2027
+ return c.json(result);
2028
+ }
2029
+ );
2030
+ router.post(
2031
+ "/rasterize/batch",
2032
+ ...shared,
2033
+ tbValidator(RasterizeBatchRequestSchema),
2034
+ async (c) => {
2035
+ const { slides, baseDir } = getValidated(c, "json");
2036
+ const safeBaseDir = resolveSafeBaseDir(baseDir);
2037
+ const effectiveSlides = slides.map((slide) => ({
2038
+ presentation: slide.presentation,
2039
+ dpi: clampVisualDpi(slide.dpi ?? DEFAULT_VISUAL_DPI)
2040
+ }));
2041
+ assertPixelBudget(effectiveSlides);
2042
+ const result = await guard(async () => {
2043
+ if (options.sourcePolicy) {
2044
+ slides.forEach(
2045
+ (slide, index) => assertSafeOutboundSources(
2046
+ slide.presentation,
2047
+ options.sourcePolicy,
2048
+ `slides[${index}].presentation`
2049
+ )
2050
+ );
1974
2051
  }
1975
- throw new HTTPException3(500, {
1976
- message: "Internal server error during rasterization"
2052
+ return getBatchRasterizer()({
2053
+ slides: effectiveSlides,
2054
+ baseDir: safeBaseDir
1977
2055
  });
1978
- }
2056
+ });
2057
+ return c.json({
2058
+ results: result.results.map((slide) => {
2059
+ if (slide.ok) return slide;
2060
+ options.onError?.(new Error(slide.error));
2061
+ return slide.stage === "build" ? { ok: false, error: slide.error, stage: slide.stage } : {
2062
+ ok: false,
2063
+ error: "Slide rasterization failed",
2064
+ stage: slide.stage
2065
+ };
2066
+ })
2067
+ });
1979
2068
  }
1980
2069
  );
1981
2070
  }
1982
- var RasterizeRequestSchema, sharedRasterizer, jsonOnly;
2071
+ var RasterizeRequestSchema, RasterizeBatchRequestSchema, sharedRasterizer, sharedBatchRasterizer, jsonOnly, DEFAULT_SLIDE_WIDTH_IN, DEFAULT_SLIDE_HEIGHT_IN, MAX_SLIDE_PIXELS, MAX_BATCH_PIXELS;
1983
2072
  var init_rasterize_route = __esm({
1984
2073
  "src/server/rasterize-route.ts"() {
1985
2074
  "use strict";
@@ -1996,6 +2085,24 @@ var init_rasterize_route = __esm({
1996
2085
  },
1997
2086
  { additionalProperties: false }
1998
2087
  );
2088
+ RasterizeBatchRequestSchema = Type2.Object(
2089
+ {
2090
+ slides: Type2.Array(
2091
+ Type2.Object(
2092
+ {
2093
+ presentation: Type2.Object({}, { additionalProperties: true }),
2094
+ dpi: Type2.Optional(
2095
+ Type2.Number({ minimum: MIN_VISUAL_DPI, maximum: MAX_VISUAL_DPI })
2096
+ )
2097
+ },
2098
+ { additionalProperties: false }
2099
+ ),
2100
+ { minItems: 1, maxItems: MAX_RASTERIZE_BATCH_SLIDES }
2101
+ ),
2102
+ baseDir: Type2.Optional(Type2.String())
2103
+ },
2104
+ { additionalProperties: false }
2105
+ );
1999
2106
  jsonOnly = async (c, next) => {
2000
2107
  const contentType = c.req.header("content-type");
2001
2108
  if (!contentType || !contentType.includes("application/json")) {
@@ -2005,6 +2112,10 @@ var init_rasterize_route = __esm({
2005
2112
  }
2006
2113
  await next();
2007
2114
  };
2115
+ DEFAULT_SLIDE_WIDTH_IN = 13.34;
2116
+ DEFAULT_SLIDE_HEIGHT_IN = 7.5;
2117
+ MAX_SLIDE_PIXELS = 64e6;
2118
+ MAX_BATCH_PIXELS = 256e6;
2008
2119
  }
2009
2120
  });
2010
2121
 
@@ -2638,6 +2749,9 @@ function createFormatRouter(adapter) {
2638
2749
  rateLimiter({
2639
2750
  limit: process.env.NODE_ENV === "production" ? 10 : 1e3,
2640
2751
  window: 15 * 60 * 1e3,
2752
+ // One bucket for /rasterize AND /rasterize/batch — without a shared
2753
+ // namespace the default per-path key would double the budget.
2754
+ namespace: "rasterize",
2641
2755
  trustProxy: config.rateLimit.trustProxy
2642
2756
  })
2643
2757
  ],
@@ -4311,7 +4425,7 @@ function createDevCommand(adapter) {
4311
4425
  }
4312
4426
 
4313
4427
  // src/cli.ts
4314
- var PACKAGE_VERSION = true ? "0.28.1" : "dev-mode";
4428
+ var PACKAGE_VERSION = true ? "0.29.0" : "dev-mode";
4315
4429
  var program = new Command2();
4316
4430
  program.name("jto").description("JSON to Office CLI - Generate .docx and .pptx from JSON").version(PACKAGE_VERSION);
4317
4431
  registerCoreCommands(program, {