beautiful-image 0.2.5 → 0.3.6
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 +99 -35
- package/dist/beautiful-image.cjs +1 -1
- package/dist/beautiful-image.js +1 -1
- package/dist/beautiful-image.node.cjs +1 -1
- package/dist/beautiful-image.node.js +1 -1
- package/dist/image-processor-BvZZwU85.js +124 -0
- package/dist/image-processor-SsMTFAkc.cjs +1 -0
- package/package.json +13 -4
- package/dist/image-processor-CfaoYWMt.js +0 -124
- package/dist/image-processor-hqbPBWVw.cjs +0 -1
- package/wasm/beautiful_image.d.ts +0 -46
- package/wasm/beautiful_image.js +0 -225
- package/wasm/beautiful_image_bg.wasm +0 -0
- package/wasm/beautiful_image_bg.wasm.d.ts +0 -10
package/README.md
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
1
|
# beautiful-image
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Compress and optimize images with minimal quality loss in the browser or on the server. Powered by Rust/WASM with zero native dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## How it works
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Most tools that compress images do it at the cost of visible quality degradation. `beautiful-image` combines resize, sharpening, and JPEG encoding tuned to produce the smallest file size while keeping the image looking sharp and clean.
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
- Image processing algorithms (sharpen, blur, etc.) that browsers don't have
|
|
12
|
-
- Fast execution via WebAssembly
|
|
13
|
-
- Battle-tested `image` crate with optimized implementations
|
|
14
|
-
|
|
15
|
-
## Use Cases
|
|
16
|
-
|
|
17
|
-
- **E-commerce** - Bulk optimize product images before upload
|
|
18
|
-
- **CMS/Blogs** - Process images on the client before saving
|
|
19
|
-
- **Social apps** - Apply filters and compress photos
|
|
20
|
-
- **Photo editors** - Real-time filter previews
|
|
21
|
-
- **Blurred previews** - Show blurred thumbnails before unlocking content
|
|
9
|
+
- **Browser** uses the native Canvas API for fast GPU-accelerated decode and resize, then hands off to WASM for sharpening and JPEG encoding
|
|
10
|
+
- **Node.js** runs the full pipeline in WASM (decode, resize, filters, encode), making it ideal for serverless environments like AWS Lambda or Google Cloud Functions with no native dependencies
|
|
22
11
|
|
|
23
12
|
## Install
|
|
24
13
|
|
|
@@ -26,37 +15,111 @@ Browsers can resize and encode JPEG natively, but **cannot apply filters** like
|
|
|
26
15
|
npm install beautiful-image
|
|
27
16
|
```
|
|
28
17
|
|
|
29
|
-
##
|
|
18
|
+
## Browser
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<input type="file" id="upload" accept="image/*" />
|
|
22
|
+
<img id="preview" />
|
|
23
|
+
```
|
|
30
24
|
|
|
31
25
|
```typescript
|
|
32
26
|
import { image } from 'beautiful-image'
|
|
33
27
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
.
|
|
28
|
+
const input = document.getElementById('upload') as HTMLInputElement
|
|
29
|
+
|
|
30
|
+
input.addEventListener('change', async () => {
|
|
31
|
+
const file = input.files?.[0]
|
|
32
|
+
if (!file) return
|
|
33
|
+
|
|
34
|
+
const result = await image(file)
|
|
35
|
+
.resize(1200)
|
|
36
|
+
.sharpen()
|
|
37
|
+
.toJpeg(80)
|
|
38
38
|
|
|
39
|
-
// result.blob
|
|
40
|
-
// result.originalSize
|
|
41
|
-
// result.optimizedSize
|
|
42
|
-
// result.compressionRatio
|
|
39
|
+
// result.blob — optimized image as Blob
|
|
40
|
+
// result.originalSize — original size in bytes
|
|
41
|
+
// result.optimizedSize — new size in bytes
|
|
42
|
+
// result.compressionRatio — 0.85 = 85% smaller
|
|
43
|
+
// result.width / result.height
|
|
44
|
+
|
|
45
|
+
document.getElementById('preview').src = URL.createObjectURL(result.blob)
|
|
46
|
+
})
|
|
43
47
|
```
|
|
44
48
|
|
|
45
|
-
|
|
49
|
+
For a full working demo see [`examples/web-demo`](./examples/web-demo).
|
|
50
|
+
|
|
51
|
+
## Node.js
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
import { image } from 'beautiful-image/node'
|
|
55
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
56
|
+
|
|
57
|
+
const input = readFileSync('./photo.jpg')
|
|
58
|
+
|
|
59
|
+
const result = await image(input)
|
|
60
|
+
.resize(1200)
|
|
61
|
+
.sharpen()
|
|
62
|
+
.toJpeg(80)
|
|
63
|
+
|
|
64
|
+
writeFileSync('./optimized.jpg', result.data)
|
|
65
|
+
|
|
66
|
+
// result.data — optimized image as Buffer
|
|
67
|
+
// result.originalSize — original size in bytes
|
|
68
|
+
// result.optimizedSize — new size in bytes
|
|
69
|
+
// result.compressionRatio — 0.85 = 85% smaller
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Lambda + S3 example
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3'
|
|
76
|
+
import { image } from 'beautiful-image/node'
|
|
77
|
+
|
|
78
|
+
const s3 = new S3Client({})
|
|
79
|
+
|
|
80
|
+
export const handler = async (event: any) => {
|
|
81
|
+
const bucket = event.Records[0].s3.bucket.name
|
|
82
|
+
const key = decodeURIComponent(event.Records[0].s3.object.key)
|
|
83
|
+
|
|
84
|
+
const { Body } = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }))
|
|
85
|
+
const input = Buffer.from(await Body!.transformToByteArray())
|
|
86
|
+
|
|
87
|
+
const result = await image(input).resize(1200).sharpen().toJpeg(80)
|
|
88
|
+
|
|
89
|
+
await s3.send(new PutObjectCommand({
|
|
90
|
+
Bucket: bucket,
|
|
91
|
+
Key: `optimized/${key}`,
|
|
92
|
+
Body: result.data,
|
|
93
|
+
ContentType: 'image/jpeg',
|
|
94
|
+
}))
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API
|
|
99
|
+
|
|
100
|
+
All methods are available in both browser and Node.js:
|
|
46
101
|
|
|
47
102
|
```typescript
|
|
48
103
|
image(file)
|
|
49
|
-
.resize(width)
|
|
50
|
-
.sharpen(sigma)
|
|
51
|
-
.blur(sigma)
|
|
52
|
-
.brightness(value)
|
|
53
|
-
.contrast(value)
|
|
54
|
-
.hueRotate(degrees)
|
|
55
|
-
.grayscale()
|
|
56
|
-
.invert()
|
|
57
|
-
.toJpeg(quality)
|
|
104
|
+
.resize(width) // resize maintaining aspect ratio
|
|
105
|
+
.sharpen(sigma) // default 1.5 — subtle to strong
|
|
106
|
+
.blur(sigma) // gaussian blur
|
|
107
|
+
.brightness(value) // -100 to 100
|
|
108
|
+
.contrast(value) // -100 to 100
|
|
109
|
+
.hueRotate(degrees) // -180 to 180
|
|
110
|
+
.grayscale() // black & white
|
|
111
|
+
.invert() // invert colors
|
|
112
|
+
.toJpeg(quality) // 1-100
|
|
58
113
|
```
|
|
59
114
|
|
|
115
|
+
## Use Cases
|
|
116
|
+
|
|
117
|
+
- **E-commerce** — Optimize product images before upload, saving storage and bandwidth
|
|
118
|
+
- **CMS/Blogs** — Process images on the client before saving, no server needed
|
|
119
|
+
- **Social apps** — Compress and filter photos before posting
|
|
120
|
+
- **Lambda/Cloud Functions** — Automatically optimize images on upload to S3 or cloud storage
|
|
121
|
+
- **Blurred previews** — Generate blurred thumbnails before unlocking content
|
|
122
|
+
|
|
60
123
|
## TODO
|
|
61
124
|
|
|
62
125
|
- [ ] More filters (sepia, vignette, noise)
|
|
@@ -65,3 +128,4 @@ image(file)
|
|
|
65
128
|
- [ ] Presets
|
|
66
129
|
- [ ] Web Worker support
|
|
67
130
|
- [ ] Batch processing
|
|
131
|
+
- [ ] `getImageDimensions()` return width/height from Node.js pipeline (`image::image_dimensions()` reads only the header, no full decode)
|
package/dist/beautiful-image.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./image-processor-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("./image-processor-SsMTFAkc.cjs");let h=!1;async function u(){h||(await a.__wbg_init(),h=!0)}class d extends a.ImageProcessor{constructor(n){super(n)}async toJpeg(n){await u();const t=await createImageBitmap(this.file),l=t.height/t.width,e=this.targetWidth?Math.min(this.targetWidth,t.width):t.width,s=Math.round(e*l),i=new OffscreenCanvas(e,s).getContext("2d");i.imageSmoothingEnabled=!0,i.imageSmoothingQuality="high",i.drawImage(t,0,0,e,s);const m=i.getImageData(0,0,e,s),p=new Uint8Array(m.data.buffer),r=a.processImage(p,e,s,n,this.ops.sharpenSigma??null,this.ops.sharpenThreshold??null,this.ops.blurSigma??null,this.ops.brightness??null,this.ops.contrast??null,this.ops.grayscale,this.ops.invert,this.ops.hueRotate??null);t.close();const c=this.file.size,g=r.length;return{blob:new Blob([new Uint8Array(r)],{type:"image/jpeg"}),originalSize:c,optimizedSize:g,compressionRatio:1-g/c,width:e,height:s}}}const w=o=>new d(o);exports.processImage=a.processImage;exports.image=w;
|
package/dist/beautiful-image.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("node:fs"),m=require("node:url"),l=require("node:path"),s=require("./image-processor-
|
|
1
|
+
"use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const c=require("node:fs"),m=require("node:url"),l=require("node:path"),s=require("./image-processor-SsMTFAkc.cjs");var i=typeof document<"u"?document.currentScript:null;const h=l.dirname(m.fileURLToPath(typeof document>"u"?require("url").pathToFileURL(__filename).href:i&&i.tagName.toUpperCase()==="SCRIPT"&&i.src||new URL("beautiful-image.node.cjs",document.baseURI).href));let a=!1;async function f(){if(!a){const t=l.join(h,"../wasm/beautiful_image_bg.wasm"),e=c.readFileSync(t);await s.__wbg_init({module_or_path:e}),a=!0}}class p extends s.ImageProcessor{constructor(e){super(e)}async toJpeg(e){await f();const u=new Uint8Array(this.file.buffer,this.file.byteOffset,this.file.byteLength),n=s.processImageFromBytes(u,this.targetWidth??null,e,this.ops.sharpenSigma??null,this.ops.sharpenThreshold??null,this.ops.blurSigma??null,this.ops.brightness??null,this.ops.contrast??null,this.ops.grayscale,this.ops.invert,this.ops.hueRotate??null),o=this.file.byteLength,r=n.length;return{data:Buffer.from(n),originalSize:o,optimizedSize:r,compressionRatio:1-r/o}}}const d=t=>new p(t);exports.processImageFromBytes=s.processImageFromBytes;exports.image=d;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync as n } from "node:fs";
|
|
2
2
|
import { fileURLToPath as l } from "node:url";
|
|
3
3
|
import { dirname as m, join as h } from "node:path";
|
|
4
|
-
import { I as p, a as u, _ as c } from "./image-processor-
|
|
4
|
+
import { I as p, a as u, _ as c } from "./image-processor-BvZZwU85.js";
|
|
5
5
|
const f = m(l(import.meta.url));
|
|
6
6
|
let r = !1;
|
|
7
7
|
async function g() {
|