Skip to content
This repository was archived by the owner on May 30, 2025. It is now read-only.

Commit b800a27

Browse files
committed
feat: Image.scale — resize images without cropping
The `scale` function can be used to scale images to a `width` and `height` using several different `fit` modes: * `Image.fit.contain` (default): Preserving aspect ratio, resize the image to be as large as possible while ensuring its dimensions are less than or equal to the `width` and `height` specified. * `Image.fit.cover`: Preserving aspect ratio, resize the image to be as small as possible while ensuring its dimensions are greater than or equal to the `width` and `height` specified. * `Image.fit.fill`: Ignoring the aspect ratio of the input, stretch the image to the exact `width` and/or `height` provided. `scale` will not expand images by default, the `allowEnlargement` option tells it to expand to fill the provided `width` and `height`.
1 parent 8346570 commit b800a27

File tree

5 files changed

+123
-8
lines changed

5 files changed

+123
-8
lines changed

examples/watermark-image/index.js

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,31 @@
11
import { Image } from '@fly/image'
22

3-
const pictureURL = "https://raw.githubusercontent.com/superfly/fly/147f2a327dc76ce6cf10c46b7ea1c19a9d8f2d87/v8env/test/fixtures/picture.jpg"
4-
const logoURL = "https://raw.githubusercontent.com/superfly/fly/147f2a327dc76ce6cf10c46b7ea1c19a9d8f2d87/v8env/test/fixtures/overlay.png"
3+
const pictureURL = "https://raw.githubusercontent.com/superfly/fly/075939824c66c3db38f94d552138a4802e0c3838/tests/v8env/tests/fixtures/picture.jpg"
4+
const logoURL = "https://raw.githubusercontent.com/superfly/fly/075939824c66c3db38f94d552138a4802e0c3838/tests/v8env/tests/fixtures/overlay.png"
55

66
fly.http.respondWith(async function (req) {
77
const url = new URL(req.url)
88

99
if (url.pathname == "/picture.jpg") {
10-
return watermarkPicture(req.headers.get("accept"))
10+
return watermarkPicture(url.searchParams.get('width'), url.searchParams.get('height'))
1111
}
1212

1313
return new Response("not found", { status: 404 })
1414
})
1515

