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 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
- const pipeline = sharp(input);
43
- const meta = await pipeline.metadata();
44
- if (!meta.width || !meta.height) {
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
- let height = Math.max(2, Math.round(width * (meta.height / meta.width)));
50
- if (height % 2 === 1)
51
- height += 1; // even rows pair cleanly into half blocks
52
- const { data, info } = await pipeline
53
- .resize(width, height, { fit: "fill" })
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 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixelati",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Turn any image into truecolor terminal art using half-block characters.",
5
5
  "type": "module",
6
6
  "bin": {