@visualizevalue/img-grid 0.1.2 → 0.1.4

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
@@ -30,6 +30,10 @@ const highlightedBuffer = await grid(images, {
30
30
  highlight: ['img2', 'img3'], // Images with these IDs will be 2×2
31
31
  maxWidth: 1920, // Maximum width of output image
32
32
  concurrency: 10, // Maximum concurrent downloads
33
+ background: '#000', // Fills background, padding, gutters, and letterboxing
34
+ padding: 16, // Space around the whole grid, in pixels
35
+ gutter: 8, // Gap between cells (rows and columns), in pixels
36
+ pixelated: true, // Nearest-neighbour resize — keeps pixel art crisp
33
37
  format: 'webp', // Output as png (default), jpeg, or webp
34
38
  quality: 80, // Quality for jpeg/webp
35
39
  onError: (img, err) => console.warn(`failed: ${img.url}`, err),
@@ -43,6 +47,8 @@ writeFileSync('grid.png', buffer)
43
47
 
44
48
  - Packs images into a compact, near-square grid
45
49
  - Supports highlighting specific images (makes them 2×2)
50
+ - Configurable `background` color, `padding` around the grid, and `gutter` between cells
51
+ - Optional nearest-neighbour scaling to keep pixel art crisp (`pixelated`)
46
52
  - Downloads images concurrently (with a configurable limit)
47
53
  - Leaves a blank cell for failed downloads instead of failing the grid
48
54
  - Output as PNG, JPEG, or WebP
@@ -55,7 +61,10 @@ writeFileSync('grid.png', buffer)
55
61
  | `highlight` | `[]` | Array of image IDs to highlight (make 2×2) |
56
62
  | `maxWidth` | `1920` | Maximum width of output image in pixels |
57
63
  | `concurrency` | `10` | Maximum concurrent image downloads |
58
- | `background` | `'#000'` | Background and letterbox color (any sharp color) |
64
+ | `background` | `'#000'` | Fills background, padding, gutters, letterboxing |
65
+ | `padding` | `0` | Space around the whole grid, in pixels |
66
+ | `gutter` | `0` | Gap between cells (rows and columns), in pixels |
67
+ | `pixelated` | `false` | Nearest-neighbour resize; keeps pixel art crisp |
59
68
  | `format` | `'png'` | Output format: `'png'`, `'jpeg'`, or `'webp'` |
60
69
  | `quality` | sharp's | Quality (1–100) for `jpeg`/`webp`; ignored for png |
61
70
  | `onError` | `undefined` | `(img, error) => void` called when an image fails |
package/dist/index.d.ts CHANGED
@@ -10,8 +10,21 @@ export interface GridOptions {
10
10
  maxWidth?: number;
11
11
  /** Maximum number of images downloaded at once. */
12
12
  concurrency?: number;
13
- /** Background and letterbox color (any sharp-compatible color). */
13
+ /**
14
+ * Color filling the background, padding, gutters, and image letterboxing
15
+ * (any sharp-compatible color).
16
+ */
14
17
  background?: string;
18
+ /** Space around the whole grid, in pixels. Filled with `background`. */
19
+ padding?: number;
20
+ /** Gap between cells (rows and columns), in pixels. Filled with `background`. */
21
+ gutter?: number;
22
+ /**
23
+ * Resize with nearest-neighbour instead of the default smooth (lanczos)
24
+ * kernel. Keeps pixel art (e.g. NFTs) crisp instead of blurring it when
25
+ * scaled up. Like CSS `image-rendering: pixelated`.
26
+ */
27
+ pixelated?: boolean;
15
28
  /** Output image format. */
16
29
  format?: ImageFormat;
17
30
  /** Quality (1–100) for lossy formats (`jpeg`, `webp`); ignored for `png`. */
package/dist/index.js CHANGED
@@ -8,6 +8,8 @@ const sharp_1 = __importDefault(require("sharp"));
8
8
  const DEFAULT_MAX_WIDTH = 1920;
9
9
  const DEFAULT_CONCURRENCY = 10;
10
10
  const DEFAULT_BACKGROUND = '#000';
11
+ const DEFAULT_PADDING = 0;
12
+ const DEFAULT_GUTTER = 0;
11
13
  const DEFAULT_FORMAT = 'png';
12
14
  const FETCH_TIMEOUT_MS = 15_000;
13
15
  const HIGHLIGHT_SPAN = 2;
@@ -19,26 +21,39 @@ const HIGHLIGHT_SPAN = 2;
19
21
  * `onError`) instead of failing the whole grid.
20
22
  */
21
23
  async function grid(images, opts = {}) {
22
- const { highlight = [], maxWidth = DEFAULT_MAX_WIDTH, concurrency = DEFAULT_CONCURRENCY, background = DEFAULT_BACKGROUND, format = DEFAULT_FORMAT, quality, onError, } = opts;
24
+ const { highlight = [], maxWidth = DEFAULT_MAX_WIDTH, concurrency = DEFAULT_CONCURRENCY, background = DEFAULT_BACKGROUND, pixelated = false, format = DEFAULT_FORMAT, quality, onError, } = opts;
25
+ const padding = Math.max(0, Math.floor(opts.padding ?? DEFAULT_PADDING));
26
+ const gutter = Math.max(0, Math.floor(opts.gutter ?? DEFAULT_GUTTER));
23
27
  const highlightIds = new Set(highlight);
24
28
  const isHighlighted = (img) => img.id !== undefined && highlightIds.has(img.id);
25
29
  const highlighted = images.filter(isHighlighted);
26
30
  const normal = images.filter((img) => !isHighlighted(img));
27
31
  const totalCells = normal.length + highlighted.length * HIGHLIGHT_SPAN ** 2;
28
- if (totalCells === 0)
29
- return encode(blankCanvas(1, 1, background), format, quality);
32
+ if (totalCells === 0) {
33
+ const side = Math.max(1, 2 * padding);
34
+ return encode(blankCanvas(side, side, background), format, quality);
35
+ }
30
36
  const { columns, rows, placements } = chooseLayout(highlighted, normal);
31
- const cellSize = Math.max(1, Math.floor(maxWidth / columns));
37
+ // Carve padding and inter-column gutters out of the width budget before
38
+ // dividing what's left among the columns, so the output never exceeds maxWidth.
39
+ const available = maxWidth - 2 * padding - (columns - 1) * gutter;
40
+ const cellSize = Math.max(1, Math.floor(available / columns));
41
+ // Top-left pixel of cell (col, row); a span-N block also covers the N−1
42
+ // gutters between the cells it spans, so it reads as one solid larger tile.
43
+ const offset = (index) => padding + index * (cellSize + gutter);
44
+ const spanSize = (span) => span * cellSize + (span - 1) * gutter;
32
45
  const layers = await mapWithConcurrency(placements, concurrency, async (placement) => {
33
- const size = cellSize * placement.span;
34
- const cell = await renderCell(placement.img, size, background, onError);
46
+ const size = spanSize(placement.span);
47
+ const cell = await renderCell(placement.img, size, background, pixelated, onError);
35
48
  return {
36
49
  input: cell,
37
- left: placement.col * cellSize,
38
- top: placement.row * cellSize,
50
+ left: offset(placement.col),
51
+ top: offset(placement.row),
39
52
  };
40
53
  });
41
- const canvas = blankCanvas(columns * cellSize, rows * cellSize, background).composite(layers);
54
+ const width = 2 * padding + columns * cellSize + (columns - 1) * gutter;
55
+ const height = 2 * padding + rows * cellSize + (rows - 1) * gutter;
56
+ const canvas = blankCanvas(width, height, background).composite(layers);
42
57
  return encode(canvas, format, quality);
43
58
  }
44
59
  /**
@@ -117,7 +132,7 @@ function pack(highlighted, normal, columns) {
117
132
  return { placements, rows: occupied.length };
118
133
  }
119
134
  /** Fetch and resize a single image; fall back to a blank cell on any failure. */
120
- async function renderCell(img, size, background, onError) {
135
+ async function renderCell(img, size, background, pixelated, onError) {
121
136
  try {
122
137
  const response = await fetch(img.url, {
123
138
  signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
@@ -127,7 +142,11 @@ async function renderCell(img, size, background, onError) {
127
142
  throw new Error(`HTTP ${response.status} for ${img.url}`);
128
143
  const data = Buffer.from(await response.arrayBuffer());
129
144
  return await (0, sharp_1.default)(data)
130
- .resize(size, size, { fit: 'contain', background })
145
+ .resize(size, size, {
146
+ fit: 'contain',
147
+ background,
148
+ ...(pixelated ? { kernel: 'nearest' } : {}),
149
+ })
131
150
  .png()
132
151
  .toBuffer();
133
152
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visualizevalue/img-grid",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Generate an image grid (PNG, JPEG, or WebP) from image URLs (and highlight images).",
5
5
  "license": "MIT",
6
6
  "author": "Visualize Value",