16-
async function watermarkPicture() {
17-
const [picture, logo] = await Promise.all([
16+
async function watermarkPicture(width, height) {
17+
let [picture, logo] = await Promise.all([
1818
loadImage(pictureURL),
1919
loadImage(logoURL)
2020
])
2121

22+
if(width || height){
23+
width = width ? parseInt(width) : width
24+
height = height ? parseInt(height) : height
25+
26+
picture = await picture.scale(width, height, { allowEnlargement: true}).toImage()
27+
}
28+
2229
const meta = picture.metadata()
2330

2431
const padPct = 0.1

packages/core/src/bridge/fly/image.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type imageOperation = (...args: any[]) => sharp.SharpInstance
1616

1717
const allowedOperations: Map<string, imageOperation> = new Map([
1818
["resize", sharp.prototype.resize],
19+
["scale", scale],
1920
["crop", sharp.prototype.crop],
2021
["embed", sharp.prototype.embed],
2122
["background", sharp.prototype.background],
@@ -172,3 +173,49 @@ function refToImage(ref: ivm.Reference<sharp.SharpInstance>) {
172173

173174
return img
174175
}
176+
177+
async function scale(this: sharp.SharpInstance, ...args: any[]) {
178+
const opts = typeof args[args.length - 1] === "object" ? args[args.length - 1] : undefined
179+
const sharpOpts = {
180+
kernel: sharp.kernel.lanczos3,
181+
fastShrinkOnLoad: true
182+
}
183+
184+
if (opts) {
185+
sharpOpts.kernel = opts.kernel
186+
sharpOpts.fastShrinkOnLoad = opts.fastShrinkOnLoad
187+
}
188+
189+
let width = typeof args[0] === "number" ? args[0] : undefined
190+
let height = typeof args[1] === "number" ? args[1] : undefined
191+
const ignoreAspectRatio = typeof opts === "object" && opts.ignoreAspectRatio === true
192+
const withoutEnlargement = typeof opts === "object" && opts.allowEnlargement === false
193+
194+
195+
if (!width || !height) {
196+
const meta = await this.metadata()
197+
width = relativeDimension(width, meta.width || 0, height, meta.height || 0)
198+
height = relativeDimension(height, meta.height || 0, width, meta.width || 0)
199+
}
200+
201+
this.resize(width, height, sharpOpts)
202+
203+
if (ignoreAspectRatio === true) {
204+
this.ignoreAspectRatio()
205+
} else {
206+
this.max()
207+
}
208+
if (withoutEnlargement) {
209+
this.withoutEnlargement()
210+
}
211+
212+
return this
213+
}
214+
215+
function relativeDimension(x: number | undefined, original: number, other: number, basis: number) {
216+
if (x && typeof x === "number") return x
217+
218+
const scale = other / basis
219+
220+
return Math.ceil(scale * original)
221+
}

packages/core/src/v8env.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class V8Environment extends EventEmitter {
3737
constructor() {
3838
super()
3939
this.bootstrapped = false
40-
if (!v8EnvCode) { throw new Error("v8env not found, please run npm build to generate it") }
40+
if (!v8EnvCode) { throw new Error("v8env not found, please run `yarn bundle` to generate it") }
4141
}
4242

4343
get isReady() {

packages/v8env/src/fly/image/index.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,27 @@ export class Image {
4949
* Pass `undefined` or `null` to auto-scale the width to match the height.
5050
* @param height Height in pixels of the resulting image.
5151
* Pass `undefind` or `null` to auto-scale the height to match the width.
52-
* @param options Resize options}
52+
* @param options Resize options
5353
*/
5454
public resize(width?: number, height?: number, options?: Image.ResizeOptions) {
5555
this._imageOperation("resize", width, height, options)
5656
return this
5757
}
5858

59+
/**
60+
* Scale image to `width` x `height`. This does not crop the image, it scales it to fit
61+
* the specified width and height, and keeps the aspect ratio by default.
62+
* @param width Width in pixels of the resulting image.
63+
* Pass `undefined` or `null` to auto-scale the width to match the height.
64+
* @param height Height in pixels of the resulting image.
65+
* Pass `undefind` or `null` to auto-scale the height to match the width.
66+
* @param options Scale options
67+
*/
68+
public scale(width?: number, height?: number, options?: Image.ScaleOptions) {
69+
this._imageOperation('scale', width, height, options)
70+
return this
71+
}
72+
5973
/**
6074
* Overlay (composite) an image over the processed (resized, extracted etc.) image.
6175
*
@@ -373,6 +387,9 @@ export namespace Image {
373387
force?: boolean
374388
}
375389

390+
/**
391+
* Options for resizing an image
392+
*/
376393
export interface ResizeOptions {
377394
/**
378395
* the kernel to use for image reduction.
@@ -384,7 +401,26 @@ export namespace Image {
384401
* and WebP shrink-on-load feature, which can lead to a slight moiré pattern on
385402
* some images. (optional, default `true`)
386403
*/
387-
fastShrinkOnLoad?: boolean
404+
fastShrinkOnLoad?: boolean,
405+
}
406+
407+
/**
408+
* Options for scaling an image (see crop for cropping)
409+
*/
410+
export interface ScaleOptions extends ResizeOptions {
411+
/**
412+
* Stretch image if resize dimensions are larger than the source image.
413+
*
414+
* Defaults to `true`
415+
*/
416+
allowEnlargement?: boolean,
417+
418+
/**
419+
* Resize to exactly the width specified, changing aspect ratio if necessary
420+
*
421+
* Defaults to `false`
422+
*/
423+
ignoreAspectRatio?: boolean
388424
}
389425

390426
export interface OverlayOptions {

tests/v8env/tests/image.spec.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,31 @@ describe("Image", () => {
3535
expect(img2.info.format).to.eq("webp")
3636
})
3737

38+
describe("scale()", () => {
39+
it("shrinks to fit, maintains aspect ratio", async () =>{
40+
const img = new Image(logo)
41+
const meta = img.metadata()
42+
43+
const scaled = await img.scale(500, 1000).toImage()
44+
const scaledMeta = scaled.metadata()
45+
46+
expect(scaledMeta.width).to.eq(500)
47+
expect(scaledMeta.height).to.eq(500)
48+
49+
expect(meta.width / scaledMeta.width).to.eq(meta.height / scaledMeta.height)
50+
})
51+
52+
it('fills dimensions, ignores aspect ratio', async () => {
53+
const img = new Image(logo)
54+
55+
const scaled = await img.scale(500, 1000, { ignoreAspectRatio: true }).toImage()
56+
const scaledMeta = scaled.metadata()
57+
58+
expect(scaledMeta.width).to.eq(500)
59+
expect(scaledMeta.height).to.eq(1000)
60+
})
61+
})
62+
3863
it("flatten()", async () => {
3964
const img = new Image(logo)
4065
const img2 = await img

0 commit comments

Comments
 (0)