dom-to-pptx 1.1.0 → 1.1.1
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/CHANGELOG.md +12 -2
- package/README.md +295 -323
- package/dist/dom-to-pptx.bundle.js +247 -95
- package/dist/dom-to-pptx.cjs +247 -95
- package/dist/dom-to-pptx.cjs.map +1 -1
- package/dist/dom-to-pptx.mjs +247 -95
- package/dist/dom-to-pptx.mjs.map +1 -1
- package/package.json +83 -83
- package/rollup.config.js +9 -14
- package/src/font-embedder.js +163 -159
- package/src/font-utils.js +32 -35
- package/src/image-processor.js +58 -19
- package/src/index.js +971 -905
- package/src/utils.js +711 -674
- package/dist/dom-to-pptx.min.js +0 -64284
package/dist/dom-to-pptx.mjs
CHANGED
|
@@ -199,11 +199,14 @@ class PPTXEmbedFonts {
|
|
|
199
199
|
|
|
200
200
|
// src/utils.js
|
|
201
201
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
// canvas context for color normalization
|
|
203
|
+
let _ctx;
|
|
204
|
+
function getCtx() {
|
|
205
|
+
if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
206
|
+
return _ctx;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Checks if any parent element has overflow: hidden which would clip this element
|
|
207
210
|
function isClippedByParent(node) {
|
|
208
211
|
let parent = node.parentElement;
|
|
209
212
|
while (parent && parent !== document.body) {
|
|
@@ -369,28 +372,58 @@ function generateCustomShapeSVG(w, h, color, opacity, radii) {
|
|
|
369
372
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
370
373
|
}
|
|
371
374
|
|
|
375
|
+
// --- REPLACE THE EXISTING parseColor FUNCTION ---
|
|
372
376
|
function parseColor(str) {
|
|
373
|
-
if (!str || str === 'transparent' || str.
|
|
377
|
+
if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
|
|
374
378
|
return { hex: null, opacity: 0 };
|
|
375
379
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
380
|
+
|
|
381
|
+
const ctx = getCtx();
|
|
382
|
+
ctx.fillStyle = str;
|
|
383
|
+
// This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
|
|
384
|
+
const computed = ctx.fillStyle;
|
|
385
|
+
|
|
386
|
+
// 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
|
|
387
|
+
if (computed.startsWith('#')) {
|
|
388
|
+
let hex = computed.slice(1); // Remove '#'
|
|
389
|
+
let opacity = 1;
|
|
390
|
+
|
|
391
|
+
// Expand shorthand #RGB -> #RRGGBB
|
|
392
|
+
if (hex.length === 3) {
|
|
393
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
394
|
+
}
|
|
395
|
+
// Expand shorthand #RGBA -> #RRGGBBAA
|
|
396
|
+
else if (hex.length === 4) {
|
|
397
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
|
|
401
|
+
if (hex.length === 8) {
|
|
402
|
+
opacity = parseInt(hex.slice(6), 16) / 255;
|
|
403
|
+
hex = hex.slice(0, 6); // Keep only RRGGBB
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
return { hex: hex.toUpperCase(), opacity };
|
|
384
407
|
}
|
|
385
|
-
|
|
408
|
+
|
|
409
|
+
// 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
|
|
410
|
+
const match = computed.match(/[\d.]+/g);
|
|
386
411
|
if (match && match.length >= 3) {
|
|
387
412
|
const r = parseInt(match[0]);
|
|
388
413
|
const g = parseInt(match[1]);
|
|
389
414
|
const b = parseInt(match[2]);
|
|
390
415
|
const a = match.length > 3 ? parseFloat(match[3]) : 1;
|
|
391
|
-
|
|
416
|
+
|
|
417
|
+
// Bitwise shift to get Hex
|
|
418
|
+
const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
|
|
419
|
+
.toString(16)
|
|
420
|
+
.slice(1)
|
|
421
|
+
.toUpperCase();
|
|
422
|
+
|
|
392
423
|
return { hex, opacity: a };
|
|
393
424
|
}
|
|
425
|
+
|
|
426
|
+
// Fallback (Parsing failed)
|
|
394
427
|
return { hex: null, opacity: 0 };
|
|
395
428
|
}
|
|
396
429
|
|
|
@@ -874,33 +907,30 @@ async function getAutoDetectedFonts(usedFamilies) {
|
|
|
874
907
|
|
|
875
908
|
// src/image-processor.js
|
|
876
909
|
|
|
877
|
-
async function getProcessedImage(src, targetW, targetH, radius) {
|
|
910
|
+
async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
|
|
878
911
|
return new Promise((resolve) => {
|
|
879
912
|
const img = new Image();
|
|
880
|
-
img.crossOrigin = 'Anonymous';
|
|
913
|
+
img.crossOrigin = 'Anonymous';
|
|
881
914
|
|
|
882
915
|
img.onload = () => {
|
|
883
916
|
const canvas = document.createElement('canvas');
|
|
884
|
-
// Double resolution
|
|
885
|
-
const scale = 2;
|
|
917
|
+
const scale = 2; // Double resolution
|
|
886
918
|
canvas.width = targetW * scale;
|
|
887
919
|
canvas.height = targetH * scale;
|
|
888
920
|
const ctx = canvas.getContext('2d');
|
|
889
921
|
ctx.scale(scale, scale);
|
|
890
922
|
|
|
891
|
-
// Normalize radius
|
|
923
|
+
// Normalize radius
|
|
892
924
|
let r = { tl: 0, tr: 0, br: 0, bl: 0 };
|
|
893
925
|
if (typeof radius === 'number') {
|
|
894
926
|
r = { tl: radius, tr: radius, br: radius, bl: radius };
|
|
895
927
|
} else if (typeof radius === 'object' && radius !== null) {
|
|
896
|
-
r = { ...r, ...radius };
|
|
928
|
+
r = { ...r, ...radius };
|
|
897
929
|
}
|
|
898
930
|
|
|
899
|
-
// 1. Draw
|
|
931
|
+
// 1. Draw Mask
|
|
900
932
|
ctx.beginPath();
|
|
901
|
-
|
|
902
|
-
// Border Radius Clamping Logic (CSS Spec)
|
|
903
|
-
// Prevents corners from overlapping if radii are too large for the container
|
|
933
|
+
// ... (radius clamping logic remains the same) ...
|
|
904
934
|
const factor = Math.min(
|
|
905
935
|
targetW / (r.tl + r.tr) || Infinity,
|
|
906
936
|
targetH / (r.tr + r.br) || Infinity,
|
|
@@ -909,13 +939,9 @@ async function getProcessedImage(src, targetW, targetH, radius) {
|
|
|
909
939
|
);
|
|
910
940
|
|
|
911
941
|
if (factor < 1) {
|
|
912
|
-
r.tl *= factor;
|
|
913
|
-
r.tr *= factor;
|
|
914
|
-
r.br *= factor;
|
|
915
|
-
r.bl *= factor;
|
|
942
|
+
r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
|
|
916
943
|
}
|
|
917
944
|
|
|
918
|
-
// Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
|
|
919
945
|
ctx.moveTo(r.tl, 0);
|
|
920
946
|
ctx.lineTo(targetW - r.tr, 0);
|
|
921
947
|
ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
|
|
@@ -925,22 +951,58 @@ async function getProcessedImage(src, targetW, targetH, radius) {
|
|
|
925
951
|
ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
|
|
926
952
|
ctx.lineTo(0, r.tl);
|
|
927
953
|
ctx.arcTo(0, 0, r.tl, 0, r.tl);
|
|
928
|
-
|
|
929
954
|
ctx.closePath();
|
|
930
955
|
ctx.fillStyle = '#000';
|
|
931
956
|
ctx.fill();
|
|
932
957
|
|
|
933
|
-
// 2. Composite Source-In
|
|
958
|
+
// 2. Composite Source-In
|
|
934
959
|
ctx.globalCompositeOperation = 'source-in';
|
|
935
960
|
|
|
936
|
-
// 3. Draw Image
|
|
961
|
+
// 3. Draw Image with Object Fit logic
|
|
937
962
|
const wRatio = targetW / img.width;
|
|
938
963
|
const hRatio = targetH / img.height;
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
964
|
+
let renderW, renderH;
|
|
965
|
+
|
|
966
|
+
if (objectFit === 'contain') {
|
|
967
|
+
const fitScale = Math.min(wRatio, hRatio);
|
|
968
|
+
renderW = img.width * fitScale;
|
|
969
|
+
renderH = img.height * fitScale;
|
|
970
|
+
} else if (objectFit === 'cover') {
|
|
971
|
+
const coverScale = Math.max(wRatio, hRatio);
|
|
972
|
+
renderW = img.width * coverScale;
|
|
973
|
+
renderH = img.height * coverScale;
|
|
974
|
+
} else if (objectFit === 'none') {
|
|
975
|
+
renderW = img.width;
|
|
976
|
+
renderH = img.height;
|
|
977
|
+
} else if (objectFit === 'scale-down') {
|
|
978
|
+
const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
|
|
979
|
+
renderW = img.width * scaleDown;
|
|
980
|
+
renderH = img.height * scaleDown;
|
|
981
|
+
} else {
|
|
982
|
+
// 'fill' (default)
|
|
983
|
+
renderW = targetW;
|
|
984
|
+
renderH = targetH;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Handle Object Position (simplified parsing for "x% y%" or keywords)
|
|
988
|
+
let posX = 0.5; // Default center
|
|
989
|
+
let posY = 0.5;
|
|
990
|
+
|
|
991
|
+
const posParts = objectPosition.split(' ');
|
|
992
|
+
if (posParts.length > 0) {
|
|
993
|
+
const parsePos = (val) => {
|
|
994
|
+
if (val === 'left' || val === 'top') return 0;
|
|
995
|
+
if (val === 'center') return 0.5;
|
|
996
|
+
if (val === 'right' || val === 'bottom') return 1;
|
|
997
|
+
if (val.includes('%')) return parseFloat(val) / 100;
|
|
998
|
+
return 0.5; // fallback
|
|
999
|
+
};
|
|
1000
|
+
posX = parsePos(posParts[0]);
|
|
1001
|
+
posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const renderX = (targetW - renderW) * posX;
|
|
1005
|
+
const renderY = (targetH - renderH) * posY;
|
|
944
1006
|
|
|
945
1007
|
ctx.drawImage(img, renderX, renderY, renderW, renderH);
|
|
946
1008
|
|
|
@@ -996,34 +1058,37 @@ async function exportToPptx(target, options = {}) {
|
|
|
996
1058
|
await processSlide(root, slide, pptx);
|
|
997
1059
|
}
|
|
998
1060
|
|
|
999
|
-
|
|
1061
|
+
// 3. Font Embedding Logic
|
|
1000
1062
|
let finalBlob;
|
|
1001
1063
|
let fontsToEmbed = options.fonts || [];
|
|
1002
1064
|
|
|
1003
1065
|
if (options.autoEmbedFonts) {
|
|
1004
1066
|
// A. Scan DOM for used font families
|
|
1005
1067
|
const usedFamilies = getUsedFontFamilies(elements);
|
|
1006
|
-
|
|
1068
|
+
|
|
1007
1069
|
// B. Scan CSS for URLs matches
|
|
1008
1070
|
const detectedFonts = await getAutoDetectedFonts(usedFamilies);
|
|
1009
|
-
|
|
1071
|
+
|
|
1010
1072
|
// C. Merge (Avoid duplicates)
|
|
1011
|
-
const explicitNames = new Set(fontsToEmbed.map(f => f.name));
|
|
1073
|
+
const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
|
|
1012
1074
|
for (const autoFont of detectedFonts) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1075
|
+
if (!explicitNames.has(autoFont.name)) {
|
|
1076
|
+
fontsToEmbed.push(autoFont);
|
|
1077
|
+
}
|
|
1016
1078
|
}
|
|
1017
|
-
|
|
1079
|
+
|
|
1018
1080
|
if (detectedFonts.length > 0) {
|
|
1019
|
-
|
|
1081
|
+
console.log(
|
|
1082
|
+
'Auto-detected fonts:',
|
|
1083
|
+
detectedFonts.map((f) => f.name)
|
|
1084
|
+
);
|
|
1020
1085
|
}
|
|
1021
1086
|
}
|
|
1022
1087
|
|
|
1023
1088
|
if (fontsToEmbed.length > 0) {
|
|
1024
1089
|
// Generate initial PPTX
|
|
1025
1090
|
const initialBlob = await pptx.write({ outputType: 'blob' });
|
|
1026
|
-
|
|
1091
|
+
|
|
1027
1092
|
// Load into Embedder
|
|
1028
1093
|
const zip = await JSZip.loadAsync(initialBlob);
|
|
1029
1094
|
const embedder = new PPTXEmbedFonts();
|
|
@@ -1035,7 +1100,7 @@ async function exportToPptx(target, options = {}) {
|
|
|
1035
1100
|
const response = await fetch(fontCfg.url);
|
|
1036
1101
|
if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
|
|
1037
1102
|
const buffer = await response.arrayBuffer();
|
|
1038
|
-
|
|
1103
|
+
|
|
1039
1104
|
// Infer type
|
|
1040
1105
|
const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
|
|
1041
1106
|
let type = 'ttf';
|
|
@@ -1174,11 +1239,8 @@ async function processSlide(root, slide, pptx) {
|
|
|
1174
1239
|
|
|
1175
1240
|
/**
|
|
1176
1241
|
* Optimized html2canvas wrapper
|
|
1177
|
-
*
|
|
1178
|
-
|
|
1179
|
-
/**
|
|
1180
|
-
* Optimized html2canvas wrapper
|
|
1181
|
-
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
1242
|
+
* Fixes icon clipping by adding padding in the clone to capture font bleed,
|
|
1243
|
+
* then scaling the result to fit the original bounding box.
|
|
1182
1244
|
*/
|
|
1183
1245
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
1184
1246
|
return new Promise((resolve) => {
|
|
@@ -1194,51 +1256,76 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
1194
1256
|
html2canvas(node, {
|
|
1195
1257
|
backgroundColor: null,
|
|
1196
1258
|
logging: false,
|
|
1197
|
-
scale: 3, //
|
|
1198
|
-
useCORS: true,
|
|
1259
|
+
scale: 3, // High resolution capture
|
|
1260
|
+
useCORS: true,
|
|
1199
1261
|
onclone: (clonedDoc) => {
|
|
1200
1262
|
const clonedNode = clonedDoc.getElementById(tempId);
|
|
1201
1263
|
if (clonedNode) {
|
|
1202
|
-
// --- FIX
|
|
1203
|
-
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
1204
|
-
clonedNode.style.overflow = 'visible';
|
|
1205
|
-
|
|
1206
|
-
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
1207
|
-
// (Applies to <i>, <span>, or standard icon classes)
|
|
1264
|
+
// --- FIX FOR ICON CLIPPING ---
|
|
1208
1265
|
const tag = clonedNode.tagName;
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1266
|
+
// Detect icons: I tags, SPAN tags, or elements with icon classes
|
|
1267
|
+
const isIcon =
|
|
1268
|
+
tag === 'I' ||
|
|
1269
|
+
tag === 'SPAN' ||
|
|
1270
|
+
clonedNode.className.includes('fa-') ||
|
|
1271
|
+
clonedNode.className.includes('icon');
|
|
1272
|
+
|
|
1273
|
+
if (isIcon) {
|
|
1274
|
+
// 1. Use inline-flex to center the glyph content
|
|
1275
|
+
clonedNode.style.display = 'flex';
|
|
1213
1276
|
clonedNode.style.justifyContent = 'center';
|
|
1214
1277
|
clonedNode.style.alignItems = 'center';
|
|
1215
1278
|
|
|
1216
|
-
//
|
|
1217
|
-
clonedNode.style.
|
|
1279
|
+
// 2. Reset constraints that might crop content
|
|
1280
|
+
clonedNode.style.overflow = 'visible';
|
|
1281
|
+
clonedNode.style.lineHeight = 'normal';
|
|
1282
|
+
|
|
1283
|
+
// 3. Add padding to the capture area.
|
|
1284
|
+
// This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
|
|
1285
|
+
// are captured in the canvas instead of being cropped.
|
|
1286
|
+
// 'content-box' ensures padding adds to the total width/height.
|
|
1287
|
+
clonedNode.style.boxSizing = 'content-box';
|
|
1288
|
+
clonedNode.style.padding = '10px';
|
|
1289
|
+
|
|
1290
|
+
// 4. Ensure the content box itself matches the original size (minimum)
|
|
1291
|
+
// so the icon doesn't collapse.
|
|
1292
|
+
clonedNode.style.minWidth = `${width}px`;
|
|
1293
|
+
clonedNode.style.minHeight = `${height}px`;
|
|
1218
1294
|
|
|
1219
|
-
//
|
|
1220
|
-
clonedNode.style.
|
|
1221
|
-
clonedNode.style.verticalAlign = 'middle';
|
|
1295
|
+
// Clear margins that might displace the capture
|
|
1296
|
+
clonedNode.style.margin = '0';
|
|
1222
1297
|
}
|
|
1223
1298
|
}
|
|
1224
1299
|
},
|
|
1225
1300
|
})
|
|
1226
1301
|
.then((canvas) => {
|
|
1227
|
-
// Restore
|
|
1302
|
+
// Restore ID
|
|
1228
1303
|
if (originalId) node.id = originalId;
|
|
1229
1304
|
else node.removeAttribute('id');
|
|
1230
1305
|
|
|
1306
|
+
// Create destination canvas with the EXACT original dimensions
|
|
1231
1307
|
const destCanvas = document.createElement('canvas');
|
|
1232
1308
|
destCanvas.width = width;
|
|
1233
1309
|
destCanvas.height = height;
|
|
1234
1310
|
const ctx = destCanvas.getContext('2d');
|
|
1235
1311
|
|
|
1236
|
-
//
|
|
1237
|
-
//
|
|
1238
|
-
//
|
|
1239
|
-
|
|
1312
|
+
// --- SCALE TO FIT ---
|
|
1313
|
+
// The captured 'canvas' is now larger than 'destCanvas' because we added padding.
|
|
1314
|
+
// We draw the larger captured image into the smaller destination box.
|
|
1315
|
+
// This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
|
|
1316
|
+
ctx.drawImage(
|
|
1317
|
+
canvas,
|
|
1318
|
+
0,
|
|
1319
|
+
0,
|
|
1320
|
+
canvas.width,
|
|
1321
|
+
canvas.height, // Source: Full captured size (with padding)
|
|
1322
|
+
0,
|
|
1323
|
+
0,
|
|
1324
|
+
width,
|
|
1325
|
+
height // Dest: Original requested size
|
|
1326
|
+
);
|
|
1240
1327
|
|
|
1241
|
-
// --- Border Radius Clipping (
|
|
1328
|
+
// --- Border Radius Clipping (Preserve existing logic) ---
|
|
1242
1329
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
1243
1330
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
1244
1331
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -1450,6 +1537,9 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1450
1537
|
}
|
|
1451
1538
|
}
|
|
1452
1539
|
|
|
1540
|
+
const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
|
|
1541
|
+
const objectPosition = style.objectPosition || '50% 50%';
|
|
1542
|
+
|
|
1453
1543
|
const item = {
|
|
1454
1544
|
type: 'image',
|
|
1455
1545
|
zIndex,
|
|
@@ -1458,7 +1548,14 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1458
1548
|
};
|
|
1459
1549
|
|
|
1460
1550
|
const job = async () => {
|
|
1461
|
-
const processed = await getProcessedImage(
|
|
1551
|
+
const processed = await getProcessedImage(
|
|
1552
|
+
node.src,
|
|
1553
|
+
widthPx,
|
|
1554
|
+
heightPx,
|
|
1555
|
+
radii,
|
|
1556
|
+
objectFit,
|
|
1557
|
+
objectPosition
|
|
1558
|
+
);
|
|
1462
1559
|
if (processed) item.options.data = processed;
|
|
1463
1560
|
else item.skip = true;
|
|
1464
1561
|
};
|
|
@@ -1558,16 +1655,45 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1558
1655
|
const isList = style.display === 'list-item';
|
|
1559
1656
|
if (isList) {
|
|
1560
1657
|
const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
|
|
1561
|
-
const
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1658
|
+
const listStyleType = style.listStyleType || 'disc';
|
|
1659
|
+
const listStylePos = style.listStylePosition || 'outside';
|
|
1660
|
+
|
|
1661
|
+
let marker = null;
|
|
1662
|
+
|
|
1663
|
+
// 1. Determine the marker character based on list-style-type
|
|
1664
|
+
if (listStyleType !== 'none') {
|
|
1665
|
+
if (listStyleType === 'decimal') {
|
|
1666
|
+
// Calculate index for ordered lists (1., 2., etc.)
|
|
1667
|
+
const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
|
|
1668
|
+
marker = `${index}.`;
|
|
1669
|
+
} else if (listStyleType === 'circle') {
|
|
1670
|
+
marker = '○';
|
|
1671
|
+
} else if (listStyleType === 'square') {
|
|
1672
|
+
marker = '■';
|
|
1673
|
+
} else {
|
|
1674
|
+
marker = '•'; // Default to disc
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
// 2. Apply alignment and add marker
|
|
1679
|
+
if (marker) {
|
|
1680
|
+
// Only shift the text box to the left if the bullet is OUTSIDE the content box.
|
|
1681
|
+
// Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
|
|
1682
|
+
if (listStylePos === 'outside') {
|
|
1683
|
+
const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
|
|
1684
|
+
x -= bulletShift;
|
|
1685
|
+
w += bulletShift;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Add the bullet + 3 spaces for visual separation
|
|
1689
|
+
textParts.push({
|
|
1690
|
+
text: marker + ' ',
|
|
1691
|
+
options: {
|
|
1692
|
+
color: parseColor(style.color).hex || '000000',
|
|
1693
|
+
fontSize: fontSizePt,
|
|
1694
|
+
},
|
|
1695
|
+
});
|
|
1696
|
+
}
|
|
1571
1697
|
}
|
|
1572
1698
|
|
|
1573
1699
|
node.childNodes.forEach((child, index) => {
|
|
@@ -1647,6 +1773,7 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1647
1773
|
}
|
|
1648
1774
|
|
|
1649
1775
|
if (textPayload) {
|
|
1776
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
1650
1777
|
items.push({
|
|
1651
1778
|
type: 'text',
|
|
1652
1779
|
zIndex: zIndex + 1,
|
|
@@ -1726,21 +1853,46 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1726
1853
|
|
|
1727
1854
|
if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
1728
1855
|
|
|
1729
|
-
|
|
1730
|
-
const
|
|
1731
|
-
|
|
1856
|
+
// 1. Calculate dimensions first
|
|
1857
|
+
const minDimension = Math.min(widthPx, heightPx);
|
|
1858
|
+
|
|
1859
|
+
let rawRadius = parseFloat(style.borderRadius) || 0;
|
|
1860
|
+
const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
|
|
1861
|
+
|
|
1862
|
+
// 2. Normalize radius to pixels
|
|
1863
|
+
let radiusPx = rawRadius;
|
|
1864
|
+
if (isPercentage) {
|
|
1865
|
+
radiusPx = (rawRadius / 100) * minDimension;
|
|
1866
|
+
}
|
|
1732
1867
|
|
|
1733
1868
|
let shapeType = pptx.ShapeType.rect;
|
|
1734
|
-
|
|
1735
|
-
|
|
1869
|
+
|
|
1870
|
+
// 3. Determine Shape Logic
|
|
1871
|
+
const isSquare = Math.abs(widthPx - heightPx) < 1;
|
|
1872
|
+
const isFullyRound = radiusPx >= minDimension / 2;
|
|
1873
|
+
|
|
1874
|
+
// CASE A: It is an Ellipse if:
|
|
1875
|
+
// 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
|
|
1876
|
+
// 2. OR it is a perfect square and fully rounded (a circle)
|
|
1877
|
+
if (isFullyRound && (isPercentage || isSquare)) {
|
|
1878
|
+
shapeType = pptx.ShapeType.ellipse;
|
|
1879
|
+
}
|
|
1880
|
+
// CASE B: It is a Rounded Rectangle (including "Pill" shapes)
|
|
1881
|
+
else if (radiusPx > 0) {
|
|
1736
1882
|
shapeType = pptx.ShapeType.roundRect;
|
|
1737
|
-
|
|
1883
|
+
let r = radiusPx / minDimension;
|
|
1884
|
+
if (r > 0.5) r = 0.5;
|
|
1885
|
+
if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
|
|
1886
|
+
|
|
1887
|
+
shapeOpts.rectRadius = r;
|
|
1738
1888
|
}
|
|
1739
1889
|
|
|
1740
1890
|
if (textPayload) {
|
|
1891
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
1741
1892
|
const textOptions = {
|
|
1742
1893
|
shape: shapeType,
|
|
1743
1894
|
...shapeOpts,
|
|
1895
|
+
rotate: rotation,
|
|
1744
1896
|
align: textPayload.align,
|
|
1745
1897
|
valign: textPayload.valign,
|
|
1746
1898
|
inset: textPayload.inset,
|