cross-image 0.2.1 → 0.2.2

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.
@@ -13,6 +13,11 @@ exports.invert = invert;
13
13
  exports.grayscale = grayscale;
14
14
  exports.fillRect = fillRect;
15
15
  exports.crop = crop;
16
+ exports.boxBlur = boxBlur;
17
+ exports.gaussianBlur = gaussianBlur;
18
+ exports.sharpen = sharpen;
19
+ exports.sepia = sepia;
20
+ exports.medianFilter = medianFilter;
16
21
  /**
17
22
  * Composite one image on top of another at a specified position
18
23
  * @param base Base image data (RGBA)
@@ -240,3 +245,233 @@ function crop(data, width, height, x, y, cropWidth, cropHeight) {
240
245
  }
241
246
  return { data: result, width: actualWidth, height: actualHeight };
242
247
  }
248
+ /**
249
+ * Apply a box blur filter to an image
250
+ * @param data Image data (RGBA)
251
+ * @param width Image width
252
+ * @param height Image height
253
+ * @param radius Blur radius (default: 1)
254
+ * @returns New image data with box blur applied
255
+ */
256
+ function boxBlur(data, width, height, radius = 1) {
257
+ const result = new Uint8Array(data.length);
258
+ const clampedRadius = Math.max(1, Math.floor(radius));
259
+ for (let y = 0; y < height; y++) {
260
+ for (let x = 0; x < width; x++) {
261
+ let r = 0, g = 0, b = 0, a = 0;
262
+ let count = 0;
263
+ // Iterate over kernel
264
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
265
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
266
+ const px = x + kx;
267
+ const py = y + ky;
268
+ // Check bounds
269
+ if (px >= 0 && px < width && py >= 0 && py < height) {
270
+ const idx = (py * width + px) * 4;
271
+ r += data[idx];
272
+ g += data[idx + 1];
273
+ b += data[idx + 2];
274
+ a += data[idx + 3];
275
+ count++;
276
+ }
277
+ }
278
+ }
279
+ const outIdx = (y * width + x) * 4;
280
+ result[outIdx] = Math.round(r / count);
281
+ result[outIdx + 1] = Math.round(g / count);
282
+ result[outIdx + 2] = Math.round(b / count);
283
+ result[outIdx + 3] = Math.round(a / count);
284
+ }
285
+ }
286
+ return result;
287
+ }
288
+ /**
289
+ * Generate a Gaussian kernel for blur
290
+ * @param radius Kernel radius
291
+ * @param sigma Standard deviation (if not provided, calculated from radius)
292
+ * @returns Gaussian kernel as 1D array
293
+ */
294
+ function generateGaussianKernel(radius, sigma) {
295
+ const size = radius * 2 + 1;
296
+ const kernel = new Array(size);
297
+ const s = sigma ?? radius / 3;
298
+ const s2 = 2 * s * s;
299
+ let sum = 0;
300
+ for (let i = 0; i < size; i++) {
301
+ const x = i - radius;
302
+ kernel[i] = Math.exp(-(x * x) / s2);
303
+ sum += kernel[i];
304
+ }
305
+ // Normalize
306
+ for (let i = 0; i < size; i++) {
307
+ kernel[i] /= sum;
308
+ }
309
+ return kernel;
310
+ }
311
+ /**
312
+ * Apply Gaussian blur to an image
313
+ * @param data Image data (RGBA)
314
+ * @param width Image width
315
+ * @param height Image height
316
+ * @param radius Blur radius (default: 1)
317
+ * @param sigma Optional standard deviation (if not provided, calculated from radius)
318
+ * @returns New image data with Gaussian blur applied
319
+ */
320
+ function gaussianBlur(data, width, height, radius = 1, sigma) {
321
+ const clampedRadius = Math.max(1, Math.floor(radius));
322
+ const kernel = generateGaussianKernel(clampedRadius, sigma);
323
+ // Apply horizontal pass
324
+ const temp = new Uint8Array(data.length);
325
+ for (let y = 0; y < height; y++) {
326
+ for (let x = 0; x < width; x++) {
327
+ let r = 0, g = 0, b = 0, a = 0;
328
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
329
+ const px = Math.max(0, Math.min(width - 1, x + kx));
330
+ const idx = (y * width + px) * 4;
331
+ const weight = kernel[kx + clampedRadius];
332
+ r += data[idx] * weight;
333
+ g += data[idx + 1] * weight;
334
+ b += data[idx + 2] * weight;
335
+ a += data[idx + 3] * weight;
336
+ }
337
+ const outIdx = (y * width + x) * 4;
338
+ temp[outIdx] = Math.round(r);
339
+ temp[outIdx + 1] = Math.round(g);
340
+ temp[outIdx + 2] = Math.round(b);
341
+ temp[outIdx + 3] = Math.round(a);
342
+ }
343
+ }
344
+ // Apply vertical pass
345
+ const result = new Uint8Array(data.length);
346
+ for (let y = 0; y < height; y++) {
347
+ for (let x = 0; x < width; x++) {
348
+ let r = 0, g = 0, b = 0, a = 0;
349
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
350
+ const py = Math.max(0, Math.min(height - 1, y + ky));
351
+ const idx = (py * width + x) * 4;
352
+ const weight = kernel[ky + clampedRadius];
353
+ r += temp[idx] * weight;
354
+ g += temp[idx + 1] * weight;
355
+ b += temp[idx + 2] * weight;
356
+ a += temp[idx + 3] * weight;
357
+ }
358
+ const outIdx = (y * width + x) * 4;
359
+ result[outIdx] = Math.round(r);
360
+ result[outIdx + 1] = Math.round(g);
361
+ result[outIdx + 2] = Math.round(b);
362
+ result[outIdx + 3] = Math.round(a);
363
+ }
364
+ }
365
+ return result;
366
+ }
367
+ /**
368
+ * Apply sharpen filter to an image
369
+ * @param data Image data (RGBA)
370
+ * @param width Image width
371
+ * @param height Image height
372
+ * @param amount Sharpening amount (0-1, default: 0.5)
373
+ * @returns New image data with sharpening applied
374
+ */
375
+ function sharpen(data, width, height, amount = 0.5) {
376
+ const result = new Uint8Array(data.length);
377
+ const clampedAmount = Math.max(0, Math.min(1, amount));
378
+ // Sharpen kernel (Laplacian-based)
379
+ // Center weight is 1 + 4*amount, neighbors are -amount
380
+ const center = 1 + 4 * clampedAmount;
381
+ const neighbor = -clampedAmount;
382
+ for (let y = 0; y < height; y++) {
383
+ for (let x = 0; x < width; x++) {
384
+ const idx = (y * width + x) * 4;
385
+ let r = data[idx] * center;
386
+ let g = data[idx + 1] * center;
387
+ let b = data[idx + 2] * center;
388
+ // Apply kernel to neighbors (4-connected)
389
+ const neighbors = [
390
+ { dx: 0, dy: -1 }, // top
391
+ { dx: -1, dy: 0 }, // left
392
+ { dx: 1, dy: 0 }, // right
393
+ { dx: 0, dy: 1 }, // bottom
394
+ ];
395
+ for (const { dx, dy } of neighbors) {
396
+ const nx = x + dx;
397
+ const ny = y + dy;
398
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
399
+ const nIdx = (ny * width + nx) * 4;
400
+ r += data[nIdx] * neighbor;
401
+ g += data[nIdx + 1] * neighbor;
402
+ b += data[nIdx + 2] * neighbor;
403
+ }
404
+ }
405
+ result[idx] = Math.max(0, Math.min(255, Math.round(r)));
406
+ result[idx + 1] = Math.max(0, Math.min(255, Math.round(g)));
407
+ result[idx + 2] = Math.max(0, Math.min(255, Math.round(b)));
408
+ result[idx + 3] = data[idx + 3]; // Alpha unchanged
409
+ }
410
+ }
411
+ return result;
412
+ }
413
+ /**
414
+ * Apply sepia tone effect to an image
415
+ * @param data Image data (RGBA)
416
+ * @returns New image data with sepia tone applied
417
+ */
418
+ function sepia(data) {
419
+ const result = new Uint8Array(data.length);
420
+ for (let i = 0; i < data.length; i += 4) {
421
+ const r = data[i];
422
+ const g = data[i + 1];
423
+ const b = data[i + 2];
424
+ // Sepia transformation matrix
425
+ result[i] = Math.min(255, Math.round(r * 0.393 + g * 0.769 + b * 0.189));
426
+ result[i + 1] = Math.min(255, Math.round(r * 0.349 + g * 0.686 + b * 0.168));
427
+ result[i + 2] = Math.min(255, Math.round(r * 0.272 + g * 0.534 + b * 0.131));
428
+ result[i + 3] = data[i + 3]; // Alpha unchanged
429
+ }
430
+ return result;
431
+ }
432
+ /**
433
+ * Apply median filter to reduce noise
434
+ * @param data Image data (RGBA)
435
+ * @param width Image width
436
+ * @param height Image height
437
+ * @param radius Filter radius (default: 1)
438
+ * @returns New image data with median filter applied
439
+ */
440
+ function medianFilter(data, width, height, radius = 1) {
441
+ const result = new Uint8Array(data.length);
442
+ const clampedRadius = Math.max(1, Math.floor(radius));
443
+ for (let y = 0; y < height; y++) {
444
+ for (let x = 0; x < width; x++) {
445
+ const rValues = [];
446
+ const gValues = [];
447
+ const bValues = [];
448
+ const aValues = [];
449
+ // Collect values in kernel window
450
+ for (let ky = -clampedRadius; ky <= clampedRadius; ky++) {
451
+ for (let kx = -clampedRadius; kx <= clampedRadius; kx++) {
452
+ const px = x + kx;
453
+ const py = y + ky;
454
+ if (px >= 0 && px < width && py >= 0 && py < height) {
455
+ const idx = (py * width + px) * 4;
456
+ rValues.push(data[idx]);
457
+ gValues.push(data[idx + 1]);
458
+ bValues.push(data[idx + 2]);
459
+ aValues.push(data[idx + 3]);
460
+ }
461
+ }
462
+ }
463
+ // Sort and get median
464
+ rValues.sort((a, b) => a - b);
465
+ gValues.sort((a, b) => a - b);
466
+ bValues.sort((a, b) => a - b);
467
+ aValues.sort((a, b) => a - b);
468
+ const mid = Math.floor(rValues.length / 2);
469
+ const outIdx = (y * width + x) * 4;
470
+ result[outIdx] = rValues[mid];
471
+ result[outIdx + 1] = gValues[mid];
472
+ result[outIdx + 2] = bValues[mid];
473
+ result[outIdx + 3] = aValues[mid];
474
+ }
475
+ }
476
+ return result;
477
+ }