pptx-glimpse 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,19 @@
1
1
  # pptx-glimpse
2
2
 
3
- PPTX スライドを SVG / PNG に変換する TypeScript ライブラリ。
3
+ A TypeScript library for converting PPTX slides to SVG / PNG.
4
4
 
5
- ## インストール
5
+ ## Requirements
6
+
7
+ - **Node.js >= 20** (does not work in browser environments)
8
+ - Requires a platform supported by [sharp](https://sharp.pixelplumbing.com/), which is used for PNG conversion
9
+
10
+ ## Installation
6
11
 
7
12
  ```bash
8
13
  npm install pptx-glimpse
9
14
  ```
10
15
 
11
- ## 使い方
16
+ ## Usage
12
17
 
13
18
  ```typescript
14
19
  import { readFileSync } from "fs";
@@ -16,138 +21,138 @@ import { convertPptxToSvg, convertPptxToPng } from "pptx-glimpse";
16
21
 
17
22
  const pptx = readFileSync("presentation.pptx");
18
23
 
19
- // SVG に変換
24
+ // Convert to SVG
20
25
  const svgResults = await convertPptxToSvg(pptx);
21
26
  // [{ slideNumber: 1, svg: "<svg>...</svg>" }, ...]
22
27
 
23
- // PNG に変換
28
+ // Convert to PNG
24
29
  const pngResults = await convertPptxToPng(pptx);
25
30
  // [{ slideNumber: 1, png: Buffer, width: 960, height: 540 }, ...]
26
31
  ```
27
32
 
28
- ## サポート状況
29
-
30
- PowerPoint の静的なビジュアルコンテンツの変換に対応しています。アニメーションやトランジションなど、動的な要素には対応していません。
31
-
32
- ### 対応している機能
33
-
34
- #### 図形
35
-
36
- | 機能 | 詳細 |
37
- | -------------- | ------------------------------------------------------------------------- |
38
- | プリセット図形 | 113 種類 (矩形、楕円、矢印、フローチャート、吹き出し、星形、数学記号など) |
39
- | カスタム図形 | カスタムパス (moveTo, lnTo, cubicBezTo, close) による任意の図形描画 |
40
- | コネクター | 線コネクターの描画、線スタイル・色・太さの設定 |
41
- | グループ | 図形のグループ化、ネストされたグループ |
42
- | 変形 | 位置、サイズ、回転、反転 (flipH/flipV)、調整値 |
43
-
44
- #### テキスト
45
-
46
- | 機能 | 詳細 |
47
- | ---------------- | --------------------------------------------------------------------------------------------------------------- |
48
- | 文字書式 | フォントサイズ、フォントファミリー (東アジアフォント対応)、太字、斜体、下線、取り消し線、文字色、上付き・下付き |
49
- | 段落書式 | 水平配置 (left/center/right/justify)、垂直アンカー (top/center/bottom)、行間、段落前後スペース、インデント |
50
- | 箇条書き | 文字箇条書き (buChar)、自動番号 (buAutoNum、9 種類)、箇条書きフォント・色・サイズ |
51
- | テキストボックス | 折り返し (square/none)、自動調整 (noAutofit/normAutofit/spAutofit)、フォントスケール、マージン |
52
- | 単語折り返し | 英語・日本語・CJK テキストの折り返し、複数フォントサイズ混在時の折り返し |
53
-
54
- #### 塗りつぶし
55
-
56
- | 機能 | 詳細 |
57
- | -------------- | -------------------------------------------------------- |
58
- | ソリッドカラー | RGB 色指定、透明度 |
59
- | グラデーション | 線形グラデーション、複数グラデーションストップ、角度指定 |
60
- | 画像塗りつぶし | PNG/JPEG/GIF、引き伸ばし (stretch) モード |
61
- | 塗りつぶしなし | noFill 指定 |
62
-
63
- #### 線・枠線
64
-
65
- | 機能 | 詳細 |
66
- | ------------ | ------------------------------------------------------------- |
67
- | 線スタイル | 線幅、ソリッドカラー、透明度 |
68
- | 破線スタイル | solid, dash, dot, dashDot, lgDash, lgDashDot, sysDash, sysDot |
33
+ ## Feature Support
69
34
 
70
- ####
35
+ Supports conversion of static visual content in PowerPoint. Dynamic elements such as animations and transitions are not supported.
71
36
 
72
- | 機能 | 詳細 |
73
- | ------------ | ----------------------------------------------------------------------------- |
74
- | 色指定方式 | RGB (srgbClr)、テーマ色 (schemeClr)、システム色 (sysClr) |
75
- | テーマカラー | カラースキーム (dk1, lt1, dk2, lt2, accent1-6, hlink, folHlink)、カラーマップ |
76
- | 色変換 | 輝度調整 (lumMod/lumOff)、ティント (tint)、シェード (shade)、透明度 (alpha) |
37
+ ### Supported Features
77
38
 
78
- #### エフェクト
39
+ #### Shapes
79
40
 
80
- | 機能 | 詳細 |
81
- | ------------------ | ---------------------------------- |
82
- | 外側の影 | ぼかし半径、距離、方向、色・透明度 |
83
- | 内側の影 | ぼかし半径、距離、方向、色・透明度 |
84
- | 光彩 | 半径、色・透明度 |
85
- | ぼかし (Soft Edge) | 半径 |
41
+ | Feature | Details |
42
+ | -------------- | ------------------------------------------------------------------------------------------------ |
43
+ | Preset shapes | 113 types (rectangles, ellipses, arrows, flowcharts, callouts, stars, math symbols, etc.) |
44
+ | Custom shapes | Arbitrary shape drawing using custom paths (moveTo, lnTo, cubicBezTo, close) |
45
+ | Connectors | Line connector drawing, line style / color / width settings |
46
+ | Groups | Shape grouping, nested groups |
47
+ | Transforms | Position, size, rotation, flip (flipH/flipV), adjustment values |
86
48
 
87
- #### 画像
49
+ #### Text
88
50
 
89
- | 機能 | 詳細 |
90
- | -------------- | -------------------------------------------------- |
91
- | 画像要素 | PNG/JPEG/GIF、位置・サイズ・回転・反転、エフェクト |
92
- | 画像塗りつぶし | 図形・背景への画像塗りつぶし |
51
+ | Feature | Details |
52
+ | -------------- | -------------------------------------------------------------------------------------------------------------------- |
53
+ | Character formatting | Font size, font family (East Asian font support), bold, italic, underline, strikethrough, font color, superscript / subscript |
54
+ | Paragraph formatting | Horizontal alignment (left/center/right/justify), vertical anchor (top/center/bottom), line spacing, before/after paragraph spacing, indent |
55
+ | Bullet points | Character bullets (buChar), auto-numbering (buAutoNum, 9 types), bullet font / color / size |
56
+ | Text boxes | Word wrap (square/none), auto-fit (noAutofit/normAutofit/spAutofit), font scaling, margins |
57
+ | Word wrapping | Word wrapping for English, Japanese, and CJK text, wrapping with mixed font sizes |
93
58
 
94
- ####
59
+ #### Fill
95
60
 
96
- | 機能 | 詳細 |
97
- | ------------ | ---------------------------------------------------------------------- |
98
- | テーブル構造 | 行・列のグリッド、セル結合 (gridSpan/rowSpan)、行の高さ・列の幅 |
99
- | セルの書式 | テキスト、塗りつぶし (solid/gradient/image)、枠線 (上下左右、スタイル) |
61
+ | Feature | Details |
62
+ | -------------- | ---------------------------------------------------- |
63
+ | Solid color | RGB color specification, transparency |
64
+ | Gradient | Linear gradient, multiple gradient stops, angle |
65
+ | Image fill | PNG/JPEG/GIF, stretch mode |
66
+ | No fill | noFill specification |
100
67
 
101
- #### グラフ
68
+ #### Lines & Borders
102
69
 
103
- | 機能 | 詳細 |
104
- | ---------- | -------------------------------------------------------------------------- |
105
- | 対応グラフ | 棒グラフ (縦棒・横棒)、折れ線グラフ、円グラフ、散布図 |
106
- | グラフ要素 | タイトル、凡例 (位置指定)、系列 (名前・値・カテゴリ・色)、カテゴリ軸、値軸 |
70
+ | Feature | Details |
71
+ | -------------- | ------------------------------------------------------------- |
72
+ | Line style | Line width, solid color, transparency |
73
+ | Dash style | solid, dash, dot, dashDot, lgDash, lgDashDot, sysDash, sysDot |
107
74
 
108
- #### 背景・スライド設定
75
+ #### Colors
109
76
 
110
- | 機能 | 詳細 |
77
+ | Feature | Details |
111
78
  | -------------- | ------------------------------------------------------------------------------------ |
112
- | 背景 | ソリッド、グラデーション、画像。スライド レイアウト マスターの順でフォールバック |
113
- | スライドサイズ | 16:9、4:3、カスタムサイズ |
114
- | テーマ | テーマカラースキーム、テーマフォント (majorFont/minorFont) |
115
-
116
- ### 対応していない機能
117
-
118
- | カテゴリ | 未対応の機能 |
119
- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------- |
120
- | 塗りつぶし | パターン塗りつぶし、放射状グラデーション、パスグラデーション |
121
- | エフェクト | 反射、3D 回転・押し出し、アーティスティック効果 |
122
- | グラフ | 面グラフ、レーダー、ドーナツ、バブル、株価、複合、ヒストグラム、ボックスプロット、ウォーターフォール、ツリーマップ、サンバースト |
123
- | グラフの詳細機能 | データラベル、軸タイトル・目盛り・グリッド線、誤差範囲、トレンドライン |
124
- | テキスト | 縦書きテキスト、テキスト個別のエフェクト (影・光彩)、テキストのアウトライン、テキストの列 |
125
- | 表 | 表スタイルのテンプレート適用、対角線枠線 |
126
- | 図形 | 図形の結合 (Union/Subtract/Intersect/Fragment) |
127
- | SmartArt | SmartArt 全般 |
128
- | マルチメディア | 動画・音声の埋め込み |
129
- | アニメーション | オブジェクトアニメーション、スライドトランジション |
130
- | リンク | ハイパーリンク |
131
- | スライド要素 | スライドノート、コメント、ヘッダー・フッター、スライド番号・日付 |
132
- | 画像フォーマット | EMF/WMF (パースされるがレンダリング未対応) |
133
- | その他 | マクロ・VBA、セクション、ズームスライド |
134
-
135
- ## テストレンダリング
136
-
137
- PPTX ファイルを指定して、SVG PNG の変換結果を確認できます。
79
+ | Color types | RGB (srgbClr), theme color (schemeClr), system color (sysClr) |
80
+ | Theme colors | Color scheme (dk1, lt1, dk2, lt2, accent1-6, hlink, folHlink), color map |
81
+ | Color transforms | Luminance adjustment (lumMod/lumOff), tint, shade, transparency (alpha) |
82
+
83
+ #### Effects
84
+
85
+ | Feature | Details |
86
+ | -------------- | ------------------------------------------ |
87
+ | Outer shadow | Blur radius, distance, direction, color / transparency |
88
+ | Inner shadow | Blur radius, distance, direction, color / transparency |
89
+ | Glow | Radius, color / transparency |
90
+ | Soft edge | Radius |
91
+
92
+ #### Images
93
+
94
+ | Feature | Details |
95
+ | -------------- | ---------------------------------------------------------- |
96
+ | Image elements | PNG/JPEG/GIF, position / size / rotation / flip, effects |
97
+ | Image fill | Image fill for shapes and backgrounds |
98
+
99
+ #### Tables
100
+
101
+ | Feature | Details |
102
+ | ---------------- | --------------------------------------------------------------------------- |
103
+ | Table structure | Row and column grid, cell merging (gridSpan/rowSpan), row height / column width |
104
+ | Cell formatting | Text, fill (solid/gradient/image), borders (top/bottom/left/right, styles) |
105
+
106
+ #### Charts
107
+
108
+ | Feature | Details |
109
+ | ---------------- | ------------------------------------------------------------------------------------ |
110
+ | Supported charts | Bar chart (vertical/horizontal), line chart, pie chart, scatter plot |
111
+ | Chart elements | Title, legend (position), series (name/values/categories/color), category axis, value axis |
112
+
113
+ #### Background & Slide Settings
114
+
115
+ | Feature | Details |
116
+ | -------------- | --------------------------------------------------------------------------------------- |
117
+ | Background | Solid, gradient, image. Fallback order: slide → layout → master |
118
+ | Slide size | 16:9, 4:3, custom sizes |
119
+ | Theme | Theme color scheme, theme fonts (majorFont/minorFont) |
120
+
121
+ ### Unsupported Features
122
+
123
+ | Category | Unsupported features |
124
+ | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- |
125
+ | Fill | Pattern fill, radial gradient, path gradient |
126
+ | Effects | Reflection, 3D rotation / extrusion, artistic effects |
127
+ | Charts | Area, radar, doughnut, bubble, stock, combo, histogram, box plot, waterfall, treemap, sunburst |
128
+ | Chart details | Data labels, axis titles / tick marks / grid lines, error bars, trendlines |
129
+ | Text | Vertical text, individual text effects (shadow/glow), text outline, text columns |
130
+ | Tables | Table style template application, diagonal borders |
131
+ | Shapes | Shape operations (Union/Subtract/Intersect/Fragment) |
132
+ | SmartArt | All SmartArt features |
133
+ | Multimedia | Embedded video / audio |
134
+ | Animations | Object animations, slide transitions |
135
+ | Links | Hyperlinks |
136
+ | Slide elements | Slide notes, comments, headers / footers, slide numbers / dates |
137
+ | Image formats | EMF/WMF (parsed but not rendered) |
138
+ | Other | Macros / VBA, sections, zoom slides |
139
+
140
+ ## Test Rendering
141
+
142
+ You can specify a PPTX file to preview SVG and PNG conversion results.
138
143
 
139
144
  ```bash
140
145
  npm run render -- <pptx-file> [output-dir]
141
146
  ```
142
147
 
143
- - `output-dir` を省略すると `./output` に出力されます
148
+ - If `output-dir` is omitted, output is saved to `./output`
144
149
 
145
150
  ```bash
146
- #
151
+ # Examples
147
152
  npm run render -- presentation.pptx
148
153
  npm run render -- presentation.pptx ./my-output
149
154
  ```
150
155
 
151
- ## ライセンス
156
+ ## License
152
157
 
153
158
  MIT
package/dist/index.cjs CHANGED
@@ -246,11 +246,12 @@ function parseRelationships(xml) {
246
246
  const id = rel["@_Id"];
247
247
  const type = rel["@_Type"];
248
248
  const target = rel["@_Target"];
249
+ const targetMode = rel["@_TargetMode"];
249
250
  if (!id || !type || !target) {
250
251
  console.warn(`${WARN_PREFIX3} Relationship: entry missing required attribute, skipping`);
251
252
  continue;
252
253
  }
253
- rels.set(id, { id, type, target });
254
+ rels.set(id, { id, type, target, ...targetMode && { targetMode } });
254
255
  }
255
256
  return rels;
256
257
  }
