@visualizevalue/img-grid 0.1.3 → 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,7 +30,9 @@ 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', // Background and letterbox color
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
34
36
  pixelated: true, // Nearest-neighbour resize — keeps pixel art crisp
35
37
  format: 'webp', // Output as png (default), jpeg, or webp
36
38
  quality: 80, // Quality for jpeg/webp
@@ -45,6 +47,7 @@ writeFileSync('grid.png', buffer)
45
47
 
46
48
  - Packs images into a compact, near-square grid
47
49
  - Supports highlighting specific images (makes them 2×2)
50
+ - Configurable `background` color, `padding` around the grid, and `gutter` between cells
48
51
  - Optional nearest-neighbour scaling to keep pixel art crisp (`pixelated`)
49
52
  - Downloads images concurrently (with a configurable limit)
50
53
  - Leaves a blank cell for failed downloads instead of failing the grid
@@ -58,7 +61,9 @@ writeFileSync('grid.png', buffer)
58
61
  | `highlight` | `[]` | Array of image IDs to highlight (make 2×2) |
59
62
  | `maxWidth` | `1920` | Maximum width of output image in pixels |
60
63
  | `concurrency` | `10` | Maximum concurrent image downloads |
61
- | `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 |
62
67
  | `pixelated` | `false` | Nearest-neighbour resize; keeps pixel art crisp |
63
68
  | `format` | `'png'` | Output format: `'png'`, `'jpeg'`, or `'webp'` |
64
69
  | `quality` | sharp's | Quality (1–100) for `jpeg`/`webp`; ignored for png |
package/dist/index.d.ts CHANGED
@@ -10,8 +10,15 @@ 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;
15
22
  /**
16
23
  * Resize with nearest-neighbour instead of the default smooth (lanczos)
17
24
  * kernel. Keeps pixel art (e.g. NFTs) crisp instead of blurring it when
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;
@@ -20,25 +22,38 @@ const HIGHLIGHT_SPAN = 2;
20
22
  */
21
23
  async function grid(images, opts = {}) {
22
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;
46
+ const size = spanSize(placement.span);
34
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
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visualizevalue/img-grid",
3
- "version": "0.1.3",
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",