pptx-glimpse 0.1.4 → 0.2.0

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.
Files changed (3) hide show
  1. package/dist/index.cjs +340 -17
  2. package/dist/index.js +340 -17
  3. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3135,7 +3135,7 @@ function renderTextDecorations(x, y, segmentWidth, fontSizePx, props) {
3135
3135
  }
3136
3136
  return lines;
3137
3137
  }
3138
- function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, fontResolver) {
3138
+ function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, fontResolver, vert) {
3139
3139
  const fontSize = (props.fontSize ?? defaultFontSize) * fontScale;
3140
3140
  const fontSizePx = fontSize * PX_PER_PT2;
3141
3141
  const parts = [];
@@ -3168,7 +3168,50 @@ function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, font
3168
3168
  }
3169
3169
  totalWidth += segWidth;
3170
3170
  };
3171
- if (needsScriptSplit(props)) {
3171
+ const processCjkUpright = (segText, fontFamily, fontFamilyEa) => {
3172
+ if (segText.length === 0) return;
3173
+ const font = fontResolver.resolveFont(fontFamily, fontFamilyEa);
3174
+ const fillAttrs = buildPathFillAttrs(props);
3175
+ for (const char of segText) {
3176
+ const charWidth = getTextMeasurer().measureTextWidth(
3177
+ char,
3178
+ fontSize,
3179
+ props.bold,
3180
+ fontFamily,
3181
+ fontFamilyEa
3182
+ );
3183
+ if (font) {
3184
+ const charX = x + totalWidth;
3185
+ const path = font.getPath(char, charX, effectiveY, fontSizePx);
3186
+ const pathData = path.toPathData(2);
3187
+ if (pathData && pathData.length > 0) {
3188
+ const cx = charX + charWidth / 2;
3189
+ const cy = effectiveY - fontSizePx * (font.ascender + font.descender) / 2 / font.unitsPerEm;
3190
+ parts.push(
3191
+ `<g transform="rotate(-90, ${cx.toFixed(2)}, ${cy.toFixed(2)})"><path d="${pathData}" ${fillAttrs}/></g>`
3192
+ );
3193
+ }
3194
+ }
3195
+ if (props.underline || props.strikethrough) {
3196
+ parts.push(
3197
+ ...renderTextDecorations(x + totalWidth, effectiveY, charWidth, fontSizePx, props)
3198
+ );
3199
+ }
3200
+ totalWidth += charWidth;
3201
+ }
3202
+ };
3203
+ if (vert === "eaVert") {
3204
+ const scriptParts = splitByScript(processedText);
3205
+ for (const part of scriptParts) {
3206
+ const ff = part.isEa ? props.fontFamilyEa ?? props.fontFamily : props.fontFamily;
3207
+ const ffEa = part.isEa ? props.fontFamilyEa : props.fontFamilyEa;
3208
+ if (part.isEa) {
3209
+ processCjkUpright(part.text, ff, ffEa);
3210
+ } else {
3211
+ processSegment(part.text, ff, ffEa);
3212
+ }
3213
+ }
3214
+ } else if (needsScriptSplit(props)) {
3172
3215
  const scriptParts = splitByScript(processedText);
3173
3216
  for (const part of scriptParts) {
3174
3217
  const ff = part.isEa ? props.fontFamilyEa : props.fontFamily;
@@ -3334,7 +3377,8 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
3334
3377
  currentY,
3335
3378
  fontScale,
3336
3379
  defaultFontSize,
3337
- fontResolver
3380
+ fontResolver,
3381
+ bodyProperties.vert
3338
3382
  );
3339
3383
  if (result.svg) elements.push(result.svg);
3340
3384
  currentX += result.width;
@@ -3381,7 +3425,8 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
3381
3425
  currentY,
3382
3426
  fontScale,
3383
3427
  defaultFontSize,
3384
- fontResolver
3428
+ fontResolver,
3429
+ bodyProperties.vert
3385
3430
  );
3386
3431
  if (result.svg) elements.push(result.svg);
3387
3432
  currentX += result.width;
@@ -3824,6 +3869,15 @@ function renderChart(element) {
3824
3869
  case "radar":
3825
3870
  parts.push(renderRadarChart(chart, plotX, plotY, plotW, plotH));
3826
3871
  break;
3872
+ case "stock":
3873
+ parts.push(renderStockChart(chart, plotX, plotY, plotW, plotH));
3874
+ break;
3875
+ case "surface":
3876
+ parts.push(renderSurfaceChart(chart, plotX, plotY, plotW, plotH));
3877
+ break;
3878
+ case "ofPie":
3879
+ parts.push(renderOfPieChart(chart, plotX, plotY, plotW, plotH));
3880
+ break;
3827
3881
  }
3828
3882
  }
3829
3883
  if (chart.legend && chart.series.length > 0) {
@@ -4195,9 +4249,237 @@ function renderRadarChart(chart, x, y, w, h) {
4195
4249
  }
4196
4250
  return parts.join("");
4197
4251
  }
