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
|
@@ -62410,11 +62410,14 @@
|
|
|
62410
62410
|
|
|
62411
62411
|
// src/utils.js
|
|
62412
62412
|
|
|
62413
|
-
|
|
62414
|
-
|
|
62415
|
-
|
|
62416
|
-
|
|
62417
|
-
|
|
62413
|
+
// canvas context for color normalization
|
|
62414
|
+
let _ctx;
|
|
62415
|
+
function getCtx() {
|
|
62416
|
+
if (!_ctx) _ctx = document.createElement('canvas').getContext('2d', { willReadFrequently: true });
|
|
62417
|
+
return _ctx;
|
|
62418
|
+
}
|
|
62419
|
+
|
|
62420
|
+
// Checks if any parent element has overflow: hidden which would clip this element
|
|
62418
62421
|
function isClippedByParent(node) {
|
|
62419
62422
|
let parent = node.parentElement;
|
|
62420
62423
|
while (parent && parent !== document.body) {
|
|
@@ -62580,28 +62583,58 @@
|
|
|
62580
62583
|
return 'data:image/svg+xml;base64,' + btoa(svg);
|
|
62581
62584
|
}
|
|
62582
62585
|
|
|
62586
|
+
// --- REPLACE THE EXISTING parseColor FUNCTION ---
|
|
62583
62587
|
function parseColor(str) {
|
|
62584
|
-
if (!str || str === 'transparent' || str.
|
|
62588
|
+
if (!str || str === 'transparent' || str.trim() === 'rgba(0, 0, 0, 0)') {
|
|
62585
62589
|
return { hex: null, opacity: 0 };
|
|
62586
62590
|
}
|
|
62587
|
-
|
|
62588
|
-
|
|
62589
|
-
|
|
62590
|
-
|
|
62591
|
-
|
|
62592
|
-
|
|
62593
|
-
|
|
62594
|
-
|
|
62591
|
+
|
|
62592
|
+
const ctx = getCtx();
|
|
62593
|
+
ctx.fillStyle = str;
|
|
62594
|
+
// This forces the browser to resolve variables and convert formats (oklch -> rgb/hex)
|
|
62595
|
+
const computed = ctx.fillStyle;
|
|
62596
|
+
|
|
62597
|
+
// 1. Handle Hex Output (e.g. #ff0000 or #ff0000ff)
|
|
62598
|
+
if (computed.startsWith('#')) {
|
|
62599
|
+
let hex = computed.slice(1); // Remove '#'
|
|
62600
|
+
let opacity = 1;
|
|
62601
|
+
|
|
62602
|
+
// Expand shorthand #RGB -> #RRGGBB
|
|
62603
|
+
if (hex.length === 3) {
|
|
62604
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
62605
|
+
}
|
|
62606
|
+
// Expand shorthand #RGBA -> #RRGGBBAA
|
|
62607
|
+
else if (hex.length === 4) {
|
|
62608
|
+
hex = hex.split('').map(c => c + c).join('');
|
|
62609
|
+
}
|
|
62610
|
+
|
|
62611
|
+
// Handle 8-digit Hex (RRGGBBAA) - PptxGenJS fails if we send 8 digits
|
|
62612
|
+
if (hex.length === 8) {
|
|
62613
|
+
opacity = parseInt(hex.slice(6), 16) / 255;
|
|
62614
|
+
hex = hex.slice(0, 6); // Keep only RRGGBB
|
|
62615
|
+
}
|
|
62616
|
+
|
|
62617
|
+
return { hex: hex.toUpperCase(), opacity };
|
|
62595
62618
|
}
|
|
62596
|
-
|
|
62619
|
+
|
|
62620
|
+
// 2. Handle RGB/RGBA Output (e.g. "rgb(55, 65, 81)" or "rgba(55, 65, 81, 1)")
|
|
62621
|
+
const match = computed.match(/[\d.]+/g);
|
|
62597
62622
|
if (match && match.length >= 3) {
|
|
62598
62623
|
const r = parseInt(match[0]);
|
|
62599
62624
|
const g = parseInt(match[1]);
|
|
62600
62625
|
const b = parseInt(match[2]);
|
|
62601
62626
|
const a = match.length > 3 ? parseFloat(match[3]) : 1;
|
|
62602
|
-
|
|
62627
|
+
|
|
62628
|
+
// Bitwise shift to get Hex
|
|
62629
|
+
const hex = ((1 << 24) + (r << 16) + (g << 8) + b)
|
|
62630
|
+
.toString(16)
|
|
62631
|
+
.slice(1)
|
|
62632
|
+
.toUpperCase();
|
|
62633
|
+
|
|
62603
62634
|
return { hex, opacity: a };
|
|
62604
62635
|
}
|
|
62636
|
+
|
|
62637
|
+
// Fallback (Parsing failed)
|
|
62605
62638
|
return { hex: null, opacity: 0 };
|
|
62606
62639
|
}
|
|
62607
62640
|
|
|
@@ -63085,33 +63118,30 @@
|
|
|
63085
63118
|
|
|
63086
63119
|
// src/image-processor.js
|
|
63087
63120
|
|
|
63088
|
-
async function getProcessedImage(src, targetW, targetH, radius) {
|
|
63121
|
+
async function getProcessedImage(src, targetW, targetH, radius, objectFit = 'fill', objectPosition = '50% 50%') {
|
|
63089
63122
|
return new Promise((resolve) => {
|
|
63090
63123
|
const img = new Image();
|
|
63091
|
-
img.crossOrigin = 'Anonymous';
|
|
63124
|
+
img.crossOrigin = 'Anonymous';
|
|
63092
63125
|
|
|
63093
63126
|
img.onload = () => {
|
|
63094
63127
|
const canvas = document.createElement('canvas');
|
|
63095
|
-
// Double resolution
|
|
63096
|
-
const scale = 2;
|
|
63128
|
+
const scale = 2; // Double resolution
|
|
63097
63129
|
canvas.width = targetW * scale;
|
|
63098
63130
|
canvas.height = targetH * scale;
|
|
63099
63131
|
const ctx = canvas.getContext('2d');
|
|
63100
63132
|
ctx.scale(scale, scale);
|
|
63101
63133
|
|
|
63102
|
-
// Normalize radius
|
|
63134
|
+
// Normalize radius
|
|
63103
63135
|
let r = { tl: 0, tr: 0, br: 0, bl: 0 };
|
|
63104
63136
|
if (typeof radius === 'number') {
|
|
63105
63137
|
r = { tl: radius, tr: radius, br: radius, bl: radius };
|
|
63106
63138
|
} else if (typeof radius === 'object' && radius !== null) {
|
|
63107
|
-
r = { ...r, ...radius };
|
|
63139
|
+
r = { ...r, ...radius };
|
|
63108
63140
|
}
|
|
63109
63141
|
|
|
63110
|
-
// 1. Draw
|
|
63142
|
+
// 1. Draw Mask
|
|
63111
63143
|
ctx.beginPath();
|
|
63112
|
-
|
|
63113
|
-
// Border Radius Clamping Logic (CSS Spec)
|
|
63114
|
-
// Prevents corners from overlapping if radii are too large for the container
|
|
63144
|
+
// ... (radius clamping logic remains the same) ...
|
|
63115
63145
|
const factor = Math.min(
|
|
63116
63146
|
targetW / (r.tl + r.tr) || Infinity,
|
|
63117
63147
|
targetH / (r.tr + r.br) || Infinity,
|
|
@@ -63120,13 +63150,9 @@
|
|
|
63120
63150
|
);
|
|
63121
63151
|
|
|
63122
63152
|
if (factor < 1) {
|
|
63123
|
-
r.tl *= factor;
|
|
63124
|
-
r.tr *= factor;
|
|
63125
|
-
r.br *= factor;
|
|
63126
|
-
r.bl *= factor;
|
|
63153
|
+
r.tl *= factor; r.tr *= factor; r.br *= factor; r.bl *= factor;
|
|
63127
63154
|
}
|
|
63128
63155
|
|
|
63129
|
-
// Draw path: Top-Left -> Top-Right -> Bottom-Right -> Bottom-Left
|
|
63130
63156
|
ctx.moveTo(r.tl, 0);
|
|
63131
63157
|
ctx.lineTo(targetW - r.tr, 0);
|
|
63132
63158
|
ctx.arcTo(targetW, 0, targetW, r.tr, r.tr);
|
|
@@ -63136,22 +63162,58 @@
|
|
|
63136
63162
|
ctx.arcTo(0, targetH, 0, targetH - r.bl, r.bl);
|
|
63137
63163
|
ctx.lineTo(0, r.tl);
|
|
63138
63164
|
ctx.arcTo(0, 0, r.tl, 0, r.tl);
|
|
63139
|
-
|
|
63140
63165
|
ctx.closePath();
|
|
63141
63166
|
ctx.fillStyle = '#000';
|
|
63142
63167
|
ctx.fill();
|
|
63143
63168
|
|
|
63144
|
-
// 2. Composite Source-In
|
|
63169
|
+
// 2. Composite Source-In
|
|
63145
63170
|
ctx.globalCompositeOperation = 'source-in';
|
|
63146
63171
|
|
|
63147
|
-
// 3. Draw Image
|
|
63172
|
+
// 3. Draw Image with Object Fit logic
|
|
63148
63173
|
const wRatio = targetW / img.width;
|
|
63149
63174
|
const hRatio = targetH / img.height;
|
|
63150
|
-
|
|
63151
|
-
|
|
63152
|
-
|
|
63153
|
-
|
|
63154
|
-
|
|
63175
|
+
let renderW, renderH;
|
|
63176
|
+
|
|
63177
|
+
if (objectFit === 'contain') {
|
|
63178
|
+
const fitScale = Math.min(wRatio, hRatio);
|
|
63179
|
+
renderW = img.width * fitScale;
|
|
63180
|
+
renderH = img.height * fitScale;
|
|
63181
|
+
} else if (objectFit === 'cover') {
|
|
63182
|
+
const coverScale = Math.max(wRatio, hRatio);
|
|
63183
|
+
renderW = img.width * coverScale;
|
|
63184
|
+
renderH = img.height * coverScale;
|
|
63185
|
+
} else if (objectFit === 'none') {
|
|
63186
|
+
renderW = img.width;
|
|
63187
|
+
renderH = img.height;
|
|
63188
|
+
} else if (objectFit === 'scale-down') {
|
|
63189
|
+
const scaleDown = Math.min(1, Math.min(wRatio, hRatio));
|
|
63190
|
+
renderW = img.width * scaleDown;
|
|
63191
|
+
renderH = img.height * scaleDown;
|
|
63192
|
+
} else {
|
|
63193
|
+
// 'fill' (default)
|
|
63194
|
+
renderW = targetW;
|
|
63195
|
+
renderH = targetH;
|
|
63196
|
+
}
|
|
63197
|
+
|
|
63198
|
+
// Handle Object Position (simplified parsing for "x% y%" or keywords)
|
|
63199
|
+
let posX = 0.5; // Default center
|
|
63200
|
+
let posY = 0.5;
|
|
63201
|
+
|
|
63202
|
+
const posParts = objectPosition.split(' ');
|
|
63203
|
+
if (posParts.length > 0) {
|
|
63204
|
+
const parsePos = (val) => {
|
|
63205
|
+
if (val === 'left' || val === 'top') return 0;
|
|
63206
|
+
if (val === 'center') return 0.5;
|
|
63207
|
+
if (val === 'right' || val === 'bottom') return 1;
|
|
63208
|
+
if (val.includes('%')) return parseFloat(val) / 100;
|
|
63209
|
+
return 0.5; // fallback
|
|
63210
|
+
};
|
|
63211
|
+
posX = parsePos(posParts[0]);
|
|
63212
|
+
posY = posParts.length > 1 ? parsePos(posParts[1]) : 0.5;
|
|
63213
|
+
}
|
|
63214
|
+
|
|
63215
|
+
const renderX = (targetW - renderW) * posX;
|
|
63216
|
+
const renderY = (targetH - renderH) * posY;
|
|
63155
63217
|
|
|
63156
63218
|
ctx.drawImage(img, renderX, renderY, renderW, renderH);
|
|
63157
63219
|
|
|
@@ -63207,34 +63269,37 @@
|
|
|
63207
63269
|
await processSlide(root, slide, pptx);
|
|
63208
63270
|
}
|
|
63209
63271
|
|
|
63210
|
-
|
|
63272
|
+
// 3. Font Embedding Logic
|
|
63211
63273
|
let finalBlob;
|
|
63212
63274
|
let fontsToEmbed = options.fonts || [];
|
|
63213
63275
|
|
|
63214
63276
|
if (options.autoEmbedFonts) {
|
|
63215
63277
|
// A. Scan DOM for used font families
|
|
63216
63278
|
const usedFamilies = getUsedFontFamilies(elements);
|
|
63217
|
-
|
|
63279
|
+
|
|
63218
63280
|
// B. Scan CSS for URLs matches
|
|
63219
63281
|
const detectedFonts = await getAutoDetectedFonts(usedFamilies);
|
|
63220
|
-
|
|
63282
|
+
|
|
63221
63283
|
// C. Merge (Avoid duplicates)
|
|
63222
|
-
const explicitNames = new Set(fontsToEmbed.map(f => f.name));
|
|
63284
|
+
const explicitNames = new Set(fontsToEmbed.map((f) => f.name));
|
|
63223
63285
|
for (const autoFont of detectedFonts) {
|
|
63224
|
-
|
|
63225
|
-
|
|
63226
|
-
|
|
63286
|
+
if (!explicitNames.has(autoFont.name)) {
|
|
63287
|
+
fontsToEmbed.push(autoFont);
|
|
63288
|
+
}
|
|
63227
63289
|
}
|
|
63228
|
-
|
|
63290
|
+
|
|
63229
63291
|
if (detectedFonts.length > 0) {
|
|
63230
|
-
|
|
63292
|
+
console.log(
|
|
63293
|
+
'Auto-detected fonts:',
|
|
63294
|
+
detectedFonts.map((f) => f.name)
|
|
63295
|
+
);
|
|
63231
63296
|
}
|
|
63232
63297
|
}
|
|
63233
63298
|
|
|
63234
63299
|
if (fontsToEmbed.length > 0) {
|
|
63235
63300
|
// Generate initial PPTX
|
|
63236
63301
|
const initialBlob = await pptx.write({ outputType: 'blob' });
|
|
63237
|
-
|
|
63302
|
+
|
|
63238
63303
|
// Load into Embedder
|
|
63239
63304
|
const zip = await JSZip.loadAsync(initialBlob);
|
|
63240
63305
|
const embedder = new PPTXEmbedFonts();
|
|
@@ -63246,7 +63311,7 @@
|
|
|
63246
63311
|
const response = await fetch(fontCfg.url);
|
|
63247
63312
|
if (!response.ok) throw new Error(`Failed to fetch ${fontCfg.url}`);
|
|
63248
63313
|
const buffer = await response.arrayBuffer();
|
|
63249
|
-
|
|
63314
|
+
|
|
63250
63315
|
// Infer type
|
|
63251
63316
|
const ext = fontCfg.url.split('.').pop().split(/[?#]/)[0].toLowerCase();
|
|
63252
63317
|
let type = 'ttf';
|
|
@@ -63385,11 +63450,8 @@
|
|
|
63385
63450
|
|
|
63386
63451
|
/**
|
|
63387
63452
|
* Optimized html2canvas wrapper
|
|
63388
|
-
*
|
|
63389
|
-
|
|
63390
|
-
/**
|
|
63391
|
-
* Optimized html2canvas wrapper
|
|
63392
|
-
* Includes fix for cropped icons by adjusting styles in the cloned document.
|
|
63453
|
+
* Fixes icon clipping by adding padding in the clone to capture font bleed,
|
|
63454
|
+
* then scaling the result to fit the original bounding box.
|
|
63393
63455
|
*/
|
|
63394
63456
|
async function elementToCanvasImage(node, widthPx, heightPx) {
|
|
63395
63457
|
return new Promise((resolve) => {
|
|
@@ -63405,51 +63467,76 @@
|
|
|
63405
63467
|
html2canvas(node, {
|
|
63406
63468
|
backgroundColor: null,
|
|
63407
63469
|
logging: false,
|
|
63408
|
-
scale: 3, //
|
|
63409
|
-
useCORS: true,
|
|
63470
|
+
scale: 3, // High resolution capture
|
|
63471
|
+
useCORS: true,
|
|
63410
63472
|
onclone: (clonedDoc) => {
|
|
63411
63473
|
const clonedNode = clonedDoc.getElementById(tempId);
|
|
63412
63474
|
if (clonedNode) {
|
|
63413
|
-
// --- FIX
|
|
63414
|
-
// 1. Force overflow visible so glyphs bleeding out aren't cut
|
|
63415
|
-
clonedNode.style.overflow = 'visible';
|
|
63416
|
-
|
|
63417
|
-
// 2. Adjust alignment for Icons to prevent baseline clipping
|
|
63418
|
-
// (Applies to <i>, <span>, or standard icon classes)
|
|
63475
|
+
// --- FIX FOR ICON CLIPPING ---
|
|
63419
63476
|
const tag = clonedNode.tagName;
|
|
63420
|
-
|
|
63421
|
-
|
|
63422
|
-
|
|
63423
|
-
|
|
63477
|
+
// Detect icons: I tags, SPAN tags, or elements with icon classes
|
|
63478
|
+
const isIcon =
|
|
63479
|
+
tag === 'I' ||
|
|
63480
|
+
tag === 'SPAN' ||
|
|
63481
|
+
clonedNode.className.includes('fa-') ||
|
|
63482
|
+
clonedNode.className.includes('icon');
|
|
63483
|
+
|
|
63484
|
+
if (isIcon) {
|
|
63485
|
+
// 1. Use inline-flex to center the glyph content
|
|
63486
|
+
clonedNode.style.display = 'flex';
|
|
63424
63487
|
clonedNode.style.justifyContent = 'center';
|
|
63425
63488
|
clonedNode.style.alignItems = 'center';
|
|
63426
63489
|
|
|
63427
|
-
//
|
|
63428
|
-
clonedNode.style.
|
|
63490
|
+
// 2. Reset constraints that might crop content
|
|
63491
|
+
clonedNode.style.overflow = 'visible';
|
|
63492
|
+
clonedNode.style.lineHeight = 'normal';
|
|
63493
|
+
|
|
63494
|
+
// 3. Add padding to the capture area.
|
|
63495
|
+
// This ensures parts of the glyph sticking out of the bounding box (ascenders/descenders)
|
|
63496
|
+
// are captured in the canvas instead of being cropped.
|
|
63497
|
+
// 'content-box' ensures padding adds to the total width/height.
|
|
63498
|
+
clonedNode.style.boxSizing = 'content-box';
|
|
63499
|
+
clonedNode.style.padding = '10px';
|
|
63500
|
+
|
|
63501
|
+
// 4. Ensure the content box itself matches the original size (minimum)
|
|
63502
|
+
// so the icon doesn't collapse.
|
|
63503
|
+
clonedNode.style.minWidth = `${width}px`;
|
|
63504
|
+
clonedNode.style.minHeight = `${height}px`;
|
|
63429
63505
|
|
|
63430
|
-
//
|
|
63431
|
-
clonedNode.style.
|
|
63432
|
-
clonedNode.style.verticalAlign = 'middle';
|
|
63506
|
+
// Clear margins that might displace the capture
|
|
63507
|
+
clonedNode.style.margin = '0';
|
|
63433
63508
|
}
|
|
63434
63509
|
}
|
|
63435
63510
|
},
|
|
63436
63511
|
})
|
|
63437
63512
|
.then((canvas) => {
|
|
63438
|
-
// Restore
|
|
63513
|
+
// Restore ID
|
|
63439
63514
|
if (originalId) node.id = originalId;
|
|
63440
63515
|
else node.removeAttribute('id');
|
|
63441
63516
|
|
|
63517
|
+
// Create destination canvas with the EXACT original dimensions
|
|
63442
63518
|
const destCanvas = document.createElement('canvas');
|
|
63443
63519
|
destCanvas.width = width;
|
|
63444
63520
|
destCanvas.height = height;
|
|
63445
63521
|
const ctx = destCanvas.getContext('2d');
|
|
63446
63522
|
|
|
63447
|
-
//
|
|
63448
|
-
//
|
|
63449
|
-
//
|
|
63450
|
-
|
|
63523
|
+
// --- SCALE TO FIT ---
|
|
63524
|
+
// The captured 'canvas' is now larger than 'destCanvas' because we added padding.
|
|
63525
|
+
// We draw the larger captured image into the smaller destination box.
|
|
63526
|
+
// This effectively "zooms out" slightly, ensuring the bleed is visible within the bounds.
|
|
63527
|
+
ctx.drawImage(
|
|
63528
|
+
canvas,
|
|
63529
|
+
0,
|
|
63530
|
+
0,
|
|
63531
|
+
canvas.width,
|
|
63532
|
+
canvas.height, // Source: Full captured size (with padding)
|
|
63533
|
+
0,
|
|
63534
|
+
0,
|
|
63535
|
+
width,
|
|
63536
|
+
height // Dest: Original requested size
|
|
63537
|
+
);
|
|
63451
63538
|
|
|
63452
|
-
// --- Border Radius Clipping (
|
|
63539
|
+
// --- Border Radius Clipping (Preserve existing logic) ---
|
|
63453
63540
|
let tl = parseFloat(style.borderTopLeftRadius) || 0;
|
|
63454
63541
|
let tr = parseFloat(style.borderTopRightRadius) || 0;
|
|
63455
63542
|
let br = parseFloat(style.borderBottomRightRadius) || 0;
|
|
@@ -63661,6 +63748,9 @@
|
|
|
63661
63748
|
}
|
|
63662
63749
|
}
|
|
63663
63750
|
|
|
63751
|
+
const objectFit = style.objectFit || 'fill'; // default CSS behavior is fill
|
|
63752
|
+
const objectPosition = style.objectPosition || '50% 50%';
|
|
63753
|
+
|
|
63664
63754
|
const item = {
|
|
63665
63755
|
type: 'image',
|
|
63666
63756
|
zIndex,
|
|
@@ -63669,7 +63759,14 @@
|
|
|
63669
63759
|
};
|
|
63670
63760
|
|
|
63671
63761
|
const job = async () => {
|
|
63672
|
-
const processed = await getProcessedImage(
|
|
63762
|
+
const processed = await getProcessedImage(
|
|
63763
|
+
node.src,
|
|
63764
|
+
widthPx,
|
|
63765
|
+
heightPx,
|
|
63766
|
+
radii,
|
|
63767
|
+
objectFit,
|
|
63768
|
+
objectPosition
|
|
63769
|
+
);
|
|
63673
63770
|
if (processed) item.options.data = processed;
|
|
63674
63771
|
else item.skip = true;
|
|
63675
63772
|
};
|
|
@@ -63769,16 +63866,45 @@
|
|
|
63769
63866
|
const isList = style.display === 'list-item';
|
|
63770
63867
|
if (isList) {
|
|
63771
63868
|
const fontSizePt = parseFloat(style.fontSize) * 0.75 * config.scale;
|
|
63772
|
-
const
|
|
63773
|
-
|
|
63774
|
-
|
|
63775
|
-
|
|
63776
|
-
|
|
63777
|
-
|
|
63778
|
-
|
|
63779
|
-
|
|
63780
|
-
|
|
63781
|
-
|
|
63869
|
+
const listStyleType = style.listStyleType || 'disc';
|
|
63870
|
+
const listStylePos = style.listStylePosition || 'outside';
|
|
63871
|
+
|
|
63872
|
+
let marker = null;
|
|
63873
|
+
|
|
63874
|
+
// 1. Determine the marker character based on list-style-type
|
|
63875
|
+
if (listStyleType !== 'none') {
|
|
63876
|
+
if (listStyleType === 'decimal') {
|
|
63877
|
+
// Calculate index for ordered lists (1., 2., etc.)
|
|
63878
|
+
const index = Array.prototype.indexOf.call(node.parentNode.children, node) + 1;
|
|
63879
|
+
marker = `${index}.`;
|
|
63880
|
+
} else if (listStyleType === 'circle') {
|
|
63881
|
+
marker = '○';
|
|
63882
|
+
} else if (listStyleType === 'square') {
|
|
63883
|
+
marker = '■';
|
|
63884
|
+
} else {
|
|
63885
|
+
marker = '•'; // Default to disc
|
|
63886
|
+
}
|
|
63887
|
+
}
|
|
63888
|
+
|
|
63889
|
+
// 2. Apply alignment and add marker
|
|
63890
|
+
if (marker) {
|
|
63891
|
+
// Only shift the text box to the left if the bullet is OUTSIDE the content box.
|
|
63892
|
+
// Tailwind 'list-inside' puts the bullet inside the box, so we must NOT shift X.
|
|
63893
|
+
if (listStylePos === 'outside') {
|
|
63894
|
+
const bulletShift = (parseFloat(style.fontSize) || 16) * PX_TO_INCH * config.scale * 1.5;
|
|
63895
|
+
x -= bulletShift;
|
|
63896
|
+
w += bulletShift;
|
|
63897
|
+
}
|
|
63898
|
+
|
|
63899
|
+
// Add the bullet + 3 spaces for visual separation
|
|
63900
|
+
textParts.push({
|
|
63901
|
+
text: marker + ' ',
|
|
63902
|
+
options: {
|
|
63903
|
+
color: parseColor(style.color).hex || '000000',
|
|
63904
|
+
fontSize: fontSizePt,
|
|
63905
|
+
},
|
|
63906
|
+
});
|
|
63907
|
+
}
|
|
63782
63908
|
}
|
|
63783
63909
|
|
|
63784
63910
|
node.childNodes.forEach((child, index) => {
|
|
@@ -63858,6 +63984,7 @@
|
|
|
63858
63984
|
}
|
|
63859
63985
|
|
|
63860
63986
|
if (textPayload) {
|
|
63987
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
63861
63988
|
items.push({
|
|
63862
63989
|
type: 'text',
|
|
63863
63990
|
zIndex: zIndex + 1,
|
|
@@ -63937,21 +64064,46 @@
|
|
|
63937
64064
|
|
|
63938
64065
|
if (hasShadow) shapeOpts.shadow = getVisibleShadow(shadowStr, config.scale);
|
|
63939
64066
|
|
|
63940
|
-
|
|
63941
|
-
const
|
|
63942
|
-
|
|
64067
|
+
// 1. Calculate dimensions first
|
|
64068
|
+
const minDimension = Math.min(widthPx, heightPx);
|
|
64069
|
+
|
|
64070
|
+
let rawRadius = parseFloat(style.borderRadius) || 0;
|
|
64071
|
+
const isPercentage = style.borderRadius && style.borderRadius.toString().includes('%');
|
|
64072
|
+
|
|
64073
|
+
// 2. Normalize radius to pixels
|
|
64074
|
+
let radiusPx = rawRadius;
|
|
64075
|
+
if (isPercentage) {
|
|
64076
|
+
radiusPx = (rawRadius / 100) * minDimension;
|
|
64077
|
+
}
|
|
63943
64078
|
|
|
63944
64079
|
let shapeType = pptx.ShapeType.rect;
|
|
63945
|
-
|
|
63946
|
-
|
|
64080
|
+
|
|
64081
|
+
// 3. Determine Shape Logic
|
|
64082
|
+
const isSquare = Math.abs(widthPx - heightPx) < 1;
|
|
64083
|
+
const isFullyRound = radiusPx >= minDimension / 2;
|
|
64084
|
+
|
|
64085
|
+
// CASE A: It is an Ellipse if:
|
|
64086
|
+
// 1. It is explicitly "50%" (standard CSS way to make ovals/circles)
|
|
64087
|
+
// 2. OR it is a perfect square and fully rounded (a circle)
|
|
64088
|
+
if (isFullyRound && (isPercentage || isSquare)) {
|
|
64089
|
+
shapeType = pptx.ShapeType.ellipse;
|
|
64090
|
+
}
|
|
64091
|
+
// CASE B: It is a Rounded Rectangle (including "Pill" shapes)
|
|
64092
|
+
else if (radiusPx > 0) {
|
|
63947
64093
|
shapeType = pptx.ShapeType.roundRect;
|
|
63948
|
-
|
|
64094
|
+
let r = radiusPx / minDimension;
|
|
64095
|
+
if (r > 0.5) r = 0.5;
|
|
64096
|
+
if (minDimension < 100) r = r * 0.25; // Small size adjustment for small shapes
|
|
64097
|
+
|
|
64098
|
+
shapeOpts.rectRadius = r;
|
|
63949
64099
|
}
|
|
63950
64100
|
|
|
63951
64101
|
if (textPayload) {
|
|
64102
|
+
textPayload.text[0].options.fontSize = Math.floor(textPayload.text[0]?.options?.fontSize)|| 12;
|
|
63952
64103
|
const textOptions = {
|
|
63953
64104
|
shape: shapeType,
|
|
63954
64105
|
...shapeOpts,
|
|
64106
|
+
rotate: rotation,
|
|
63955
64107
|
align: textPayload.align,
|
|
63956
64108
|
valign: textPayload.valign,
|
|
63957
64109
|
inset: textPayload.inset,
|