pptx-kit-preview 0.5.0 → 0.6.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.
@@ -30,7 +30,7 @@ const defaultMeasurer = (text, spec) => {
30
30
  const FALLBACK_ASCENT = .9;
31
31
  const FALLBACK_DESCENT = .22;
32
32
  const FALLBACK_LINEGAP = .08;
33
- const CENTER_ANCHOR_DROP = .036;
33
+ const BASELINE_LEADING_DROP = .036;
34
34
  const GRID_NUDGE_X = -.75;
35
35
  const specOf = (piece) => ({
36
36
  family: piece.family,
@@ -56,7 +56,7 @@ const fmt = (n) => {
56
56
  const r = Math.round(n * 100) / 100;
57
57
  return Object.is(r, -0) ? "0" : String(r);
58
58
  };
59
- const layoutTextSvg = (input, measure) => {
59
+ const layoutCore = (input, measure) => {
60
60
  const widthCache = /* @__PURE__ */ new Map();
61
61
  const metricCache = /* @__PURE__ */ new Map();
62
62
  const key = (text, s) => `${s.family}|${s.sizePx}|${s.bold}|${s.italic}|${s.letterSpacingPx}|${text}`;
@@ -170,11 +170,21 @@ const layoutTextSvg = (input, measure) => {
170
170
  line.textAnchor = "end";
171
171
  line.anchorX = wrapRight;
172
172
  }
173
- if (isFirst && bullet) line.bullet = {
174
- x: firstLeft,
175
- baselineDy: 0,
176
- b: bullet
177
- };
173
+ if (isFirst && bullet) {
174
+ let bulletX = firstLeft;
175
+ if (para.align === "center" || para.align === "right") {
176
+ let end = toks.length;
177
+ while (end > 0 && (toks[end - 1].isSpace || toks[end - 1].isBreak)) end--;
178
+ let lineW = 0;
179
+ for (let ti = 0; ti < end; ti++) if (!toks[ti].isBreak) lineW += toks[ti].width;
180
+ bulletX = (para.align === "right" ? wrapRight - lineW : (lineLeft + wrapRight) / 2 - lineW / 2) - (lineLeft - firstLeft);
181
+ }
182
+ line.bullet = {
183
+ x: bulletX,
184
+ baselineDy: 0,
185
+ b: bullet
186
+ };
187
+ }
178
188
  line.advance = lineAdvance(line, para);
179
189
  cursorY += line.advance;
180
190
  lines.push(line);
@@ -189,7 +199,7 @@ const layoutTextSvg = (input, measure) => {
189
199
  const vert = input.vert ?? "none";
190
200
  const cx = input.boxXpx + input.boxWpx / 2;
191
201
  const cy = input.boxYpx + input.boxHpx / 2;
192
- const frame = vert === "none" ? {
202
+ const frame = vert === "none" || vert === "upright" ? {
193
203
  x: input.boxXpx,
194
204
  y: input.boxYpx,
195
205
  w: input.boxWpx,
@@ -201,25 +211,43 @@ const layoutTextSvg = (input, measure) => {
201
211
  h: input.boxWpx
202
212
  };
203
213
  const columns = vert === "none" ? input.columns ?? null : null;
204
- const body = emitPlacements(columns && columns.count >= 2 ? placeColumns(frame, columns, input.anchor, buildLines) : placeSingle(frame, input.anchor, buildLines));
205
- if (vert === "none") return body;
214
+ const { placements, requiredH } = columns && columns.count >= 2 ? placeColumns(frame, columns, input.anchor, buildLines) : placeSingle(frame, input.anchor, buildLines);
215
+ return {
216
+ placements,
217
+ requiredH,
218
+ vert,
219
+ cx,
220
+ cy
221
+ };
222
+ };
223
+ const layoutTextSvg = (input, measure) => {
224
+ const { placements, vert, cx, cy } = layoutCore(input, measure);
225
+ const body = emitPlacements(placements);
226
+ if (vert === "none" || vert === "upright") return body;
206
227
  return `<g transform="rotate(${vert === "cw90" ? 90 : 270} ${fmt(cx)} ${fmt(cy)})">${body}</g>`;
207
228
  };
229
+ /** Content height (px) the body would occupy at the given input's font sizes —
230
+ * for a single column the block height, for multi-column the tallest filled
231
+ * column. The SVG normAutofit path uses this to pick a shrink scale. */
232
+ const measureTextBodyHeight = (input, measure) => layoutCore(input, measure).requiredH;
208
233
  const anchorOffsetY = (frameY, frameH, blockH, anchor, firstLine) => {
209
234
  let offsetY = frameY;
210
235
  if (anchor === "center") offsetY = frameY + (frameH - blockH) / 2;
211
236
  else if (anchor === "bottom") offsetY = frameY + (frameH - blockH);
212
- if ((anchor === "center" || anchor === "bottom") && firstLine) offsetY += CENTER_ANCHOR_DROP * (firstLine.ascent + firstLine.descent);
237
+ if (firstLine) offsetY += BASELINE_LEADING_DROP * (firstLine.ascent + firstLine.descent);
213
238
  return offsetY;
214
239
  };
215
240
  const placeSingle = (frame, anchor, buildLines) => {
216
241
  const { lines, blockH } = buildLines(frame.x, frame.x + frame.w);
217
242
  const offsetY = anchorOffsetY(frame.y, frame.h, blockH, anchor, lines[0]);
218
- return lines.map((line) => ({
219
- line,
220
- baselineY: offsetY + line.topY + topPad(line) + line.ascent,
221
- dx: 0
222
- }));
243
+ return {
244
+ placements: lines.map((line) => ({
245
+ line,
246
+ baselineY: offsetY + line.topY + topPad(line) + line.ascent,
247
+ dx: 0
248
+ })),
249
+ requiredH: blockH
250
+ };
223
251
  };
224
252
  const placeColumns = (frame, columns, anchor, buildLines) => {
225
253
  const gap = columns.gapPx;
@@ -227,30 +255,38 @@ const placeColumns = (frame, columns, anchor, buildLines) => {
227
255
  const { lines } = buildLines(frame.x, frame.x + colW);
228
256
  let col = 0;
229
257
  let colStartTopY = 0;
230
- const colHasLine = [];
258
+ let rowTopY = 0;
259
+ let curColHasLine = false;
231
260
  let tallest = 0;
232
261
  const placed = [];
233
262
  for (const line of lines) {
234
- const localBottom = line.topY - colStartTopY + line.advance;
235
- if (col < columns.count - 1 && localBottom > frame.h && colHasLine[col]) {
236
- col += 1;
263
+ if (line.topY - colStartTopY + line.ascent + line.descent > frame.h && curColHasLine) {
264
+ if (col + 1 < columns.count) col += 1;
265
+ else {
266
+ col = 0;
267
+ rowTopY += frame.h;
268
+ }
237
269
  colStartTopY = line.topY;
270
+ curColHasLine = false;
238
271
  }
239
- const localTopY = line.topY - colStartTopY;
272
+ const localTopY = rowTopY + (line.topY - colStartTopY);
240
273
  placed.push({
241
274
  line,
242
275
  localTopY,
243
276
  col
244
277
  });
245
- colHasLine[col] = true;
278
+ curColHasLine = true;
246
279
  if (localTopY + line.advance > tallest) tallest = localTopY + line.advance;
247
280
  }
248
281
  const offsetY = anchorOffsetY(frame.y, frame.h, tallest, anchor, lines[0]);
249
- return placed.map(({ line, localTopY, col: c }) => ({
250
- line,
251
- baselineY: offsetY + localTopY + topPad(line) + line.ascent,
252
- dx: c * (colW + gap)
253
- }));
282
+ return {
283
+ placements: placed.map(({ line, localTopY, col: c }) => ({
284
+ line,
285
+ baselineY: offsetY + localTopY + topPad(line) + line.ascent,
286
+ dx: c * (colW + gap)
287
+ })),
288
+ requiredH: tallest
289
+ };
254
290
  };
255
291
  const emitPlacements = (placements) => {
256
292
  const parts = [];
@@ -281,27 +317,76 @@ const emitLine = (line, baselineY, dx) => {
281
317
  while (toks.length > 0 && (toks[toks.length - 1].isSpace || toks[toks.length - 1].isBreak)) toks.pop();
282
318
  const content = toks.filter((t) => !t.isBreak);
283
319
  if (content.length === 0) return "";
284
- const tspans = groupTokens(content).map((g) => tspan(g)).join("");
320
+ const groups = groupTokens(content);
321
+ const tspans = groups.map((g) => tspan(g)).join("");
285
322
  if (tspans === "") return "";
286
- return `<text x="${fmt(line.anchorX + dx + GRID_NUDGE_X)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve">${tspans}</text>`;
323
+ const x0 = line.anchorX + dx + GRID_NUDGE_X;
324
+ return `<text x="${fmt(x0)}" y="${fmt(baselineY)}" text-anchor="${line.textAnchor}" xml:space="preserve">${tspans}</text>` + emitWavyUnderlines(groups, line.textAnchor, x0, baselineY);
287
325
  };
288
326
  const groupTokens = (toks) => {
289
327
  const groups = [];
290
328
  for (const t of toks) {
291
329
  if (t.isBreak) continue;
292
330
  const last = groups[groups.length - 1];
293
- if (last && samePiece(last.piece, t.piece)) last.text += t.text;
294
- else groups.push({
331
+ if (last && samePiece(last.piece, t.piece)) {
332
+ last.text += t.text;
333
+ last.width += t.width;
334
+ } else groups.push({
295
335
  text: t.text,
296
- piece: t.piece
336
+ piece: t.piece,
337
+ width: t.width
297
338
  });
298
339
  }
299
340
  return groups;
300
341
  };
342
+ const SUPERSCRIPT_SHIFT_RATIO = .33;
343
+ const SUBSCRIPT_SHIFT_RATIO = .16;
344
+ const baselineShiftPxOf = (p) => p.superSub === 1 ? p.sizePx * SUPERSCRIPT_SHIFT_RATIO : p.superSub === -1 ? -p.sizePx * SUBSCRIPT_SHIFT_RATIO : 0;
345
+ const SUPER_SUB_SIZE_RATIO = .65;
346
+ const renderedSizePxOf = (p) => p.superSub !== 0 ? p.sizePx * SUPER_SUB_SIZE_RATIO : p.sizePx;
347
+ const emitWavyUnderlines = (groups, textAnchor, x0, baselineY) => {
348
+ if (!groups.some((g) => g.piece.underline === "wavy")) return "";
349
+ const totalWidth = groups.reduce((sum, g) => sum + g.width, 0);
350
+ let cursor = textAnchor === "middle" ? x0 - totalWidth / 2 : textAnchor === "end" ? x0 - totalWidth : x0;
351
+ const parts = [];
352
+ for (const g of groups) {
353
+ if (g.piece.underline === "wavy" && g.width > 0) {
354
+ const y = baselineY - baselineShiftPxOf(g.piece);
355
+ parts.push(wavyPath(cursor, cursor + g.width, y, g.piece));
356
+ }
357
+ cursor += g.width;
358
+ }
359
+ return parts.join("");
360
+ };
361
+ const WAVY_AMPLITUDE_RATIO = .045;
362
+ const WAVY_AMPLITUDE_MIN_PX = .6;
363
+ const WAVY_PERIOD_RATIO = .18;
364
+ const WAVY_PERIOD_MIN_PX = 2;
365
+ const WAVY_BASELINE_OFFSET_RATIO = .12;
366
+ const WAVY_STROKE_WIDTH_RATIO = .06;
367
+ const WAVY_STROKE_WIDTH_MIN_PX = .6;
368
+ const wavyPath = (x1, x2, baselineY, piece) => {
369
+ const size = renderedSizePxOf(piece);
370
+ const amp = Math.max(WAVY_AMPLITUDE_MIN_PX, size * WAVY_AMPLITUDE_RATIO);
371
+ const period = Math.max(WAVY_PERIOD_MIN_PX, size * WAVY_PERIOD_RATIO);
372
+ const y = baselineY + size * WAVY_BASELINE_OFFSET_RATIO;
373
+ let d = `M${fmt(x1)} ${fmt(y)}`;
374
+ let cx = x1;
375
+ let up = true;
376
+ while (cx < x2 - .01) {
377
+ const half = Math.min(period / 2, x2 - cx);
378
+ const midX = cx + half;
379
+ d += ` Q${fmt(cx + half / 2)} ${fmt(y + (up ? -amp : amp))} ${fmt(midX)} ${fmt(y)}`;
380
+ cx = midX;
381
+ up = !up;
382
+ }
383
+ const strokeWidth = Math.max(WAVY_STROKE_WIDTH_MIN_PX, size * WAVY_STROKE_WIDTH_RATIO);
384
+ return `<path d="${d}" stroke="${piece.fillHex}" stroke-width="${fmt(strokeWidth)}" fill="none"/>`;
385
+ };
301
386
  const samePiece = (a, b) => a.family === b.family && a.sizePx === b.sizePx && a.bold === b.bold && a.italic === b.italic && a.letterSpacingPx === b.letterSpacingPx && a.fillHex === b.fillHex && a.underline === b.underline && a.strike === b.strike && a.superSub === b.superSub && a.href === b.href;
302
387
  const tspan = (g) => {
303
388
  const p = g.piece;
304
- const sizePx = p.superSub !== 0 ? p.sizePx * .65 : p.sizePx;
389
+ const sizePx = renderedSizePxOf(p);
305
390
  const attrs = [
306
391
  `font-family="${escapeXml$1(p.family)}"`,
307
392
  `font-size="${fmt(sizePx)}"`,
@@ -310,12 +395,11 @@ const tspan = (g) => {
310
395
  if (p.bold) attrs.push("font-weight=\"700\"");
311
396
  if (p.italic) attrs.push("font-style=\"italic\"");
312
397
  const deco = [];
313
- if (p.underline) deco.push("underline");
398
+ if (p.underline === "single") deco.push("underline");
314
399
  if (p.strike) deco.push("line-through");
315
400
  if (deco.length) attrs.push(`text-decoration="${deco.join(" ")}"`);
316
401
  if (p.letterSpacingPx !== 0) attrs.push(`letter-spacing="${fmt(p.letterSpacingPx)}"`);
317
- if (p.superSub === 1) attrs.push(`baseline-shift="${fmt(p.sizePx * .33)}"`);
318
- else if (p.superSub === -1) attrs.push(`baseline-shift="${fmt(-p.sizePx * .16)}"`);
402
+ if (p.superSub !== 0) attrs.push(`baseline-shift="${fmt(baselineShiftPxOf(p))}"`);
319
403
  return `<tspan ${attrs.join(" ")}>${escapeXml$1(g.text)}</tspan>`;
320
404
  };
321
405
  const wrapTokens = (tokens, wrap, firstAvail, avail) => {
@@ -348,8 +432,8 @@ const wrapTokens = (tokens, wrap, firstAvail, avail) => {
348
432
  continue;
349
433
  }
350
434
  const limit = first ? firstAvail : avail;
351
- const contentW = lineW - trailingSpaceW;
352
- if (wrap && contentW > 0 && contentW + tok.width > limit + .5) {
435
+ const hasContent = lineW - trailingSpaceW > 0;
436
+ if (wrap && hasContent && lineW + tok.width > limit + .5) {
353
437
  close();
354
438
  cur.push(tok);
355
439
  lineW = tok.width;
@@ -374,6 +458,7 @@ const PX_PER_PT = 96 / 72;
374
458
  const DEFAULT_BODY_PT = 18;
375
459
  const DEFAULT_TITLE_PT = 44;
376
460
  const DEFAULT_FONT = "Calibri, 'Helvetica Neue', Arial, sans-serif";
461
+ const DEFAULT_BULLET_FONT = "Arial";
377
462
  const DEFAULT_INSET_X = 91440;
378
463
  const DEFAULT_INSET_Y = 45720;
379
464
  const u8ToBase64 = (data) => {
@@ -585,59 +670,65 @@ const gradientDef = (grad, theme) => {
585
670
  fillAttr: `url(#${id})`
586
671
  };
587
672
  };
673
+ const ORTHO_TILE = 4;
674
+ const ORTHO_WIDE_TILE = 16;
675
+ const DIAG_TILE = 16;
676
+ const MOTIF_TILE = 8;
677
+ const PATTERN_TILE_SIZE = {
678
+ horz: ORTHO_TILE,
679
+ ltHorz: ORTHO_TILE,
680
+ narHorz: ORTHO_TILE,
681
+ dashHorz: ORTHO_TILE,
682
+ horzBrick: ORTHO_TILE,
683
+ dkHorz: ORTHO_TILE,
684
+ vert: ORTHO_TILE,
685
+ ltVert: ORTHO_TILE,
686
+ narVert: ORTHO_TILE,
687
+ dashVert: ORTHO_TILE,
688
+ dkVert: ORTHO_TILE,
689
+ ltHorzCross: ORTHO_TILE,
690
+ cross: ORTHO_TILE,
691
+ dotGrid: ORTHO_TILE,
692
+ smGrid: ORTHO_TILE,
693
+ dkHorzCross: ORTHO_WIDE_TILE,
694
+ lgGrid: ORTHO_WIDE_TILE,
695
+ plaid: ORTHO_WIDE_TILE,
696
+ wave: MOTIF_TILE,
697
+ zigZag: MOTIF_TILE,
698
+ weave: MOTIF_TILE,
699
+ divot: MOTIF_TILE,
700
+ sphere: MOTIF_TILE,
701
+ solidDmnd: MOTIF_TILE,
702
+ openDmnd: MOTIF_TILE
703
+ };
588
704
  const patternDef = (pat) => {
589
705
  const id = mintId();
590
706
  const fg = pat.foreground;
591
707
  const bg = pat.background;
592
708
  const preset = pat.preset;
593
709
  let body = "";
594
- const W = 8;
595
- const H = 8;
710
+ const PCT_DENSITY_FLOOR = .05;
711
+ const PCT_TILE_MIN = 6;
712
+ const pctMatch = /^pct(\d+)$/.exec(preset);
713
+ const pctDensity = pctMatch ? Math.min(100, Math.max(0, Number.parseInt(pctMatch[1], 10))) / 100 : null;
714
+ const W = pctDensity !== null ? Math.max(PCT_TILE_MIN, DIAG_TILE * 2 * Math.sqrt(PCT_DENSITY_FLOOR / Math.max(pctDensity, PCT_DENSITY_FLOOR))) : PATTERN_TILE_SIZE[preset] ?? DIAG_TILE;
715
+ const H = W;
596
716
  const stripe = (orientation, width = 1) => {
597
717
  if (orientation === "h") return `<path d="M0 ${H / 2}H${W}" stroke="${fg}" stroke-width="${width}"/>`;
598
718
  if (orientation === "v") return `<path d="M${W / 2} 0V${H}" stroke="${fg}" stroke-width="${width}"/>`;
599
719
  if (orientation === "d") return `<path d="M0 0L${W} ${H}" stroke="${fg}" stroke-width="${width}"/>`;
600
720
  return `<path d="M${W} 0L0 ${H}" stroke="${fg}" stroke-width="${width}"/>`;
601
721
  };
602
- const BAYER4 = [
603
- 0,
604
- 8,
605
- 2,
606
- 10,
607
- 12,
608
- 4,
609
- 14,
610
- 6,
611
- 3,
612
- 11,
613
- 1,
614
- 9,
615
- 15,
616
- 7,
617
- 13,
618
- 5
619
- ];
620
- const screen = (density) => {
621
- const threshold = density * 16;
622
- const out = [];
623
- for (let i = 0; i < 16; i++) if (BAYER4[i] < threshold) {
624
- const cx = i % 4 * 2;
625
- const cy = Math.floor(i / 4) * 2;
626
- out.push(`<rect x="${cx}" y="${cy}" width="2" height="2" fill="${fg}"/>`);
627
- }
628
- return out.join("");
629
- };
630
- const pctMatch = /^pct(\d+)$/.exec(preset);
631
- if (pctMatch) body = screen(Math.min(100, Math.max(0, Number.parseInt(pctMatch[1], 10))) / 100);
632
- else if (preset === "horzBrick" || preset === "ltHorizontal" || preset === "narHorz") body = stripe("h", .8);
633
- else if (preset === "dkHorizontal") body = stripe("h", 2);
634
- else if (preset === "ltVertical" || preset === "narVert") body = stripe("v", .8);
635
- else if (preset === "dkVertical") body = stripe("v", 2);
722
+ if (pctDensity !== null) body = pctDensity >= .3 ? stripe("d", .8) + stripe("a", .8) : stripe("d", .8);
723
+ else if (preset === "horz" || preset === "ltHorz" || preset === "narHorz" || preset === "dashHorz" || preset === "horzBrick") body = stripe("h", .8);
724
+ else if (preset === "dkHorz") body = stripe("h", 2);
725
+ else if (preset === "vert" || preset === "ltVert" || preset === "narVert" || preset === "dashVert") body = stripe("v", .8);
726
+ else if (preset === "dkVert") body = stripe("v", 2);
636
727
  else if (preset === "ltUpDiag" || preset === "wdUpDiag") body = stripe("d", .8);
637
728
  else if (preset === "dkUpDiag") body = stripe("d", 2);
638
729
  else if (preset === "ltDnDiag" || preset === "wdDnDiag") body = stripe("a", .8);
639
730
  else if (preset === "dkDnDiag") body = stripe("a", 2);
640
- else if (preset === "ltHorzCross" || preset === "smGrid" || preset === "cross") body = stripe("h", .8) + stripe("v", .8);
731
+ else if (preset === "ltHorzCross" || preset === "smGrid" || preset === "cross" || preset === "dotGrid") body = stripe("h", .8) + stripe("v", .8);
641
732
  else if (preset === "dkHorzCross" || preset === "lgGrid" || preset === "plaid") body = stripe("h", 2) + stripe("v", 2);
642
733
  else if (preset === "diagCross" || preset === "trellis" || preset === "shingle" || preset === "dashUpDiag" || preset === "dashDnDiag") body = stripe("d", .8) + stripe("a", .8);
643
734
  else if (preset === "dkUpDiagStripe" || preset === "dkDnDiagStripe") body = stripe(preset === "dkUpDiagStripe" ? "d" : "a", 2);
@@ -645,7 +736,7 @@ const patternDef = (pat) => {
645
736
  else if (preset === "weave" || preset === "divot") body = `<path d="M0 0L4 4 0 8M4 0L8 4 4 8" stroke="${fg}" stroke-width="0.8" fill="none"/>`;
646
737
  else if (preset === "sphere") body = `<circle cx="4" cy="4" r="3" fill="${fg}" fill-opacity="0.7"/>`;
647
738
  else if (preset === "solidDmnd" || preset === "openDmnd") body = `<path d="M4 1L7 4 4 7 1 4Z" fill="${preset === "solidDmnd" ? fg : "none"}" stroke="${fg}" stroke-width="0.6"/>`;
648
- else body = screen(.5);
739
+ else body = stripe("d", .8) + stripe("a", .8);
649
740
  return {
650
741
  defs: `<defs><pattern id="${id}" patternUnits="userSpaceOnUse" width="${W}" height="${H}"><rect width="${W}" height="${H}" fill="${bg}"/>${body}</pattern></defs>`,
651
742
  fillAttr: `url(#${id})`
@@ -829,42 +920,54 @@ const PRESET_POINTS = {
829
920
  star16: () => star(16),
830
921
  star24: () => star(24),
831
922
  star32: () => star(32),
832
- rightArrow: () => [
833
- [0, .3],
834
- [.65, .3],
835
- [.65, 0],
836
- [1, .5],
837
- [.65, 1],
838
- [.65, .7],
839
- [0, .7]
840
- ],
841
- leftArrow: () => [
842
- [1, .3],
843
- [.35, .3],
844
- [.35, 0],
845
- [0, .5],
846
- [.35, 1],
847
- [.35, .7],
848
- [1, .7]
849
- ],
850
- upArrow: () => [
851
- [.3, 1],
852
- [.3, .35],
853
- [0, .35],
854
- [.5, 0],
855
- [1, .35],
856
- [.7, .35],
857
- [.7, 1]
858
- ],
859
- downArrow: () => [
860
- [.3, 0],
861
- [.3, .65],
862
- [0, .65],
863
- [.5, 1],
864
- [1, .65],
865
- [.7, .65],
866
- [.7, 0]
867
- ],
923
+ rightArrow: (w, h) => {
924
+ const bx = 1 - Math.min(1, .5 * Math.min(w, h) / w);
925
+ return [
926
+ [0, .25],
927
+ [bx, .25],
928
+ [bx, 0],
929
+ [1, .5],
930
+ [bx, 1],
931
+ [bx, .75],
932
+ [0, .75]
933
+ ];
934
+ },
935
+ leftArrow: (w, h) => {
936
+ const bx = Math.min(1, .5 * Math.min(w, h) / w);
937
+ return [
938
+ [1, .25],
939
+ [bx, .25],
940
+ [bx, 0],
941
+ [0, .5],
942
+ [bx, 1],
943
+ [bx, .75],
944
+ [1, .75]
945
+ ];
946
+ },
947
+ upArrow: (w, h) => {
948
+ const by = Math.min(1, .5 * Math.min(w, h) / h);
949
+ return [
950
+ [.25, 1],
951
+ [.25, by],
952
+ [0, by],
953
+ [.5, 0],
954
+ [1, by],
955
+ [.75, by],
956
+ [.75, 1]
957
+ ];
958
+ },
959
+ downArrow: (w, h) => {
960
+ const by = 1 - Math.min(1, .5 * Math.min(w, h) / h);
961
+ return [
962
+ [.25, 0],
963
+ [.25, by],
964
+ [0, by],
965
+ [.5, 1],
966
+ [1, by],
967
+ [.75, by],
968
+ [.75, 0]
969
+ ];
970
+ },
868
971
  leftRightArrow: () => [
869
972
  [0, .5],
870
973
  [.18, .2],
@@ -1745,9 +1848,14 @@ const renderRun = (text, format, theme, effectivePt, _wasDefault = false) => {
1745
1848
  const strike = format?.strike;
1746
1849
  const hasUnderline = underline !== void 0 && underline !== false && underline !== "none";
1747
1850
  const hasStrike = strike !== void 0 && strike !== false && strike !== "noStrike";
1748
- if (hasUnderline && hasStrike) styles.push("text-decoration:underline line-through");
1749
- else if (hasUnderline) styles.push("text-decoration:underline");
1750
- else if (hasStrike) styles.push("text-decoration:line-through");
1851
+ const isWavyUnderline = typeof underline === "string" && underline.startsWith("wavy");
1852
+ const nestedWavyUnderline = hasUnderline && hasStrike && isWavyUnderline;
1853
+ if (nestedWavyUnderline) styles.push("text-decoration:line-through");
1854
+ else if (hasUnderline && hasStrike) styles.push("text-decoration:underline line-through");
1855
+ else if (hasUnderline) {
1856
+ styles.push("text-decoration:underline");
1857
+ if (isWavyUnderline) styles.push("text-decoration-style:wavy");
1858
+ } else if (hasStrike) styles.push("text-decoration:line-through");
1751
1859
  if (format?.color !== void 0 && format.color !== null) styles.push(`color:${resolveColor(format.color, theme, "#000000")}`);
1752
1860
  if (format?.spc !== void 0 && format.spc !== 0) {
1753
1861
  const trackingPx = format.spc / 100 * PX_PER_PT;
@@ -1762,13 +1870,18 @@ const renderRun = (text, format, theme, effectivePt, _wasDefault = false) => {
1762
1870
  else if (format?.cap === "small") styles.push("font-variant:small-caps");
1763
1871
  if (format?.highlight !== void 0 && format.highlight !== null) styles.push(`background-color:${resolveColor(format.highlight, theme, "#FFFF00")}`);
1764
1872
  const html = text.split("\n").map((part) => escapeXml(part)).join("<br/>");
1765
- return `<span style="${styles.join(";")}">${html}</span>`;
1873
+ const content = nestedWavyUnderline ? `<span style="text-decoration:underline;text-decoration-style:wavy">${html}</span>` : html;
1874
+ return `<span style="${styles.join(";")}">${content}</span>`;
1766
1875
  };
1767
1876
  const LINE_HEIGHT = 1.05;
1768
1877
  const AVG_GLYPH_W_RATIO = .55;
1769
- const hasUnderlineFmt = (fmt) => {
1878
+ const AUTOFIT_FLOOR = .25;
1879
+ const AUTOFIT_STEP = .05;
1880
+ const underlineStyleOf = (fmt) => {
1770
1881
  const u = fmt?.underline;
1771
- return u !== void 0 && u !== false && u !== "none";
1882
+ if (u === void 0 || u === false || u === "none") return "none";
1883
+ if (typeof u === "string" && u.startsWith("wavy")) return "wavy";
1884
+ return "single";
1772
1885
  };
1773
1886
  const hasStrikeFmt = (fmt) => {
1774
1887
  const s = fmt?.strike;
@@ -1778,15 +1891,15 @@ const alignOf = (a) => a === "center" || a === "right" || a === "justify" ? a :
1778
1891
  const verticalLayoutOf = (vert) => {
1779
1892
  switch (vert) {
1780
1893
  case "vert":
1781
- case "eaVert":
1894
+ case "eaVert": return "cw90";
1782
1895
  case "wordArtVert":
1783
- case "wordArtVertRtl": return "cw90";
1896
+ case "wordArtVertRtl": return "upright";
1784
1897
  case "vert270":
1785
1898
  case "mongolianVert": return "cw270";
1786
1899
  case null: return "none";
1787
1900
  }
1788
1901
  };
1789
- const buildAndLayoutSvgText = (a) => {
1902
+ const buildSvgTextInput = (a) => {
1790
1903
  const scale = a.autoFitScale;
1791
1904
  const paragraphs = a.paraData.map((para, pi) => {
1792
1905
  const pieces = [];
@@ -1800,7 +1913,7 @@ const buildAndLayoutSvgText = (a) => {
1800
1913
  const hlinkColor = a.theme ? normalizeHex(a.theme.hyperlink) : "#0563C1";
1801
1914
  fmt = {
1802
1915
  ...fmt,
1803
- color: fmt?.color ?? hlinkColor,
1916
+ color: hlinkColor,
1804
1917
  underline: fmt?.underline ?? true
1805
1918
  };
1806
1919
  }
@@ -1817,16 +1930,26 @@ const buildAndLayoutSvgText = (a) => {
1817
1930
  italic: fmt?.italic ?? false,
1818
1931
  letterSpacingPx,
1819
1932
  fillHex,
1820
- underline: hasUnderlineFmt(fmt),
1933
+ underline: underlineStyleOf(fmt),
1821
1934
  strike: hasStrikeFmt(fmt),
1822
1935
  superSub,
1823
1936
  href: run.href ?? null
1824
1937
  };
1825
1938
  const segs = run.text.split("\n");
1826
1939
  segs.forEach((seg, i) => {
1827
- pieces.push({
1940
+ const segText = caps ? seg.toUpperCase() : seg;
1941
+ if (a.vert === "upright") for (const g of Array.from(segText)) {
1942
+ const last = pieces[pieces.length - 1];
1943
+ if (last !== void 0 && !last.isBreak) pieces.push(breakPiece());
1944
+ pieces.push({
1945
+ ...base,
1946
+ text: g,
1947
+ isBreak: false
1948
+ });
1949
+ }
1950
+ else pieces.push({
1828
1951
  ...base,
1829
- text: caps ? seg.toUpperCase() : seg,
1952
+ text: segText,
1830
1953
  isBreak: false
1831
1954
  });
1832
1955
  if (i < segs.length - 1) pieces.push(breakPiece());
@@ -1858,7 +1981,7 @@ const buildAndLayoutSvgText = (a) => {
1858
1981
  fallbackSizePx: a.defaultPt * scale * PX_PER_PT
1859
1982
  };
1860
1983
  });
1861
- return layoutTextSvg({
1984
+ return {
1862
1985
  boxXpx: a.innerX / EMU_PER_PX,
1863
1986
  boxYpx: a.innerY / EMU_PER_PX,
1864
1987
  boxWpx: a.innerW / EMU_PER_PX,
@@ -1868,8 +1991,9 @@ const buildAndLayoutSvgText = (a) => {
1868
1991
  paragraphs,
1869
1992
  vert: a.vert,
1870
1993
  columns: a.columns
1871
- }, a.measure);
1994
+ };
1872
1995
  };
1996
+ const buildAndLayoutSvgText = (a) => layoutTextSvg(buildSvgTextInput(a), a.measure);
1873
1997
  const breakPiece = () => ({
1874
1998
  text: "",
1875
1999
  family: "",
@@ -1878,7 +2002,7 @@ const breakPiece = () => ({
1878
2002
  italic: false,
1879
2003
  letterSpacingPx: 0,
1880
2004
  fillHex: "#000000",
1881
- underline: false,
2005
+ underline: "none",
1882
2006
  strike: false,
1883
2007
  superSub: 0,
1884
2008
  href: null,
@@ -1889,17 +2013,52 @@ const buildBullet = (a, para, pi) => {
1889
2013
  const numberLabel = a.numberLabels[pi] ?? null;
1890
2014
  if (!(para.bulletStyle === "bullet" || explicitChar !== null || numberLabel !== null || para.bulletIsPicture || para.bulletStyle !== "none" && para.level > 0)) return null;
1891
2015
  const char = para.bulletIsPicture ? "■" : numberLabel ?? explicitChar ?? bulletChar(para.level);
1892
- const baseSizePx = a.defaultPt * PX_PER_PT * a.autoFitScale;
2016
+ const baseSizePx = (para.runs.find((r) => r.text !== "\n" && r.text !== "")?.sizePt ?? a.defaultPt) * PX_PER_PT * a.autoFitScale;
1893
2017
  const sizePx = para.bulletDetail.sizePct !== null ? baseSizePx * para.bulletDetail.sizePct : para.bulletDetail.sizePts !== null ? para.bulletDetail.sizePts * PX_PER_PT * a.autoFitScale : baseSizePx;
1894
2018
  const fillHex = para.bulletDetail.color ? resolveColor(para.bulletDetail.color, a.theme, "#000000") : a.defaultColor;
1895
2019
  return {
1896
2020
  text: char,
1897
- family: substituteFamily(para.bulletDetail.font ?? a.themeFace),
2021
+ family: substituteFamily(para.bulletDetail.font ?? DEFAULT_BULLET_FONT),
1898
2022
  sizePx,
1899
2023
  fillHex,
1900
2024
  ...para.bulletImageHref ? { imageHref: para.bulletImageHref } : {}
1901
2025
  };
1902
2026
  };
2027
+ const presetTextRect = (preset) => {
2028
+ switch (preset) {
2029
+ case "triangle": return {
2030
+ l: .25,
2031
+ t: .5,
2032
+ r: .75,
2033
+ b: 1
2034
+ };
2035
+ case "diamond": return {
2036
+ l: .25,
2037
+ t: .25,
2038
+ r: .75,
2039
+ b: .75
2040
+ };
2041
+ case "pentagon": return {
2042
+ l: .191,
2043
+ t: .236,
2044
+ r: .809,
2045
+ b: 1
2046
+ };
2047
+ case "star5": return {
2048
+ l: .309,
2049
+ t: .382,
2050
+ r: .691,
2051
+ b: .764
2052
+ };
2053
+ case "leftRightArrow": return {
2054
+ l: .18,
2055
+ t: .35,
2056
+ r: .82,
2057
+ b: .65
2058
+ };
2059
+ default: return null;
2060
+ }
2061
+ };
1903
2062
  const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
1904
2063
  let paragraphCount;
1905
2064
  try {
@@ -1937,11 +2096,33 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
1937
2096
  const tIns = margins.top ?? DEFAULT_INSET_Y;
1938
2097
  const rIns = margins.right ?? DEFAULT_INSET_X;
1939
2098
  const bIns = margins.bottom ?? DEFAULT_INSET_Y;
1940
- const innerX = bounds.x + lIns;
1941
- const innerY = bounds.y + tIns;
1942
- const innerW = Math.max(0, bounds.w - lIns - rIns);
1943
- const innerH = Math.max(0, bounds.h - tIns - bIns);
2099
+ const pRect = presetTextRect(getShapePreset(shape));
2100
+ const rectX = pRect ? bounds.x + pRect.l * bounds.w : bounds.x;
2101
+ const rectY = pRect ? bounds.y + pRect.t * bounds.h : bounds.y;
2102
+ const rectW = pRect ? (pRect.r - pRect.l) * bounds.w : bounds.w;
2103
+ const rectH = pRect ? (pRect.b - pRect.t) * bounds.h : bounds.h;
2104
+ let innerX = rectX + lIns;
2105
+ let innerY = rectY + tIns;
2106
+ let innerW = rectW - lIns - rIns;
2107
+ let innerH = rectH - tIns - bIns;
2108
+ if (pRect && (innerW <= 0 || innerH <= 0)) {
2109
+ innerX = rectX;
2110
+ innerY = rectY;
2111
+ innerW = rectW;
2112
+ innerH = rectH;
2113
+ }
1944
2114
  if (innerW <= 0 || innerH <= 0) return "";
2115
+ const svgTextRect = (v) => v === "none" || v === "upright" ? {
2116
+ x: innerX,
2117
+ y: innerY,
2118
+ w: innerW,
2119
+ h: innerH
2120
+ } : {
2121
+ x: bounds.x + tIns,
2122
+ y: bounds.y + lIns,
2123
+ w: Math.max(0, bounds.w - tIns - bIns),
2124
+ h: Math.max(0, bounds.h - lIns - rIns)
2125
+ };
1945
2126
  const paraData = [];
1946
2127
  let hasAnyText = false;
1947
2128
  for (let p = 0; p < paragraphCount; p++) {
@@ -2121,6 +2302,45 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2121
2302
  numberLabels[i] = formatAutoNum(num, counter);
2122
2303
  }
2123
2304
  }
2305
+ if (authoredAutofit && autoFitScale === 1) {
2306
+ const fitVert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
2307
+ const fitCols = getShapeTextColumns(shape);
2308
+ const fitColumns = fitVert === "none" && fitCols && fitCols.count >= 2 ? {
2309
+ count: fitCols.count,
2310
+ gapPx: fitCols.gapEmu !== void 0 ? fitCols.gapEmu / EMU_PER_PX : 12
2311
+ } : null;
2312
+ const fitRect = svgTextRect(fitVert);
2313
+ const fitBoxPx = (fitVert === "cw90" || fitVert === "cw270" ? fitRect.w : fitRect.h) / EMU_PER_PX;
2314
+ const fitArgsBase = {
2315
+ pres,
2316
+ shape,
2317
+ theme,
2318
+ paraData,
2319
+ numberLabels,
2320
+ lineHeightScale,
2321
+ defaultPt,
2322
+ themeFace,
2323
+ defaultColor: activeDeckTextColor,
2324
+ anchor: anchor === "center" || anchor === "bottom" ? anchor : "top",
2325
+ wrap: effectiveBody.wrap !== "none",
2326
+ innerX: fitRect.x,
2327
+ innerY: fitRect.y,
2328
+ innerW: fitRect.w,
2329
+ innerH: fitRect.h,
2330
+ measure: ctx.measure,
2331
+ vert: fitVert,
2332
+ columns: fitColumns
2333
+ };
2334
+ let s = 1;
2335
+ while (s > AUTOFIT_FLOOR) {
2336
+ if (measureTextBodyHeight(buildSvgTextInput({
2337
+ ...fitArgsBase,
2338
+ autoFitScale: s
2339
+ }), ctx.measure) <= fitBoxPx) break;
2340
+ s -= AUTOFIT_STEP;
2341
+ }
2342
+ autoFitScale = Math.max(AUTOFIT_FLOOR, s);
2343
+ }
2124
2344
  const paragraphs = [];
2125
2345
  for (let pi = 0; pi < paraData.length; pi++) {
2126
2346
  const para = paraData[pi];
@@ -2130,7 +2350,7 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2130
2350
  const hlinkColor = theme ? normalizeHex(theme.hyperlink) : "#0563C1";
2131
2351
  runFmt = {
2132
2352
  ...runFmt,
2133
- color: runFmt?.color ?? hlinkColor,
2353
+ color: hlinkColor,
2134
2354
  underline: runFmt?.underline ?? true
2135
2355
  };
2136
2356
  }
@@ -2162,8 +2382,8 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2162
2382
  const explicitChar = para.bulletStyle !== null && typeof para.bulletStyle === "object" && "char" in para.bulletStyle ? para.bulletStyle.char : null;
2163
2383
  const numberLabel = numberLabels[pi];
2164
2384
  const showBullet = para.bulletStyle === "bullet" || explicitChar !== null || numberLabel !== null || para.bulletIsPicture || para.bulletStyle !== "none" && para.level > 0;
2385
+ const baseBulletPx = (para.runs.find((r) => r.text !== "\n" && r.text !== "")?.sizePt ?? defaultPt) * PX_PER_PT * autoFitScale;
2165
2386
  if (showBullet && para.bulletImageHref) {
2166
- const baseBulletPx = defaultPt * PX_PER_PT * autoFitScale;
2167
2387
  const bulletPx = para.bulletDetail.sizePct !== null ? baseBulletPx * para.bulletDetail.sizePct : para.bulletDetail.sizePts !== null ? para.bulletDetail.sizePts * PX_PER_PT * autoFitScale : baseBulletPx;
2168
2388
  const imgStyles = [
2169
2389
  `width:${bulletPx.toFixed(2)}px`,
@@ -2179,7 +2399,8 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2179
2399
  if (para.bulletDetail.color) bulletStyles.push(`color:${resolveColor(para.bulletDetail.color, theme, "#000000")}`);
2180
2400
  if (para.bulletDetail.sizePct !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePct * 100).toFixed(1)}%`);
2181
2401
  else if (para.bulletDetail.sizePts !== null) bulletStyles.push(`font-size:${(para.bulletDetail.sizePts * PX_PER_PT * autoFitScale).toFixed(2)}px`);
2182
- if (para.bulletDetail.font) bulletStyles.push(`font-family:${escapeXml(para.bulletDetail.font)}, ${DEFAULT_FONT}`);
2402
+ else bulletStyles.push(`font-size:${baseBulletPx.toFixed(2)}px`);
2403
+ bulletStyles.push(para.bulletDetail.font ? `font-family:${escapeXml(para.bulletDetail.font)}, ${DEFAULT_FONT}` : `font-family:${DEFAULT_BULLET_FONT}, ${DEFAULT_FONT}`);
2183
2404
  prefix = `<span style="${bulletStyles.join(";")}">${escapeXml(char)}</span>`;
2184
2405
  }
2185
2406
  paragraphs.push(`<p style="${pStyles.join(";")}">${prefix}${runHtmls.join("") || "&#8203;"}</p>`);
@@ -2187,7 +2408,6 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2187
2408
  const justify = ANCHOR_TO_CSS[anchor] ?? "flex-start";
2188
2409
  const defaultColor = activeDeckTextColor;
2189
2410
  if (ctx.mode === "svg") {
2190
- const svgScale = authoredAutofit?.fontScale ?? 1;
2191
2411
  const svgLineScale = 1 - (authoredAutofit?.lnSpcReduction ?? 0);
2192
2412
  const svgVert = verticalLayoutOf(effectiveBody.vert ?? getShapeTextDirection(shape));
2193
2413
  const svgCols = getShapeTextColumns(shape);
@@ -2195,31 +2415,37 @@ const renderTextBody = (pres, shape, bounds, theme, phType, ctx) => {
2195
2415
  count: svgCols.count,
2196
2416
  gapPx: svgCols.gapEmu !== void 0 ? svgCols.gapEmu / EMU_PER_PX : 12
2197
2417
  } : null;
2198
- const svgInner = buildAndLayoutSvgText({
2418
+ const { x: vInnerX, y: vInnerY, w: vInnerW, h: vInnerH } = svgTextRect(svgVert);
2419
+ if (vInnerW <= 0 || vInnerH <= 0) return "";
2420
+ const svgArgsBase = {
2199
2421
  pres,
2200
2422
  shape,
2201
2423
  theme,
2202
2424
  paraData,
2203
2425
  numberLabels,
2204
- autoFitScale: svgScale,
2205
2426
  lineHeightScale: svgLineScale,
2206
2427
  defaultPt,
2207
2428
  themeFace,
2208
2429
  defaultColor,
2209
2430
  anchor: anchor === "center" || anchor === "bottom" ? anchor : "top",
2210
2431
  wrap: effectiveBody.wrap !== "none",
2211
- innerX,
2212
- innerY,
2213
- innerW,
2214
- innerH,
2432
+ innerX: vInnerX,
2433
+ innerY: vInnerY,
2434
+ innerW: vInnerW,
2435
+ innerH: vInnerH,
2215
2436
  measure: ctx.measure,
2216
2437
  vert: svgVert,
2217
2438
  columns: svgColumns
2439
+ };
2440
+ const svgScale = authoredAutofit ? autoFitScale : 1;
2441
+ const svgInner = buildAndLayoutSvgText({
2442
+ ...svgArgsBase,
2443
+ autoFitScale: svgScale
2218
2444
  });
2219
2445
  const bodyRotDegSvg = getShapeTextBodyRotationDeg(shape);
2220
2446
  if (bodyRotDegSvg !== null && bodyRotDegSvg !== 0) {
2221
- const pivotX = innerX + innerW / 2;
2222
- const pivotY = innerY + innerH / 2;
2447
+ const pivotX = vInnerX + vInnerW / 2;
2448
+ const pivotY = vInnerY + vInnerH / 2;
2223
2449
  return `<g transform="rotate(${bodyRotDegSvg} ${E(pivotX)} ${E(pivotY)})">${svgInner}</g>`;
2224
2450
  }
2225
2451
  return svgInner;
@@ -2272,15 +2498,16 @@ const accentSequence = (theme) => {
2272
2498
  ].map((c) => normalizeHex(c)).filter((c) => /^#[0-9A-Fa-f]{6}$/.test(c));
2273
2499
  return hexes.length > 0 ? hexes : fallbacks;
2274
2500
  };
2275
- const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = false, legendOverlay = false, hasLegend = true) => {
2501
+ const DEFAULT_CHART_TITLE_PT = 13;
2502
+ const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = false, legendOverlay = false, hasLegend = true, titlePx = DEFAULT_CHART_TITLE_PT * PX_PER_PT) => {
2276
2503
  const x = xEmu / EMU_PER_PX;
2277
2504
  const y = yEmu / EMU_PER_PX;
2278
2505
  const w = wEmu / EMU_PER_PX;
2279
2506
  const h = hEmu / EMU_PER_PX;
2280
- const titleStrip = hasTitle && !titleOverlay ? 18 : 0;
2507
+ const titleStrip = hasTitle && !titleOverlay ? Math.round(titlePx * 2.4) : 0;
2281
2508
  const legendStrip = hasLegend && !legendOverlay ? 18 : 0;
2282
2509
  const padding = 8;
2283
- const yAxisGutter = hasAxes ? 40 : 0;
2510
+ const yAxisGutter = hasAxes ? 32 : 0;
2284
2511
  const xAxisGutter = hasAxes ? 18 : 0;
2285
2512
  return {
2286
2513
  x,
@@ -2291,7 +2518,7 @@ const layoutChart = (xEmu, yEmu, wEmu, hEmu, hasTitle, hasAxes, titleOverlay = f
2291
2518
  plotY: y + titleStrip + padding,
2292
2519
  plotW: Math.max(0, w - 2 * padding - yAxisGutter),
2293
2520
  plotH: Math.max(0, h - titleStrip - legendStrip - xAxisGutter - 2 * padding),
2294
- titleY: y + (titleOverlay ? 14 : titleStrip - 2),
2521
+ titleY: y + (titleOverlay ? 14 : padding + titlePx * .7),
2295
2522
  legendY: y + h - (legendOverlay ? 18 : legendStrip / 2)
2296
2523
  };
2297
2524
  };
@@ -2379,12 +2606,13 @@ const DISPLAY_UNIT_LABEL = {
2379
2606
  const DEFAULT_AXIS_COLOR = "#000000";
2380
2607
  const DEFAULT_GRID_COLOR = "#D9D9D9";
2381
2608
  const AXIS_TICK_LEN = 5;
2609
+ const chartFontPx = (sizePt) => (sizePt * PX_PER_PT).toFixed(1);
2382
2610
  const axisTickAttrs = (style) => {
2383
2611
  const sz = style?.sizePt ?? 10;
2384
2612
  const fill = style?.color ?? "#6B7280";
2385
2613
  const weight = style?.bold ? " font-weight=\"600\"" : "";
2386
2614
  const italic = style?.italic ? " font-style=\"italic\"" : "";
2387
- return `font-family="sans-serif" font-size="${sz.toFixed(1)}" fill="${fill}"${weight}${italic}`;
2615
+ return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}"${weight}${italic}`;
2388
2616
  };
2389
2617
  const renderValueAxis = (f, axis) => {
2390
2618
  const ticks = axis.majorUnit ? (() => {
@@ -2537,11 +2765,11 @@ const seriesMinMax = (spec) => {
2537
2765
  };
2538
2766
  const renderChartTitle = (f, title, style) => {
2539
2767
  if (!title) return "";
2540
- const sz = style?.sizePt ?? 13;
2768
+ const sz = style?.sizePt ?? DEFAULT_CHART_TITLE_PT;
2541
2769
  const fill = style?.color ?? "#1F2937";
2542
2770
  const weight = style?.bold === false ? "400" : "600";
2543
2771
  const fontStyleAttr = style?.italic ? " font-style=\"italic\"" : "";
2544
- return `<text x="${px(f.x + f.w / 2)}" y="${px(f.titleY)}" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="${sz.toFixed(1)}" fill="${fill}" font-weight="${weight}"${fontStyleAttr}>${escapeXml(title)}</text>`;
2772
+ return `<text x="${px(f.x + f.w / 2)}" y="${px(f.titleY)}" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${fontStyleAttr}>${escapeXml(title)}</text>`;
2545
2773
  };
2546
2774
  const renderChartLegend = (f, names, colors, position = "b", textStyle, markerSymbols) => {
2547
2775
  if (names.length === 0) return "";
@@ -2549,7 +2777,7 @@ const renderChartLegend = (f, names, colors, position = "b", textStyle, markerSy
2549
2777
  const fill = textStyle?.color ?? "#374151";
2550
2778
  const weight = textStyle?.bold ? " font-weight=\"600\"" : "";
2551
2779
  const italic = textStyle?.italic ? " font-style=\"italic\"" : "";
2552
- const textAttrs = `font-family="sans-serif" font-size="${sz.toFixed(1)}" fill="${fill}"${weight}${italic}`;
2780
+ const textAttrs = `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}"${weight}${italic}`;
2553
2781
  const swatch = (i, swatchX, swatchY) => {
2554
2782
  const color = colors[i % colors.length];
2555
2783
  const sym = markerSymbols?.[i];
@@ -2836,7 +3064,7 @@ const dataLabelTextAttrs = (spec, seriesIdx, fallbackFill, fallbackSizePt = 9, f
2836
3064
  const fill = style?.color ?? fallbackFill;
2837
3065
  const weight = style?.bold ?? fallbackBold ? " font-weight=\"600\"" : "";
2838
3066
  const italic = style?.italic ? " font-style=\"italic\"" : "";
2839
- return `font-family="sans-serif" font-size="${sz.toFixed(1)}" fill="${fill}"${weight}${italic}`;
3067
+ return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}"${weight}${italic}`;
2840
3068
  };
2841
3069
  const renderBarChart = (f, spec, colors) => {
2842
3070
  const N = pointCount(spec);
@@ -2930,11 +3158,13 @@ const renderLineChart = (f, spec, colors, fill) => {
2930
3158
  const isPercent = grouping === "percentStacked";
2931
3159
  const { min, max } = seriesMinMax(spec);
2932
3160
  const range = max - min || 1;
2933
- const step = N > 1 ? f.plotW / (N - 1) : 0;
3161
+ const band = N > 1 ? fill ? f.plotW / (N - 1) : f.plotW / N : 0;
3162
+ const xAt = (c) => fill ? f.plotX + c * band : f.plotX + (c + .5) * band;
2934
3163
  const baseY = f.plotY + f.plotH - (0 - min) / range * f.plotH;
2935
3164
  const out = [];
2936
3165
  out.push(`<line x1="${px(f.plotX)}" y1="${px(baseY)}" x2="${px(f.plotX + f.plotW)}" y2="${px(baseY)}" stroke="#E5E7EB" stroke-width="0.5"/>`);
2937
3166
  const accumulated = Array.from({ length: N }, () => 0);
3167
+ const perSeries = [];
2938
3168
  for (let s = 0; s < spec.series.length; s++) {
2939
3169
  const series = spec.series[s];
2940
3170
  if (!series) continue;
@@ -2943,7 +3173,7 @@ const renderLineChart = (f, spec, colors, fill) => {
2943
3173
  const ptsRaw = [];
2944
3174
  const basePtsRaw = [];
2945
3175
  for (let c = 0; c < N; c++) {
2946
- const xp = f.plotX + c * step;
3176
+ const xp = xAt(c);
2947
3177
  const rawV = series.values[c];
2948
3178
  const isNullish = rawV === null || rawV === void 0 || !Number.isFinite(rawV);
2949
3179
  let v;
@@ -2982,10 +3212,22 @@ const renderLineChart = (f, spec, colors, fill) => {
2982
3212
  }
2983
3213
  return path.trim();
2984
3214
  })();
2985
- if (fill) {
2986
- const back = basePts.slice().reverse().map(([xp, yp]) => `L${px(xp)},${px(yp)}`).join(" ");
2987
- out.push(`<path d="${dPath} ${back} Z" fill="${color}" stroke="none"/>`);
2988
- }
3215
+ perSeries.push({
3216
+ s,
3217
+ series,
3218
+ color,
3219
+ ptsRaw,
3220
+ pts,
3221
+ basePts,
3222
+ dPath
3223
+ });
3224
+ }
3225
+ const paintOrder = fill && !isStacked ? perSeries.slice().reverse() : perSeries;
3226
+ if (fill) for (const { color, basePts, dPath } of paintOrder) {
3227
+ const back = basePts.slice().reverse().map(([xp, yp]) => `L${px(xp)},${px(yp)}`).join(" ");
3228
+ out.push(`<path d="${dPath} ${back} Z" fill="${color}" stroke="none"/>`);
3229
+ }
3230
+ for (const { s, series, color, ptsRaw, pts, dPath } of paintOrder) {
2989
3231
  const lineWPx = series.lineWidthEmu ? Math.max(.3, series.lineWidthEmu / EMU_PER_PX) : 1.5;
2990
3232
  const dashAttr = series.lineDash ? (() => {
2991
3233
  const sw = lineWPx;
@@ -3058,7 +3300,7 @@ const renderLineChart = (f, spec, colors, fill) => {
3058
3300
  }
3059
3301
  }
3060
3302
  if (!isStacked && (spec.dropLines || spec.hiLowLines) && spec.series.length > 0) for (let c = 0; c < N; c++) {
3061
- const xp = f.plotX + c * step;
3303
+ const xp = xAt(c);
3062
3304
  if (spec.dropLines) {
3063
3305
  const firstVal = spec.series[0]?.values[c];
3064
3306
  if (firstVal !== null && firstVal !== void 0 && Number.isFinite(firstVal)) {
@@ -3136,12 +3378,12 @@ const renderPieChart = (f, spec, colors, doughnut) => {
3136
3378
  const labelX = sx + labelR * Math.cos(labelMid);
3137
3379
  const labelY = sy + labelR * Math.sin(labelMid);
3138
3380
  const labels = [];
3139
- if (spec.dataLabels?.showValue) labels.push(formatDataLabelValue(spec, 0, v));
3140
- if (spec.dataLabels?.showPercent) labels.push(`${(v / total * 100).toFixed(0)}%`);
3141
3381
  if (spec.dataLabels?.showCategory) {
3142
3382
  const catLabel = spec.categories[i];
3143
3383
  if (catLabel) labels.push(catLabel);
3144
3384
  }
3385
+ if (spec.dataLabels?.showValue) labels.push(formatDataLabelValue(spec, 0, v));
3386
+ if (spec.dataLabels?.showPercent) labels.push(`${(v / total * 100).toFixed(0)}%`);
3145
3387
  if (labels.length > 0) out.push(`<text x="${px(labelX)}" y="${px(labelY)}" text-anchor="middle" dominant-baseline="middle" ${dataLabelTextAttrs(spec, 0, labelFill, 10, true)}>${escapeXml(labels.join(spec.series[0]?.dataLabels?.separator ?? spec.dataLabels?.separator ?? " "))}</text>`);
3146
3388
  }
3147
3389
  return out.join("");
@@ -3363,7 +3605,7 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
3363
3605
  const isCartesian = spec.kind === "column" || spec.kind === "bar" || spec.kind === "line" || spec.kind === "area";
3364
3606
  const hasAxes = isCartesian || spec.kind === "scatter" || spec.kind === "bubble";
3365
3607
  const hasLegend = spec.legend !== void 0 && spec.legend.position !== null;
3366
- const f = layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend);
3608
+ const f = layoutChart(x, y, w, h, !!spec.title, hasAxes, spec.titleOverlay ?? false, spec.legend?.overlay ?? false, hasLegend, (spec.titleStyle?.sizePt ?? DEFAULT_CHART_TITLE_PT) * PX_PER_PT);
3367
3609
  const allNamesForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? Array.from(spec.categories) : spec.series.map((s) => s.name);
3368
3610
  const allColorsForLegend = spec.kind === "pie" || spec.kind === "doughnut" ? spec.categories.map((_, i) => spec.series[0]?.pointColors?.[i] ?? spec.series[0]?.color ?? colors[i % colors.length] ?? "#888") : spec.series.map((s, i) => s.color ?? colors[i % colors.length] ?? "#888");
3369
3611
  const hiddenSet = new Set(spec.legend?.hiddenIndices ?? []);
@@ -3443,13 +3685,13 @@ const renderChart = (shape, x, y, w, h, transform, theme) => {
3443
3685
  const fill = style?.color ?? "#374151";
3444
3686
  const weight = style?.bold === false ? "400" : "600";
3445
3687
  const italicAttr = style?.italic ? " font-style=\"italic\"" : "";
3446
- return `font-family="sans-serif" font-size="${sz.toFixed(1)}" fill="${fill}" font-weight="${weight}"${italicAttr}`;
3688
+ return `font-family="sans-serif" font-size="${chartFontPx(sz)}" fill="${fill}" font-weight="${weight}"${italicAttr}`;
3447
3689
  };
3448
3690
  const valueAxisTitleRot = spec.valueAxisTitleRotationDeg ?? -90;
3449
3691
  const valueAxisTitleSvg = spec.valueAxisTitle ? `<text x="${px(f.plotX - 26)}" y="${px(f.plotY + f.plotH / 2)}" text-anchor="middle" ${axisTitleAttrs(spec.valueAxisTitleStyle)} transform="rotate(${valueAxisTitleRot} ${px(f.plotX - 26)} ${px(f.plotY + f.plotH / 2)})">${escapeXml(spec.valueAxisTitle)}</text>` : "";
3450
3692
  const catTitleRot = spec.categoryAxisTitleRotationDeg ?? 0;
3451
3693
  const catTitleCx = f.plotX + f.plotW / 2;
3452
- const catTitleCy = f.plotY + f.plotH + 16;
3694
+ const catTitleCy = f.plotY + f.plotH + 28;
3453
3695
  const catTitleTransform = catTitleRot !== 0 ? ` transform="rotate(${catTitleRot} ${px(catTitleCx)} ${px(catTitleCy)})"` : "";
3454
3696
  const categoryAxisTitleSvg = spec.categoryAxisTitle ? `<text x="${px(catTitleCx)}" y="${px(catTitleCy)}" text-anchor="middle" ${axisTitleAttrs(spec.categoryAxisTitleStyle)}${catTitleTransform}>${escapeXml(spec.categoryAxisTitle)}</text>` : "";
3455
3697
  return [
@@ -3584,7 +3826,8 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
3584
3826
  const flags = getTableStyleFlags(shape);
3585
3827
  const accent = theme ? normalizeHex(theme.accent1) : "#4472C4";
3586
3828
  const headerFill = accent;
3587
- const bandFill = mixHex(accent, "#FFFFFF", .92);
3829
+ const bandFill = mixHex(accent, "#FFFFFF", .12);
3830
+ const bandFill2 = mixHex(accent, "#FFFFFF", .27);
3588
3831
  const textColor = activeDeckTextColor;
3589
3832
  const tableThemeFace = getPresentationFonts(pres)?.minorLatin ?? null;
3590
3833
  const out = [];
@@ -3610,8 +3853,8 @@ const renderTable = (shape, pres, x, y, w, h, transform, theme, ctx) => {
3610
3853
  else if (flags.lastRow && r === dims.rows - 1) resolvedFill = headerFill;
3611
3854
  else if (flags.firstCol && c === 0) resolvedFill = bandFill;
3612
3855
  else if (flags.lastCol && c === dims.cols - 1) resolvedFill = bandFill;
3613
- else if (flags.bandRow && r % 2 === (flags.firstRow ? 0 : 1)) resolvedFill = bandFill;
3614
- else if (flags.bandCol && c % 2 === (flags.firstCol ? 0 : 1)) resolvedFill = bandFill;
3856
+ else if (flags.bandRow) resolvedFill = (r - (flags.firstRow ? 1 : 0)) % 2 === 0 ? bandFill : bandFill2;
3857
+ else if (flags.bandCol) resolvedFill = (c - (flags.firstCol ? 1 : 0)) % 2 === 0 ? bandFill : bandFill2;
3615
3858
  else resolvedFill = "none";
3616
3859
  const cellTextColor = resolvedFill === headerFill ? "#FFFFFF" : textColor;
3617
3860
  out.push(`<rect x="${px(cx)}" y="${px(cy)}" width="${px(cw)}" height="${px(ch)}" fill="${resolvedFill}"/>`);
@@ -3794,7 +4037,8 @@ const renderShape = (shape, pres, theme, ctx) => {
3794
4037
  if (flip.horizontal) transforms.push(`translate(${E(2 * cx)} 0) scale(-1 1)`);
3795
4038
  if (flip.vertical) transforms.push(`translate(0 ${E(2 * cy)}) scale(1 -1)`);
3796
4039
  const transform = transforms.length > 0 ? ` transform="${transforms.join(" ")}"` : "";
3797
- const textTransform = rotation !== 0 ? ` transform="rotate(${rotation} ${E(cx)} ${E(cy)})"` : "";
4040
+ const textRotation = (flip.vertical ? rotation + 180 : rotation) % 360;
4041
+ const textTransform = textRotation !== 0 ? ` transform="rotate(${textRotation} ${E(cx)} ${E(cy)})"` : "";
3798
4042
  const textOverlay = kind === "shape" || kind === "graphicFrame" ? renderTextBody(pres, shape, {
3799
4043
  x,
3800
4044
  y,
@@ -3818,11 +4062,14 @@ const renderShape = (shape, pres, theme, ctx) => {
3818
4062
  y1 = y + h;
3819
4063
  y2 = y;
3820
4064
  }
4065
+ const connectorTransform = rotation !== 0 ? ` transform="rotate(${rotation} ${E(cx)} ${E(cy)})"` : "";
3821
4066
  const strokeColor = p.stroke === "none" ? resolveColor("scheme:tx1", theme, "#1F2937") : p.stroke;
3822
4067
  const sa = p.strokeAttrs ? ` ${p.strokeAttrs}` : "";
3823
4068
  const ma = p.markerAttrs ?? "";
4069
+ const capDefault = sa.includes("stroke-linecap") ? "" : " stroke-linecap=\"round\"";
4070
+ const joinDefault = sa.includes("stroke-linejoin") ? "" : " stroke-linejoin=\"round\"";
3824
4071
  const preset = getShapePreset(shape) ?? "line";
3825
- if (preset === "straightConnector1" || preset === "line") return `${p.defs}<line x1="${E(x1)}" y1="${E(y1)}" x2="${E(x2)}" y2="${E(y2)}" stroke="${strokeColor}" stroke-width="${E(sw)}" stroke-linecap="round"${sa}${ma}${transform}/>`;
4072
+ if (preset === "straightConnector1" || preset === "line") return `${p.defs}<line x1="${E(x1)}" y1="${E(y1)}" x2="${E(x2)}" y2="${E(y2)}" stroke="${strokeColor}" stroke-width="${E(sw)}"${capDefault}${sa}${ma}${connectorTransform}/>`;
3826
4073
  const px1 = x1 / EMU_PER_PX;
3827
4074
  const py1 = y1 / EMU_PER_PX;
3828
4075
  const px2 = x2 / EMU_PER_PX;
@@ -3847,7 +4094,7 @@ const renderShape = (shape, pres, theme, ctx) => {
3847
4094
  const c2x = px1 + 2 * (px2 - px1) / 3;
3848
4095
  d += ` C${c1x.toFixed(2)} ${py1.toFixed(2)} ${c2x.toFixed(2)} ${py2.toFixed(2)} ${px2.toFixed(2)} ${py2.toFixed(2)}`;
3849
4096
  } else d += ` L${px2.toFixed(2)} ${py2.toFixed(2)}`;
3850
- return `${p.defs}<path d="${d}" fill="none" stroke="${strokeColor}" stroke-width="${E(sw)}" stroke-linecap="round" stroke-linejoin="round"${sa}${ma}${transform}/>`;
4097
+ return `${p.defs}<path d="${d}" fill="none" stroke="${strokeColor}" stroke-width="${E(sw)}"${capDefault}${joinDefault}${sa}${ma}${connectorTransform}/>`;
3851
4098
  }
3852
4099
  if (kind === "group") {
3853
4100
  const xform = getGroupTransform(shape);
@@ -3923,7 +4170,7 @@ const renderShape = (shape, pres, theme, ctx) => {
3923
4170
  if (pathFn) geomSvg = `<path d="${pathFn(x / EMU_PER_PX, y / EMU_PER_PX, w / EMU_PER_PX, h / EMU_PER_PX)}" fill="${p.fill}" stroke="${p.stroke}" stroke-width="${E(p.strokeWidth)}" fill-rule="evenodd"${sa}${ma}/>`;
3924
4171
  else {
3925
4172
  const pointsFn = PRESET_POINTS[preset];
3926
- if (pointsFn) geomSvg = `<polygon points="${pointsFn().map(([nx, ny]) => `${E(x + nx * w)},${E(y + ny * h)}`).join(" ")}" fill="${p.fill}" stroke="${p.stroke}" stroke-width="${E(p.strokeWidth)}"${sa}${ma}/>`;
4173
+ if (pointsFn) geomSvg = `<polygon points="${pointsFn(w / EMU_PER_PX, h / EMU_PER_PX).map(([nx, ny]) => `${E(x + nx * w)},${E(y + ny * h)}`).join(" ")}" fill="${p.fill}" stroke="${p.stroke}" stroke-width="${E(p.strokeWidth)}"${sa}${ma}/>`;
3927
4174
  else geomSvg = `<rect x="${E(x)}" y="${E(y)}" width="${E(w)}" height="${E(h)}" fill="${p.fill}" stroke="${p.stroke}" stroke-width="${E(p.strokeWidth)}"${sa}${ma} data-pptx-preset="${escapeXml(preset)}"><title>${escapeXml(`preset: ${preset}`)}</title></rect>`;
3928
4175
  }
3929
4176
  }
@@ -4040,11 +4287,13 @@ const buildEffectsFilter = (pres, shape) => {
4040
4287
  primitives.push(`<feGaussianBlur in="SourceAlpha" stdDeviation="${blurPx.toFixed(2)}" result="innerBlur${i}"/>`, `<feOffset in="innerBlur${i}" dx="${dx.toFixed(2)}" dy="${dy.toFixed(2)}" result="innerOff${i}"/>`, `<feComposite in="innerOff${i}" in2="SourceAlpha" operator="arithmetic" k2="-1" k3="1" result="innerMask${i}"/>`, `<feFlood flood-color="${color}" flood-opacity="${opacity.toFixed(3)}" result="innerCol${i}"/>`, `<feComposite in="innerCol${i}" in2="innerMask${i}" operator="in" result="innerOut${i}"/>`);
4041
4288
  layers.push(`innerOut${i}`);
4042
4289
  } else if (e.kind === "glow") {
4043
- const blurPx = e.radiusEmu / EMU_PER_PX / 2;
4290
+ const radiusPx = e.radiusEmu / EMU_PER_PX;
4291
+ const dilatePx = radiusPx * .5;
4292
+ const featherPx = radiusPx * .3;
4044
4293
  const color = e.color || "#FFFFFF";
4045
4294
  const opacity = e.opacity ?? 1;
4046
4295
  const i = primitives.length;
4047
- primitives.push(`<feMorphology in="SourceAlpha" operator="dilate" radius="${(blurPx / 4).toFixed(2)}" result="glowExp${i}"/>`, `<feGaussianBlur in="glowExp${i}" stdDeviation="${blurPx.toFixed(2)}" result="glowBlur${i}"/>`, `<feFlood flood-color="${color}" flood-opacity="${opacity.toFixed(3)}" result="glowCol${i}"/>`, `<feComposite in="glowCol${i}" in2="glowBlur${i}" operator="in" result="glowOut${i}"/>`);
4296
+ primitives.push(`<feMorphology in="SourceAlpha" operator="dilate" radius="${dilatePx.toFixed(2)}" result="glowExp${i}"/>`, `<feGaussianBlur in="glowExp${i}" stdDeviation="${featherPx.toFixed(2)}" result="glowBlur${i}"/>`, `<feFlood flood-color="${color}" flood-opacity="${opacity.toFixed(3)}" result="glowCol${i}"/>`, `<feComposite in="glowCol${i}" in2="glowBlur${i}" operator="in" result="glowOut${i}"/>`);
4048
4297
  layers.push(`glowOut${i}`);
4049
4298
  } else if (e.kind === "softEdge") {
4050
4299
  const blurPx = e.radiusEmu / EMU_PER_PX / 2;
@@ -4174,4 +4423,4 @@ const detectImageFormatLocal = (bytes) => {
4174
4423
  //#endregion
4175
4424
  export { SERIF as a, substituteFamily as c, SANS as i, ARIAL as n, TIMES as o, MONO as r, defaultMeasurer as s, renderSlideSvg as t };
4176
4425
 
4177
- //# sourceMappingURL=src-CDTTqUfI.js.map
4426
+ //# sourceMappingURL=src-Q2z_XgT6.js.map