screenitshot 0.1.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/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
package/dist/cli.js ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from 'commander';
3
+ import { screenshot } from './index.js';
4
+ import { readFile } from 'fs/promises';
5
+ import { resolve, dirname } from 'path';
6
+ import { fileURLToPath } from 'url';
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const packageJson = JSON.parse(await readFile(resolve(__dirname, '../package.json'), 'utf-8'));
10
+ const program = new Command();
11
+ program
12
+ .name('screenitshot')
13
+ .description('Convert various file formats to high-quality screenshots')
14
+ .version(packageJson.version)
15
+ .argument('<input>', 'Input file path')
16
+ .argument('[output]', 'Output image path')
17
+ .option('-f, --format <format>', 'Output image format (png, jpeg, webp)', 'png')
18
+ .option('-w, --width <width>', 'Viewport width', '1920')
19
+ .option('-h, --height <height>', 'Viewport height', '1080')
20
+ .option('-p, --page <page>', 'Page number for multi-page documents', '1')
21
+ .action(async (input, output, options) => {
22
+ try {
23
+ console.log(`Converting ${input}...`);
24
+ const result = await screenshot(input, {
25
+ output,
26
+ format: options.format,
27
+ width: parseInt(options.width),
28
+ height: parseInt(options.height),
29
+ page: parseInt(options.page),
30
+ });
31
+ console.log(`✓ Screenshot saved to ${result.path}`);
32
+ console.log(` Format: ${result.format}`);
33
+ console.log(` Size: ${result.width}x${result.height}`);
34
+ }
35
+ catch (error) {
36
+ const err = error;
37
+ // User-friendly error messages
38
+ if (err.message.includes('ENOENT') || err.message.includes('no such file')) {
39
+ console.error(`Error: Input file not found: ${input}`);
40
+ }
41
+ else if (err.message.includes('Unsupported file format')) {
42
+ console.error(`Error: ${err.message}`);
43
+ console.error('Supported formats: PDF');
44
+ }
45
+ else if (err.message.includes('No template available')) {
46
+ console.error(`Error: Format not yet supported`);
47
+ console.error('Currently supported: PDF');
48
+ }
49
+ else if (err.message.includes('page')) {
50
+ console.error(`Error: Invalid page number or page not found`);
51
+ }
52
+ else {
53
+ console.error(`Error: ${err.message}`);
54
+ // Show stack trace in debug mode
55
+ if (process.env.DEBUG) {
56
+ console.error(err.stack);
57
+ }
58
+ }
59
+ process.exit(1);
60
+ }
61
+ });
62
+ program.parse();
63
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { FileFormat } from './types.js';
2
+ export declare function detectFormat(filePath: string): Promise<FileFormat>;
3
+ //# sourceMappingURL=detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,wBAAsB,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CA0BxE"}
@@ -0,0 +1,28 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { extname } from 'path';
3
+ export async function detectFormat(filePath) {
4
+ const ext = extname(filePath).toLowerCase();
5
+ // Basic extension-based detection
6
+ const extensionMap = {
7
+ '.pdf': 'pdf',
8
+ '.epub': 'epub',
9
+ '.docx': 'docx',
10
+ };
11
+ if (ext in extensionMap) {
12
+ return extensionMap[ext];
13
+ }
14
+ // Fallback: check magic bytes
15
+ try {
16
+ const buffer = await readFile(filePath);
17
+ const magic = buffer.slice(0, 4).toString('hex');
18
+ if (magic === '25504446')
19
+ return 'pdf'; // %PDF
20
+ if (buffer.slice(0, 2).toString() === 'PK')
21
+ return 'epub'; // ZIP-based
22
+ }
23
+ catch {
24
+ // Ignore read errors
25
+ }
26
+ return 'unknown';
27
+ }
28
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../src/detector.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAG/B,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,QAAgB;IACjD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC,WAAW,EAAE,CAAC;IAE5C,kCAAkC;IAClC,MAAM,YAAY,GAA+B;QAC/C,MAAM,EAAE,KAAK;QACb,OAAO,EAAE,MAAM;QACf,OAAO,EAAE,MAAM;KAChB,CAAC;IAEF,IAAI,GAAG,IAAI,YAAY,EAAE,CAAC;QACxB,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAED,8BAA8B;IAC9B,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QAEjD,IAAI,KAAK,KAAK,UAAU;YAAE,OAAO,KAAK,CAAC,CAAE,OAAO;QAChD,IAAI,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,KAAK,IAAI;YAAE,OAAO,MAAM,CAAC,CAAE,YAAY;IAC1E,CAAC;IAAC,MAAM,CAAC;QACP,qBAAqB;IACvB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
@@ -0,0 +1,4 @@
1
+ import type { ScreenshotOptions, ScreenshotResult } from './types.js';
2
+ export { type ScreenshotOptions, type ScreenshotResult, type FileFormat } from './types.js';
3
+ export declare function screenshot(inputPath: string, options?: ScreenshotOptions): Promise<ScreenshotResult>;
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gBAAgB,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAE5F,wBAAsB,UAAU,CAC9B,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC,CAS3B"}
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import { detectFormat } from './detector.js';
2
+ import { Renderer } from './renderer.js';
3
+ export async function screenshot(inputPath, options = {}) {
4
+ const format = await detectFormat(inputPath);
5
+ if (format === 'unknown') {
6
+ throw new Error(`Unsupported file format: ${inputPath}`);
7
+ }
8
+ const renderer = new Renderer();
9
+ return await renderer.render(inputPath, format, options);
10
+ }
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAKzC,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,SAAiB,EACjB,UAA6B,EAAE;IAE/B,MAAM,MAAM,GAAG,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAE7C,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;QACzB,MAAM,IAAI,KAAK,CAAC,4BAA4B,SAAS,EAAE,CAAC,CAAC;IAC3D,CAAC;IAED,MAAM,QAAQ,GAAG,IAAI,QAAQ,EAAE,CAAC;IAChC,OAAO,MAAM,QAAQ,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { FileFormat, ScreenshotOptions, ScreenshotResult } from './types.js';
2
+ export declare class Renderer {
3
+ private getTemplatePath;
4
+ private injectDataIntoPage;
5
+ render(inputPath: string, format: FileFormat, options?: ScreenshotOptions): Promise<ScreenshotResult>;
6
+ }
7
+ //# sourceMappingURL=renderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.d.ts","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAKlF,qBAAa,QAAQ;IACnB,OAAO,CAAC,eAAe;YAeT,kBAAkB;IAa1B,MAAM,CACV,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,UAAU,EAClB,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,gBAAgB,CAAC;CAsE7B"}
@@ -0,0 +1,82 @@
1
+ import { chromium } from 'playwright';
2
+ import { readFile } from 'fs/promises';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ export class Renderer {
8
+ getTemplatePath(format) {
9
+ const templateMap = {
10
+ pdf: resolve(__dirname, '../templates/pdf.html'),
11
+ epub: resolve(__dirname, '../templates/epub.html'),
12
+ docx: resolve(__dirname, '../templates/docx.html'),
13
+ unknown: '',
14
+ };
15
+ const path = templateMap[format];
16
+ if (!path) {
17
+ throw new Error(`No template available for format: ${format}`);
18
+ }
19
+ return path;
20
+ }
21
+ async injectDataIntoPage(page, fileBase64, pageNumber = 1) {
22
+ // Inject data into page globals before template loads
23
+ await page.addInitScript(({ fileBase64: fb64, pageNum }) => {
24
+ // Override the placeholder values
25
+ globalThis.fileBase64 = fb64;
26
+ globalThis.pageNumber = pageNum;
27
+ }, { fileBase64, pageNum: pageNumber });
28
+ }
29
+ async render(inputPath, format, options = {}) {
30
+ const { output, format: imageFormat = 'png', width = 1920, height = 1080, page: pageNumber = 1, } = options;
31
+ const outputPath = output || inputPath.replace(/\.[^.]+$/, `.${imageFormat}`);
32
+ // Launch browser
33
+ const browser = await chromium.launch({
34
+ headless: true,
35
+ });
36
+ try {
37
+ const page = await browser.newPage({
38
+ viewport: { width, height },
39
+ });
40
+ // Read and encode file as base64
41
+ const fileData = await readFile(inputPath);
42
+ const fileBase64 = fileData.toString('base64');
43
+ // Inject data before loading template
44
+ await this.injectDataIntoPage(page, fileBase64, pageNumber);
45
+ // Load template
46
+ const templatePath = this.getTemplatePath(format);
47
+ await page.goto(`file://${templatePath}`);
48
+ // Wait for render complete and get metadata
49
+ const metadata = await page.evaluate(async () => {
50
+ const renderComplete = globalThis.renderComplete;
51
+ if (!renderComplete) {
52
+ throw new Error('window.renderComplete not found');
53
+ }
54
+ // Await the promise to get metadata
55
+ return await renderComplete;
56
+ });
57
+ // Resize viewport to match actual rendered content
58
+ await page.setViewportSize({
59
+ width: metadata.width,
60
+ height: metadata.height,
61
+ });
62
+ // Take screenshot at exact rendered size
63
+ await page.screenshot({
64
+ path: outputPath,
65
+ type: imageFormat,
66
+ fullPage: false,
67
+ });
68
+ await browser.close();
69
+ return {
70
+ path: outputPath,
71
+ format: imageFormat,
72
+ width: metadata.width,
73
+ height: metadata.height,
74
+ };
75
+ }
76
+ catch (error) {
77
+ await browser.close();
78
+ throw error;
79
+ }
80
+ }
81
+ }
82
+ //# sourceMappingURL=renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"renderer.js","sourceRoot":"","sources":["../src/renderer.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,22 @@
1
+ export interface ScreenshotOptions {
2
+ output?: string;
3
+ format?: 'png' | 'jpeg' | 'webp';
4
+ width?: number;
5
+ height?: number;
6
+ page?: number;
7
+ }
8
+ export interface ScreenshotResult {
9
+ path: string;
10
+ format: string;
11
+ width: number;
12
+ height: number;
13
+ }
14
+ export interface RenderMetadata {
15
+ width: number;
16
+ height: number;
17
+ pageCount: number;
18
+ pageNumber: number;
19
+ scale: number;
20
+ }
21
+ export type FileFormat = 'pdf' | 'epub' | 'docx' | 'unknown';
22
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,40 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from '@typescript-eslint/eslint-plugin';
3
+ import tsparser from '@typescript-eslint/parser';
4
+
5
+ export default [
6
+ eslint.configs.recommended,
7
+ {
8
+ files: ['**/*.ts'],
9
+ languageOptions: {
10
+ parser: tsparser,
11
+ parserOptions: {
12
+ ecmaVersion: 'latest',
13
+ sourceType: 'module',
14
+ },
15
+ globals: {
16
+ console: 'readonly',
17
+ process: 'readonly',
18
+ __dirname: 'readonly',
19
+ __filename: 'readonly',
20
+ Buffer: 'readonly',
21
+ global: 'readonly',
22
+ setTimeout: 'readonly',
23
+ clearTimeout: 'readonly',
24
+ setInterval: 'readonly',
25
+ clearInterval: 'readonly',
26
+ },
27
+ },
28
+ plugins: {
29
+ '@typescript-eslint': tseslint,
30
+ },
31
+ rules: {
32
+ ...tseslint.configs.recommended.rules,
33
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
34
+ '@typescript-eslint/no-explicit-any': 'warn',
35
+ },
36
+ },
37
+ {
38
+ ignores: ['node_modules/**', 'dist/**', 'templates/**'],
39
+ },
40
+ ];
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "screenitshot",
3
+ "version": "0.1.0",
4
+ "description": "Convert various file formats to high-quality screenshots",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "screenitshot": "dist/cli.js"
9
+ },
10
+ "type": "module",
11
+ "scripts": {
12
+ "build": "tsc && npm run copy-templates",
13
+ "copy-templates": "mkdir -p templates && cp ../render/dist/pdf.html templates/",
14
+ "dev": "tsc --watch",
15
+ "lint": "eslint \"src/**/*.ts\"",
16
+ "lint:fix": "eslint \"src/**/*.ts\" --fix",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "keywords": [
20
+ "screenshot",
21
+ "pdf",
22
+ "converter",
23
+ "playwright",
24
+ "chromium"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "dependencies": {
29
+ "playwright": "^1.40.1",
30
+ "commander": "^11.1.0"
31
+ },
32
+ "devDependencies": {
33
+ "@types/node": "^20.10.6",
34
+ "@typescript-eslint/eslint-plugin": "^7.0.0",
35
+ "@typescript-eslint/parser": "^7.0.0",
36
+ "eslint": "^8.56.0",
37
+ "typescript": "^5.3.3"
38
+ },
39
+ "engines": {
40
+ "node": ">=18.0.0"
41
+ },
42
+ "publishConfig": {
43
+ "access": "public"
44
+ }
45
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { screenshot } from './index.js';
5
+ import { readFile } from 'fs/promises';
6
+ import { resolve, dirname } from 'path';
7
+ import { fileURLToPath } from 'url';
8
+
9
+ const __filename = fileURLToPath(import.meta.url);
10
+ const __dirname = dirname(__filename);
11
+
12
+ const packageJson = JSON.parse(
13
+ await readFile(resolve(__dirname, '../package.json'), 'utf-8')
14
+ );
15
+
16
+ const program = new Command();
17
+
18
+ program
19
+ .name('screenitshot')
20
+ .description('Convert various file formats to high-quality screenshots')
21
+ .version(packageJson.version)
22
+ .argument('<input>', 'Input file path')
23
+ .argument('[output]', 'Output image path')
24
+ .option('-f, --format <format>', 'Output image format (png, jpeg, webp)', 'png')
25
+ .option('-w, --width <width>', 'Viewport width', '1920')
26
+ .option('-h, --height <height>', 'Viewport height', '1080')
27
+ .option('-p, --page <page>', 'Page number for multi-page documents', '1')
28
+ .action(async (input: string, output: string | undefined, options: any) => {
29
+ try {
30
+ console.log(`Converting ${input}...`);
31
+
32
+ const result = await screenshot(input, {
33
+ output,
34
+ format: options.format,
35
+ width: parseInt(options.width),
36
+ height: parseInt(options.height),
37
+ page: parseInt(options.page),
38
+ });
39
+
40
+ console.log(`✓ Screenshot saved to ${result.path}`);
41
+ console.log(` Format: ${result.format}`);
42
+ console.log(` Size: ${result.width}x${result.height}`);
43
+ } catch (error) {
44
+ const err = error as Error;
45
+
46
+ // User-friendly error messages
47
+ if (err.message.includes('ENOENT') || err.message.includes('no such file')) {
48
+ console.error(`Error: Input file not found: ${input}`);
49
+ } else if (err.message.includes('Unsupported file format')) {
50
+ console.error(`Error: ${err.message}`);
51
+ console.error('Supported formats: PDF');
52
+ } else if (err.message.includes('No template available')) {
53
+ console.error(`Error: Format not yet supported`);
54
+ console.error('Currently supported: PDF');
55
+ } else if (err.message.includes('page')) {
56
+ console.error(`Error: Invalid page number or page not found`);
57
+ } else {
58
+ console.error(`Error: ${err.message}`);
59
+
60
+ // Show stack trace in debug mode
61
+ if (process.env.DEBUG) {
62
+ console.error(err.stack);
63
+ }
64
+ }
65
+
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ program.parse();
@@ -0,0 +1,31 @@
1
+ import { readFile } from 'fs/promises';
2
+ import { extname } from 'path';
3
+ import type { FileFormat } from './types.js';
4
+
5
+ export async function detectFormat(filePath: string): Promise<FileFormat> {
6
+ const ext = extname(filePath).toLowerCase();
7
+
8
+ // Basic extension-based detection
9
+ const extensionMap: Record<string, FileFormat> = {
10
+ '.pdf': 'pdf',
11
+ '.epub': 'epub',
12
+ '.docx': 'docx',
13
+ };
14
+
15
+ if (ext in extensionMap) {
16
+ return extensionMap[ext];
17
+ }
18
+
19
+ // Fallback: check magic bytes
20
+ try {
21
+ const buffer = await readFile(filePath);
22
+ const magic = buffer.slice(0, 4).toString('hex');
23
+
24
+ if (magic === '25504446') return 'pdf'; // %PDF
25
+ if (buffer.slice(0, 2).toString() === 'PK') return 'epub'; // ZIP-based
26
+ } catch {
27
+ // Ignore read errors
28
+ }
29
+
30
+ return 'unknown';
31
+ }
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { detectFormat } from './detector.js';
2
+ import { Renderer } from './renderer.js';
3
+ import type { ScreenshotOptions, ScreenshotResult } from './types.js';
4
+
5
+ export { type ScreenshotOptions, type ScreenshotResult, type FileFormat } from './types.js';
6
+
7
+ export async function screenshot(
8
+ inputPath: string,
9
+ options: ScreenshotOptions = {}
10
+ ): Promise<ScreenshotResult> {
11
+ const format = await detectFormat(inputPath);
12
+
13
+ if (format === 'unknown') {
14
+ throw new Error(`Unsupported file format: ${inputPath}`);
15
+ }
16
+
17
+ const renderer = new Renderer();
18
+ return await renderer.render(inputPath, format, options);
19
+ }
@@ -0,0 +1,113 @@
1
+ import { chromium, type Page } from 'playwright';
2
+ import { readFile } from 'fs/promises';
3
+ import { resolve, dirname } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import type { FileFormat, ScreenshotOptions, ScreenshotResult } from './types.js';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+
10
+ export class Renderer {
11
+ private getTemplatePath(format: FileFormat): string {
12
+ const templateMap: Record<FileFormat, string> = {
13
+ pdf: resolve(__dirname, '../templates/pdf.html'),
14
+ epub: resolve(__dirname, '../templates/epub.html'),
15
+ docx: resolve(__dirname, '../templates/docx.html'),
16
+ unknown: '',
17
+ };
18
+
19
+ const path = templateMap[format];
20
+ if (!path) {
21
+ throw new Error(`No template available for format: ${format}`);
22
+ }
23
+ return path;
24
+ }
25
+
26
+ private async injectDataIntoPage(
27
+ page: Page,
28
+ fileBase64: string,
29
+ pageNumber: number = 1
30
+ ): Promise<void> {
31
+ // Inject data into page globals before template loads
32
+ await page.addInitScript(({ fileBase64: fb64, pageNum }: { fileBase64: string; pageNum: number }) => {
33
+ // Override the placeholder values
34
+ (globalThis as any).fileBase64 = fb64;
35
+ (globalThis as any).pageNumber = pageNum;
36
+ }, { fileBase64, pageNum: pageNumber });
37
+ }
38
+
39
+ async render(
40
+ inputPath: string,
41
+ format: FileFormat,
42
+ options: ScreenshotOptions = {}
43
+ ): Promise<ScreenshotResult> {
44
+ const {
45
+ output,
46
+ format: imageFormat = 'png',
47
+ width = 1920,
48
+ height = 1080,
49
+ page: pageNumber = 1,
50
+ } = options;
51
+
52
+ const outputPath = output || inputPath.replace(/\.[^.]+$/, `.${imageFormat}`);
53
+
54
+ // Launch browser
55
+ const browser = await chromium.launch({
56
+ headless: true,
57
+ });
58
+
59
+ try {
60
+ const page = await browser.newPage({
61
+ viewport: { width, height },
62
+ });
63
+
64
+ // Read and encode file as base64
65
+ const fileData = await readFile(inputPath);
66
+ const fileBase64 = fileData.toString('base64');
67
+
68
+ // Inject data before loading template
69
+ await this.injectDataIntoPage(page, fileBase64, pageNumber);
70
+
71
+ // Load template
72
+ const templatePath = this.getTemplatePath(format);
73
+ await page.goto(`file://${templatePath}`);
74
+
75
+ // Wait for render complete and get metadata
76
+ const metadata = await page.evaluate(async () => {
77
+ const renderComplete = (globalThis as any).renderComplete;
78
+
79
+ if (!renderComplete) {
80
+ throw new Error('window.renderComplete not found');
81
+ }
82
+
83
+ // Await the promise to get metadata
84
+ return await renderComplete;
85
+ });
86
+
87
+ // Resize viewport to match actual rendered content
88
+ await page.setViewportSize({
89
+ width: metadata.width,
90
+ height: metadata.height,
91
+ });
92
+
93
+ // Take screenshot at exact rendered size
94
+ await page.screenshot({
95
+ path: outputPath,
96
+ type: imageFormat as 'png' | 'jpeg',
97
+ fullPage: false,
98
+ });
99
+
100
+ await browser.close();
101
+
102
+ return {
103
+ path: outputPath,
104
+ format: imageFormat,
105
+ width: metadata.width,
106
+ height: metadata.height,
107
+ };
108
+ } catch (error) {
109
+ await browser.close();
110
+ throw error;
111
+ }
112
+ }
113
+ }
package/src/types.ts ADDED
@@ -0,0 +1,24 @@
1
+ export interface ScreenshotOptions {
2
+ output?: string;
3
+ format?: 'png' | 'jpeg' | 'webp';
4
+ width?: number;
5
+ height?: number;
6
+ page?: number;
7
+ }
8
+
9
+ export interface ScreenshotResult {
10
+ path: string;
11
+ format: string;
12
+ width: number;
13
+ height: number;
14
+ }
15
+
16
+ export interface RenderMetadata {
17
+ width: number;
18
+ height: number;
19
+ pageCount: number;
20
+ pageNumber: number;
21
+ scale: number;
22
+ }
23
+
24
+ export type FileFormat = 'pdf' | 'epub' | 'docx' | 'unknown';
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "node",
6
+ "lib": ["ES2022"],
7
+ "outDir": "./dist",
8
+ "rootDir": "./src",
9
+ "declaration": true,
10
+ "declarationMap": true,
11
+ "sourceMap": true,
12
+ "strict": true,
13
+ "esModuleInterop": true,
14
+ "skipLibCheck": true,
15
+ "forceConsistentCasingInFileNames": true,
16
+ "resolveJsonModule": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist"]
20
+ }