@hirokisakabe/pom-cli 0.4.0 → 0.5.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @hirokisakabe/pom-cli
2
2
 
3
- CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview and build presentations from pom XML / Markdown.
3
+ CLI tool for [pom](https://github.com/hirokisakabe/pom) — preview, build, and render presentations from pom XML / Markdown.
4
4
 
5
5
  ## Installation
6
6
 
@@ -83,9 +83,38 @@ To print per-step timing on stderr:
83
83
  pom build slides.pom.xml -o output.pptx --verbose
84
84
  ```
85
85
 
86
+ ### Render
87
+
88
+ Renders each slide to a PNG (default) or SVG image — no LibreOffice or other external tools required.
89
+
90
+ ```bash
91
+ pom render slides.pom.xml -o ./images
92
+ pom render slides.pom.md -o ./images
93
+ ```
94
+
95
+ The images are written to the output directory as `slide-01.png`, `slide-02.png`, ... The directory is created if it does not exist. The rendering pipeline is the same as the preview server, so the images match what you see in `pom preview`.
96
+
97
+ To output SVG instead of PNG:
98
+
99
+ ```bash
100
+ pom render slides.pom.xml -o ./images --format svg
101
+ ```
102
+
103
+ To render only specific slides (1-based, comma-separated) — useful when re-checking just the slides you edited:
104
+
105
+ ```bash
106
+ pom render slides.pom.xml -o ./images --slides 2,5
107
+ ```
108
+
109
+ To print per-step timing on stderr:
110
+
111
+ ```bash
112
+ pom render slides.pom.xml -o ./images --verbose
113
+ ```
114
+
86
115
  ## Fonts
87
116
 
88
- This package bundles Carlito and Noto Sans CJK JP fonts for SVG rendering. These fonts are used when converting slides to SVG in the preview server. System fonts are not scanned.
117
+ This package bundles Carlito and Noto Sans CJK JP fonts for image rendering. These fonts are used when converting slides to SVG in the preview server and to PNG / SVG in `pom render`. System fonts are not scanned.
89
118
 
90
119
  ## License
91
120
 
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAWA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CAwEf;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../src/build.ts"],"names":[],"mappings":"AAWA,wBAAsB,QAAQ,CAC5B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,OAAO,CAAC,IAAI,CAAC,CA0Cf;AAED,wBAAsB,aAAa,CACjC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAO,GAClC,OAAO,CAAC,IAAI,CAAC,CAuDf"}
package/dist/build.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { buildPptx, DiagnosticsError } from "@hirokisakabe/pom";
4
- import { parseMd } from "@hirokisakabe/pom-md";
4
+ import { loadInput } from "./input.js";
5
5
  import { watchInputFile } from "./watch.js";
6
6
  function makeLog(verbose) {
7
7
  if (!verbose)
@@ -18,37 +18,7 @@ export async function runBuild(inputFile, outputFile, options = {}) {
18
18
  throw new Error(`Input file not found: ${absInput}`);
19
19
  }
20
20
  log(`Reading file: ${absInput}`);
21
- const content = fs.readFileSync(absInput, "utf-8");
22
- const ext = path.extname(absInput);
23
- let xml;
24
- let slideWidth = 1280;
25
- let slideHeight = 720;
26
- let masterPptxData;
27
- if (ext === ".md") {
28
- const t = Date.now();
29
- const result = parseMd(content);
30
- xml = result.xml;
31
- slideWidth = result.meta.size.w;
32
- slideHeight = result.meta.size.h;
33
- log(`Parsing Markdown... done (${Date.now() - t}ms)`);
34
- if (result.meta.masterPptx) {
35
- const masterPath = path.resolve(path.dirname(absInput), result.meta.masterPptx);
36
- try {
37
- masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
38
- }
39
- catch (e) {
40
- if (e instanceof Error && "code" in e && e.code === "ENOENT") {
41
- console.warn(`Warning: masterPptx not found: ${masterPath}`);
42
- }
43
- else {
44
- throw e;
45
- }
46
- }
47
- }
48
- }
49
- else {
50
- xml = content;
51
- }
21
+ const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(absInput, log);
52
22
  const t1 = Date.now();
53
23
  const { pptx } = await buildPptx(xml, { w: slideWidth, h: slideHeight }, {
54
24
  ...(masterPptxData ? { masterPptx: masterPptxData } : {}),
package/dist/cli.js CHANGED
@@ -4,12 +4,25 @@ import { Command } from "commander";
4
4
  import { DiagnosticsError } from "@hirokisakabe/pom";
5
5
  import { runBuild, runBuildWatch } from "./build.js";
6
6
  import { runPreview } from "./preview.js";
7
+ import { runRender } from "./render.js";
7
8
  const require = createRequire(import.meta.url);
8
9
  const { version } = require("../package.json");
9
10
  const program = new Command();
11
+ function printBuildError(err) {
12
+ if (err instanceof DiagnosticsError) {
13
+ const count = err.diagnostics.length;
14
+ console.error(`✗ Build failed (${count} ${count === 1 ? "error" : "errors"})\n`);
15
+ for (const d of err.diagnostics) {
16
+ console.error(` [${d.code}] ${d.message}`);
17
+ }
18
+ }
19
+ else {
20
+ console.error(err instanceof Error ? err.message : String(err));
21
+ }
22
+ }
10
23
  program
11
24
  .name("pom")
12
- .description("CLI tool for pom — preview and build presentations")
25
+ .description("CLI tool for pom — preview, build, and render presentations")
13
26
  .version(version);
14
27
  program
15
28
  .command("preview")
@@ -54,18 +67,41 @@ program
54
67
  }
55
68
  else {
56
69
  runBuild(input, options.o, { verbose: options.verbose }).catch((err) => {
57
- if (err instanceof DiagnosticsError) {
58
- const count = err.diagnostics.length;
59
- console.error(`✗ Build failed (${count} ${count === 1 ? "error" : "errors"})\n`);
60
- for (const d of err.diagnostics) {
61
- console.error(` [${d.code}] ${d.message}`);
62
- }
63
- }
64
- else {
65
- console.error(err instanceof Error ? err.message : String(err));
66
- }
70
+ printBuildError(err);
67
71
  process.exit(1);
68
72
  });
69
73
  }
70
74
  });
75
+ program
76
+ .command("render")
77
+ .description("Render each slide to a PNG or SVG image")
78
+ .argument("<input>", "Input file (.pom.xml or .pom.md)")
79
+ .requiredOption("-o <dir>", "Output directory for rendered images")
80
+ .option("--format <format>", "Output format: png or svg", "png")
81
+ .option("--slides <numbers>", "Comma-separated slide numbers to render (e.g. 2,5)")
82
+ .option("--verbose", "Show build step timing on stderr")
83
+ .action((input, options) => {
84
+ if (options.format !== "png" && options.format !== "svg") {
85
+ console.error(`Invalid format: ${options.format} (expected "png" or "svg")`);
86
+ process.exit(1);
87
+ }
88
+ const format = options.format;
89
+ let slides;
90
+ if (options.slides !== undefined) {
91
+ slides = options.slides.split(",").map((s) => Number(s.trim()));
92
+ if (slides.length === 0 ||
93
+ slides.some((n) => !Number.isInteger(n) || n <= 0)) {
94
+ console.error(`Invalid slides: ${options.slides} (expected comma-separated slide numbers, e.g. 2,5)`);
95
+ process.exit(1);
96
+ }
97
+ }
98
+ runRender(input, options.o, {
99
+ format,
100
+ slides,
101
+ verbose: options.verbose,
102
+ }).catch((err) => {
103
+ printBuildError(err);
104
+ process.exit(1);
105
+ });
106
+ });
71
107
  program.parse();
@@ -0,0 +1,3 @@
1
+ export declare const EXTRA_FONT_MAPPING: Record<string, string>;
2
+ export declare function resolveBundledFontsDir(): string;
3
+ //# sourceMappingURL=glimpse.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glimpse.d.ts","sourceRoot":"","sources":["../src/glimpse.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,kBAAkB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAGrD,CAAC;AAEF,wBAAgB,sBAAsB,IAAI,MAAM,CAQ/C"}
@@ -0,0 +1,15 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { fileURLToPath } from "url";
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ export const EXTRA_FONT_MAPPING = {
6
+ "游ゴシック Light": "Noto Sans CJK JP",
7
+ "Yu Gothic Light": "Noto Sans CJK JP",
8
+ };
9
+ export function resolveBundledFontsDir() {
10
+ const fontsDir = path.resolve(__dirname, "../fonts");
11
+ if (!fs.existsSync(fontsDir)) {
12
+ throw new Error(`Bundled fonts directory not found: ${fontsDir}. The package may be corrupted.`);
13
+ }
14
+ return fontsDir;
15
+ }
@@ -0,0 +1,8 @@
1
+ export interface LoadedInput {
2
+ xml: string;
3
+ slideWidth: number;
4
+ slideHeight: number;
5
+ masterPptxData?: Uint8Array;
6
+ }
7
+ export declare function loadInput(absInput: string, log?: (msg: string) => void): LoadedInput;
8
+ //# sourceMappingURL=input.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"input.d.ts","sourceRoot":"","sources":["../src/input.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,UAAU,CAAC;CAC7B;AAED,wBAAgB,SAAS,CACvB,QAAQ,EAAE,MAAM,EAChB,GAAG,GAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAe,GACpC,WAAW,CAqCb"}
package/dist/input.js ADDED
@@ -0,0 +1,37 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { parseMd } from "@hirokisakabe/pom-md";
4
+ export function loadInput(absInput, log = () => { }) {
5
+ const content = fs.readFileSync(absInput, "utf-8");
6
+ const ext = path.extname(absInput);
7
+ let xml;
8
+ let slideWidth = 1280;
9
+ let slideHeight = 720;
10
+ let masterPptxData;
11
+ if (ext === ".md") {
12
+ const t = Date.now();
13
+ const result = parseMd(content);
14
+ xml = result.xml;
15
+ slideWidth = result.meta.size.w;
16
+ slideHeight = result.meta.size.h;
17
+ log(`Parsing Markdown... done (${Date.now() - t}ms)`);
18
+ if (result.meta.masterPptx) {
19
+ const masterPath = path.resolve(path.dirname(absInput), result.meta.masterPptx);
20
+ try {
21
+ masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
22
+ }
23
+ catch (e) {
24
+ if (e instanceof Error && "code" in e && e.code === "ENOENT") {
25
+ console.warn(`Warning: masterPptx not found: ${masterPath}`);
26
+ }
27
+ else {
28
+ throw e;
29
+ }
30
+ }
31
+ }
32
+ }
33
+ else {
34
+ xml = content;
35
+ }
36
+ return { xml, slideWidth, slideHeight, masterPptxData };
37
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAqZA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,IAAI,CAmIN"}
1
+ {"version":3,"file":"preview.d.ts","sourceRoot":"","sources":["../src/preview.ts"],"names":[],"mappings":"AAyWA,wBAAgB,UAAU,CACxB,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,MAAqB,EAC3B,OAAO,GAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAClD,IAAI,CAmIN"}
package/dist/preview.js CHANGED
@@ -1,13 +1,12 @@
1
1
  import { spawn } from "child_process";
2
2
  import fs from "fs";
3
3
  import http from "http";
4
- import { fileURLToPath } from "url";
5
4
  import path from "path";
6
5
  import { buildPptx } from "@hirokisakabe/pom";
7
- import { parseMd } from "@hirokisakabe/pom-md";
8
6
  import { convertPptxToSvg } from "pptx-glimpse";
7
+ import { EXTRA_FONT_MAPPING, resolveBundledFontsDir } from "./glimpse.js";
8
+ import { loadInput } from "./input.js";
9
9
  import { watchInputFile } from "./watch.js";
10
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
11
10
  const DEFAULT_PORT = 3000;
12
11
  function makeLog(verbose) {
13
12
  if (!verbose)
@@ -36,43 +35,9 @@ function openBrowser(url) {
36
35
  });
37
36
  child.unref();
38
37
  }
39
- const EXTRA_FONT_MAPPING = {
40
- "游ゴシック Light": "Noto Sans CJK JP",
41
- "Yu Gothic Light": "Noto Sans CJK JP",
42
- };
43
38
  async function generateSvgs(inputFile, verbose = false) {
44
39
  const log = makeLog(verbose);
45
- const content = fs.readFileSync(inputFile, "utf-8");
46
- const ext = path.extname(inputFile);
47
- let xml;
48
- let slideWidth = 1280;
49
- let slideHeight = 720;
50
- let masterPptxData;
51
- if (ext === ".md") {
52
- const t = Date.now();
53
- const result = parseMd(content);
54
- xml = result.xml;
55
- slideWidth = result.meta.size.w;
56
- slideHeight = result.meta.size.h;
57
- log(`Parsing Markdown... done (${Date.now() - t}ms)`);
58
- if (result.meta.masterPptx) {
59
- const masterPath = path.resolve(path.dirname(inputFile), result.meta.masterPptx);
60
- try {
61
- masterPptxData = new Uint8Array(fs.readFileSync(masterPath));
62
- }
63
- catch (e) {
64
- if (e instanceof Error && "code" in e && e.code === "ENOENT") {
65
- process.stderr.write(`Warning: masterPptx not found: ${masterPath}\n`);
66
- }
67
- else {
68
- throw e;
69
- }
70
- }
71
- }
72
- }
73
- else {
74
- xml = content;
75
- }
40
+ const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(inputFile, log);
76
41
  if (!xml.trim()) {
77
42
  return { type: "empty" };
78
43
  }
@@ -86,11 +51,7 @@ async function generateSvgs(inputFile, verbose = false) {
86
51
  if (!(buffer instanceof Uint8Array)) {
87
52
  throw new Error("Unexpected output type from pptx.write");
88
53
  }
89
- const fontsDir = path.resolve(__dirname, "../fonts");
90
- if (!fs.existsSync(fontsDir)) {
91
- throw new Error(`Bundled fonts directory not found: ${fontsDir}. The package may be corrupted.`);
92
- }
93
- const fontDirs = [fontsDir];
54
+ const fontDirs = [resolveBundledFontsDir()];
94
55
  const t2 = Date.now();
95
56
  const slides = await convertPptxToSvg(buffer, {
96
57
  width: slideWidth,
@@ -0,0 +1,7 @@
1
+ export type RenderFormat = "png" | "svg";
2
+ export declare function runRender(inputFile: string, outputDir: string, options?: {
3
+ format?: RenderFormat;
4
+ slides?: number[];
5
+ verbose?: boolean;
6
+ }): Promise<void>;
7
+ //# sourceMappingURL=render.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,CAAC;AAOzC,wBAAsB,SAAS,CAC7B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE;IACP,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;CACd,GACL,OAAO,CAAC,IAAI,CAAC,CAoFf"}
package/dist/render.js ADDED
@@ -0,0 +1,71 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ import { buildPptx } from "@hirokisakabe/pom";
4
+ import { convertPptxToPng, convertPptxToSvg } from "pptx-glimpse";
5
+ import { EXTRA_FONT_MAPPING, resolveBundledFontsDir } from "./glimpse.js";
6
+ import { loadInput } from "./input.js";
7
+ function makeLog(verbose) {
8
+ if (!verbose)
9
+ return (_msg) => { };
10
+ return (msg) => process.stderr.write(`[pom] ${msg}\n`);
11
+ }
12
+ export async function runRender(inputFile, outputDir, options = {}) {
13
+ const verbose = options.verbose ?? false;
14
+ const format = options.format ?? "png";
15
+ const log = makeLog(verbose);
16
+ const totalStart = Date.now();
17
+ const absInput = path.resolve(inputFile);
18
+ const absOutputDir = path.resolve(outputDir);
19
+ if (!fs.existsSync(absInput)) {
20
+ throw new Error(`Input file not found: ${absInput}`);
21
+ }
22
+ log(`Reading file: ${absInput}`);
23
+ const { xml, slideWidth, slideHeight, masterPptxData } = loadInput(absInput, log);
24
+ const t1 = Date.now();
25
+ const { pptx } = await buildPptx(xml, { w: slideWidth, h: slideHeight }, {
26
+ textMeasurement: "fallback",
27
+ ...(masterPptxData ? { masterPptx: masterPptxData } : {}),
28
+ strict: true,
29
+ });
30
+ log(`Building PPTX... done (${Date.now() - t1}ms)`);
31
+ const buffer = await pptx.write({ outputType: "uint8array" });
32
+ if (!(buffer instanceof Uint8Array)) {
33
+ throw new Error("Unexpected output type from pptx.write");
34
+ }
35
+ const convertOptions = {
36
+ width: slideWidth,
37
+ fontDirs: [resolveBundledFontsDir()],
38
+ fontMapping: EXTRA_FONT_MAPPING,
39
+ skipSystemFonts: true,
40
+ ...(options.slides ? { slides: options.slides } : {}),
41
+ };
42
+ const t2 = Date.now();
43
+ let outputs;
44
+ if (format === "svg") {
45
+ const slides = await convertPptxToSvg(buffer, convertOptions);
46
+ outputs = slides.map((s) => ({ slideNumber: s.slideNumber, data: s.svg }));
47
+ }
48
+ else {
49
+ const slides = await convertPptxToPng(buffer, convertOptions);
50
+ outputs = slides.map((s) => ({ slideNumber: s.slideNumber, data: s.png }));
51
+ }
52
+ log(`Rendering ${format.toUpperCase()}... done (${Date.now() - t2}ms)`);
53
+ if (options.slides) {
54
+ const rendered = new Set(outputs.map((o) => o.slideNumber));
55
+ const missing = options.slides.filter((n) => !rendered.has(n));
56
+ if (missing.length > 0) {
57
+ console.warn(`Warning: slide ${missing.join(", ")} not found in the presentation`);
58
+ }
59
+ }
60
+ if (outputs.length === 0) {
61
+ throw new Error("No slides were rendered");
62
+ }
63
+ fs.mkdirSync(absOutputDir, { recursive: true });
64
+ const padWidth = Math.max(2, ...outputs.map((o) => String(o.slideNumber).length));
65
+ for (const { slideNumber, data } of outputs) {
66
+ const file = path.join(absOutputDir, `slide-${String(slideNumber).padStart(padWidth, "0")}.${format}`);
67
+ fs.writeFileSync(file, data);
68
+ console.log(`Saved: ${file}`);
69
+ }
70
+ log(`Total: ${Date.now() - totalStart}ms`);
71
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@hirokisakabe/pom-cli",
3
- "version": "0.4.0",
4
- "description": "CLI tool for pom — preview and build presentations",
3
+ "version": "0.5.0",
4
+ "description": "CLI tool for pom — preview, build, and render presentations",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "pom": "./dist/cli.js"