json2pptx 0.5.2 → 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("%")) {
@@ -909,8 +912,9 @@ function getShadowOption(shadow, ratioPx2Pt) {
909
912
  angle = 225;
910
913
  }
911
914
  return {
915
+ type: "outer",
912
916
  color: c.color,
913
- transparency: (1 - c.alpha) * 100,
917
+ opacity: c.alpha,
914
918
  blur: ((_b = shadow.blur) != null ? _b : 0) / ratioPx2Pt,
915
919
  offset,
916
920
  angle
@@ -1052,6 +1056,74 @@ function applySlideBackground(slide, slideJson, theme) {
1052
1056
  slide.background = { color: c.color, transparency: (1 - c.alpha) * 100 };
1053
1057
  }
1054
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
+
1055
1127
  // src/renderers/layout.ts
1056
1128
  function applyPptxLayout(pptx, width, height) {
1057
1129
  const viewportRatio = height / width;
@@ -1096,7 +1168,7 @@ var toPoints = (d) => {
1096
1168
  if (d.includes("NaN") || d.includes("undefined") || d.includes("null")) return [];
1097
1169
  let pathData;
1098
1170
  try {
1099
- pathData = new import_svg_pathdata.SVGPathData(d);
1171
+ pathData = new import_svg_pathdata.SVGPathData(d).toAbs().normalizeST().normalizeHVZ(false);
1100
1172
  } catch {
1101
1173
  return [];
1102
1174
  }
@@ -1289,33 +1361,33 @@ function addTextElement(slide, element, presentation, slideIndex, elementIndex,
1289
1361
  if (element.flipV) options.flipV = element.flipV;
1290
1362
  slide.addText(textProps, options);
1291
1363
  }
1292
- async function addImageElement(slide, element, ratioPx2Inch) {
1293
- 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;
1294
1366
  if (element.type !== "image" || !element.src) return;
1367
+ const objectName = `image-${slideIndex}-${(_a = element.id) != null ? _a : elementIndex}`;
1295
1368
  const options = {
1296
- x: ((_a = element.left) != null ? _a : 0) / ratioPx2Inch,
1297
- y: ((_b = element.top) != null ? _b : 0) / ratioPx2Inch,
1298
- w: ((_c = element.width) != null ? _c : 0) / ratioPx2Inch,
1299
- 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
1300
1374
  };
1301
1375
  if (isBase64Image(element.src)) options.data = element.src;
1302
1376
  else options.data = await resolveImageData(element.src);
1303
1377
  if (element.flipH) options.flipH = element.flipH;
1304
1378
  if (element.flipV) options.flipV = element.flipV;
1305
1379
  if (element.rotate) options.rotate = element.rotate;
1306
- const filterOpacity = (_f = getFilterOpacity((_e = element.filters) == null ? void 0 : _e.opacity)) != null ? _f : 1;
1307
1380
  const elementOpacity = getElementOpacity(element.opacity);
1308
- const imageOpacity = filterOpacity * elementOpacity;
1309
- if (imageOpacity !== 1) {
1310
- options.transparency = (1 - imageOpacity) * 100;
1381
+ if (elementOpacity !== 1) {
1382
+ options.transparency = (1 - elementOpacity) * 100;
1311
1383
  }
1312
- if ((_g = element.clip) == null ? void 0 : _g.range) {
1384
+ if ((_f = element.clip) == null ? void 0 : _f.range) {
1313
1385
  if (element.clip.shape === "ellipse") options.rounding = true;
1314
1386
  const [start, end] = element.clip.range;
1315
1387
  const [startX, startY] = start;
1316
1388
  const [endX, endY] = end;
1317
- const originW = ((_h = element.width) != null ? _h : 0) / ((endX - startX) / ratioPx2Inch);
1318
- 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);
1319
1391
  options.w = originW / ratioPx2Inch;
1320
1392
  options.h = originH / ratioPx2Inch;
1321
1393
  options.sizing = {
@@ -1327,6 +1399,14 @@ async function addImageElement(slide, element, ratioPx2Inch) {
1327
1399
  };
1328
1400
  }
1329
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
+ }
1330
1410
  }
1331
1411
  function addShapeElement(slide, element, ratioPx2Pt, ratioPx2Inch, slideIndex, elementIndex, fillPatches) {
1332
1412
  var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n;
@@ -1385,7 +1465,7 @@ function addShapeElement(slide, element, ratioPx2Pt, ratioPx2Inch, slideIndex, e
1385
1465
  fontFace: element.text.defaultFontName || DEFAULT_FONT_FACE,
1386
1466
  color: "#000000",
1387
1467
  paraSpaceBefore: 0,
1388
- valign: element.text.align === "middle" ? "mid" : element.text.align,
1468
+ valign: element.text.align === "middle" ? "middle" : element.text.align,
1389
1469
  fill: { color: "FFFFFF", transparency: 100 },
1390
1470
  fit: "none",
1391
1471
  objectName: `shape-text-${slideIndex}-${(_n = element.id) != null ? _n : elementIndex}`
@@ -1490,11 +1570,12 @@ function parseDataUrlImage(dataUrl) {
1490
1570
  return { mime, data, ext: (_a = extMap[mime]) != null ? _a : "png" };
1491
1571
  }
1492
1572
  async function createPPTX(presentation) {
1493
- var _a, _b, _c, _d;
1573
+ var _a, _b, _c, _d, _e;
1494
1574
  const parsedPresentation = (0, import_json2pptx_schema.parseDocument)(presentation);
1495
1575
  const normalizedPresentation = parsedPresentation;
1496
1576
  const pptx = new import_pptxgenjs.default();
1497
1577
  const fillPatches = [];
1578
+ const imageFilterPatches = [];
1498
1579
  const width = parsedPresentation.width;
1499
1580
  const height = parsedPresentation.height;
1500
1581
  const ratioPx2Inch = 96 * (width / 960);
@@ -1523,7 +1604,7 @@ async function createPPTX(presentation) {
1523
1604
  textPadding,
1524
1605
  fillPatches
1525
1606
  );
1526
- await addImageElement(slide, element, ratioPx2Inch);
1607
+ await addImageElement(slide, element, ratioPx2Inch, slideIndex, elementIndex, imageFilterPatches);
1527
1608
  addShapeElement(
1528
1609
  slide,
1529
1610
  element,
@@ -1542,7 +1623,7 @@ async function createPPTX(presentation) {
1542
1623
  outputType: "arraybuffer",
1543
1624
  compression: true
1544
1625
  });
1545
- if (!fillPatches.length) {
1626
+ if (!fillPatches.length && !imageFilterPatches.length) {
1546
1627
  return { blob: new Blob([pptxBuffer]), fileName };
1547
1628
  }
1548
1629
  const zip = await import_jszip.default.loadAsync(pptxBuffer);
@@ -1587,6 +1668,18 @@ async function createPPTX(presentation) {
1587
1668
  const nextSlideXml = patch.kind === "background" ? applyBackgroundFillPatch(slideXml, patch.fill, relId) : applyShapeFillPatch(slideXml, patch.objectName, patch.fill, relId);
1588
1669
  slideCache.set(slideNumber, nextSlideXml);
1589
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
+ }
1590
1683
  for (const [slideNumber, xml] of slideCache.entries()) {
1591
1684
  zip.file(`ppt/slides/slide${slideNumber}.xml`, xml);
1592
1685
  }