pixelati 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 +2 -0
- package/dist/cli.js +13 -0
- package/dist/render.d.ts +12 -0
- package/dist/render.js +41 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,8 +44,10 @@ pixelati <image> [options]
|
|
|
44
44
|
| Option | Description |
|
|
45
45
|
|---|---|
|
|
46
46
|
| `-w, --width <n>` | Output width in columns. Defaults to the terminal width, or 80 when piped. |
|
|
47
|
+
| `-H, --height <n>` | Maximum height in rows. The image is scaled to fit within width × height, preserving aspect, so tall images stay inside the box. |
|
|
47
48
|
| `-t, --threshold <n>` | Alpha cutoff from 0 to 255. Pixels at or below it are treated as transparent. Default 128. |
|
|
48
49
|
| `-b, --background <hex>` | Composite the image onto this colour instead of leaving transparent gaps (for example `#1e1e2e`). |
|
|
50
|
+
| `-T, --trim` | Crop uniform or transparent borders before scaling, so a subject centred in a large canvas (like a sprite) fills the frame instead of rendering tiny. |
|
|
49
51
|
| `-o, --output <file>` | Write the art to a file instead of printing it. |
|
|
50
52
|
| `-h, --help` | Show help. |
|
|
51
53
|
|
package/dist/cli.js
CHANGED
|
@@ -8,14 +8,17 @@ Usage:
|
|
|
8
8
|
|
|
9
9
|
Options:
|
|
10
10
|
-w, --width <n> Output width in columns (default: terminal width or 80)
|
|
11
|
+
-H, --height <n> Max height in rows; scales to fit within width × height
|
|
11
12
|
-t, --threshold <n> Alpha cutoff 0-255; at or below is transparent (default: 128)
|
|
12
13
|
-b, --background <hex> Composite onto this colour instead of leaving gaps (e.g. #1e1e2e)
|
|
13
14
|
-o, --output <file> Write the art to a file instead of stdout
|
|
15
|
+
-T, --trim Crop uniform/transparent borders so the subject fills the frame
|
|
14
16
|
-h, --help Show this help
|
|
15
17
|
|
|
16
18
|
Examples:
|
|
17
19
|
pixelati logo.png
|
|
18
20
|
pixelati photo.jpg -w 60
|
|
21
|
+
pixelati sprite.png -T -w 24
|
|
19
22
|
pixelati icon.png -b "#000000"
|
|
20
23
|
pixelati banner.png -w 100 -o banner.ans
|
|
21
24
|
`;
|
|
@@ -38,6 +41,10 @@ function parseArgs(argv) {
|
|
|
38
41
|
case "--width":
|
|
39
42
|
args.width = Number(next());
|
|
40
43
|
break;
|
|
44
|
+
case "-H":
|
|
45
|
+
case "--height":
|
|
46
|
+
args.height = Number(next());
|
|
47
|
+
break;
|
|
41
48
|
case "-t":
|
|
42
49
|
case "--threshold":
|
|
43
50
|
args.threshold = Number(next());
|
|
@@ -50,6 +57,10 @@ function parseArgs(argv) {
|
|
|
50
57
|
case "--output":
|
|
51
58
|
args.output = next();
|
|
52
59
|
break;
|
|
60
|
+
case "-T":
|
|
61
|
+
case "--trim":
|
|
62
|
+
args.trim = true;
|
|
63
|
+
break;
|
|
53
64
|
default:
|
|
54
65
|
if (a.startsWith("-"))
|
|
55
66
|
throw new Error(`Unknown option: ${a}`);
|
|
@@ -75,8 +86,10 @@ async function main() {
|
|
|
75
86
|
}
|
|
76
87
|
const opts = {
|
|
77
88
|
width: args.width ?? process.stdout.columns ?? 80,
|
|
89
|
+
height: args.height,
|
|
78
90
|
threshold: args.threshold,
|
|
79
91
|
background: args.background,
|
|
92
|
+
trim: args.trim,
|
|
80
93
|
};
|
|
81
94
|
const lines = await renderToLines(args.input, opts);
|
|
82
95
|
if (args.output) {
|
package/dist/render.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface RenderOptions {
|
|
2
2
|
/** Target width in terminal columns (one column per source pixel). */
|
|
3
3
|
width?: number;
|
|
4
|
+
/**
|
|
5
|
+
* Maximum height in terminal rows (each row is two pixels tall). When set,
|
|
6
|
+
* the image is scaled to fit within width × height, preserving aspect, so a
|
|
7
|
+
* tall image stays inside the box instead of overflowing.
|
|
8
|
+
*/
|
|
9
|
+
height?: number;
|
|
4
10
|
/** Alpha cutoff (0-255). Pixels at or below this are treated as transparent. */
|
|
5
11
|
threshold?: number;
|
|
6
12
|
/**
|
|
@@ -9,6 +15,12 @@ export interface RenderOptions {
|
|
|
9
15
|
* pixels render as blanks.
|
|
10
16
|
*/
|
|
11
17
|
background?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Crop uniform/transparent borders before scaling so the subject fills the
|
|
20
|
+
* frame. Useful for sprites or logos centred in a large transparent canvas,
|
|
21
|
+
* which would otherwise render tiny and blurry.
|
|
22
|
+
*/
|
|
23
|
+
trim?: boolean;
|
|
12
24
|
}
|
|
13
25
|
interface RGBA {
|
|
14
26
|
r: number;
|
package/dist/render.js
CHANGED
|
@@ -39,18 +39,50 @@ export async function renderToLines(input, opts = {}) {
|
|
|
39
39
|
const width = Math.max(1, Math.floor(opts.width ?? DEFAULT_WIDTH));
|
|
40
40
|
const threshold = opts.threshold ?? DEFAULT_THRESHOLD;
|
|
41
41
|
const base = opts.background ? parseHex(opts.background) : null;
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
// Optionally crop uniform/transparent borders first, so the subject fills the
|
|
43
|
+
// frame before scaling. trim() can throw on a uniform image; fall back to the
|
|
44
|
+
// original in that case. Work out the source dimensions after any trim.
|
|
45
|
+
let source = input;
|
|
46
|
+
let srcWidth;
|
|
47
|
+
let srcHeight;
|
|
48
|
+
if (opts.trim) {
|
|
49
|
+
try {
|
|
50
|
+
const t = await sharp(input).trim().toBuffer({ resolveWithObject: true });
|
|
51
|
+
source = t.data;
|
|
52
|
+
srcWidth = t.info.width;
|
|
53
|
+
srcHeight = t.info.height;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
const m = await sharp(input).metadata();
|
|
57
|
+
srcWidth = m.width ?? 0;
|
|
58
|
+
srcHeight = m.height ?? 0;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
const m = await sharp(input).metadata();
|
|
63
|
+
srcWidth = m.width ?? 0;
|
|
64
|
+
srcHeight = m.height ?? 0;
|
|
65
|
+
}
|
|
66
|
+
if (!srcWidth || !srcHeight) {
|
|
45
67
|
throw new Error("Could not read image dimensions.");
|
|
46
68
|
}
|
|
47
69
|
// Preserve aspect ratio. Each pixel is one column wide and (paired) one text
|
|
48
|
-
// row holds two pixels, so square pixels keep the picture's proportions.
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
70
|
+
// row holds two pixels, so square pixels keep the picture's proportions. With
|
|
71
|
+
// a height cap, scale to fit within width × (height rows) so a tall image
|
|
72
|
+
// stays inside the box instead of overflowing.
|
|
73
|
+
let targetW = width;
|
|
74
|
+
let targetH = Math.round(width * (srcHeight / srcWidth));
|
|
75
|
+
if (opts.height && opts.height > 0) {
|
|
76
|
+
const maxPxH = Math.floor(opts.height) * 2;
|
|
77
|
+
const scale = Math.min(width / srcWidth, maxPxH / srcHeight);
|
|
78
|
+
targetW = Math.max(1, Math.round(srcWidth * scale));
|
|
79
|
+
targetH = Math.round(srcHeight * scale);
|
|
80
|
+
}
|
|
81
|
+
targetH = Math.max(2, targetH);
|
|
82
|
+
if (targetH % 2 === 1)
|
|
83
|
+
targetH += 1; // even rows pair cleanly into half blocks
|
|
84
|
+
const { data, info } = await sharp(source)
|
|
85
|
+
.resize(targetW, targetH, { fit: "fill" })
|
|
54
86
|
.ensureAlpha()
|
|
55
87
|
.raw()
|
|
56
88
|
.toBuffer({ resolveWithObject: true });
|