Skip to content

Commit 604450c

Browse files
matmenclaude
andcommitted
Image#region: a region as a new image, the image unchanged
Like crop on a clone, without copying the whole image first (it was done as new Image(w, h).composite(source, -x, -y)). Clipped like crop, whose bounds it shares; on a frame it gives a plain image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BsU2GDnECphmYTjLB5UeZv
1 parent 55733e1 commit 604450c

4 files changed

Lines changed: 55 additions & 2 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
## 3.7.1
44

5+
### Features
6+
7+
- `Image#region(x, y, width, height)` copies a region into a new image and leaves the image as it is (like `crop`
8+
on a clone, without copying the whole image). It's clipped like `crop`; on a frame it gives a plain image.
9+
510
### Improvements
611

712
- Optimized lossy WebP animations are smaller: like libwebp's, they leave out pixels that changed by up to a tolerance

‎README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ deno add jsr:@matmen/imagescript # Deno (npm:imagescript works too)
5151
fonts and color emoji)
5252
- [Resizing](https://imagescript.matmen.dev/classes/Image.html#resize) (bilinear, bicubic, area averaging and nearest neighbor)
5353
- Image manipulation functions ([crop](https://imagescript.matmen.dev/classes/Image.html#crop)
54+
, [region](https://imagescript.matmen.dev/classes/Image.html#region)
5455
, [rotate](https://imagescript.matmen.dev/classes/Image.html#rotate)
5556
, [cropCircle](https://imagescript.matmen.dev/classes/Image.html#cropcircle)
5657
, [roundCorners](https://imagescript.matmen.dev/classes/Image.html#roundcorners)

‎src/image.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,28 @@ export class Image {
884884
* @param height The new images height
885885
*/
886886
crop(x: number, y: number, width: number, height: number): this {
887+
return this.__apply__(this.__crop__(...this.__crop_bounds__(x, y, width, height)));
888+
}
889+
890+
/**
891+
* Copies a region of the image into a new image, this one stays unchanged (like {@link Image.crop} on a clone,
892+
* without copying the whole image). The region is clipped to the image, so the result can be smaller than the
893+
* requested size
894+
* @param x The x offset (0-based)
895+
* @param y The y offset (0-based)
896+
* @param width The region's width
897+
* @param height The region's height
898+
*/
899+
region(x: number, y: number, width: number, height: number): Image {
900+
return this.__crop__(...this.__crop_bounds__(x, y, width, height));
901+
}
902+
903+
/**
904+
* A crop region clipped to the image as [x, y, width, height]
905+
* @internal
906+
* @ignore
907+
*/
908+
protected __crop_bounds__(x: number, y: number, width: number, height: number): [number, number, number, number] {
887909
x = ~~finite(x, 'x');
888910
y = ~~finite(y, 'y');
889911
width = ~~finite(width, 'width');
@@ -894,7 +916,7 @@ export class Image {
894916
if (right <= left || bottom <= top)
895917
throw new RangeError('Crop region does not overlap the image');
896918

897-
return this.__apply__(this.__crop__(left, top, right - left, bottom - top));
919+
return [left, top, right - left, bottom - top];
898920
}
899921

900922
/**

‎tests/crop.js‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
const fs = require('fs').promises;
2-
const {Image} = require('..');
2+
const {Image, Frame} = require('..');
33
const samePixels = require('./util/pixels');
44

55
(async () => {
@@ -14,4 +14,29 @@ const samePixels = require('./util/pixels');
1414

1515
const target = await fs.readFile('./tests/targets/crop.png');
1616
if (!await samePixels(target, encoded)) process.exit(1);
17+
18+
// region: the same pixels as a new image, the source unchanged; clipped like crop
19+
const panic = message => {
20+
console.error(message);
21+
process.exit(1);
22+
};
23+
const source = new Image(512, 512).fill((x, y) => Image.hslToColor((x + y) / 1024, 1, .5));
24+
const before = Buffer.from(source.bitmap);
25+
const region = source.region(128, 128, 128, 128);
26+
if (!await samePixels(target, await region.encode(1, {creationTime: 0, software: ''}))) panic('region: wrong pixels');
27+
if (!Buffer.from(source.bitmap).equals(before)) panic('region: changed the source');
28+
region.fill(0);
29+
if (!Buffer.from(source.bitmap).equals(before)) panic('region: shares memory with the source');
30+
31+
const clipped = source.region(-10, 500, 30, 30);
32+
if (clipped.width !== 20 || clipped.height !== 12) panic(`region: clipped to ${clipped.width}x${clipped.height} instead of 20x12`);
33+
if (clipped.getPixelAt(1, 1) !== source.getPixelAt(1, 501)) panic('region: clipped region has wrong pixels');
34+
let threw = false;
35+
try { source.region(600, 0, 10, 10); } catch (err) { threw = err instanceof RangeError; }
36+
if (!threw) panic('region: a region outside the image should throw a RangeError');
37+
38+
// a frame's region is a plain image, the frame stays as it is
39+
const frame = Frame.from(source, {duration: 100, xOffset: 4, yOffset: 6});
40+
const part = frame.region(0, 0, 10, 10);
41+
if (part instanceof Frame || part.width !== 10 || frame.width !== 512 || frame.xOffset !== 4) panic('region: frames');
1742
})();

0 commit comments

Comments
 (0)