@fullgreengn/converter 1.0.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 +137 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# @fullgreen/converter
|
|
2
|
+
|
|
3
|
+
A CLI to convert:
|
|
4
|
+
|
|
5
|
+
- `.heic` -> `.jpg` or `.png` (via `sharp`)
|
|
6
|
+
- `.docx` -> `.pdf` (via `mammoth` + `puppeteer`)
|
|
7
|
+
|
|
8
|
+
## Requirements
|
|
9
|
+
|
|
10
|
+
- Node.js 20+
|
|
11
|
+
- `pnpm`
|
|
12
|
+
|
|
13
|
+
## Install Dependencies
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
If `pnpm` blocks install scripts (common in pnpm v10+), allow Puppeteer and install Chromium:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm approve-builds
|
|
23
|
+
pnpm rebuild puppeteer
|
|
24
|
+
pnpm exec puppeteer browsers install chrome
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Build
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm build
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
### Direct command mode
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
fullgreen-convert <input> [output] [--format jpg|png]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Examples:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Auto output path: ./photo.jpg
|
|
45
|
+
fullgreen-convert ./photo.heic
|
|
46
|
+
|
|
47
|
+
# Explicit output path and format
|
|
48
|
+
fullgreen-convert ./photo.heic ./photo.png --format png
|
|
49
|
+
|
|
50
|
+
# Auto output path: ./report.pdf
|
|
51
|
+
fullgreen-convert ./report.docx
|
|
52
|
+
|
|
53
|
+
# Explicit output path
|
|
54
|
+
fullgreen-convert ./report.docx ./exports/report.pdf
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Interactive mode
|
|
58
|
+
|
|
59
|
+
If no arguments are provided, the CLI prompts for input and output details:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
fullgreen-convert
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Run with pnpm dlx
|
|
66
|
+
|
|
67
|
+
After publishing to npm, run without global install:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pnpm dlx @fullgreen/converter ./photo.heic
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
You can also pass output and format:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
pnpm dlx @fullgreen/converter ./photo.heic ./photo.png --format png
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Test Locally with Global Link
|
|
80
|
+
|
|
81
|
+
From project root:
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
pnpm install
|
|
85
|
+
pnpm build
|
|
86
|
+
pnpm link --global
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Then use it anywhere:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
fullgreen-convert /absolute/path/to/input.heic
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
To remove the global link:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
pnpm unlink --global @fullgreen/converter
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Development Scripts
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
pnpm dev
|
|
105
|
+
pnpm typecheck
|
|
106
|
+
pnpm build
|
|
107
|
+
pnpm test
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Notes
|
|
111
|
+
|
|
112
|
+
- HEIC conversion supports only JPG and PNG outputs.
|
|
113
|
+
- DOCX conversion is Node-only and uses an embedded Chromium runtime from `puppeteer`.
|
|
114
|
+
- The first install can take longer because `puppeteer` downloads a browser binary.
|
|
115
|
+
- Unsupported file extensions are rejected with clear error messages.
|
|
116
|
+
|
|
117
|
+
## Troubleshooting HEIC Errors
|
|
118
|
+
|
|
119
|
+
If you see errors like:
|
|
120
|
+
|
|
121
|
+
- `heif: Error while loading plugin: Support for this compression format has not been built in`
|
|
122
|
+
- `bad seek` while reading a `.heic` file
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
node -v
|
|
128
|
+
pnpm rebuild sharp
|
|
129
|
+
pnpm up sharp
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Use Node.js 20 or 22 LTS if possible. Node 25 may have native-module compatibility gaps.
|
|
133
|
+
|
|
134
|
+
On macOS, this CLI automatically falls back to `sips` when `sharp/libheif` cannot decode a HEIC variant.
|
|
135
|
+
|
|
136
|
+
If conversion still fails, the source HEIC may be corrupted or encoded with a codec your runtime cannot decode.
|
|
137
|
+
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import u from"path";import{access as B}from"fs/promises";import p from"chalk";import{Command as N}from"commander";import l from"inquirer";import h from"ora";import{access as x}from"fs/promises";import F from"path";import b from"mammoth";import C from"puppeteer";async function g(e){let{inputPath:t,outputPath:n}=e;await S(t);let r=F.extname(t).toLowerCase();if(r!==".docx")throw new Error(`Unsupported document input format: ${r||"unknown"}. Only .docx is supported.`);let o=await O(t);return await T(o,n),n}async function O(e){try{let t=await b.convertToHtml({path:e});if(!t.value?.trim())throw new Error("DOCX to HTML conversion returned empty content.");return $(t.value)}catch(t){let n=t instanceof Error?t.message:String(t);throw new Error(`DOCX parsing failed: ${n}`)}}async function T(e,t){let n;try{n=await C.launch({headless:!0});let r=await n.newPage();await r.setContent(e,{waitUntil:"networkidle0"}),await r.pdf({path:t,format:"A4",printBackground:!0,margin:{top:"18mm",right:"14mm",bottom:"18mm",left:"14mm"}})}catch(r){let o=r instanceof Error?r.message:String(r),i=o.toLowerCase();throw i.includes("could not find chrome")||i.includes("failed to launch the browser process")?new Error(`PDF rendering failed: Puppeteer browser is not available.
|
|
3
|
+
If you are using pnpm v10+, allow install scripts and install Chromium:
|
|
4
|
+
1) pnpm approve-builds
|
|
5
|
+
2) pnpm rebuild puppeteer
|
|
6
|
+
3) pnpm exec puppeteer browsers install chrome`):new Error(`PDF rendering failed: ${o}`)}finally{n&&await n.close()}}function $(e){return`<!doctype html>
|
|
7
|
+
<html>
|
|
8
|
+
<head>
|
|
9
|
+
<meta charset="utf-8" />
|
|
10
|
+
<style>
|
|
11
|
+
body {
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
|
|
13
|
+
line-height: 1.5;
|
|
14
|
+
font-size: 12pt;
|
|
15
|
+
}
|
|
16
|
+
img {
|
|
17
|
+
max-width: 100%;
|
|
18
|
+
height: auto;
|
|
19
|
+
}
|
|
20
|
+
table {
|
|
21
|
+
border-collapse: collapse;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
td, th {
|
|
25
|
+
border: 1px solid #ddd;
|
|
26
|
+
padding: 6px;
|
|
27
|
+
}
|
|
28
|
+
</style>
|
|
29
|
+
</head>
|
|
30
|
+
<body>
|
|
31
|
+
${e}
|
|
32
|
+
</body>
|
|
33
|
+
</html>`}async function S(e){try{await x(e)}catch{throw new Error(`Input file not found: ${e}`)}}import{access as H}from"fs/promises";import j from"path";import{execFile as k}from"child_process";import{promisify as R}from"util";import D from"sharp";var A=R(k);async function d(e){let{inputPath:t,outputPath:n,format:r}=e;await M(t);let o=j.extname(t).toLowerCase();if(o!==".heic")throw new Error(`Unsupported image input format: ${o||"unknown"}. Only .heic is supported.`);if(!["jpg","png"].includes(r))throw new Error(`Unsupported output image format: ${r}. Supported formats are jpg and png.`);try{let i=D(t,{failOn:"error"});r==="jpg"?await i.jpeg({quality:90}).toFile(n):await i.png().toFile(n)}catch(i){if(L(i))try{return await U({inputPath:t,outputPath:n,format:r}),n}catch(s){throw f(i,t,s)}throw f(i,t)}return n}function f(e,t,n){let r=e instanceof Error?e.message:String(e),o=r.toLowerCase(),i=n?`
|
|
34
|
+
Fallback (macOS sips) error: ${n instanceof Error?n.message:String(n)}`:"";return o.includes("support for this compression format has not been built in")||o.includes("heif: error while loading plugin")?new Error(`HEIC decoding is not available in the current sharp/libvips runtime.
|
|
35
|
+
Input: ${t}
|
|
36
|
+
Try one of the following:
|
|
37
|
+
1) Use Node.js 20 or 22 LTS (Node 25 may not have compatible native binaries yet)
|
|
38
|
+
2) Reinstall sharp for your platform: pnpm rebuild sharp
|
|
39
|
+
3) Update sharp to the latest version
|
|
40
|
+
4) Convert this HEIC using another tool and retry
|
|
41
|
+
Original error: ${r}`+i):o.includes("no decoding plugin installed for this compression format")?new Error(`HEIC decoding plugin is unavailable for this runtime.
|
|
42
|
+
Input: ${t}
|
|
43
|
+
1) Reinstall sharp for your platform: pnpm rebuild sharp
|
|
44
|
+
Original error: ${r}`+i):o.includes("bad seek")||o.includes("invalid input")?new Error(`The HEIC file appears corrupted or partially unreadable: ${t}
|
|
45
|
+
Original error: ${r}`+i):new Error(`Image conversion failed for ${t}: ${r}${i}`)}function L(e){if(process.platform!=="darwin")return!1;let t=(e instanceof Error?e.message:String(e)).toLowerCase();return t.includes("support for this compression format has not been built in")||t.includes("heif: error while loading plugin")||t.includes("no decoding plugin installed for this compression format")}async function U(e){let{inputPath:t,outputPath:n,format:r}=e;await A("sips",["-s","format",r==="jpg"?"jpeg":"png",t,"--out",n])}async function M(e){try{await H(e)}catch{throw new Error(`Input file not found: ${e}`)}}var w=new N;w.name("fullgreen-convert").description("Convert .heic images to .jpg/.png and .docx documents to .pdf").argument("[input]","input file path").argument("[output]","output file path").option("-f, --format <format>","output format for HEIC conversion (jpg|png)").action(async(e,t,n)=>{try{if(!e){await z();return}await q(e,t,n??{})}catch(r){E(r)}});w.parseAsync(process.argv).catch(e=>E(e));async function q(e,t,n){let r=m(e);await I(r);let o=v(r),i=G(n?.format),s=X({inputPath:r,outputPathRaw:t,inputType:o,imageFormat:i}),c=h(`Converting ${p.cyan(u.basename(r))}...`).start();try{await y({inputPath:r,outputPath:s,inputType:o,imageFormat:i}),c.succeed(p.green(`Conversion completed: ${s}`))}catch(a){throw c.fail(p.red("Conversion failed")),a}}async function z(){let e=await l.prompt([{type:"input",name:"inputPathRaw",message:"Input file path:",validate:a=>a.trim().length>0?!0:"Please provide an input file path."}]),t=m(e.inputPathRaw);await I(t);let n=v(t),r="jpg";n==="heic"&&(r=(await l.prompt([{type:"list",name:"format",message:"Select output image format:",choices:[{name:"JPG",value:"jpg"},{name:"PNG",value:"png"}]}])).format);let o=P({inputPath:t,inputType:n,imageFormat:r}),i=await l.prompt([{type:"input",name:"outputPathRaw",message:"Output file path:",default:o,filter:a=>a.trim(),validate:a=>a.trim().length>0?!0:"Please provide an output file path."}]),s=m(i.outputPathRaw),c=h(`Converting ${p.cyan(u.basename(t))}...`).start();try{await y({inputPath:t,outputPath:s,inputType:n,imageFormat:r}),c.succeed(p.green(`Conversion completed: ${s}`))}catch(a){throw c.fail(p.red("Conversion failed")),a}}async function y(e){let{inputPath:t,outputPath:n,inputType:r,imageFormat:o}=e;if(r==="heic"){await d({inputPath:t,outputPath:n,format:o});return}await g({inputPath:t,outputPath:n})}function v(e){let t=u.extname(e).toLowerCase();if(t===".heic")return"heic";if(t===".docx")return"docx";throw new Error(`Unsupported input format: ${t||"unknown"}. Supported formats are .heic and .docx.`)}function G(e){if(!e)return"jpg";let t=e.toLowerCase();if(t!=="jpg"&&t!=="png")throw new Error(`Invalid format option: ${e}. Allowed values are jpg or png.`);return t}function X(e){let{inputPath:t,outputPathRaw:n,inputType:r,imageFormat:o}=e;return n?m(n):P({inputPath:t,inputType:r,imageFormat:o})}function P(e){let{inputPath:t,inputType:n,imageFormat:r}=e,o=n==="heic"?r:"pdf",i=u.parse(t);return u.join(i.dir,`${i.name}.${o}`)}function m(e){return u.resolve(process.cwd(),e)}async function I(e){try{await B(e)}catch{throw new Error(`Input file not found: ${e}`)}}function E(e){let t=e instanceof Error?e.message:String(e);console.error(p.red(`Error: ${t}`)),process.exit(1)}
|
|
46
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/converters/document.ts","../src/converters/image.ts"],"sourcesContent":["\nimport path from 'node:path';\nimport { access } from 'node:fs/promises';\nimport chalk from 'chalk';\nimport { Command } from 'commander';\nimport inquirer from 'inquirer';\nimport ora from 'ora';\nimport { convertDocxToPdf } from './converters/document.js';\nimport { convertHeicToImage, type ImageOutputFormat } from './converters/image.js';\n\ntype SupportedInputType = 'heic' | 'docx';\n\ninterface CliOptions {\n format?: ImageOutputFormat;\n}\n\nconst program = new Command();\n\nprogram\n .name('fullgreen-convert')\n .description('Convert .heic images to .jpg/.png and .docx documents to .pdf')\n .argument('[input]', 'input file path')\n .argument('[output]', 'output file path')\n .option('-f, --format <format>', 'output format for HEIC conversion (jpg|png)')\n .action(async (input?: string, output?: string, options?: CliOptions) => {\n\ttry {\n\t if (!input) {\n\t\tawait runInteractiveMode();\n\t\treturn;\n\t }\n\n\t await runDirectMode(input, output, options ?? {});\n\t} catch (error) {\n\t handleFatalError(error);\n\t}\n });\n\nprogram.parseAsync(process.argv).catch((error) => handleFatalError(error));\n\nasync function runDirectMode(inputPathRaw: string, outputPathRaw?: string, options?: CliOptions): Promise<void> {\n const inputPath = resolvePath(inputPathRaw);\n await ensureFileExists(inputPath);\n\n const inputType = detectInputType(inputPath);\n const imageFormat = resolveImageFormat(options?.format);\n const outputPath = resolveOutputPath({ inputPath, outputPathRaw, inputType, imageFormat });\n\n const spinner = ora(`Converting ${chalk.cyan(path.basename(inputPath))}...`).start();\n try {\n\tawait convertByType({ inputPath, outputPath, inputType, imageFormat });\n\tspinner.succeed(chalk.green(`Conversion completed: ${outputPath}`));\n } catch (error) {\n\tspinner.fail(chalk.red('Conversion failed'));\n\tthrow error;\n }\n}\n\nasync function runInteractiveMode(): Promise<void> {\n const answers = await inquirer.prompt<{\n\tinputPathRaw: string;\n }>([\n\t{\n\t type: 'input',\n\t name: 'inputPathRaw',\n\t message: 'Input file path:',\n\t validate: (value: string) => (value.trim().length > 0 ? true : 'Please provide an input file path.'),\n\t},\n ]);\n\n const inputPath = resolvePath(answers.inputPathRaw);\n await ensureFileExists(inputPath);\n const inputType = detectInputType(inputPath);\n\n let imageFormat: ImageOutputFormat = 'jpg';\n if (inputType === 'heic') {\n\tconst imageAnswers = await inquirer.prompt<{\n\t format: ImageOutputFormat;\n\t}>([\n\t {\n\t\ttype: 'list',\n\t\tname: 'format',\n\t\tmessage: 'Select output image format:',\n\t\tchoices: [\n\t\t { name: 'JPG', value: 'jpg' },\n\t\t { name: 'PNG', value: 'png' },\n\t\t],\n\t },\n\t]);\n\timageFormat = imageAnswers.format;\n }\n\n const defaultOutputPath = buildDefaultOutputPath({ inputPath, inputType, imageFormat });\n const outputAnswers = await inquirer.prompt<{\n\toutputPathRaw: string;\n }>([\n\t{\n\t type: 'input',\n\t name: 'outputPathRaw',\n\t message: 'Output file path:',\n\t default: defaultOutputPath,\n\t filter: (value: string) => value.trim(),\n\t validate: (value: string) => (value.trim().length > 0 ? true : 'Please provide an output file path.'),\n\t},\n ]);\n\n const outputPath = resolvePath(outputAnswers.outputPathRaw);\n const spinner = ora(`Converting ${chalk.cyan(path.basename(inputPath))}...`).start();\n try {\n\tawait convertByType({ inputPath, outputPath, inputType, imageFormat });\n\tspinner.succeed(chalk.green(`Conversion completed: ${outputPath}`));\n } catch (error) {\n\tspinner.fail(chalk.red('Conversion failed'));\n\tthrow error;\n }\n}\n\nasync function convertByType(params: {\n inputPath: string;\n outputPath: string;\n inputType: SupportedInputType;\n imageFormat: ImageOutputFormat;\n}): Promise<void> {\n const { inputPath, outputPath, inputType, imageFormat } = params;\n\n if (inputType === 'heic') {\n\tawait convertHeicToImage({\n\t inputPath,\n\t outputPath,\n\t format: imageFormat,\n\t});\n\treturn;\n }\n\n await convertDocxToPdf({\n\tinputPath,\n\toutputPath,\n });\n}\n\nfunction detectInputType(inputPath: string): SupportedInputType {\n const extension = path.extname(inputPath).toLowerCase();\n if (extension === '.heic') {\n\treturn 'heic';\n }\n\n if (extension === '.docx') {\n\treturn 'docx';\n }\n\n throw new Error(`Unsupported input format: ${extension || 'unknown'}. Supported formats are .heic and .docx.`);\n}\n\nfunction resolveImageFormat(format?: string): ImageOutputFormat {\n if (!format) {\n\treturn 'jpg';\n }\n\n const normalized = format.toLowerCase();\n if (normalized !== 'jpg' && normalized !== 'png') {\n\tthrow new Error(`Invalid format option: ${format}. Allowed values are jpg or png.`);\n }\n\n return normalized;\n}\n\nfunction resolveOutputPath(params: {\n inputPath: string;\n outputPathRaw?: string;\n inputType: SupportedInputType;\n imageFormat: ImageOutputFormat;\n}): string {\n const { inputPath, outputPathRaw, inputType, imageFormat } = params;\n if (outputPathRaw) {\n\treturn resolvePath(outputPathRaw);\n }\n\n return buildDefaultOutputPath({ inputPath, inputType, imageFormat });\n}\n\nfunction buildDefaultOutputPath(params: {\n inputPath: string;\n inputType: SupportedInputType;\n imageFormat: ImageOutputFormat;\n}): string {\n const { inputPath, inputType, imageFormat } = params;\n const outputExt = inputType === 'heic' ? imageFormat : 'pdf';\n const parsed = path.parse(inputPath);\n return path.join(parsed.dir, `${parsed.name}.${outputExt}`);\n}\n\nfunction resolvePath(filePath: string): string {\n return path.resolve(process.cwd(), filePath);\n}\n\nasync function ensureFileExists(filePath: string): Promise<void> {\n try {\n\tawait access(filePath);\n } catch {\n\tthrow new Error(`Input file not found: ${filePath}`);\n }\n}\n\nfunction handleFatalError(error: unknown): never {\n const message = error instanceof Error ? error.message : String(error);\n console.error(chalk.red(`Error: ${message}`));\n process.exit(1);\n}\n","import { access } from 'node:fs/promises';\nimport path from 'node:path';\nimport mammoth from 'mammoth';\nimport puppeteer from 'puppeteer';\n\nexport interface ConvertDocxToPdfOptions {\n inputPath: string;\n outputPath: string;\n}\n\nexport async function convertDocxToPdf(options: ConvertDocxToPdfOptions): Promise<string> {\n const { inputPath, outputPath } = options;\n\n await ensureFileExists(inputPath);\n\n const inputExtension = path.extname(inputPath).toLowerCase();\n if (inputExtension !== '.docx') {\n throw new Error(`Unsupported document input format: ${inputExtension || 'unknown'}. Only .docx is supported.`);\n }\n\n const html = await convertDocxToHtml(inputPath);\n await renderHtmlToPdf(html, outputPath);\n return outputPath;\n}\n\nasync function convertDocxToHtml(inputPath: string): Promise<string> {\n try {\n const result = await mammoth.convertToHtml({ path: inputPath });\n if (!result.value?.trim()) {\n throw new Error('DOCX to HTML conversion returned empty content.');\n }\n\n return wrapHtmlDocument(result.value);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`DOCX parsing failed: ${message}`);\n }\n}\n\nasync function renderHtmlToPdf(html: string, outputPath: string): Promise<void> {\n let browser: Awaited<ReturnType<typeof puppeteer.launch>> | undefined;\n\n try {\n browser = await puppeteer.launch({ headless: true });\n const page = await browser.newPage();\n await page.setContent(html, { waitUntil: 'networkidle0' });\n await page.pdf({\n path: outputPath,\n format: 'A4',\n printBackground: true,\n margin: {\n top: '18mm',\n right: '14mm',\n bottom: '18mm',\n left: '14mm',\n },\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n const lowered = message.toLowerCase();\n\n if (lowered.includes('could not find chrome') || lowered.includes('failed to launch the browser process')) {\n throw new Error(\n 'PDF rendering failed: Puppeteer browser is not available.\\n' +\n 'If you are using pnpm v10+, allow install scripts and install Chromium:\\n' +\n '1) pnpm approve-builds\\n' +\n '2) pnpm rebuild puppeteer\\n' +\n '3) pnpm exec puppeteer browsers install chrome',\n );\n }\n\n throw new Error(`PDF rendering failed: ${message}`);\n } finally {\n if (browser) {\n await browser.close();\n }\n }\n}\n\nfunction wrapHtmlDocument(content: string): string {\n return `<!doctype html>\n<html>\n <head>\n <meta charset=\"utf-8\" />\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, Arial, sans-serif;\n line-height: 1.5;\n font-size: 12pt;\n }\n img {\n max-width: 100%;\n height: auto;\n }\n table {\n border-collapse: collapse;\n width: 100%;\n }\n td, th {\n border: 1px solid #ddd;\n padding: 6px;\n }\n </style>\n </head>\n <body>\n ${content}\n </body>\n</html>`;\n}\n\nasync function ensureFileExists(filePath: string): Promise<void> {\n try {\n await access(filePath);\n } catch {\n throw new Error(`Input file not found: ${filePath}`);\n }\n}\n\n","import { access } from 'node:fs/promises';\nimport path from 'node:path';\nimport { execFile } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport sharp from 'sharp';\n\nexport type ImageOutputFormat = 'jpg' | 'png';\n\nexport interface ConvertHeicToImageOptions {\n inputPath: string;\n outputPath: string;\n format: ImageOutputFormat;\n}\n\nconst execFileAsync = promisify(execFile);\n\nexport async function convertHeicToImage(options: ConvertHeicToImageOptions): Promise<string> {\n const { inputPath, outputPath, format } = options;\n\n await ensureFileExists(inputPath);\n\n const inputExtension = path.extname(inputPath).toLowerCase();\n if (inputExtension !== '.heic') {\n throw new Error(`Unsupported image input format: ${inputExtension || 'unknown'}. Only .heic is supported.`);\n }\n\n if (!['jpg', 'png'].includes(format)) {\n throw new Error(`Unsupported output image format: ${format}. Supported formats are jpg and png.`);\n }\n\n try {\n const image = sharp(inputPath, { failOn: 'error' });\n\n if (format === 'jpg') {\n await image.jpeg({ quality: 90 }).toFile(outputPath);\n } else {\n await image.png().toFile(outputPath);\n }\n } catch (error) {\n if (canUseMacOSSipsFallback(error)) {\n try {\n await convertHeicWithSips({ inputPath, outputPath, format });\n return outputPath;\n } catch (fallbackError) {\n throw mapHeicRuntimeError(error, inputPath, fallbackError);\n }\n }\n\n throw mapHeicRuntimeError(error, inputPath);\n }\n\n return outputPath;\n}\n\nfunction mapHeicRuntimeError(error: unknown, inputPath: string, fallbackError?: unknown): Error {\n const rawMessage = error instanceof Error ? error.message : String(error);\n const lowered = rawMessage.toLowerCase();\n const fallbackMessage = fallbackError\n ? `\\nFallback (macOS sips) error: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`\n : '';\n\n if (lowered.includes('support for this compression format has not been built in') || lowered.includes('heif: error while loading plugin')) {\n return new Error(\n `HEIC decoding is not available in the current sharp/libvips runtime.\\n` +\n `Input: ${inputPath}\\n` +\n 'Try one of the following:\\n' +\n '1) Use Node.js 20 or 22 LTS (Node 25 may not have compatible native binaries yet)\\n' +\n '2) Reinstall sharp for your platform: pnpm rebuild sharp\\n' +\n '3) Update sharp to the latest version\\n' +\n '4) Convert this HEIC using another tool and retry\\n' +\n `Original error: ${rawMessage}` +\n fallbackMessage,\n );\n }\n\n if (lowered.includes('no decoding plugin installed for this compression format')) {\n return new Error(\n `HEIC decoding plugin is unavailable for this runtime.\\n` +\n `Input: ${inputPath}\\n` +\n '1) Reinstall sharp for your platform: pnpm rebuild sharp\\n' +\n `Original error: ${rawMessage}` +\n fallbackMessage,\n );\n }\n\n if (lowered.includes('bad seek') || lowered.includes('invalid input')) {\n return new Error(\n `The HEIC file appears corrupted or partially unreadable: ${inputPath}\\n` +\n `Original error: ${rawMessage}` +\n fallbackMessage,\n );\n }\n\n return new Error(`Image conversion failed for ${inputPath}: ${rawMessage}${fallbackMessage}`);\n}\n\nfunction canUseMacOSSipsFallback(error: unknown): boolean {\n if (process.platform !== 'darwin') {\n return false;\n }\n\n const message = (error instanceof Error ? error.message : String(error)).toLowerCase();\n return (\n message.includes('support for this compression format has not been built in') ||\n message.includes('heif: error while loading plugin') ||\n message.includes('no decoding plugin installed for this compression format')\n );\n}\n\nasync function convertHeicWithSips(options: ConvertHeicToImageOptions): Promise<void> {\n const { inputPath, outputPath, format } = options;\n const sipsFormat = format === 'jpg' ? 'jpeg' : 'png';\n\n await execFileAsync('sips', ['-s', 'format', sipsFormat, inputPath, '--out', outputPath]);\n}\n\nasync function ensureFileExists(filePath: string): Promise<void> {\n try {\n await access(filePath);\n } catch {\n throw new Error(`Input file not found: ${filePath}`);\n }\n}\n\n"],"mappings":"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","names":["path","access","chalk","Command","inquirer","ora","access","path","mammoth","puppeteer","convertDocxToPdf","options","inputPath","outputPath","ensureFileExists","inputExtension","html","convertDocxToHtml","renderHtmlToPdf","result","wrapHtmlDocument","error","message","browser","page","lowered","content","filePath","access","path","execFile","promisify","sharp","execFileAsync","convertHeicToImage","options","inputPath","outputPath","format","ensureFileExists","inputExtension","image","error","canUseMacOSSipsFallback","convertHeicWithSips","fallbackError","mapHeicRuntimeError","rawMessage","lowered","fallbackMessage","message","filePath","program","Command","input","output","options","runInteractiveMode","runDirectMode","error","handleFatalError","inputPathRaw","outputPathRaw","inputPath","resolvePath","ensureFileExists","inputType","detectInputType","imageFormat","resolveImageFormat","outputPath","resolveOutputPath","spinner","ora","chalk","path","convertByType","answers","inquirer","value","defaultOutputPath","buildDefaultOutputPath","outputAnswers","params","convertHeicToImage","convertDocxToPdf","extension","format","normalized","outputExt","parsed","filePath","access","message"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fullgreengn/converter",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Professional CLI to convert HEIC and DOCX files.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"fullgreen-convert": "dist/index.js"
|
|
7
|
+
},
|
|
8
|
+
"type": "module",
|
|
9
|
+
"main": "dist/index.js",
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=20 <25"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"chalk": "^5.6.2",
|
|
18
|
+
"commander": "^14.0.3",
|
|
19
|
+
"inquirer": "^13.4.1",
|
|
20
|
+
"mammoth": "^1.9.1",
|
|
21
|
+
"ora": "^9.3.0",
|
|
22
|
+
"puppeteer": "^24.7.2",
|
|
23
|
+
"sharp": "^0.34.5"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "^25.6.0",
|
|
27
|
+
"tsup": "^8.5.1",
|
|
28
|
+
"typescript": "^5.5.3"
|
|
29
|
+
},
|
|
30
|
+
"publishConfig": {
|
|
31
|
+
"access": "public"
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsup",
|
|
35
|
+
"dev": "tsup --watch",
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"test": "pnpm build && node --test test/smoke.test.mjs"
|
|
39
|
+
}
|
|
40
|
+
}
|