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.cjs
CHANGED
|
@@ -229,11 +229,14 @@ class PPTXEmbedFonts {
|
|
|
229
229
|
|
|
230
230
|
// src/utils.js
|
|
231
231
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
232
|
+
// canvas context for color normalization
|
|
233
|
+
let _ctx;
|
|
234
|
+
function getCtx() {
|
|
235
|
+
if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
236
|
+
return _ctx;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Checks if any parent element has overflow: hidden which would clip this element
|
|
237
240
|
function isClippedByParent(node) {
|
|
238
241
|
let parent = node.parentElement;
|
|
239
242
|
while (parent && parent !== document.body) {
|
|
@@ -399,28 +402,58 @@ function generateCustomShapeSVG(w, h, color, opacity, radii) {
|
|
|
399
402
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
400
403
|
}
|
|
401
404
|
|
|
405
|
+
// --- REPLACE THE EXISTING parseColor FUNCTION ---
|
|
402
406
|
function parseColor(str) {
|
|
403
|
-
if (!str || str === 'transparent' || str.
|
|
407
|
+
if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
|
|
404
408
|
return { hex: null, opacity: 0 };
|
|
405
409
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
410
|
+
|
|
411
|
+
const ctx = getCtx();
|
|
412
|
+
ctx.fillStyle = str;
|
|
413
|
+
// This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
|
|
414
|
+
const computed = ctx.fillStyle;
|
|
415
|
+
|
|
416
|
+
// 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
|
|
417
|
+
if (computed.startsWith('#')) {
|
|
418
|
+
let hex = computed.slice(1); // Remove '#'
|
|
419
|
+
let opacity = 1;
|
|
420
|
+
|
|
421
|
+
// Expand shorthand #RGB -> #RRGGBB
|
|
422
|
+
if (hex.length === 3) {
|
|
423
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
424
|
+
}
|
|
425
|
+
// Expand shorthand #RGBA -> #RRGGBBAA
|
|
426
|
+
else if (hex.length === 4) {
|
|
427
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
|
|
431
|
+
if (hex.length === 8) {
|
|
432
|
+
opacity = parseInt(hex.slice(6), 16) / 255;
|
|
433
|
+
hex = hex.slice(0, 6); // Keep only RRGGBB
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return { hex: hex.toUpperCase(), opacity };
|
|
414
437
|
}
|
|
415
|
-
|
|
438
|
+
|
|
439
|
+
// 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
|
|
440
|
+
const match = computed.match(/[\d.]+/g);
|
|
416
441
|
if (match && match.length >= 3) {
|
|
417
442
|
const r = parseInt(match[0]);
|
|
418
443
|
const g = parseInt(match[1]);
|
|
419
444
|
const b = parseInt(match[2]);
|
|
420
445
|
const a = match.length > 3 ? parseFloat(match[3]) : 1;
|
|
421
|
-
|
|
446
|
+
|
|
447
|
+
// Bitwise shift to get Hex
|
|
448
|
+
const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
|
|
449
|
+
.toString(16)
|
|
450
|
+
.slice(1)
|
|
451
|
+
.toUpperCase();
|
|
452
|
+
|
|
422
453
|
return { hex, opacity: a };
|
|
423
454
|
}
|
|
455
|
+
|
|
456
|
+
// Fallback (Parsing failed)
|
|
424
457
|
return { hex: null, opacity: 0 };
|
|
425
458
|
}
|
|
426
459
|
|
|
@@ -904,33 +937,30 @@ async function getAutoDetectedFonts(usedFamilies) {
|
|
|
904
937
|
|
|
905
938
|
// src/image-processor.js
|
|
906
939
|
|
|
907
|
-
async function getProcessedImage(src, targetW, targetH, radius) {
|
|
940
|
+
async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
|
|
908
941
|
return new Promise((resolve) => {
|
|
909
942
|
const img = new Image();
|
|
910
|
-
img.crossOrigin = 'Anonymous';
|
|
943
|
+
img.crossOrigin = 'Anonymous';
|
|
911
944
|
|
|
912
945
|
img.onload = () => {
|
|
913
946
|
const canvas = document.createElement('canvas');
|
|
914
|
-
// Double resolution
|
|
915
|
-
const scale = 2;
|
|
947
|
+
const scale = 2; // Double resolution
|
|
916
948
|
canvas.width = targetW * scale;
|
|
917
949
|
canvas.height = targetH * scale;
|
|
918
950
|
const ctx = canvas.getContext('2d');
|
|
919
951
|
ctx.scale(scale, scale);
|
|
920
952
|
|
|
921
|
-
// Normalize radius
|
|
953
|
+
// Normalize radius
|
|
922
954
|
let r = { tl: 0, tr: 0, br: 0, bl: 0 };
|
|
923
955
|
if (typeof radius === 'number') {
|
|
924
956
|
r = { tl: radius, tr: radius, br: radius, bl: radius };
|
|
925
957
|
} else if (typeof radius === 'object' && radius !== null) {
|
|
926
|
-
r = { ...r, ...radius };
|
|
958
|
+
r = { ...r, ...radius };
|
|
927
959
|
}
|
|
928
960
|
|
|
929
|
-
// 1. Draw
|
|
961
|
+
// 1. Draw Mask
|
|
930
962
|
ctx.beginPath();
|
|
931
|
-
|
|
932
|
-
// Border Radius Clamping Logic (CSS Spec)
|
|
933
|
-
// Prevents corners from overlapping if radii are too large for the container
|
|
963
|
+
// ... (radius clamping logic remains the same) ...
|
|
934
964
|
const factor = Math.min(
|
|
935
965
|
targetW / (r.tl + r.tr) || Infinity,
|
|
936
966
|
targetH / (r.tr + r.br) || Infinity,
|
|
@@ -939,13 +969,9 @@ async function getProcessedImage(src, targetW, targetH, radius) {
|
|
|
939
969
|
);
|
|
940
970
|
|
|
941
971
|
if (factor < 1) {
|
|
942
|
-
r.tl *= factor;
|
|
943
|
-
r.tr *= factor;
|
|
944
|
-
r.br *= factor;
|
|
945
|
-
r.bl *= factor;
|
|
972
|
+
r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
|
|
946
973
|
}
|
|
947
974
|
|
|
948
|
-
// Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
|
|
949
975
|
ctx.moveTo(r.tl, 0);
|
|
950
976
|
ctx.lineTo(targetW - r.tr, 0);
|
|
951
977
|
ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
|
|
@@ -955,22 +981,58 @@ async function getProcessedImage(src, targetW, targetH, radius) {
|
|
|
955
981
|
ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
|
|
956
982
|
ctx.lineTo(0, r.tl);
|
|
957
983
|
ctx.arcTo(0, 0, r.tl, 0, r.tl);
|
|
958
|
-
|
|
959
984
|
ctx.closePath();
|
|
960
985
|
ctx.fillStyle = '#000';
|
|
961
986
|
ctx.fill();
|
|
962
987
|
|
|
963
|
-
// 2. Composite Source-In
|
|
988
|
+
// 2. Composite Source-In
|
|
964
989
|
ctx.globalCompositeOperation = 'source-in';
|
|
965
990
|
|
|
966
|
-
// 3. Draw Image
|
|
991
|
+
// 3. Draw Image with Object Fit logic
|
|
967
992
|
const wRatio = targetW / img.width;
|
|
968
993
|
const hRatio = targetH / img.height;
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
994
|
+
let renderW, renderH;
|
|
995
|
+
|
|
996
|
+
if (objectFit === 'contain') {
|
|
997
|
+
const fitScale = Math.min(wRatio, hRatio);
|
|
998
|
+
renderW = img.width * fitScale;
|
|
999
|
+
renderH = img.height * fitScale;
|
|
1000
|
+
} else if (objectFit === 'cover') {
|
|
1001
|
+
const coverScale = Math.max(wRatio, hRatio);
|
|
1002
|
+
renderW = img.width * coverScale;
|
|
1003
|
+
renderH = img.height * coverScale;
|
|
1004
|
+
} else if (objectFit === 'none') {
|
|
1005
|
+
renderW = img.width;
|
|
1006
|
+
renderH = img.height;
|
|
1007
|
+
} else if (objectFit === 'scale-down') {
|
|
1008
|
+
const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
|
|
1009
|
+
renderW = img.width * scaleDown;
|
|
1010
|
+
renderH = img.height * scaleDown;
|
|
1011
|
+
} else {
|
|
1012
|
+
// 'fill' (default)
|
|
1013
|
+
renderW = targetW;
|
|
1014
|
+
renderH = targetH;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// Handle Object Position (simplified parsing for "x% y%" or keywords)
|
|
1018
|
+
let posX = 0.5; // Default center
|
|
1019
|
+
let posY = 0.5;
|
|
1020
|
+
|
|
1021
|
+
const posParts = objectPosition.split(' ');
|
|
1022
|
+
if (posParts.length > 0) {
|
|
1023
|
+
const parsePos = (val) => {
|
|
1024
|
+
if (val === 'left' || val === 'top') return 0;
|
|
1025
|
+
if (val === 'center') return 0.5;
|
|
1026
|
+
if (val === 'right' || val === 'bottom') return 1;
|
|
1027
|
+
if (val.includes('%')) return parseFloat(val) / 100;
|
|
1028
|
+
return 0.5; // fallback
|
|
1029
|
+
};
|
|
1030
|
+
posX = parsePos(posParts[0]);
|
|
1031
|
+
posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
const renderX = (targetW - renderW) * posX;
|
|
1035
|
+
const renderY = (targetH - renderH) * posY;
|
|
974
1036
|
|
|
975
1037
|
ctx.drawImage(img, renderX, renderY, renderW, renderH);
|
|
976
1038
|
|
|
@@ -1026,34 +1088,37 @@ async function exportToPptx(target, options = {}) {
|
|
|
1026
1088
|
await processSlide(root, slide, pptx);
|
|
1027
1089
|
}
|
|
1028
1090
|
|
|
1029
|
-
|
|
1091
|
+
// 3. Font Embedding Logic
|
|
1030
1092
|
let finalBlob;
|
|
1031
1093
|
let fontsToEmbed = options.fonts || [];
|
|
1032
1094
|
|
|
1033
1095
|
if (options.autoEmbedFonts) {
|
|
1034
1096
|
// A. Scan DOM for used font families
|
|
1035
1097
|
const usedFamilies = getUsedFontFamilies(elements);
|
|
1036
|
-
|
|
1098
|
+
|
|
1037
1099
|
// B. Scan CSS for URLs matches
|
|
1038
1100
|
const detectedFonts = await getAutoDetectedFonts(usedFamilies);
|
|
1039
|
-
|
|
1101
|
+
|
|
1040
1102
|
// C. Merge (Avoid duplicates)
|
|
1041
|
-
const explicitNames = new Set(fontsToEmbed.map(f => f.name));
|
|
1103
|
+
const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
|
|
1042
1104
|
for (const autoFont of detectedFonts) {
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1105
|
+
if (!explicitNames.has(autoFont.name)) {
|
|
1106
|
+
fontsToEmbed.push(autoFont);
|
|
1107
|
+
}
|
|
1046
1108
|
}
|
|
1047
|
-
|
|
1109
|
+
|
|
1048
1110
|
if (detectedFonts.length > 0) {
|
|
1049
|
-
|
|
1111
|
+
console.log(
|
|
1112
|
+
'Auto-detected fonts:',
|
|
1113
|
+
detectedFonts.map((f) => f.name)
|
|
1114
|
+
);
|
|
1050
1115
|
}
|
|
1051
1116
|
}
|
|
1052
1117
|
|
|
1053
1118
|
if (fontsToEmbed.length > 0) {
|
|
1054
1119
|
// Generate initial PPTX
|
|
1055
1120
|
const initialBlob = await pptx.write({ outputType: 'blob' });
|
|
1056
|
-
|
|
1121
|
+
|
|
1057
1122
|
// Load into Embedder
|
|
1058
1123
|
const zip = await JSZip__default["default"].loadAsync(initialBlob);
|
|
1059
1124
|
const embedder = new PPTXEmbedFonts();
|
|
@@ -1065,7 +1130,7 @@ async function exportToPptx(target, options = {}) {
|
|
|
1065
1130
|
const response = await fetch(fontCfg.url);
|
|
1066
1131
|
if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
|
|
1067
1132
|
const buffer = await response.arrayBuffer();
|
|
1068
|
-
|
|
1133
|
+
|
|
1069
1134
|
// Infer type
|
|
1070
1135
|
const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
|
|
1071
1136
|
let type = 'ttf';
|
|
@@ -1204,11 +1269,8 @@ async function processSlide(root, slide, pptx) {
|
|
|
1204
1269
|
|
|
1205
1270
|
/**
|
|
1206
1271
|
* Optimized html2canvas wrapper
|
|
1207
|
-
*
|
|
1208
|
-
|
|
1209
|
-
/**
|
|
1210
|
-
* Optimized html2canvas wrapper
|
|
1211
|
-
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
1272
|
+
* Fixes icon clipping by adding padding in the clone to capture font bleed,
|
|
1273
|
+
* then scaling the result to fit the original bounding box.
|
|
1212
1274
|
*/
|
|
1213
1275
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
1214
1276
|
return new Promise((resolve) => {
|
|
@@ -1224,51 +1286,76 @@ async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
|
1224
1286
|
html2canvas__default["default"](node, {
|
|
1225
1287
|
backgroundColor: null,
|
|
1226
1288
|
logging: false,
|
|
1227
|
-
scale: 3, //
|
|
1228
|
-
useCORS: true,
|
|
1289
|
+
scale: 3, // High resolution capture
|
|
1290
|
+
useCORS: true,
|
|
1229
1291
|
onclone: (clonedDoc) => {
|
|
1230
1292
|
const clonedNode = clonedDoc.getElementById(tempId);
|
|
1231
1293
|
if (clonedNode) {
|
|
1232
|
-
// --- FIX
|
|
1233
|
-
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
1234
|
-
clonedNode.style.overflow = 'visible';
|
|
1235
|
-
|
|
1236
|
-
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
1237
|
-
// (Applies to <i>, <span>, or standard icon classes)
|
|
1294
|
+
// --- FIX FOR ICON CLIPPING ---
|
|
1238
1295
|
const tag = clonedNode.tagName;
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1296
|
+
// Detect icons: I tags, SPAN tags, or elements with icon classes
|
|
1297
|
+
const isIcon =
|
|
1298
|
+
tag === 'I' ||
|
|
1299
|
+
tag === 'SPAN' ||
|
|
1300
|
+
clonedNode.className.includes('fa-') ||
|
|
1301
|
+
clonedNode.className.includes('icon');
|
|
1302
|
+
|
|
1303
|
+
if (isIcon) {
|
|
1304
|
+
// 1. Use inline-flex to center the glyph content
|
|
1305
|
+
clonedNode.style.display = 'flex';
|
|
1243
1306
|
clonedNode.style.justifyContent = 'center';
|
|
1244
1307
|
clonedNode.style.alignItems = 'center';
|
|
1245
1308
|
|
|
1246
|
-
//
|
|
1247
|
-
clonedNode.style.
|
|
1309
|
+
// 2. Reset constraints that might crop content
|
|
1310
|
+
clonedNode.style.overflow = 'visible';
|
|
1311
|
+
clonedNode.style.lineHeight = 'normal';
|
|
1312
|
+
|
|
1313
|
+
// 3. Add padding to the capture area.
|
|
1314
|
+
// This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
|
|
1315
|
+
// are captured in the canvas instead of being cropped.
|
|
1316
|
+
// 'content-box' ensures padding adds to the total width/height.
|
|
1317
|
+
clonedNode.style.boxSizing = 'content-box';
|
|
1318
|
+
clonedNode.style.padding = '10px';
|
|
1319
|
+
|
|
1320
|
+
// 4. Ensure the content box itself matches the original size (minimum)
|
|
1321
|
+
// so the icon doesn't collapse.
|
|
1322
|
+
clonedNode.style.minWidth = `${width}px`;
|
|
1323
|
+
clonedNode.style.minHeight = `${height}px`;
|
|
1248
1324
|
|
|
1249
|
-
//
|
|
1250
|
-
clonedNode.style.
|
|
1251
|
-
clonedNode.style.verticalAlign = 'middle';
|
|
1325
|
+
// Clear margins that might displace the capture
|
|
1326
|
+
clonedNode.style.margin = '0';
|
|
1252
1327
|
}
|
|
1253
1328
|
}
|
|
1254
1329
|
},
|
|
1255
1330
|
})
|
|
1256
1331
|
.then((canvas) => {
|
|
1257
|
-
// Restore
|
|
1332
|
+
// Restore ID
|
|
1258
1333
|
if (originalId) node.id = originalId;
|
|
1259
1334
|
else node.removeAttribute('id');
|
|
1260
1335
|
|
|
1336
|
+
// Create destination canvas with the EXACT original dimensions
|
|
1261
1337
|
const destCanvas = document.createElement('canvas');
|
|
1262
1338
|
destCanvas.width = width;
|
|
1263
1339
|
destCanvas.height = height;
|
|
1264
1340
|
const ctx = destCanvas.getContext('2d');
|
|
1265
1341
|
|
|
1266
|
-
//
|
|
1267
|
-
//
|
|
1268
|
-
//
|
|
1269
|
-
|
|
1342
|
+
// --- SCALE TO FIT ---
|
|
1343
|
+
// The captured 'canvas' is now larger than 'destCanvas' because we added padding.
|
|
1344
|
+
// We draw the larger captured image into the smaller destination box.
|
|
1345
|
+
// This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
|
|
1346
|
+
ctx.drawImage(
|
|
1347
|
+
canvas,
|
|
1348
|
+
0,
|
|
1349
|
+
0,
|
|
1350
|
+
canvas.width,
|
|
1351
|
+
canvas.height, // Source: Full captured size (with padding)
|
|
1352
|
+
0,
|
|
1353
|
+
0,
|
|
1354
|
+
width,
|
|
1355
|
+
height // Dest: Original requested size
|
|
1356
|
+
);
|
|
1270
1357
|
|
|
1271
|
-
// --- Border Radius Clipping (
|
|
1358
|
+
// --- Border Radius Clipping (Preserve existing logic) ---
|
|
1272
1359
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
1273
1360
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
1274
1361
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -1480,6 +1567,9 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1480
1567
|
}
|
|
1481
1568
|
}
|
|
1482
1569
|
|
|
1570
|
+
const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
|
|
1571
|
+
const objectPosition = style.objectPosition || '50% 50%';
|
|
1572
|
+
|
|
1483
1573
|
const item = {
|
|
1484
1574
|
type: 'image',
|
|
1485
1575
|
zIndex,
|
|
@@ -1488,7 +1578,14 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1488
1578
|
};
|
|
1489
1579
|
|
|
1490
1580
|
const job = async () => {
|
|
1491
|
-
const processed = await getProcessedImage(
|
|
1581
|
+
const processed = await getProcessedImage(
|
|
1582
|
+
node.src,
|
|
1583
|
+
widthPx,
|
|
1584
|
+
heightPx,
|
|
1585
|
+
radii,
|
|
1586
|
+
objectFit,
|
|
1587
|
+
objectPosition
|
|
1588
|
+
);
|
|
1492
1589
|
if (processed) item.options.data = processed;
|
|
1493
1590
|
else item.skip = true;
|
|
1494
1591
|
};
|
|
@@ -1588,16 +1685,45 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1588
1685
|
const isList = style.display === 'list-item';
|
|
1589
1686
|
if (isList) {
|
|
1590
1687
|
const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
|
|
1591
|
-
const
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1688
|
+
const listStyleType = style.listStyleType || 'disc';
|
|
1689
|
+
const listStylePos = style.listStylePosition || 'outside';
|
|
1690
|
+
|
|
1691
|
+
let marker = null;
|
|
1692
|
+
|
|
1693
|
+
// 1. Determine the marker character based on list-style-type
|
|
1694
|
+
if (listStyleType !== 'none') {
|
|
1695
|
+
if (listStyleType === 'decimal') {
|
|
1696
|
+
// Calculate index for ordered lists (1., 2., etc.)
|
|
1697
|
+
const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
|
|
1698
|
+
marker = `${index}.`;
|
|
1699
|
+
} else if (listStyleType === 'circle') {
|
|
1700
|
+
marker = '○';
|
|
1701
|
+
} else if (listStyleType === 'square') {
|
|
1702
|
+
marker = '■';
|
|
1703
|
+
} else {
|
|
1704
|
+
marker = '•'; // Default to disc
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// 2. Apply alignment and add marker
|
|
1709
|
+
if (marker) {
|
|
1710
|
+
// Only shift the text box to the left if the bullet is OUTSIDE the content box.
|
|
1711
|
+
// Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
|
|
1712
|
+
if (listStylePos === 'outside') {
|
|
1713
|
+
const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
|
|
1714
|
+
x -= bulletShift;
|
|
1715
|
+
w += bulletShift;
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// Add the bullet + 3 spaces for visual separation
|
|
1719
|
+
textParts.push({
|
|
1720
|
+
text: marker + ' ',
|
|
1721
|
+
options: {
|
|
1722
|
+
color: parseColor(style.color).hex || '000000',
|
|
1723
|
+
fontSize: fontSizePt,
|
|
1724
|
+
},
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1601
1727
|
}
|
|
1602
1728
|
|
|
1603
1729
|
node.childNodes.forEach((child, index) => {
|
|
@@ -1677,6 +1803,7 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1677
1803
|
}
|
|
1678
1804
|
|
|
1679
1805
|
if (textPayload) {
|
|
1806
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
1680
1807
|
items.push({
|
|
1681
1808
|
type: 'text',
|
|
1682
1809
|
zIndex: zIndex + 1,
|
|
@@ -1756,21 +1883,46 @@ function prepareRenderItem(node, config, domOrder, pptx, effectiveZIndex, comput
|
|
|
1756
1883
|
|
|
1757
1884
|
if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
1758
1885
|
|
|
1759
|
-
|
|
1760
|
-
const
|
|
1761
|
-
|
|
1886
|
+
// 1. Calculate dimensions first
|
|
1887
|
+
const minDimension = Math.min(widthPx, heightPx);
|
|
1888
|
+
|
|
1889
|
+
let rawRadius = parseFloat(style.borderRadius) || 0;
|
|
1890
|
+
const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
|
|
1891
|
+
|
|
1892
|
+
// 2. Normalize radius to pixels
|
|
1893
|
+
let radiusPx = rawRadius;
|
|
1894
|
+
if (isPercentage) {
|
|
1895
|
+
radiusPx = (rawRadius / 100) * minDimension;
|
|
1896
|
+
}
|
|
1762
1897
|
|
|
1763
1898
|
let shapeType = pptx.ShapeType.rect;
|
|
1764
|
-
|
|
1765
|
-
|
|
1899
|
+
|
|
1900
|
+
// 3. Determine Shape Logic
|
|
1901
|
+
const isSquare = Math.abs(widthPx - heightPx) < 1;
|
|
1902
|
+
const isFullyRound = radiusPx >= minDimension / 2;
|
|
1903
|
+
|
|
1904
|
+
// CASE A: It is an Ellipse if:
|
|
1905
|
+
// 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
|
|
1906
|
+
// 2. OR it is a perfect square and fully rounded (a circle)
|
|
1907
|
+
if (isFullyRound && (isPercentage || isSquare)) {
|
|
1908
|
+
shapeType = pptx.ShapeType.ellipse;
|
|
1909
|
+
}
|
|
1910
|
+
// CASE B: It is a Rounded Rectangle (including "Pill" shapes)
|
|
1911
|
+
else if (radiusPx > 0) {
|
|
1766
1912
|
shapeType = pptx.ShapeType.roundRect;
|
|
1767
|
-
|
|
1913
|
+
let r = radiusPx / minDimension;
|
|
1914
|
+
if (r > 0.5) r = 0.5;
|
|
1915
|
+
if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
|
|
1916
|
+
|
|
1917
|
+
shapeOpts.rectRadius = r;
|
|
1768
1918
|
}
|
|
1769
1919
|
|
|
1770
1920
|
if (textPayload) {
|
|
1921
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
1771
1922
|
const textOptions = {
|
|
1772
1923
|
shape: shapeType,
|
|
1773
1924
|
...shapeOpts,
|
|
1925
|
+
rotate: rotation,
|
|
1774
1926
|
align: textPayload.align,
|
|
1775
1927
|
valign: textPayload.valign,
|
|
1776
1928
|
inset: textPayload.inset,
|