@visualizevalue/img-grid 0.1.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 ADDED
@@ -0,0 +1,53 @@
1
+ # img-grid
2
+
3
+ Generate image grids from URLs (and highlight images).
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @visualizevalue/img-grid
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```javascript
14
+ import { grid } from '@visualizevalue/img-grid'
15
+ import { writeFileSync } from 'fs'
16
+
17
+ // Define image URLs with optional IDs
18
+ const images = [
19
+ { url: 'https://example.com/image1.jpg', id: 'img1' },
20
+ { url: 'https://example.com/image2.jpg', id: 'img2' },
21
+ { url: 'https://example.com/image3.jpg', id: 'img3' },
22
+ { url: 'https://example.com/image4.jpg', id: 'img4' },
23
+ ]
24
+
25
+ // Create a grid with default options
26
+ const buffer = await grid(images)
27
+
28
+ // Or highlight specific images (makes them 2×2)
29
+ const highlightedBuffer = await grid(images, {
30
+ highlight: ['img2', 'img3'], // Images with these IDs will be 2×2
31
+ maxWidth: 1920, // Maximum width of output image
32
+ concurrency: 10, // Maximum concurrent downloads
33
+ })
34
+
35
+ // Save to file
36
+ writeFileSync('grid.png', buffer)
37
+ ```
38
+
39
+ ## Features
40
+
41
+ - Arranges images in an optimal grid layout
42
+ - Supports highlighting specific images (makes them 2×2)
43
+ - Handles failed image downloads with placeholders
44
+ - Limits concurrent downloads
45
+ - Resizes all images to fit grid cells
46
+
47
+ ## Options
48
+
49
+ | Option | Default | Description |
50
+ | ------------- | ------- | ------------------------------------------ |
51
+ | `highlight` | `[]` | Array of image IDs to highlight (make 2×2) |
52
+ | `maxWidth` | `1920` | Maximum width of output image in pixels |
53
+ | `concurrency` | `10` | Maximum concurrent image downloads |
@@ -0,0 +1,10 @@
1
+ export interface Img {
2
+ url: string;
3
+ id?: string;
4
+ }
5
+ export interface GridOptions {
6
+ highlight?: string[];
7
+ maxWidth?: number;
8
+ concurrency?: number;
9
+ }
10
+ export declare function grid(images: Img[], opts?: GridOptions): Promise<Buffer>;
package/dist/index.js ADDED
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.grid = grid;
7
+ const sharp_1 = __importDefault(require("sharp"));
8
+ async function grid(images, opts = {}) {
9
+ const highlightIds = new Set(opts.highlight ?? []);
10
+ const concurrency = opts.concurrency ?? 10;
11
+ const highlightScale = 2;
12
+ const maxWidth = opts.maxWidth ?? 1920;
13
+ const highlightedImages = images.filter((img) => img.id && highlightIds.has(img.id));
14
+ const normalImages = images.filter((img) => !highlightIds.has(img.id ?? ''));
15
+ const totalCells = normalImages.length + highlightedImages.length * highlightScale * highlightScale;
16
+ const gridSize = Math.ceil(Math.sqrt(totalCells));
17
+ const cellSize = Math.floor(maxWidth / gridSize);
18
+ const highlightedCellSize = cellSize * highlightScale;
19
+ const gridWidth = Math.max(gridSize, Math.ceil(Math.sqrt(totalCells * 1.5)));
20
+ const gridHeight = gridSize * highlightScale;
21
+ const grid = Array(gridHeight)
22
+ .fill(0)
23
+ .map(() => Array(gridWidth).fill(false));
24
+ function isFree(x, y, width, height) {
25
+ for (let dy = 0; dy < height; dy++) {
26
+ for (let dx = 0; dx < width; dx++) {
27
+ if (y + dy >= gridHeight || x + dx >= gridWidth || grid[y + dy][x + dx]) {
28
+ return false;
29
+ }
30
+ }
31
+ }
32
+ return true;
33
+ }
34
+ function markOccupied(x, y, width, height) {
35
+ for (let dy = 0; dy < height; dy++) {
36
+ for (let dx = 0; dx < width; dx++) {
37
+ grid[y + dy][x + dx] = true;
38
+ }
39
+ }
40
+ }
41
+ function findSpot(width, height) {
42
+ for (let y = 0; y <= gridHeight - height; y++) {
43
+ for (let x = 0; x <= gridWidth - width; x++) {
44
+ if (isFree(x, y, width, height)) {
45
+ return [x, y];
46
+ }
47
+ }
48
+ }
49
+ return null;
50
+ }
51
+ let inFlight = 0;
52
+ async function fetchImage(url, size) {
53
+ while (inFlight >= concurrency) {
54
+ await new Promise((resolve) => setTimeout(resolve, 100));
55
+ }
56
+ inFlight++;
57
+ try {
58
+ const response = await fetch(url, {
59
+ signal: AbortSignal.timeout(15000),
60
+ headers: { Accept: 'image/*' },
61
+ });
62
+ if (!response.ok)
63
+ throw new Error(`HTTP error ${response.status}`);
64
+ const data = await response.arrayBuffer();
65
+ return (0, sharp_1.default)(Buffer.from(data))
66
+ .resize(size, size, { fit: 'contain', background: '#000' })
67
+ .toBuffer();
68
+ }
69
+ catch (error) {
70
+ // return placeholder if error
71
+ return (0, sharp_1.default)({
72
+ create: {
73
+ width: size,
74
+ height: size,
75
+ channels: 3,
76
+ background: '#007F00',
77
+ },
78
+ })
79
+ .png()
80
+ .toBuffer();
81
+ }
82
+ finally {
83
+ inFlight--;
84
+ }
85
+ }
86
+ const compositeLayers = [];
87
+ let maxY = 0;
88
+ for (const img of highlightedImages) {
89
+ const spot = findSpot(highlightScale, highlightScale);
90
+ if (!spot)
91
+ continue;
92
+ const [x, y] = spot;
93
+ markOccupied(x, y, highlightScale, highlightScale);
94
+ const imageBuffer = await fetchImage(img.url, highlightedCellSize);
95
+ compositeLayers.push({
96
+ input: imageBuffer,
97
+ left: x * cellSize,
98
+ top: y * cellSize,
99
+ });
100
+ maxY = Math.max(maxY, y + highlightScale);
101
+ }
102
+ for (const img of normalImages) {
103
+ const spot = findSpot(1, 1);
104
+ if (!spot)
105
+ break;
106
+ const [x, y] = spot;
107
+ markOccupied(x, y, 1, 1);
108
+ const imageBuffer = await fetchImage(img.url, cellSize);
109
+ compositeLayers.push({
110
+ input: imageBuffer,
111
+ left: x * cellSize,
112
+ top: y * cellSize,
113
+ });
114
+ maxY = Math.max(maxY, y + 1);
115
+ }
116
+ const outputHeight = Math.max(1, maxY) * cellSize;
117
+ const outputWidth = gridWidth * cellSize;
118
+ return (0, sharp_1.default)({
119
+ create: {
120
+ width: outputWidth,
121
+ height: outputHeight,
122
+ channels: 3,
123
+ background: '#000',
124
+ },
125
+ })
126
+ .composite(compositeLayers)
127
+ .png()
128
+ .toBuffer();
129
+ }
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "@visualizevalue/img-grid",
3
+ "version": "0.1.0",
4
+ "description": "Generate a PNG grid from image URLs (and highlight images).",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": "./dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "packageManager": "npm@11.4.0",
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "format": "prettier --write",
17
+ "prepublishOnly": "npm run build"
18
+ },
19
+ "dependencies": {
20
+ "sharp": "^0.33"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^20",
24
+ "prettier": "^3.5.3",
25
+ "tsx": "^4",
26
+ "typescript": "^5"
27
+ }
28
+ }