json2pptx 0.5.3 → 0.5.4

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/index.d.mts CHANGED
@@ -33,7 +33,11 @@ type ElementOutline = {
33
33
  style?: 'solid' | 'dashed' | 'dotted';
34
34
  };
35
35
  type ElementFilters = {
36
- opacity?: string;
36
+ opacity?: string | number;
37
+ grayscale?: string | number;
38
+ blur?: string | number;
39
+ sepia?: string | number;
40
+ saturate?: string | number;
37
41
  };
38
42
  type ElementClip = {
39
43
  shape?: 'ellipse' | string;
package/dist/index.d.ts CHANGED
@@ -33,7 +33,11 @@ type ElementOutline = {
33
33
  style?: 'solid' | 'dashed' | 'dotted';
34
34
  };
35
35
  type ElementFilters = {
36
- opacity?: string;
36
+ opacity?: string | number;
37
+ grayscale?: string | number;
38
+ blur?: string | number;
39
+ sepia?: string | number;
40
+ saturate?: string | number;
37
41
  };
38
42
  type ElementClip = {
39
43
  shape?: 'ellipse' | string;
package/dist/index.js CHANGED
@@ -764,7 +764,10 @@ function clampOpacity(value) {
764
764
  return Math.min(1, Math.max(0, value));
765
765
  }
766
766
  function getOpacityRatio(value) {
767
- if (!value) return void 0;
767
+ if (value === void 0 || value === null) return void 0;
768
+ if (typeof value === "number") {
769
+ return value > 1 ? Math.min(1, Math.max(0, value / 100)) : Math.min(1, Math.max(0, value));
770
+ }
768
771
  const normalized = value.trim();
769
772
  if (!normalized) return void 0;
770
773
  if (normalized.endsWith("%")) {
@@ -1053,6 +1056,74 @@ function applySlideBackground(slide, slideJson, theme) {
1053
1056
  slide.background = { color: c.color, transparency: (1 - c.alpha) * 100 };
1054
1057
  }
1055
1058
 
1059
+ // src/renderers/image-patch.ts
1060
+ function stripImageFilterTags(value) {
1061
+ return value.replace(/<a:alphaModFix\b[^>]*\/>/g, "").replace(/<a:grayscl\s*\/>/g, "");
1062
+ }
1063
+ function hasEnabledGrayscale(value) {
1064
+ if (value === void 0 || value === null) return false;
1065
+ if (typeof value === "number") return value > 0;
1066
+ const normalized = value.trim();
1067
+ if (!normalized) return false;
1068
+ if (normalized.endsWith("%")) {
1069
+ const percent = Number.parseFloat(normalized);
1070
+ return Number.isFinite(percent) && percent > 0;
1071
+ }
1072
+ const numeric = Number.parseFloat(normalized);
1073
+ return Number.isFinite(numeric) && numeric > 0;
1074
+ }
1075
+ function buildImageFilterXml(filters) {
1076
+ if (!filters) return "";
1077
+ const xml = [];
1078
+ const opacity = getFilterOpacity(filters.opacity);
1079
+ if (opacity !== void 0) {
1080
+ xml.push(`<a:alphaModFix amt="${Math.round(opacity * 1e5)}"/>`);
1081
+ }
1082
+ if (hasEnabledGrayscale(filters.grayscale)) {
1083
+ xml.push("<a:grayscl/>");
1084
+ }
1085
+ return xml.join("");
1086
+ }
1087
+ function imageFiltersRequireXmlPatch(filters) {
1088
+ if (!filters) return false;
1089
+ return getFilterOpacity(filters.opacity) !== void 0 || hasEnabledGrayscale(filters.grayscale);
1090
+ }
1091
+ function applyImageFilterPatch(slideXml, objectName, filters) {
1092
+ const filterXml = buildImageFilterXml(filters);
1093
+ if (!filterXml) return slideXml;
1094
+ const nameToken = `name="${objectName}"`;
1095
+ let cursor = 0;
1096
+ let result = slideXml;
1097
+ while (true) {
1098
+ const nameIndex = result.indexOf(nameToken, cursor);
1099
+ if (nameIndex === -1) break;
1100
+ const picStart = result.lastIndexOf("<p:pic", nameIndex);
1101
+ const picEnd = result.indexOf("</p:pic>", nameIndex);
1102
+ if (picStart === -1 || picEnd === -1) break;
1103
+ const picXml = result.slice(picStart, picEnd + "</p:pic>".length);
1104
+ const blipOpenClose = picXml.match(/<a:blip\b([^>]*)>([\s\S]*?)<\/a:blip>/);
1105
+ let updatedPicXml = picXml;
1106
+ if (blipOpenClose) {
1107
+ const attrs = blipOpenClose[1];
1108
+ const inner = stripImageFilterTags(blipOpenClose[2]);
1109
+ const nextBlip = `<a:blip${attrs}>${filterXml}${inner}</a:blip>`;
1110
+ updatedPicXml = picXml.replace(blipOpenClose[0], nextBlip);
1111
+ } else {
1112
+ const blipSelfClosing = picXml.match(/<a:blip\b([^>]*)\/>/);
1113
+ if (!blipSelfClosing) {
1114
+ cursor = picEnd + 1;
1115
+ continue;
1116
+ }
1117
+ const attrs = blipSelfClosing[1];
1118
+ const nextBlip = `<a:blip${attrs}>${filterXml}</a:blip>`;
1119
+ updatedPicXml = picXml.replace(blipSelfClosing[0], nextBlip);
1120
+ }
1121
+ result = result.slice(0, picStart) + updatedPicXml + result.slice(picEnd + "</p:pic>".length);
1122
+ cursor = picStart + updatedPicXml.length;
1123
+ }
1124
+ return result;
1125
+ }
1126
+
1056
1127
  // src/renderers/layout.ts
1057
1128
  function applyPptxLayout(pptx, width, height) {
1058
1129
  const viewportRatio = height / width;
@@ -1097,7 +1168,7 @@ var toPoints = (d) => {
1097
1168
  if (d.includes("NaN") || d.includes("undefined") || d.includes("null")) return [];
1098
1169
  let pathData;
1099
1170
  try {
1100
- pathData = new import_svg_pathdata.SVGPathData(d);
1171
+ pathData = new import_svg_pathdata.SVGPathData(d).toAbs().normalizeST().normalizeHVZ(false);
1101
1172
  } catch {
1102
1173
  return [];
1103
1174
  }
@@ -1290,33 +1361,33 @@ function addTextElement(slide, element, presentation, slideIndex, elementIndex,
1290
1361
  if (element.flipV) options.flipV = element.flipV;
1291
1362
  slide.addText(textProps, options);
1292
1363
  }
1293
- async function addImageElement(slide, element, ratioPx2Inch) {
1294
- var _a, _b, _c, _d, _e, _f, _g, _h, _i;
1364
+ async function addImageElement(slide, element, ratioPx2Inch, slideIndex, elementIndex, imageFilterPatches) {
1365
+ var _a, _b, _c, _d, _e, _f, _g, _h;
1295
1366
  if (element.type !== "image" || !element.src) return;
1367
+ const objectName = `image-${slideIndex}-${(_a = element.id) != null ? _a : elementIndex}`;
1296
1368
  const options = {
1297
- x: ((_a = element.left) != null ? _a : 0) / ratioPx2Inch,
1298
- y: ((_b = element.top) != null ? _b : 0) / ratioPx2Inch,
1299
- w: ((_c = element.width) != null ? _c : 0) / ratioPx2Inch,
1300
- h: ((_d = element.height) != null ? _d : 0) / ratioPx2Inch
1369
+ x: ((_b = element.left) != null ? _b : 0) / ratioPx2Inch,
1370
+ y: ((_c = element.top) != null ? _c : 0) / ratioPx2Inch,
1371
+ w: ((_d = element.width) != null ? _d : 0) / ratioPx2Inch,
1372
+ h: ((_e = element.height) != null ? _e : 0) / ratioPx2Inch,
1373
+ objectName
1301
1374
  };
1302
1375
  if (isBase64Image(element.src)) options.data = element.src;
1303
1376
  else options.data = await resolveImageData(element.src);
1304
1377
  if (element.flipH) options.flipH = element.flipH;
1305
1378
  if (element.flipV) options.flipV = element.flipV;
1306
1379
  if (element.rotate) options.rotate = element.rotate;
1307
- const filterOpacity = (_f = getFilterOpacity((_e = element.filters) == null ? void 0 : _e.opacity)) != null ? _f : 1;
1308
1380
  const elementOpacity = getElementOpacity(element.opacity);
1309
- const imageOpacity = filterOpacity * elementOpacity;
1310
- if (imageOpacity !== 1) {
1311
- options.transparency = (1 - imageOpacity) * 100;
1381
+ if (elementOpacity !== 1) {
1382
+ options.transparency = (1 - elementOpacity) * 100;
1312
1383
  }
1313
- if ((_g = element.clip) == null ? void 0 : _g.range) {
1384
+ if ((_f = element.clip) == null ? void 0 : _f.range) {
1314
1385
  if (element.clip.shape === "ellipse") options.rounding = true;
1315
1386
  const [start, end] = element.clip.range;
1316
1387
  const [startX, startY] = start;
1317
1388
  const [endX, endY] = end;
1318
- const originW = ((_h = element.width) != null ? _h : 0) / ((endX - startX) / ratioPx2Inch);
1319
- const originH = ((_i = element.height) != null ? _i : 0) / ((endY - startY) / ratioPx2Inch);
1389
+ const originW = ((_g = element.width) != null ? _g : 0) / ((endX - startX) / ratioPx2Inch);
1390
+ const originH = ((_h = element.height) != null ? _h : 0) / ((endY - startY) / ratioPx2Inch);
1320
1391
  options.w = originW / ratioPx2Inch;
1321
1392
  options.h = originH / ratioPx2Inch;
1322
1393
  options.sizing = {
@@ -1328,6 +1399,14 @@ async function addImageElement(slide, element, ratioPx2Inch) {
1328
1399
  };
1329
1400
  }
1330
1401
  slide.addImage(options);
1402
+ if (imageFiltersRequireXmlPatch(element.filters)) {
1403
+ imageFilterPatches.push({
1404
+ kind: "image",
1405
+ slideIndex,
1406
+ objectName,
1407
+ filters: element.filters
1408
+ });
1409
+ }
1331
1410
  }
1332
1411
  function addShapeElement(slide, element, ratioPx2Pt, ratioPx2Inch, slideIndex, elementIndex, fillPatches) {
1333
1412
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
@@ -1491,11 +1570,12 @@ function parseDataUrlImage(dataUrl) {
1491
1570
  return { mime, data, ext: (_a = extMap[mime]) != null ? _a : "png" };
1492
1571
  }
1493
1572
  async function createPPTX(presentation) {
1494
- var _a, _b, _c, _d;
1573
+ var _a, _b, _c, _d, _e;
1495
1574
  const parsedPresentation = (0, import_json2pptx_schema.parseDocument)(presentation);
1496
1575
  const normalizedPresentation = parsedPresentation;
1497
1576
  const pptx = new import_pptxgenjs.default();
1498
1577
  const fillPatches = [];
1578
+ const imageFilterPatches = [];
1499
1579
  const width = parsedPresentation.width;
1500
1580
  const height = parsedPresentation.height;
1501
1581
  const ratioPx2Inch = 96 * (width / 960);
@@ -1524,7 +1604,7 @@ async function createPPTX(presentation) {
1524
1604
  textPadding,
1525
1605
  fillPatches
1526
1606
  );
1527
- await addImageElement(slide, element, ratioPx2Inch);
1607
+ await addImageElement(slide, element, ratioPx2Inch, slideIndex, elementIndex, imageFilterPatches);
1528
1608
  addShapeElement(
1529
1609
  slide,
1530
1610
  element,
@@ -1543,7 +1623,7 @@ async function createPPTX(presentation) {
1543
1623
  outputType: "arraybuffer",
1544
1624
  compression: true
1545
1625
  });
1546
- if (!fillPatches.length) {
1626
+ if (!fillPatches.length && !imageFilterPatches.length) {
1547
1627
  return { blob: new Blob([pptxBuffer]), fileName };
1548
1628
  }
1549
1629
  const zip = await import_jszip.default.loadAsync(pptxBuffer);
@@ -1588,6 +1668,18 @@ async function createPPTX(presentation) {
1588
1668
  const nextSlideXml = patch.kind === "background" ? applyBackgroundFillPatch(slideXml, patch.fill, relId) : applyShapeFillPatch(slideXml, patch.objectName, patch.fill, relId);
1589
1669
  slideCache.set(slideNumber, nextSlideXml);
1590
1670
  }
1671
+ for (const patch of imageFilterPatches) {
1672
+ const slideNumber = patch.slideIndex + 1;
1673
+ const slidePath = `ppt/slides/slide${slideNumber}.xml`;
1674
+ const slideXml = (_e = slideCache.get(slideNumber)) != null ? _e : zip.file(slidePath) ? await zip.file(slidePath).async("string") : "";
1675
+ if (!slideXml) {
1676
+ continue;
1677
+ }
1678
+ slideCache.set(
1679
+ slideNumber,
1680
+ applyImageFilterPatch(slideXml, patch.objectName, patch.filters)
1681
+ );
1682
+ }
1591
1683
  for (const [slideNumber, xml] of slideCache.entries()) {
1592
1684
  zip.file(`ppt/slides/slide${slideNumber}.xml`, xml);
1593
1685
  }