@@ -630,7 +631,7 @@ function parseShapeTree(spTree, rels, slidePath, archive, colorResolver, context
630
631
  const elements = [];
631
632
  const shapes = spTree.sp ?? [];
632
633
  for (const sp of shapes) {
633
- const shape = parseShape(sp, colorResolver);
634
+ const shape = parseShape(sp, colorResolver, rels);
634
635
  if (shape) {
635
636
  elements.push(shape);
636
637
  } else {
@@ -675,7 +676,7 @@ function parseShapeTree(spTree, rels, slidePath, archive, colorResolver, context
675
676
  }
676
677
  return elements;
677
678
  }
678
- function parseShape(sp, colorResolver) {
679
+ function parseShape(sp, colorResolver, rels) {
679
680
  const spPr = sp.spPr;
680
681
  if (!spPr) return null;
681
682
  const transform = parseTransform(spPr.xfrm);
@@ -683,7 +684,7 @@ function parseShape(sp, colorResolver) {
683
684
  const geometry = parseGeometry(spPr);
684
685
  const fill = parseFillFromNode(spPr, colorResolver);
685
686
  const outline = parseOutline(spPr.ln, colorResolver);
686
- const textBody = parseTextBody(sp.txBody, colorResolver);
687
+ const textBody = parseTextBody(sp.txBody, colorResolver, rels);
687
688
  const effects = parseEffectList(spPr.effectLst, colorResolver);
688
689
  const ph = sp.nvSpPr?.nvPr?.ph;
689
690
  const placeholderType = ph ? ph["@_type"] ?? "body" : void 0;
@@ -899,7 +900,7 @@ function parseCustomGeometryPaths(custGeom) {
899
900
  }
900
901
  return svgParts.length > 0 ? svgParts.join(" ") : null;
901
902
  }
902
- function parseTextBody(txBody, colorResolver) {
903
+ function parseTextBody(txBody, colorResolver, rels) {
903
904
  if (!txBody) return null;
904
905
  const bodyPr = txBody.bodyPr;
905
906
  let autoFit = "noAutofit";
@@ -929,7 +930,7 @@ function parseTextBody(txBody, colorResolver) {
929
930
  const paragraphs = [];
930
931
  const pList = txBody.p ?? [];
931
932
  for (const p of pList) {
932
- paragraphs.push(parseParagraph(p, colorResolver));
933
+ paragraphs.push(parseParagraph(p, colorResolver, rels));
933
934
  }
934
935
  if (paragraphs.length === 0) return null;
935
936
  return { paragraphs, bodyProperties };
@@ -968,7 +969,7 @@ function parseBullet(pPr, colorResolver) {
968
969
  }
969
970
  return { bullet, bulletFont, bulletColor, bulletSizePct };
970
971
  }
971
- function parseParagraph(p, colorResolver) {
972
+ function parseParagraph(p, colorResolver, rels) {
972
973
  const pPr = p.pPr;
973
974
  const { bullet, bulletFont, bulletColor, bulletSizePct } = parseBullet(pPr, colorResolver);
974
975
  const properties = {
@@ -990,12 +991,12 @@ function parseParagraph(p, colorResolver) {
990
991
  const text = r.t ?? "";
991
992
  const textContent = typeof text === "object" ? text["#text"] ?? "" : String(text);
992
993
  const rPr = r.rPr;
993
- const runProps = parseRunProperties(rPr, colorResolver);
994
+ const runProps = parseRunProperties(rPr, colorResolver, rels);
994
995
  runs.push({ text: textContent, properties: runProps });
995
996
  }
996
997
  return { runs, properties };
997
998
  }
998
- function parseRunProperties(rPr, colorResolver) {
999
+ function parseRunProperties(rPr, colorResolver, rels) {
999
1000
  if (!rPr) {
1000
1001
  return {
1001
1002
  fontSize: null,
@@ -1006,7 +1007,8 @@ function parseRunProperties(rPr, colorResolver) {
1006
1007
  underline: false,
1007
1008
  strikethrough: false,
1008
1009
  color: null,
1009
- baseline: 0
1010
+ baseline: 0,
1011
+ hyperlink: null
1010
1012
  };
1011
1013
  }
1012
1014
  const fontSize = rPr["@_sz"] ? hundredthPointToPoint(Number(rPr["@_sz"])) : null;
@@ -1021,6 +1023,7 @@ function parseRunProperties(rPr, colorResolver) {
1021
1023
  if (!rPr.solidFill && !rPr.srgbClr && !rPr.schemeClr && !rPr.sysClr) {
1022
1024
  color = null;
1023
1025
  }
1026
+ const hyperlink = parseHyperlink(rPr.hlinkClick, rels);
1024
1027
  return {
1025
1028
  fontSize,
1026
1029
  fontFamily,
@@ -1030,9 +1033,19 @@ function parseRunProperties(rPr, colorResolver) {
1030
1033
  underline,
1031
1034
  strikethrough,
1032
1035
  color,
1033
- baseline
1036
+ baseline,
1037
+ hyperlink
1034
1038
  };
1035
1039
  }
1040
+ function parseHyperlink(hlinkClick, rels) {
1041
+ if (!hlinkClick) return null;
1042
+ const rId = hlinkClick["@_r:id"] ?? hlinkClick["@_id"];
1043
+ if (!rId || !rels) return null;
1044
+ const rel = rels.get(rId);
1045
+ if (!rel) return null;
1046
+ const tooltip = hlinkClick["@_tooltip"];
1047
+ return { url: rel.target, ...tooltip && { tooltip } };
1048
+ }
1036
1049
 
1037
1050
  // src/parser/slide-master-parser.ts
1038
1051
  var WARN_PREFIX6 = "[pptx-glimpse]";
@@ -3623,23 +3636,30 @@ function buildStyleAttrs(props, fontScale = 1, fontFamilies) {
3623
3636
  return styles.join(" ");
3624
3637
  }
3625
3638
  function renderSegment(text, props, fontScale, prefix) {
3639
+ let tspanContent;
3626
3640
  if (!needsScriptSplit(props)) {
3627
3641
  const styles = buildStyleAttrs(props, fontScale);
3628
- return `<tspan ${prefix}${styles}>${escapeXml(text)}</tspan>`;
3629
- }
3630
- const parts = splitByScript(text);
3631
- const result = [];
3632
- for (let i = 0; i < parts.length; i++) {
3633
- const part = parts[i];
3634
- const fonts = part.isEa ? [props.fontFamilyEa, props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
3635
- const styles = buildStyleAttrs(props, fontScale, fonts);
3636
- if (i === 0) {
3637
- result.push(`<tspan ${prefix}${styles}>${escapeXml(part.text)}</tspan>`);
3638
- } else {
3639
- result.push(`<tspan ${styles}>${escapeXml(part.text)}</tspan>`);
3642
+ tspanContent = `<tspan ${prefix}${styles}>${escapeXml(text)}</tspan>`;
3643
+ } else {
3644
+ const parts = splitByScript(text);
3645
+ const result = [];
3646
+ for (let i = 0; i < parts.length; i++) {
3647
+ const part = parts[i];
3648
+ const fonts = part.isEa ? [props.fontFamilyEa, props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
3649
+ const styles = buildStyleAttrs(props, fontScale, fonts);
3650
+ if (i === 0) {
3651
+ result.push(`<tspan ${prefix}${styles}>${escapeXml(part.text)}</tspan>`);
3652
+ } else {
3653
+ result.push(`<tspan ${styles}>${escapeXml(part.text)}</tspan>`);
3654
+ }
3640
3655
  }
3656
+ tspanContent = result.join("");
3657
+ }
3658
+ if (props.hyperlink) {
3659
+ const href = escapeXml(props.hyperlink.url);
3660
+ return `<a href="${href}">${tspanContent}</a>`;
3641
3661
  }
3642
- return result.join("");
3662
+ return tspanContent;
3643
3663
  }
3644
3664
  function getDefaultFontSize(paragraphs) {
3645
3665
  for (const p of paragraphs) {
package/dist/index.js CHANGED
@@ -209,11 +209,12 @@ function parseRelationships(xml) {
209
209
  const id = rel["@_Id"];
210
210
  const type = rel["@_Type"];
211
211
  const target = rel["@_Target"];
212
+ const targetMode = rel["@_TargetMode"];
212
213
  if (!id || !type || !target) {
213
214
  console.warn(`${WARN_PREFIX3} Relationship: entry missing required attribute, skipping`);
214
215
  continue;
215
216
  }
216
- rels.set(id, { id, type, target });
217
+ rels.set(id, { id, type, target, ...targetMode && { targetMode } });
217
218
  }
218
219
  return rels;
219
220
  }
@@ -593,7 +594,7 @@ function parseShapeTree(spTree, rels, slidePath, archive, colorResolver, context
593
594
  const elements = [];
594
595
  const shapes = spTree.sp ?? [];
595
596
  for (const sp of shapes) {
596
- const shape = parseShape(sp, colorResolver);
597
+ const shape = parseShape(sp, colorResolver, rels);
597
598
  if (shape) {
598
599
  elements.push(shape);
599
600
  } else {
@@ -638,7 +639,7 @@ function parseShapeTree(spTree, rels, slidePath, archive, colorResolver, context
638
639
  }
639
640
  return elements;
640
641
  }
641
- function parseShape(sp, colorResolver) {
642
+ function parseShape(sp, colorResolver, rels) {
642
643
  const spPr = sp.spPr;
643
644
  if (!spPr) return null;
644
645
  const transform = parseTransform(spPr.xfrm);
@@ -646,7 +647,7 @@ function parseShape(sp, colorResolver) {
646
647
  const geometry = parseGeometry(spPr);
647
648
  const fill = parseFillFromNode(spPr, colorResolver);
648
649
  const outline = parseOutline(spPr.ln, colorResolver);
649
- const textBody = parseTextBody(sp.txBody, colorResolver);
650
+ const textBody = parseTextBody(sp.txBody, colorResolver, rels);
650
651
  const effects = parseEffectList(spPr.effectLst, colorResolver);
651
652
  const ph = sp.nvSpPr?.nvPr?.ph;
652
653
  const placeholderType = ph ? ph["@_type"] ?? "body" : void 0;
@@ -862,7 +863,7 @@ function parseCustomGeometryPaths(custGeom) {
862
863
  }
863
864
  return svgParts.length > 0 ? svgParts.join(" ") : null;
864
865
  }
865
- function parseTextBody(txBody, colorResolver) {
866
+ function parseTextBody(txBody, colorResolver, rels) {
866
867
  if (!txBody) return null;
867
868
  const bodyPr = txBody.bodyPr;
868
869
  let autoFit = "noAutofit";
@@ -892,7 +893,7 @@ function parseTextBody(txBody, colorResolver) {
892
893
  const paragraphs = [];
893
894
  const pList = txBody.p ?? [];
894
895
  for (const p of pList) {
895
- paragraphs.push(parseParagraph(p, colorResolver));
896
+ paragraphs.push(parseParagraph(p, colorResolver, rels));
896
897
  }
897
898
  if (paragraphs.length === 0) return null;
898
899
  return { paragraphs, bodyProperties };
@@ -931,7 +932,7 @@ function parseBullet(pPr, colorResolver) {
931
932
  }
932
933
  return { bullet, bulletFont, bulletColor, bulletSizePct };
933
934
  }
934
- function parseParagraph(p, colorResolver) {
935
+ function parseParagraph(p, colorResolver, rels) {
935
936
  const pPr = p.pPr;
936
937
  const { bullet, bulletFont, bulletColor, bulletSizePct } = parseBullet(pPr, colorResolver);
937
938
  const properties = {
@@ -953,12 +954,12 @@ function parseParagraph(p, colorResolver) {
953
954
  const text = r.t ?? "";
954
955
  const textContent = typeof text === "object" ? text["#text"] ?? "" : String(text);
955
956
  const rPr = r.rPr;
956
- const runProps = parseRunProperties(rPr, colorResolver);
957
+ const runProps = parseRunProperties(rPr, colorResolver, rels);
957
958
  runs.push({ text: textContent, properties: runProps });
958
959
  }
959
960
  return { runs, properties };
960
961
  }
961
- function parseRunProperties(rPr, colorResolver) {
962
+ function parseRunProperties(rPr, colorResolver, rels) {
962
963
  if (!rPr) {
963
964
  return {
964
965
  fontSize: null,
@@ -969,7 +970,8 @@ function parseRunProperties(rPr, colorResolver) {
969
970
  underline: false,
970
971
  strikethrough: false,
971
972
  color: null,
972
- baseline: 0
973
+ baseline: 0,
974
+ hyperlink: null
973
975
  };
974
976
  }
975
977
  const fontSize = rPr["@_sz"] ? hundredthPointToPoint(Number(rPr["@_sz"])) : null;
@@ -984,6 +986,7 @@ function parseRunProperties(rPr, colorResolver) {
984
986
  if (!rPr.solidFill && !rPr.srgbClr && !rPr.schemeClr && !rPr.sysClr) {
985
987
  color = null;
986
988
  }
989
+ const hyperlink = parseHyperlink(rPr.hlinkClick, rels);
987
990
  return {
988
991
  fontSize,
989
992
  fontFamily,
@@ -993,9 +996,19 @@ function parseRunProperties(rPr, colorResolver) {
993
996
  underline,
994
997
  strikethrough,
995
998
  color,
996
- baseline
999
+ baseline,
1000
+ hyperlink
997
1001
  };
998
1002
  }
1003
+ function parseHyperlink(hlinkClick, rels) {
1004
+ if (!hlinkClick) return null;
1005
+ const rId = hlinkClick["@_r:id"] ?? hlinkClick["@_id"];
1006
+ if (!rId || !rels) return null;
1007
+ const rel = rels.get(rId);
1008
+ if (!rel) return null;
1009
+ const tooltip = hlinkClick["@_tooltip"];
1010
+ return { url: rel.target, ...tooltip && { tooltip } };
1011
+ }
999
1012
 
1000
1013
  // src/parser/slide-master-parser.ts
1001
1014
  var WARN_PREFIX6 = "[pptx-glimpse]";
@@ -3586,23 +3599,30 @@ function buildStyleAttrs(props, fontScale = 1, fontFamilies) {
3586
3599
  return styles.join(" ");
3587
3600
  }
3588
3601
  function renderSegment(text, props, fontScale, prefix) {
3602
+ let tspanContent;
3589
3603
  if (!needsScriptSplit(props)) {
3590
3604
  const styles = buildStyleAttrs(props, fontScale);
3591
- return `<tspan ${prefix}${styles}>${escapeXml(text)}</tspan>`;
3592
- }
3593
- const parts = splitByScript(text);
3594
- const result = [];
3595
- for (let i = 0; i < parts.length; i++) {
3596
- const part = parts[i];
3597
- const fonts = part.isEa ? [props.fontFamilyEa, props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
3598
- const styles = buildStyleAttrs(props, fontScale, fonts);
3599
- if (i === 0) {
3600
- result.push(`<tspan ${prefix}${styles}>${escapeXml(part.text)}</tspan>`);
3601
- } else {
3602
- result.push(`<tspan ${styles}>${escapeXml(part.text)}</tspan>`);
3605
+ tspanContent = `<tspan ${prefix}${styles}>${escapeXml(text)}</tspan>`;
3606
+ } else {
3607
+ const parts = splitByScript(text);
3608
+ const result = [];
3609
+ for (let i = 0; i < parts.length; i++) {
3610
+ const part = parts[i];
3611
+ const fonts = part.isEa ? [props.fontFamilyEa, props.fontFamily] : [props.fontFamily, props.fontFamilyEa];
3612
+ const styles = buildStyleAttrs(props, fontScale, fonts);
3613
+ if (i === 0) {
3614
+ result.push(`<tspan ${prefix}${styles}>${escapeXml(part.text)}</tspan>`);
3615
+ } else {
3616
+ result.push(`<tspan ${styles}>${escapeXml(part.text)}</tspan>`);
3617
+ }
3603
3618
  }
3619
+ tspanContent = result.join("");
3620
+ }
3621
+ if (props.hyperlink) {
3622
+ const href = escapeXml(props.hyperlink.url);
3623
+ return `<a href="${href}">${tspanContent}</a>`;
3604
3624
  }
3605
- return result.join("");
3625
+ return tspanContent;
3606
3626
  }
3607
3627
  function getDefaultFontSize(paragraphs) {
3608
3628
  for (const p of paragraphs) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx-glimpse",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Convert PPTX slides to SVG and PNG",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -28,7 +28,8 @@
28
28
  "vrt:lo:docker-build": "docker build -t pptx-glimpse-vrt docker/libreoffice-vrt",
29
29
  "vrt:lo:fixtures": "docker run --rm -v \"$(pwd)\":/workspace pptx-glimpse-vrt python3 /workspace/vrt/libreoffice/generate_fixtures.py",
30
30
  "vrt:lo:reference": "docker run --rm -v \"$(pwd)\":/workspace pptx-glimpse-vrt bash /workspace/vrt/libreoffice/render_references.sh",
31
- "vrt:lo:update": "npm run vrt:lo:docker-build && npm run vrt:lo:fixtures && npm run vrt:lo:reference"
31
+ "vrt:lo:update": "npm run vrt:lo:docker-build && npm run vrt:lo:fixtures && npm run vrt:lo:reference",
32
+ "test:package": "npm run build && bash scripts/test-package.sh"
32
33
  },
33
34
  "files": [
34
35
  "dist"