4252
+ function renderStockChart(chart, x, y, w, h) {
4253
+ const parts = [];
4254
+ const { series, categories } = chart;
4255
+ if (series.length < 3) return "";
4256
+ const highSeries = series[0];
4257
+ const lowSeries = series[1];
4258
+ const closeSeries = series[2];
4259
+ const catCount = categories.length || highSeries.values.length;
4260
+ if (catCount === 0) return "";
4261
+ let maxVal = 0;
4262
+ let minVal = Infinity;
4263
+ for (const s of [highSeries, lowSeries, closeSeries]) {
4264
+ for (const v of s.values) {
4265
+ maxVal = Math.max(maxVal, v);
4266
+ minVal = Math.min(minVal, v);
4267
+ }
4268
+ }
4269
+ if (maxVal === minVal) return "";
4270
+ parts.push(
4271
+ `<line x1="${round3(x)}" y1="${round3(y + h)}" x2="${round3(x + w)}" y2="${round3(y + h)}" stroke="#D9D9D9" stroke-width="1"/>`
4272
+ );
4273
+ parts.push(
4274
+ `<line x1="${round3(x)}" y1="${round3(y)}" x2="${round3(x)}" y2="${round3(y + h)}" stroke="#D9D9D9" stroke-width="1"/>`
4275
+ );
4276
+ for (let c = 0; c < catCount; c++) {
4277
+ const label = categories[c] ?? "";
4278
+ const labelX = x + (c + 0.5) * (w / catCount);
4279
+ parts.push(
4280
+ `<text x="${round3(labelX)}" y="${round3(y + h + 15)}" text-anchor="middle" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4281
+ );
4282
+ }
4283
+ const range = maxVal - minVal;
4284
+ for (let c = 0; c < catCount; c++) {
4285
+ const cx = x + (c + 0.5) * (w / catCount);
4286
+ const highVal = highSeries.values[c] ?? 0;
4287
+ const lowVal = lowSeries.values[c] ?? 0;
4288
+ const closeVal = closeSeries.values[c] ?? 0;
4289
+ const highY = y + h - (highVal - minVal) / range * h;
4290
+ const lowY = y + h - (lowVal - minVal) / range * h;
4291
+ const closeY = y + h - (closeVal - minVal) / range * h;
4292
+ parts.push(
4293
+ `<line x1="${round3(cx)}" y1="${round3(highY)}" x2="${round3(cx)}" y2="${round3(lowY)}" stroke="#404040" stroke-width="2"/>`
4294
+ );
4295
+ const tickW = w / catCount * 0.2;
4296
+ parts.push(
4297
+ `<line x1="${round3(cx)}" y1="${round3(closeY)}" x2="${round3(cx + tickW)}" y2="${round3(closeY)}" stroke="#404040" stroke-width="2"/>`
4298
+ );
4299
+ }
4300
+ return parts.join("");
4301
+ }
4302
+ function renderSurfaceChart(chart, x, y, w, h) {
4303
+ const parts = [];
4304
+ const { series, categories } = chart;
4305
+ if (series.length === 0) return "";
4306
+ const rows = series.length;
4307
+ const cols = categories.length || Math.max(...series.map((s) => s.values.length));
4308
+ if (cols === 0) return "";
4309
+ let minVal = Infinity;
4310
+ let maxVal = -Infinity;
4311
+ for (const s of series) {
4312
+ for (const v of s.values) {
4313
+ minVal = Math.min(minVal, v);
4314
+ maxVal = Math.max(maxVal, v);
4315
+ }
4316
+ }
4317
+ if (minVal === maxVal) maxVal = minVal + 1;
4318
+ const cellW = w / cols;
4319
+ const cellH = h / rows;
4320
+ for (let r = 0; r < rows; r++) {
4321
+ for (let c = 0; c < cols; c++) {
4322
+ const val = series[r].values[c] ?? 0;
4323
+ const t = (val - minVal) / (maxVal - minVal);
4324
+ const color = heatmapColor(t);
4325
+ const cx = x + c * cellW;
4326
+ const cy = y + r * cellH;
4327
+ parts.push(
4328
+ `<rect x="${round3(cx)}" y="${round3(cy)}" width="${round3(cellW)}" height="${round3(cellH)}" fill="${color}" stroke="#FFFFFF" stroke-width="0.5"/>`
4329
+ );
4330
+ }
4331
+ }
4332
+ for (let c = 0; c < cols; c++) {
4333
+ const label = categories[c] ?? "";
4334
+ if (label) {
4335
+ const labelX = x + (c + 0.5) * cellW;
4336
+ parts.push(
4337
+ `<text x="${round3(labelX)}" y="${round3(y + h + 15)}" text-anchor="middle" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4338
+ );
4339
+ }
4340
+ }
4341
+ for (let r = 0; r < rows; r++) {
4342
+ const label = series[r].name ?? "";
4343
+ if (label) {
4344
+ const labelY = y + (r + 0.5) * cellH;
4345
+ parts.push(
4346
+ `<text x="${round3(x - 5)}" y="${round3(labelY + 4)}" text-anchor="end" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4347
+ );
4348
+ }
4349
+ }
4350
+ return parts.join("");
4351
+ }
4352
+ function heatmapColor(t) {
4353
+ const clamped = Math.max(0, Math.min(1, t));
4354
+ let r, g, b;
4355
+ if (clamped < 0.25) {
4356
+ const s = clamped / 0.25;
4357
+ r = 0;
4358
+ g = Math.round(s * 255);
4359
+ b = 255;
4360
+ } else if (clamped < 0.5) {
4361
+ const s = (clamped - 0.25) / 0.25;
4362
+ r = 0;
4363
+ g = 255;
4364
+ b = Math.round((1 - s) * 255);
4365
+ } else if (clamped < 0.75) {
4366
+ const s = (clamped - 0.5) / 0.25;
4367
+ r = Math.round(s * 255);
4368
+ g = 255;
4369
+ b = 0;
4370
+ } else {
4371
+ const s = (clamped - 0.75) / 0.25;
4372
+ r = 255;
4373
+ g = Math.round((1 - s) * 255);
4374
+ b = 0;
4375
+ }
4376
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
4377
+ }
4378
+ function renderOfPieChart(chart, x, y, w, h) {
4379
+ const parts = [];
4380
+ const series = chart.series[0];
4381
+ if (!series || series.values.length === 0) return "";
4382
+ const total = series.values.reduce((sum, v) => sum + v, 0);
4383
+ if (total === 0) return "";
4384
+ const splitPos = chart.splitPos ?? 2;
4385
+ const secondPieSize = chart.secondPieSize ?? 75;
4386
+ const isBarOfPie = chart.ofPieType === "bar";
4387
+ const splitIdx = Math.max(0, series.values.length - splitPos);
4388
+ const primaryValues = series.values.slice(0, splitIdx);
4389
+ const secondaryValues = series.values.slice(splitIdx);
4390
+ const secondaryTotal = secondaryValues.reduce((sum, v) => sum + v, 0);
4391
+ const pieW = w * 0.45;
4392
+ const pieCx = x + pieW / 2;
4393
+ const pieCy = y + h / 2;
4394
+ const pieR = Math.min(pieW, h) / 2 * 0.85;
4395
+ let currentAngle = -Math.PI / 2;
4396
+ for (let i = 0; i < primaryValues.length; i++) {
4397
+ const val = primaryValues[i];
4398
+ const sliceAngle = val / total * 2 * Math.PI;
4399
+ const color = getPieSliceColor(i, chart);
4400
+ const x1 = pieCx + pieR * Math.cos(currentAngle);
4401
+ const y1 = pieCy + pieR * Math.sin(currentAngle);
4402
+ const x2 = pieCx + pieR * Math.cos(currentAngle + sliceAngle);
4403
+ const y2 = pieCy + pieR * Math.sin(currentAngle + sliceAngle);
4404
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
4405
+ parts.push(
4406
+ `<path d="M${round3(pieCx)},${round3(pieCy)} L${round3(x1)},${round3(y1)} A${round3(pieR)},${round3(pieR)} 0 ${largeArc},1 ${round3(x2)},${round3(y2)} Z" ${fillAttr(color)}/>`
4407
+ );
4408
+ currentAngle += sliceAngle;
4409
+ }
4410
+ const otherAngleStart = currentAngle;
4411
+ const otherSliceAngle = secondaryTotal / total * 2 * Math.PI;
4412
+ const otherColor = { hex: "#D9D9D9", alpha: 1 };
4413
+ if (primaryValues.length === 0 && secondaryValues.length > 0) {
4414
+ parts.push(
4415
+ `<circle cx="${round3(pieCx)}" cy="${round3(pieCy)}" r="${round3(pieR)}" ${fillAttr(otherColor)}/>`
4416
+ );
4417
+ } else if (secondaryTotal > 0) {
4418
+ const x1 = pieCx + pieR * Math.cos(otherAngleStart);
4419
+ const y1 = pieCy + pieR * Math.sin(otherAngleStart);
4420
+ const x2 = pieCx + pieR * Math.cos(otherAngleStart + otherSliceAngle);
4421
+ const y2 = pieCy + pieR * Math.sin(otherAngleStart + otherSliceAngle);
4422
+ const largeArc = otherSliceAngle > Math.PI ? 1 : 0;
4423
+ parts.push(
4424
+ `<path d="M${round3(pieCx)},${round3(pieCy)} L${round3(x1)},${round3(y1)} A${round3(pieR)},${round3(pieR)} 0 ${largeArc},1 ${round3(x2)},${round3(y2)} Z" ${fillAttr(otherColor)}/>`
4425
+ );
4426
+ }
4427
+ const secW = w * 0.25;
4428
+ const secH = h * (secondPieSize / 100) * 0.85;
4429
+ const secX = x + w * 0.65;
4430
+ const secCy = y + h / 2;
4431
+ const lineStartX = pieCx + pieR * Math.cos(otherAngleStart);
4432
+ const lineStartY = pieCy + pieR * Math.sin(otherAngleStart);
4433
+ const lineEndStartX = pieCx + pieR * Math.cos(otherAngleStart + otherSliceAngle);
4434
+ const lineEndStartY = pieCy + pieR * Math.sin(otherAngleStart + otherSliceAngle);
4435
+ parts.push(
4436
+ `<line x1="${round3(lineStartX)}" y1="${round3(lineStartY)}" x2="${round3(secX)}" y2="${round3(secCy - secH / 2)}" stroke="#A6A6A6" stroke-width="1"/>`
4437
+ );
4438
+ parts.push(
4439
+ `<line x1="${round3(lineEndStartX)}" y1="${round3(lineEndStartY)}" x2="${round3(secX)}" y2="${round3(secCy + secH / 2)}" stroke="#A6A6A6" stroke-width="1"/>`
4440
+ );
4441
+ if (isBarOfPie) {
4442
+ let barY = secCy - secH / 2;
4443
+ for (let i = 0; i < secondaryValues.length; i++) {
4444
+ const val = secondaryValues[i];
4445
+ const barH = secondaryTotal > 0 ? val / secondaryTotal * secH : 0;
4446
+ const color = getPieSliceColor(splitIdx + i, chart);
4447
+ parts.push(
4448
+ `<rect x="${round3(secX)}" y="${round3(barY)}" width="${round3(secW)}" height="${round3(barH)}" ${fillAttr(color)}/>`
4449
+ );
4450
+ barY += barH;
4451
+ }
4452
+ } else {
4453
+ const secPieCx = secX + secW / 2;
4454
+ const secR = Math.min(secW, secH) / 2;
4455
+ let secAngle = -Math.PI / 2;
4456
+ if (secondaryValues.length === 1) {
4457
+ const color = getPieSliceColor(splitIdx, chart);
4458
+ parts.push(
4459
+ `<circle cx="${round3(secPieCx)}" cy="${round3(secCy)}" r="${round3(secR)}" ${fillAttr(color)}/>`
4460
+ );
4461
+ } else {
4462
+ for (let i = 0; i < secondaryValues.length; i++) {
4463
+ const val = secondaryValues[i];
4464
+ const sliceAngle = secondaryTotal > 0 ? val / secondaryTotal * 2 * Math.PI : 0;
4465
+ const color = getPieSliceColor(splitIdx + i, chart);
4466
+ const sx1 = secPieCx + secR * Math.cos(secAngle);
4467
+ const sy1 = secCy + secR * Math.sin(secAngle);
4468
+ const sx2 = secPieCx + secR * Math.cos(secAngle + sliceAngle);
4469
+ const sy2 = secCy + secR * Math.sin(secAngle + sliceAngle);
4470
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
4471
+ parts.push(
4472
+ `<path d="M${round3(secPieCx)},${round3(secCy)} L${round3(sx1)},${round3(sy1)} A${round3(secR)},${round3(secR)} 0 ${largeArc},1 ${round3(sx2)},${round3(sy2)} Z" ${fillAttr(color)}/>`
4473
+ );
4474
+ secAngle += sliceAngle;
4475
+ }
4476
+ }
4477
+ }
4478
+ return parts.join("");
4479
+ }
4198
4480
  function renderLegend(chart, chartW, chartH, position) {
4199
4481
  const parts = [];
4200
- const entries2 = chart.chartType === "pie" || chart.chartType === "doughnut" ? chart.categories.map((cat, i) => ({
4482
+ const entries2 = chart.chartType === "pie" || chart.chartType === "doughnut" || chart.chartType === "ofPie" ? chart.categories.map((cat, i) => ({
4201
4483
  label: cat,
4202
4484
  color: getPieSliceColor(i, chart)
4203
4485
  })) : chart.series.map((s, i) => ({
@@ -5594,7 +5876,11 @@ var CHART_TYPE_MAP = [
5594
5876
  ["bubbleChart", "bubble"],
5595
5877
  ["areaChart", "area"],
5596
5878
  ["area3DChart", "area"],
5597
- ["radarChart", "radar"]
5879
+ ["radarChart", "radar"],
5880
+ ["stockChart", "stock"],
5881
+ ["surfaceChart", "surface"],
5882
+ ["surface3DChart", "surface"],
5883
+ ["ofPieChart", "ofPie"]
5598
5884
  ];
5599
5885
  function parseChart(chartXml, colorResolver) {
5600
5886
  const parsed = parseXml(chartXml);
@@ -5605,7 +5891,17 @@ function parseChart(chartXml, colorResolver) {
5605
5891
  const plotArea = chart.plotArea;
5606
5892
  if (!plotArea) return null;
5607
5893
  const title = parseChartTitle(chart.title);
5608
- const { chartType, series, categories, barDirection, holeSize, radarStyle } = parseChartTypeAndData(plotArea, colorResolver);
5894
+ const {
5895
+ chartType,
5896
+ series,
5897
+ categories,
5898
+ barDirection,
5899
+ holeSize,
5900
+ radarStyle,
5901
+ ofPieType,
5902
+ secondPieSize,
5903
+ splitPos
5904
+ } = parseChartTypeAndData(plotArea, colorResolver);
5609
5905
  if (!chartType) return null;
5610
5906
  const legend = parseLegend(chart.legend);
5611
5907
  return {
@@ -5616,6 +5912,9 @@ function parseChart(chartXml, colorResolver) {
5616
5912
  ...barDirection !== void 0 && { barDirection },
5617
5913
  ...holeSize !== void 0 && { holeSize },
5618
5914
  ...radarStyle !== void 0 && { radarStyle },
5915
+ ...ofPieType !== void 0 && { ofPieType },
5916
+ ...secondPieSize !== void 0 && { secondPieSize },
5917
+ ...splitPos !== void 0 && { splitPos },
5619
5918
  legend
5620
5919
  };
5621
5920
  }
@@ -5634,15 +5933,23 @@ function parseChartTypeAndData(plotArea, colorResolver) {
5634
5933
  const holeSize = chartType === "doughnut" ? Number(holeSizeNode?.["@_val"] ?? 50) : void 0;
5635
5934
  const radarStyleNode = chartNode.radarStyle;
5636
5935
  const radarStyle = chartType === "radar" ? radarStyleNode?.["@_val"] ?? "standard" : void 0;
5637
- return { chartType, series, categories, barDirection, holeSize, radarStyle };
5638
- }
5639
- const knownTags = new Set(CHART_TYPE_MAP.map(([tag]) => tag));
5640
- const chartTags = ["surfaceChart", "surface3DChart", "stockChart", "ofPieChart"];
5641
- for (const tag of chartTags) {
5642
- if (!knownTags.has(tag) && plotArea[tag]) {
5643
- warn(`chart.${tag}`, `chart type "${tag}" not implemented`);
5644
- break;
5645
- }
5936
+ const ofPieTypeNode = chartNode.ofPieType;
5937
+ const ofPieType = chartType === "ofPie" ? ofPieTypeNode?.["@_val"] ?? "pie" : void 0;
5938
+ const secondPieSizeNode = chartNode.secondPieSize;
5939
+ const secondPieSize = chartType === "ofPie" ? Number(secondPieSizeNode?.["@_val"] ?? 75) : void 0;
5940
+ const splitPosNode = chartNode.splitPos;
5941
+ const splitPos = chartType === "ofPie" ? Number(splitPosNode?.["@_val"] ?? 2) : void 0;
5942
+ return {
5943
+ chartType,
5944
+ series,
5945
+ categories,
5946
+ barDirection,
5947
+ holeSize,
5948
+ radarStyle,
5949
+ ofPieType,
5950
+ secondPieSize,
5951
+ splitPos
5952
+ };
5646
5953
  }
5647
5954
  return { chartType: null, series: [], categories: [] };
5648
5955
  }
@@ -7385,7 +7692,23 @@ function findMatchingPlaceholderStyle(placeholderType, placeholderIdx, styles) {
7385
7692
  if (byIdx?.lstStyle) return byIdx.lstStyle;
7386
7693
  }
7387
7694
  const byType = styles.find((s) => s.placeholderType === placeholderType);
7388
- return byType?.lstStyle;
7695
+ if (byType?.lstStyle) return byType.lstStyle;
7696
+ const fallbackType = getPlaceholderFallbackType(placeholderType);
7697
+ if (fallbackType) {
7698
+ const byFallback = styles.find((s) => s.placeholderType === fallbackType);
7699
+ return byFallback?.lstStyle;
7700
+ }
7701
+ return void 0;
7702
+ }
7703
+ function getPlaceholderFallbackType(type) {
7704
+ switch (type) {
7705
+ case "ctrTitle":
7706
+ return "title";
7707
+ case "subTitle":
7708
+ return "body";
7709
+ default:
7710
+ return void 0;
7711
+ }
7389
7712
  }
7390
7713
  function getTxStyleForPlaceholder(placeholderType, txStyles) {
7391
7714
  if (!txStyles) return void 0;
package/dist/index.js CHANGED
@@ -3092,7 +3092,7 @@ function renderTextDecorations(x, y, segmentWidth, fontSizePx, props) {
3092
3092
  }
3093
3093
  return lines;
3094
3094
  }
3095
- function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, fontResolver) {
3095
+ function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, fontResolver, vert) {
3096
3096
  const fontSize = (props.fontSize ?? defaultFontSize) * fontScale;
3097
3097
  const fontSizePx = fontSize * PX_PER_PT2;
3098
3098
  const parts = [];
@@ -3125,7 +3125,50 @@ function renderSegmentAsPath(text, props, x, y, fontScale, defaultFontSize, font
3125
3125
  }
3126
3126
  totalWidth += segWidth;
3127
3127
  };
3128
- if (needsScriptSplit(props)) {
3128
+ const processCjkUpright = (segText, fontFamily, fontFamilyEa) => {
3129
+ if (segText.length === 0) return;
3130
+ const font = fontResolver.resolveFont(fontFamily, fontFamilyEa);
3131
+ const fillAttrs = buildPathFillAttrs(props);
3132
+ for (const char of segText) {
3133
+ const charWidth = getTextMeasurer().measureTextWidth(
3134
+ char,
3135
+ fontSize,
3136
+ props.bold,
3137
+ fontFamily,
3138
+ fontFamilyEa
3139
+ );
3140
+ if (font) {
3141
+ const charX = x + totalWidth;
3142
+ const path = font.getPath(char, charX, effectiveY, fontSizePx);
3143
+ const pathData = path.toPathData(2);
3144
+ if (pathData && pathData.length > 0) {
3145
+ const cx = charX + charWidth / 2;
3146
+ const cy = effectiveY - fontSizePx * (font.ascender + font.descender) / 2 / font.unitsPerEm;
3147
+ parts.push(
3148
+ `<g transform="rotate(-90, ${cx.toFixed(2)}, ${cy.toFixed(2)})"><path d="${pathData}" ${fillAttrs}/></g>`
3149
+ );
3150
+ }
3151
+ }
3152
+ if (props.underline || props.strikethrough) {
3153
+ parts.push(
3154
+ ...renderTextDecorations(x + totalWidth, effectiveY, charWidth, fontSizePx, props)
3155
+ );
3156
+ }
3157
+ totalWidth += charWidth;
3158
+ }
3159
+ };
3160
+ if (vert === "eaVert") {
3161
+ const scriptParts = splitByScript(processedText);
3162
+ for (const part of scriptParts) {
3163
+ const ff = part.isEa ? props.fontFamilyEa ?? props.fontFamily : props.fontFamily;
3164
+ const ffEa = part.isEa ? props.fontFamilyEa : props.fontFamilyEa;
3165
+ if (part.isEa) {
3166
+ processCjkUpright(part.text, ff, ffEa);
3167
+ } else {
3168
+ processSegment(part.text, ff, ffEa);
3169
+ }
3170
+ }
3171
+ } else if (needsScriptSplit(props)) {
3129
3172
  const scriptParts = splitByScript(processedText);
3130
3173
  for (const part of scriptParts) {
3131
3174
  const ff = part.isEa ? props.fontFamilyEa : props.fontFamily;
@@ -3291,7 +3334,8 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
3291
3334
  currentY,
3292
3335
  fontScale,
3293
3336
  defaultFontSize,
3294
- fontResolver
3337
+ fontResolver,
3338
+ bodyProperties.vert
3295
3339
  );
3296
3340
  if (result.svg) elements.push(result.svg);
3297
3341
  currentX += result.width;
@@ -3338,7 +3382,8 @@ function renderTextBodyAsPath(textBody, transform, fontResolver) {
3338
3382
  currentY,
3339
3383
  fontScale,
3340
3384
  defaultFontSize,
3341
- fontResolver
3385
+ fontResolver,
3386
+ bodyProperties.vert
3342
3387
  );
3343
3388
  if (result.svg) elements.push(result.svg);
3344
3389
  currentX += result.width;
@@ -3781,6 +3826,15 @@ function renderChart(element) {
3781
3826
  case "radar":
3782
3827
  parts.push(renderRadarChart(chart, plotX, plotY, plotW, plotH));
3783
3828
  break;
3829
+ case "stock":
3830
+ parts.push(renderStockChart(chart, plotX, plotY, plotW, plotH));
3831
+ break;
3832
+ case "surface":
3833
+ parts.push(renderSurfaceChart(chart, plotX, plotY, plotW, plotH));
3834
+ break;
3835
+ case "ofPie":
3836
+ parts.push(renderOfPieChart(chart, plotX, plotY, plotW, plotH));
3837
+ break;
3784
3838
  }
3785
3839
  }
3786
3840
  if (chart.legend && chart.series.length > 0) {
@@ -4152,9 +4206,237 @@ function renderRadarChart(chart, x, y, w, h) {
4152
4206
  }
4153
4207
  return parts.join("");
4154
4208
  }
4209
+ function renderStockChart(chart, x, y, w, h) {
4210
+ const parts = [];
4211
+ const { series, categories } = chart;
4212
+ if (series.length < 3) return "";
4213
+ const highSeries = series[0];
4214
+ const lowSeries = series[1];
4215
+ const closeSeries = series[2];
4216
+ const catCount = categories.length || highSeries.values.length;
4217
+ if (catCount === 0) return "";
4218
+ let maxVal = 0;
4219
+ let minVal = Infinity;
4220
+ for (const s of [highSeries, lowSeries, closeSeries]) {
4221
+ for (const v of s.values) {
4222
+ maxVal = Math.max(maxVal, v);
4223
+ minVal = Math.min(minVal, v);
4224
+ }
4225
+ }
4226
+ if (maxVal === minVal) return "";
4227
+ parts.push(
4228
+ `<line x1="${round3(x)}" y1="${round3(y + h)}" x2="${round3(x + w)}" y2="${round3(y + h)}" stroke="#D9D9D9" stroke-width="1"/>`
4229
+ );
4230
+ parts.push(
4231
+ `<line x1="${round3(x)}" y1="${round3(y)}" x2="${round3(x)}" y2="${round3(y + h)}" stroke="#D9D9D9" stroke-width="1"/>`
4232
+ );
4233
+ for (let c = 0; c < catCount; c++) {
4234
+ const label = categories[c] ?? "";
4235
+ const labelX = x + (c + 0.5) * (w / catCount);
4236
+ parts.push(
4237
+ `<text x="${round3(labelX)}" y="${round3(y + h + 15)}" text-anchor="middle" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4238
+ );
4239
+ }
4240
+ const range = maxVal - minVal;
4241
+ for (let c = 0; c < catCount; c++) {
4242
+ const cx = x + (c + 0.5) * (w / catCount);
4243
+ const highVal = highSeries.values[c] ?? 0;
4244
+ const lowVal = lowSeries.values[c] ?? 0;
4245
+ const closeVal = closeSeries.values[c] ?? 0;
4246
+ const highY = y + h - (highVal - minVal) / range * h;
4247
+ const lowY = y + h - (lowVal - minVal) / range * h;
4248
+ const closeY = y + h - (closeVal - minVal) / range * h;
4249
+ parts.push(
4250
+ `<line x1="${round3(cx)}" y1="${round3(highY)}" x2="${round3(cx)}" y2="${round3(lowY)}" stroke="#404040" stroke-width="2"/>`
4251
+ );
4252
+ const tickW = w / catCount * 0.2;
4253
+ parts.push(
4254
+ `<line x1="${round3(cx)}" y1="${round3(closeY)}" x2="${round3(cx + tickW)}" y2="${round3(closeY)}" stroke="#404040" stroke-width="2"/>`
4255
+ );
4256
+ }
4257
+ return parts.join("");
4258
+ }
4259
+ function renderSurfaceChart(chart, x, y, w, h) {
4260
+ const parts = [];
4261
+ const { series, categories } = chart;
4262
+ if (series.length === 0) return "";
4263
+ const rows = series.length;
4264
+ const cols = categories.length || Math.max(...series.map((s) => s.values.length));
4265
+ if (cols === 0) return "";
4266
+ let minVal = Infinity;
4267
+ let maxVal = -Infinity;
4268
+ for (const s of series) {
4269
+ for (const v of s.values) {
4270
+ minVal = Math.min(minVal, v);
4271
+ maxVal = Math.max(maxVal, v);
4272
+ }
4273
+ }
4274
+ if (minVal === maxVal) maxVal = minVal + 1;
4275
+ const cellW = w / cols;
4276
+ const cellH = h / rows;
4277
+ for (let r = 0; r < rows; r++) {
4278
+ for (let c = 0; c < cols; c++) {
4279
+ const val = series[r].values[c] ?? 0;
4280
+ const t = (val - minVal) / (maxVal - minVal);
4281
+ const color = heatmapColor(t);
4282
+ const cx = x + c * cellW;
4283
+ const cy = y + r * cellH;
4284
+ parts.push(
4285
+ `<rect x="${round3(cx)}" y="${round3(cy)}" width="${round3(cellW)}" height="${round3(cellH)}" fill="${color}" stroke="#FFFFFF" stroke-width="0.5"/>`
4286
+ );
4287
+ }
4288
+ }
4289
+ for (let c = 0; c < cols; c++) {
4290
+ const label = categories[c] ?? "";
4291
+ if (label) {
4292
+ const labelX = x + (c + 0.5) * cellW;
4293
+ parts.push(
4294
+ `<text x="${round3(labelX)}" y="${round3(y + h + 15)}" text-anchor="middle" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4295
+ );
4296
+ }
4297
+ }
4298
+ for (let r = 0; r < rows; r++) {
4299
+ const label = series[r].name ?? "";
4300
+ if (label) {
4301
+ const labelY = y + (r + 0.5) * cellH;
4302
+ parts.push(
4303
+ `<text x="${round3(x - 5)}" y="${round3(labelY + 4)}" text-anchor="end" font-size="10" fill="#595959">${escapeXml2(label)}</text>`
4304
+ );
4305
+ }
4306
+ }
4307
+ return parts.join("");
4308
+ }
4309
+ function heatmapColor(t) {
4310
+ const clamped = Math.max(0, Math.min(1, t));
4311
+ let r, g, b;
4312
+ if (clamped < 0.25) {
4313
+ const s = clamped / 0.25;
4314
+ r = 0;
4315
+ g = Math.round(s * 255);
4316
+ b = 255;
4317
+ } else if (clamped < 0.5) {
4318
+ const s = (clamped - 0.25) / 0.25;
4319
+ r = 0;
4320
+ g = 255;
4321
+ b = Math.round((1 - s) * 255);
4322
+ } else if (clamped < 0.75) {
4323
+ const s = (clamped - 0.5) / 0.25;
4324
+ r = Math.round(s * 255);
4325
+ g = 255;
4326
+ b = 0;
4327
+ } else {
4328
+ const s = (clamped - 0.75) / 0.25;
4329
+ r = 255;
4330
+ g = Math.round((1 - s) * 255);
4331
+ b = 0;
4332
+ }
4333
+ return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`;
4334
+ }
4335
+ function renderOfPieChart(chart, x, y, w, h) {
4336
+ const parts = [];
4337
+ const series = chart.series[0];
4338
+ if (!series || series.values.length === 0) return "";
4339
+ const total = series.values.reduce((sum, v) => sum + v, 0);
4340
+ if (total === 0) return "";
4341
+ const splitPos = chart.splitPos ?? 2;
4342
+ const secondPieSize = chart.secondPieSize ?? 75;
4343
+ const isBarOfPie = chart.ofPieType === "bar";
4344
+ const splitIdx = Math.max(0, series.values.length - splitPos);
4345
+ const primaryValues = series.values.slice(0, splitIdx);
4346
+ const secondaryValues = series.values.slice(splitIdx);
4347
+ const secondaryTotal = secondaryValues.reduce((sum, v) => sum + v, 0);
4348
+ const pieW = w * 0.45;
4349
+ const pieCx = x + pieW / 2;
4350
+ const pieCy = y + h / 2;
4351
+ const pieR = Math.min(pieW, h) / 2 * 0.85;
4352
+ let currentAngle = -Math.PI / 2;
4353
+ for (let i = 0; i < primaryValues.length; i++) {
4354
+ const val = primaryValues[i];
4355
+ const sliceAngle = val / total * 2 * Math.PI;
4356
+ const color = getPieSliceColor(i, chart);
4357
+ const x1 = pieCx + pieR * Math.cos(currentAngle);
4358
+ const y1 = pieCy + pieR * Math.sin(currentAngle);
4359
+ const x2 = pieCx + pieR * Math.cos(currentAngle + sliceAngle);
4360
+ const y2 = pieCy + pieR * Math.sin(currentAngle + sliceAngle);
4361
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
4362
+ parts.push(
4363
+ `<path d="M${round3(pieCx)},${round3(pieCy)} L${round3(x1)},${round3(y1)} A${round3(pieR)},${round3(pieR)} 0 ${largeArc},1 ${round3(x2)},${round3(y2)} Z" ${fillAttr(color)}/>`
4364
+ );
4365
+ currentAngle += sliceAngle;
4366
+ }
4367
+ const otherAngleStart = currentAngle;
4368
+ const otherSliceAngle = secondaryTotal / total * 2 * Math.PI;
4369
+ const otherColor = { hex: "#D9D9D9", alpha: 1 };
4370
+ if (primaryValues.length === 0 && secondaryValues.length > 0) {
4371
+ parts.push(
4372
+ `<circle cx="${round3(pieCx)}" cy="${round3(pieCy)}" r="${round3(pieR)}" ${fillAttr(otherColor)}/>`
4373
+ );
4374
+ } else if (secondaryTotal > 0) {
4375
+ const x1 = pieCx + pieR * Math.cos(otherAngleStart);
4376
+ const y1 = pieCy + pieR * Math.sin(otherAngleStart);
4377
+ const x2 = pieCx + pieR * Math.cos(otherAngleStart + otherSliceAngle);
4378
+ const y2 = pieCy + pieR * Math.sin(otherAngleStart + otherSliceAngle);
4379
+ const largeArc = otherSliceAngle > Math.PI ? 1 : 0;
4380
+ parts.push(
4381
+ `<path d="M${round3(pieCx)},${round3(pieCy)} L${round3(x1)},${round3(y1)} A${round3(pieR)},${round3(pieR)} 0 ${largeArc},1 ${round3(x2)},${round3(y2)} Z" ${fillAttr(otherColor)}/>`
4382
+ );
4383
+ }
4384
+ const secW = w * 0.25;
4385
+ const secH = h * (secondPieSize / 100) * 0.85;
4386
+ const secX = x + w * 0.65;
4387
+ const secCy = y + h / 2;
4388
+ const lineStartX = pieCx + pieR * Math.cos(otherAngleStart);
4389
+ const lineStartY = pieCy + pieR * Math.sin(otherAngleStart);
4390
+ const lineEndStartX = pieCx + pieR * Math.cos(otherAngleStart + otherSliceAngle);
4391
+ const lineEndStartY = pieCy + pieR * Math.sin(otherAngleStart + otherSliceAngle);
4392
+ parts.push(
4393
+ `<line x1="${round3(lineStartX)}" y1="${round3(lineStartY)}" x2="${round3(secX)}" y2="${round3(secCy - secH / 2)}" stroke="#A6A6A6" stroke-width="1"/>`
4394
+ );
4395
+ parts.push(
4396
+ `<line x1="${round3(lineEndStartX)}" y1="${round3(lineEndStartY)}" x2="${round3(secX)}" y2="${round3(secCy + secH / 2)}" stroke="#A6A6A6" stroke-width="1"/>`
4397
+ );
4398
+ if (isBarOfPie) {
4399
+ let barY = secCy - secH / 2;
4400
+ for (let i = 0; i < secondaryValues.length; i++) {
4401
+ const val = secondaryValues[i];
4402
+ const barH = secondaryTotal > 0 ? val / secondaryTotal * secH : 0;
4403
+ const color = getPieSliceColor(splitIdx + i, chart);
4404
+ parts.push(
4405
+ `<rect x="${round3(secX)}" y="${round3(barY)}" width="${round3(secW)}" height="${round3(barH)}" ${fillAttr(color)}/>`
4406
+ );
4407
+ barY += barH;
4408
+ }
4409
+ } else {
4410
+ const secPieCx = secX + secW / 2;
4411
+ const secR = Math.min(secW, secH) / 2;
4412
+ let secAngle = -Math.PI / 2;
4413
+ if (secondaryValues.length === 1) {
4414
+ const color = getPieSliceColor(splitIdx, chart);
4415
+ parts.push(
4416
+ `<circle cx="${round3(secPieCx)}" cy="${round3(secCy)}" r="${round3(secR)}" ${fillAttr(color)}/>`
4417
+ );
4418
+ } else {
4419
+ for (let i = 0; i < secondaryValues.length; i++) {
4420
+ const val = secondaryValues[i];
4421
+ const sliceAngle = secondaryTotal > 0 ? val / secondaryTotal * 2 * Math.PI : 0;
4422
+ const color = getPieSliceColor(splitIdx + i, chart);
4423
+ const sx1 = secPieCx + secR * Math.cos(secAngle);
4424
+ const sy1 = secCy + secR * Math.sin(secAngle);
4425
+ const sx2 = secPieCx + secR * Math.cos(secAngle + sliceAngle);
4426
+ const sy2 = secCy + secR * Math.sin(secAngle + sliceAngle);
4427
+ const largeArc = sliceAngle > Math.PI ? 1 : 0;
4428
+ parts.push(
4429
+ `<path d="M${round3(secPieCx)},${round3(secCy)} L${round3(sx1)},${round3(sy1)} A${round3(secR)},${round3(secR)} 0 ${largeArc},1 ${round3(sx2)},${round3(sy2)} Z" ${fillAttr(color)}/>`
4430
+ );
4431
+ secAngle += sliceAngle;
4432
+ }
4433
+ }
4434
+ }
4435
+ return parts.join("");
4436
+ }
4155
4437
  function renderLegend(chart, chartW, chartH, position) {
4156
4438
  const parts = [];
4157
- const entries2 = chart.chartType === "pie" || chart.chartType === "doughnut" ? chart.categories.map((cat, i) => ({
4439
+ const entries2 = chart.chartType === "pie" || chart.chartType === "doughnut" || chart.chartType === "ofPie" ? chart.categories.map((cat, i) => ({
4158
4440
  label: cat,
4159
4441
  color: getPieSliceColor(i, chart)
4160
4442
  })) : chart.series.map((s, i) => ({
@@ -5551,7 +5833,11 @@ var CHART_TYPE_MAP = [
5551
5833
  ["bubbleChart", "bubble"],
5552
5834
  ["areaChart", "area"],
5553
5835
  ["area3DChart", "area"],
5554
- ["radarChart", "radar"]
5836
+ ["radarChart", "radar"],
5837
+ ["stockChart", "stock"],
5838
+ ["surfaceChart", "surface"],
5839
+ ["surface3DChart", "surface"],
5840
+ ["ofPieChart", "ofPie"]
5555
5841
  ];
5556
5842
  function parseChart(chartXml, colorResolver) {
5557
5843
  const parsed = parseXml(chartXml);
@@ -5562,7 +5848,17 @@ function parseChart(chartXml, colorResolver) {
5562
5848
  const plotArea = chart.plotArea;
5563
5849
  if (!plotArea) return null;
5564
5850
  const title = parseChartTitle(chart.title);
5565
- const { chartType, series, categories, barDirection, holeSize, radarStyle } = parseChartTypeAndData(plotArea, colorResolver);
5851
+ const {
5852
+ chartType,
5853
+ series,
5854
+ categories,
5855
+ barDirection,
5856
+ holeSize,
5857
+ radarStyle,
5858
+ ofPieType,
5859
+ secondPieSize,
5860
+ splitPos
5861
+ } = parseChartTypeAndData(plotArea, colorResolver);
5566
5862
  if (!chartType) return null;
5567
5863
  const legend = parseLegend(chart.legend);
5568
5864
  return {
@@ -5573,6 +5869,9 @@ function parseChart(chartXml, colorResolver) {
5573
5869
  ...barDirection !== void 0 && { barDirection },
5574
5870
  ...holeSize !== void 0 && { holeSize },
5575
5871
  ...radarStyle !== void 0 && { radarStyle },
5872
+ ...ofPieType !== void 0 && { ofPieType },
5873
+ ...secondPieSize !== void 0 && { secondPieSize },
5874
+ ...splitPos !== void 0 && { splitPos },
5576
5875
  legend
5577
5876
  };
5578
5877
  }
@@ -5591,15 +5890,23 @@ function parseChartTypeAndData(plotArea, colorResolver) {
5591
5890
  const holeSize = chartType === "doughnut" ? Number(holeSizeNode?.["@_val"] ?? 50) : void 0;
5592
5891
  const radarStyleNode = chartNode.radarStyle;
5593
5892
  const radarStyle = chartType === "radar" ? radarStyleNode?.["@_val"] ?? "standard" : void 0;
5594
- return { chartType, series, categories, barDirection, holeSize, radarStyle };
5595
- }
5596
- const knownTags = new Set(CHART_TYPE_MAP.map(([tag]) => tag));
5597
- const chartTags = ["surfaceChart", "surface3DChart", "stockChart", "ofPieChart"];
5598
- for (const tag of chartTags) {
5599
- if (!knownTags.has(tag) && plotArea[tag]) {
5600
- warn(`chart.${tag}`, `chart type "${tag}" not implemented`);
5601
- break;
5602
- }
5893
+ const ofPieTypeNode = chartNode.ofPieType;
5894
+ const ofPieType = chartType === "ofPie" ? ofPieTypeNode?.["@_val"] ?? "pie" : void 0;
5895
+ const secondPieSizeNode = chartNode.secondPieSize;
5896
+ const secondPieSize = chartType === "ofPie" ? Number(secondPieSizeNode?.["@_val"] ?? 75) : void 0;
5897
+ const splitPosNode = chartNode.splitPos;
5898
+ const splitPos = chartType === "ofPie" ? Number(splitPosNode?.["@_val"] ?? 2) : void 0;
5899
+ return {
5900
+ chartType,
5901
+ series,
5902
+ categories,
5903
+ barDirection,
5904
+ holeSize,
5905
+ radarStyle,
5906
+ ofPieType,
5907
+ secondPieSize,
5908
+ splitPos
5909
+ };
5603
5910
  }
5604
5911
  return { chartType: null, series: [], categories: [] };
5605
5912
  }
@@ -7342,7 +7649,23 @@ function findMatchingPlaceholderStyle(placeholderType, placeholderIdx, styles) {
7342
7649
  if (byIdx?.lstStyle) return byIdx.lstStyle;
7343
7650
  }
7344
7651
  const byType = styles.find((s) => s.placeholderType === placeholderType);
7345
- return byType?.lstStyle;
7652
+ if (byType?.lstStyle) return byType.lstStyle;
7653
+ const fallbackType = getPlaceholderFallbackType(placeholderType);
7654
+ if (fallbackType) {
7655
+ const byFallback = styles.find((s) => s.placeholderType === fallbackType);
7656
+ return byFallback?.lstStyle;
7657
+ }
7658
+ return void 0;
7659
+ }
7660
+ function getPlaceholderFallbackType(type) {
7661
+ switch (type) {
7662
+ case "ctrTitle":
7663
+ return "title";
7664
+ case "subTitle":
7665
+ return "body";
7666
+ default:
7667
+ return void 0;
7668
+ }
7346
7669
  }
7347
7670
  function getTxStyleForPlaceholder(placeholderType, txStyles) {
7348
7671
  if (!txStyles) return void 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx-glimpse",
3
- "version": "0.1.4",
3
+ "version": "0.2.0",
4
4
  "description": "A Node.js library to render PPTX as SVG / PNG.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",