bare-media 1.8.0 → 2.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 +91 -58
- package/index.js +3 -5
- package/package.json +29 -20
- package/src/codecs.js +28 -0
- package/src/image.js +262 -0
- package/src/util.js +30 -0
- package/src/video.js +112 -0
- package/types.js +38 -0
- package/client.js +0 -78
- package/shared/codecs.js +0 -26
- package/shared/spec/build.js +0 -34
- package/shared/spec/constants.js +0 -2
- package/shared/spec/hrpc/hrpc.json +0 -44
- package/shared/spec/hrpc/index.js +0 -164
- package/shared/spec/hrpc/messages.js +0 -440
- package/shared/spec/schema/index.js +0 -440
- package/shared/spec/schema/schema.json +0 -266
- package/shared/spec/schema.js +0 -212
- package/worker/index.js +0 -25
- package/worker/media.js +0 -274
- package/worker/util.js +0 -42
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# bare-media
|
|
2
2
|
|
|
3
|
-
A set of media
|
|
3
|
+
A set of media APIs for Bare
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -10,90 +10,123 @@ npm i bare-media
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Image:
|
|
14
14
|
|
|
15
15
|
```js
|
|
16
|
-
import
|
|
16
|
+
import { image } from 'bare-media'
|
|
17
17
|
|
|
18
|
-
const
|
|
18
|
+
const preview = await image(path)
|
|
19
|
+
.decode({ maxFrames })
|
|
20
|
+
.resize({ maxWidth, maxHeight })
|
|
21
|
+
.encode({ mimetype: 'image/webp' })
|
|
19
22
|
```
|
|
20
23
|
|
|
21
|
-
|
|
24
|
+
Video:
|
|
22
25
|
|
|
23
26
|
```js
|
|
24
|
-
import {
|
|
27
|
+
import { video } from 'bare-media'
|
|
25
28
|
|
|
26
|
-
const
|
|
27
|
-
const data = await worker.createPreview({ path, maxWidth, maxHeight })
|
|
29
|
+
const frames = video(path).extractFrames({ frameIndex })
|
|
28
30
|
```
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
Terminate the worker:
|
|
32
|
+
Each method can also be used independently:
|
|
33
33
|
|
|
34
34
|
```js
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
worker.onClose = () => {
|
|
38
|
-
// worker terminated
|
|
39
|
-
}
|
|
35
|
+
const rgba = await image.decode(buffer, { maxFrames })
|
|
40
36
|
```
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
## Image API
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
import { createPreview } from 'bare-media/worker/media.js'
|
|
40
|
+
### decode()
|
|
46
41
|
|
|
47
|
-
|
|
48
|
-
|
|
42
|
+
Decode an image to RGBA
|
|
43
|
+
|
|
44
|
+
| Parameter | Type | Description |
|
|
45
|
+
| ---------------- | ------ | --------------------------------------------------------- |
|
|
46
|
+
| `buffer` | object | The encoded image |
|
|
47
|
+
| `opts.maxFrames` | number | Max number of frames to decode in case of animated images |
|
|
49
48
|
|
|
50
|
-
|
|
49
|
+
### encode()
|
|
51
50
|
|
|
52
|
-
|
|
51
|
+
Encodes an image to a specific format
|
|
53
52
|
|
|
54
|
-
|
|
53
|
+
| Parameter | Type | Description |
|
|
54
|
+
| --------------- | ------ | ----------------------------------------------------------------------------------- |
|
|
55
|
+
| `buffer` | object | The rgba image |
|
|
56
|
+
| `opts.mimetype` | string | The mimetype of the output image |
|
|
57
|
+
| `opts.maxBytes` | number | Max bytes for the encoded image (reduces quality or fps in case of animated images) |
|
|
58
|
+
| `opts...` | any | Additional encoder-specific options |
|
|
55
59
|
|
|
56
|
-
|
|
60
|
+
### crop()
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
| ----------- | ------ | ------------------------------------------------------------------------- |
|
|
60
|
-
| `path` | string | Path to the input file. Either `path`, `httpLink` or `buffer` is required |
|
|
61
|
-
| `httpLink` | string | Http link to the input file |
|
|
62
|
-
| `buffer` | object | Bytes of the input file |
|
|
63
|
-
| `mimetype` | string | Media type of the input file. If not provided it will be detected |
|
|
64
|
-
| `maxWidth` | number | Max width for the generated preview |
|
|
65
|
-
| `maxHeight` | number | Max height for the generated preview |
|
|
66
|
-
| `maxFrames` | number | Max frames for the generated preview in case the file is animated |
|
|
67
|
-
| `maxBytes` | number | Max bytes for the generated preview |
|
|
68
|
-
| `format` | string | Media type for the generated preview. Default `image/webp` |
|
|
69
|
-
| `encoding` | string | `base64` or nothing for buffer |
|
|
62
|
+
Crop an image
|
|
70
63
|
|
|
71
|
-
|
|
64
|
+
| Parameter | Type | Description |
|
|
65
|
+
| ------------- | ------ | ---------------------------- |
|
|
66
|
+
| `buffer` | object | The rgba image |
|
|
67
|
+
| `opts.left` | number | Offset from left edge |
|
|
68
|
+
| `opts.top` | number | Offset from top edge |
|
|
69
|
+
| `opts.width` | number | Width of the region to crop |
|
|
70
|
+
| `opts.height` | number | Height of the region to crop |
|
|
72
71
|
|
|
73
|
-
|
|
72
|
+
### resize()
|
|
74
73
|
|
|
75
|
-
|
|
76
|
-
| ---------- | ------ | ------------------------------------------------------------------------- |
|
|
77
|
-
| `path` | string | Path to the input file. Either `path`, `httpLink` or `buffer` is required |
|
|
78
|
-
| `httpLink` | string | Http link to the input file |
|
|
79
|
-
| `buffer` | object | Bytes of the input file |
|
|
80
|
-
| `mimetype` | string | Media type of the input file. If not provided it will be detected |
|
|
74
|
+
Resize an image
|
|
81
75
|
|
|
82
|
-
|
|
76
|
+
| Parameter | Type | Description |
|
|
77
|
+
| ---------------- | ------ | ----------------------------- |
|
|
78
|
+
| `buffer` | object | The rgba image |
|
|
79
|
+
| `opts.maxWidth` | number | Max width of the output rgba |
|
|
80
|
+
| `opts.maxHeight` | number | Max height of the output rgba |
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
### slice()
|
|
83
|
+
|
|
84
|
+
Limits an animated image to a subset of frames. If the image is not animated, it returns the same rgba.
|
|
85
|
+
|
|
86
|
+
| Parameter | Type | Description |
|
|
87
|
+
| ------------ | ------ | ------------------------------------------------------------------------ |
|
|
88
|
+
| `buffer` | object | The rgba image |
|
|
89
|
+
| `opts.start` | number | Frame index at which to start extraction. Default 0. |
|
|
90
|
+
| `opts.end` | number | Frame index at which to end extraction. Defaults to end of the animation |
|
|
91
|
+
|
|
92
|
+
### read()
|
|
93
|
+
|
|
94
|
+
Read an image from a file path, URL, or buffer.
|
|
95
|
+
|
|
96
|
+
| Parameter | Type | Description |
|
|
97
|
+
| --------- | ------ | ------------------------------------------- |
|
|
98
|
+
| `input` | object | File path, http(s) URL, or raw image buffer |
|
|
99
|
+
|
|
100
|
+
### save()
|
|
101
|
+
|
|
102
|
+
Write an encoded image buffer to a file.
|
|
103
|
+
|
|
104
|
+
| Parameter | Type | Description |
|
|
105
|
+
| ---------- | ------ | ---------------------------------------- |
|
|
106
|
+
| `filename` | string | Destination file path |
|
|
107
|
+
| `buffer` | object | Encoded image buffer |
|
|
108
|
+
| `opts` | object | Options passed through to `fs.writeFile` |
|
|
109
|
+
|
|
110
|
+
## Video API
|
|
111
|
+
|
|
112
|
+
### extractFrames()
|
|
113
|
+
|
|
114
|
+
Extracts frames from a video in RGBA
|
|
115
|
+
|
|
116
|
+
| Parameter | Type | Description |
|
|
117
|
+
| ----------------- | ------ | ------------------------------ |
|
|
118
|
+
| `fd` | number | File descriptor |
|
|
119
|
+
| `opts.frameIndex` | number | Number of the frame to extract |
|
|
120
|
+
|
|
121
|
+
## Supported Types
|
|
122
|
+
|
|
123
|
+
Helpers to check supported media types are exposed in `bare-media/types`:
|
|
85
124
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
| `mimetype` | string | Media type of the input file. If not provided it will be detected |
|
|
92
|
-
| `left` | number | Offset from left edge |
|
|
93
|
-
| `top` | number | Offset from top edge |
|
|
94
|
-
| `width` | number | Width of the region to crop |
|
|
95
|
-
| `height` | number | Height of the region to crop |
|
|
96
|
-
| `format` | string | Media type for the cropped image. Default same as the input image |
|
|
125
|
+
- `supportedImageMimetypes`: list of supported image mimetypes.
|
|
126
|
+
- `supportedVideoMimetypes`: list of supported video mimetypes.
|
|
127
|
+
- `isImageSupported(mimetype)`: returns `true` if the mimetype is a supported image format.
|
|
128
|
+
- `isVideoSupported(mimetype)`: returns `true` if the mimetype is a supported video format.
|
|
129
|
+
- `isMediaSupported(mimetype)`: returns `true` if the mimetype is either a supported image or video format.
|
|
97
130
|
|
|
98
131
|
## License
|
|
99
132
|
|
package/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bare-media",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "2.1.0",
|
|
4
|
+
"description": "A set of media APIs for Bare",
|
|
4
5
|
"main": "index.js",
|
|
5
6
|
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./types": "./types.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"src",
|
|
13
|
+
"index.js",
|
|
14
|
+
"types.js"
|
|
15
|
+
],
|
|
6
16
|
"scripts": {
|
|
7
|
-
"build:rpc": "cd shared/spec && bare ./build.js",
|
|
8
17
|
"format": "prettier --write .",
|
|
9
18
|
"format:check": "prettier --check .",
|
|
10
19
|
"lint": "npm run format:check && lunte",
|
|
@@ -13,40 +22,40 @@
|
|
|
13
22
|
"keywords": [],
|
|
14
23
|
"author": "Holepunch Inc",
|
|
15
24
|
"license": "Apache-2.0",
|
|
16
|
-
"
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/holepunchto/bare-media.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/holepunchto/bare-media/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/holepunchto/bare-media#readme",
|
|
17
33
|
"dependencies": {
|
|
18
|
-
"
|
|
34
|
+
"bare-bmp": "^1.0.0",
|
|
19
35
|
"bare-fetch": "^2.4.1",
|
|
36
|
+
"bare-ffmpeg": "^1.0.0",
|
|
20
37
|
"bare-fs": "^4.1.5",
|
|
21
38
|
"bare-gif": "^1.1.2",
|
|
22
39
|
"bare-heif": "^1.0.5",
|
|
40
|
+
"bare-ico": "^1.0.0",
|
|
23
41
|
"bare-image-resample": "^1.0.1",
|
|
24
42
|
"bare-jpeg": "^1.0.1",
|
|
25
43
|
"bare-png": "^1.0.2",
|
|
26
44
|
"bare-tiff": "^1.0.1",
|
|
27
45
|
"bare-webp": "^1.0.3",
|
|
28
|
-
"cross-worker": "^1.1.0",
|
|
29
46
|
"get-file-format": "^1.0.1",
|
|
30
|
-
"get-mime-type": "^2.0.1"
|
|
31
|
-
"hrpc": "^4.0.0",
|
|
32
|
-
"hyperschema": "^1.12.3",
|
|
33
|
-
"ready-resource": "^1.2.0",
|
|
34
|
-
"uncaughts": "^1.1.1"
|
|
47
|
+
"get-mime-type": "^2.0.1"
|
|
35
48
|
},
|
|
36
49
|
"devDependencies": {
|
|
50
|
+
"b4a": "^1.7.3",
|
|
51
|
+
"bare-os": "^3.6.2",
|
|
37
52
|
"brittle": "^3.16.3",
|
|
38
53
|
"corestore": "^7.4.5",
|
|
39
54
|
"hyperblobs": "^2.8.0",
|
|
40
55
|
"hypercore-blob-server": "^1.11.0",
|
|
41
|
-
"lunte": "^1.
|
|
42
|
-
"prettier": "^3.
|
|
43
|
-
"prettier-config-holepunch": "^
|
|
56
|
+
"lunte": "^1.4.0",
|
|
57
|
+
"prettier": "^3.7.4",
|
|
58
|
+
"prettier-config-holepunch": "^2.0.0",
|
|
44
59
|
"test-tmp": "^1.4.0"
|
|
45
|
-
}
|
|
46
|
-
"files": [
|
|
47
|
-
"shared",
|
|
48
|
-
"worker",
|
|
49
|
-
"client.js",
|
|
50
|
-
"index.js"
|
|
51
|
-
]
|
|
60
|
+
}
|
|
52
61
|
}
|
package/src/codecs.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { IMAGE } from '../types'
|
|
2
|
+
|
|
3
|
+
export const codecs = {
|
|
4
|
+
[IMAGE.AVIF]: () => import('bare-heif'),
|
|
5
|
+
[IMAGE.BMP]: () => import('bare-bmp'),
|
|
6
|
+
[IMAGE.GIF]: () => import('bare-gif'),
|
|
7
|
+
[IMAGE.HEIC]: () => import('bare-heif'),
|
|
8
|
+
[IMAGE.HEIF]: () => import('bare-heif'),
|
|
9
|
+
[IMAGE.JPEG]: () => import('bare-jpeg'),
|
|
10
|
+
[IMAGE.JPG]: () => import('bare-jpeg'),
|
|
11
|
+
[IMAGE.PNG]: () => import('bare-png'),
|
|
12
|
+
[IMAGE.TIF]: () => import('bare-tiff'),
|
|
13
|
+
[IMAGE.TIFF]: () => import('bare-tiff'),
|
|
14
|
+
[IMAGE.VND_MS_ICON]: () => import('bare-ico'),
|
|
15
|
+
[IMAGE.WEBP]: () => import('bare-webp'),
|
|
16
|
+
[IMAGE.X_ICON]: () => import('bare-ico'),
|
|
17
|
+
[IMAGE.X_MS_BMP]: () => import('bare-bmp')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function importCodec(mimetype) {
|
|
21
|
+
const codecImport = codecs[mimetype]
|
|
22
|
+
if (!codecImport) throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
|
|
23
|
+
return await codecImport()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function supportsQuality(mimetype) {
|
|
27
|
+
return { 'image/webp': true, 'image/jpeg': true }[mimetype] || false
|
|
28
|
+
}
|
package/src/image.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'bare-fs'
|
|
2
|
+
import fetch from 'bare-fetch'
|
|
3
|
+
|
|
4
|
+
import { importCodec, supportsQuality } from './codecs.js'
|
|
5
|
+
import { isHttpUrl, detectMimeType, calculateFitDimensions } from './util'
|
|
6
|
+
|
|
7
|
+
const animatableMimetypes = ['image/gif', 'image/webp']
|
|
8
|
+
|
|
9
|
+
async function read(input) {
|
|
10
|
+
let buffer
|
|
11
|
+
|
|
12
|
+
if (typeof input === 'string') {
|
|
13
|
+
if (isHttpUrl(input)) {
|
|
14
|
+
const response = await fetch(input)
|
|
15
|
+
buffer = await response.buffer()
|
|
16
|
+
} else {
|
|
17
|
+
buffer = await fs.readFile(input)
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
buffer = input
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return buffer
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function save(filename, buffer, opts) {
|
|
27
|
+
return fs.writeFile(filename, buffer, opts)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function decode(buffer, opts = {}) {
|
|
31
|
+
const { maxFrames = 0 } = opts
|
|
32
|
+
|
|
33
|
+
let rgba
|
|
34
|
+
|
|
35
|
+
const mimetype = detectMimeType(buffer)
|
|
36
|
+
const codec = await importCodec(mimetype)
|
|
37
|
+
|
|
38
|
+
if (animatableMimetypes.includes(mimetype)) {
|
|
39
|
+
const { width, height, loops, frames } = codec.decodeAnimated(buffer)
|
|
40
|
+
const data = []
|
|
41
|
+
for (const frame of frames) {
|
|
42
|
+
if (maxFrames > 0 && data.length >= maxFrames) break
|
|
43
|
+
data.push(frame)
|
|
44
|
+
}
|
|
45
|
+
rgba = { width, height, loops, frames: data }
|
|
46
|
+
} else {
|
|
47
|
+
rgba = codec.decode(buffer)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return rgba
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function encode(rgba, opts = {}) {
|
|
54
|
+
const { mimetype, maxBytes, ...codecOpts } = opts
|
|
55
|
+
|
|
56
|
+
let encoded = await _encodeRGBA(rgba, mimetype, codecOpts)
|
|
57
|
+
|
|
58
|
+
if (maxBytes) {
|
|
59
|
+
if (encoded.byteLength > maxBytes && supportsQuality(mimetype)) {
|
|
60
|
+
const MIN_QUALITY = 50
|
|
61
|
+
for (let quality = 80; quality >= MIN_QUALITY; quality -= 15) {
|
|
62
|
+
encoded = await _encodeRGBA(rgba, mimetype, { quality })
|
|
63
|
+
if (encoded.byteLength <= maxBytes) {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (encoded.byteLength > maxBytes && rgba.frames?.length > 1) {
|
|
70
|
+
const quality = 75
|
|
71
|
+
|
|
72
|
+
for (const dropEvery of [4, 3, 2]) {
|
|
73
|
+
const frames = rgba.frames.filter((frame, index) => index % dropEvery !== 0)
|
|
74
|
+
const filtered = { ...rgba, frames }
|
|
75
|
+
encoded = await _encodeRGBA(filtered, mimetype, { quality })
|
|
76
|
+
if (encoded.byteLength <= maxBytes) {
|
|
77
|
+
break
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (encoded.byteLength > maxBytes) {
|
|
82
|
+
const frames = rgba.frames.slice(0, 50).filter((frame, index) => index % 2 === 0)
|
|
83
|
+
const capped = { ...rgba, frames }
|
|
84
|
+
encoded = await _encodeRGBA(capped, mimetype, { quality })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (encoded.byteLength > maxBytes) {
|
|
88
|
+
const oneFrame = { ...rgba, frames: rgba.frames.slice(0, 1) }
|
|
89
|
+
encoded = await _encodeRGBA(oneFrame, mimetype)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (encoded.byteLength > maxBytes) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`Could not create preview under maxBytes, reached ${encoded.byteLength} bytes`
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return encoded
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function crop(rgba, opts = {}) {
|
|
104
|
+
const { left, top, width, height } = opts
|
|
105
|
+
|
|
106
|
+
if (
|
|
107
|
+
left < 0 ||
|
|
108
|
+
top < 0 ||
|
|
109
|
+
width <= 0 ||
|
|
110
|
+
height <= 0 ||
|
|
111
|
+
left + width > rgba.width ||
|
|
112
|
+
top + height > rgba.height
|
|
113
|
+
) {
|
|
114
|
+
throw new Error('Crop rectangle out of bounds')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = Buffer.alloc(width * height * 4)
|
|
118
|
+
|
|
119
|
+
for (let y = 0; y < height; y++) {
|
|
120
|
+
for (let x = 0; x < width; x++) {
|
|
121
|
+
const srcIndex = ((y + top) * rgba.width + (x + left)) * 4
|
|
122
|
+
const dstIndex = (y * width + x) * 4
|
|
123
|
+
|
|
124
|
+
data[dstIndex] = rgba.data[srcIndex]
|
|
125
|
+
data[dstIndex + 1] = rgba.data[srcIndex + 1]
|
|
126
|
+
data[dstIndex + 2] = rgba.data[srcIndex + 2]
|
|
127
|
+
data[dstIndex + 3] = rgba.data[srcIndex + 3]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return { width, height, data }
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function resize(rgba, opts = {}) {
|
|
135
|
+
const { maxWidth, maxHeight } = opts
|
|
136
|
+
const { width, height } = rgba
|
|
137
|
+
|
|
138
|
+
let maybeResizedRGBA
|
|
139
|
+
|
|
140
|
+
if (maxWidth && maxHeight && (width > maxWidth || height > maxHeight)) {
|
|
141
|
+
const { resize } = await import('bare-image-resample')
|
|
142
|
+
const dimensions = calculateFitDimensions(width, height, maxWidth, maxHeight)
|
|
143
|
+
if (Array.isArray(rgba.frames)) {
|
|
144
|
+
const frames = []
|
|
145
|
+
for (const frame of rgba.frames) {
|
|
146
|
+
const resized = resize(frame, dimensions.width, dimensions.height)
|
|
147
|
+
frames.push({ ...resized, timestamp: frame.timestamp })
|
|
148
|
+
}
|
|
149
|
+
maybeResizedRGBA = {
|
|
150
|
+
width: frames[0].width,
|
|
151
|
+
height: frames[0].height,
|
|
152
|
+
loops: rgba.loops,
|
|
153
|
+
frames
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
maybeResizedRGBA = resize(rgba, dimensions.width, dimensions.height)
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
maybeResizedRGBA = rgba
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return maybeResizedRGBA
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function slice(rgba, opts = {}) {
|
|
166
|
+
if (Array.isArray(rgba.frames)) {
|
|
167
|
+
let { start = 0, end = rgba.frames.length } = opts
|
|
168
|
+
|
|
169
|
+
start = Math.max(0, start)
|
|
170
|
+
end = Math.max(0, end)
|
|
171
|
+
|
|
172
|
+
if (start >= end) {
|
|
173
|
+
throw new Error('slice(): "start" must be less than "end"')
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
...rgba,
|
|
178
|
+
frames: rgba.frames.slice(start, end)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return rgba
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function _encodeRGBA(rgba, mimetype, opts) {
|
|
186
|
+
const codec = await importCodec(mimetype)
|
|
187
|
+
|
|
188
|
+
let encoded
|
|
189
|
+
if (Array.isArray(rgba.frames)) {
|
|
190
|
+
encoded = codec.encodeAnimated(rgba, opts)
|
|
191
|
+
} else {
|
|
192
|
+
encoded = codec.encode(rgba, opts)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return encoded
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
class ImagePipeline {
|
|
199
|
+
constructor(input) {
|
|
200
|
+
this.input = input
|
|
201
|
+
this.steps = []
|
|
202
|
+
|
|
203
|
+
const methods = ['decode', 'resize', 'crop', 'slice', 'encode']
|
|
204
|
+
for (let method of methods) {
|
|
205
|
+
this[method] = (opts) => {
|
|
206
|
+
this.steps.push({ op: method, opts })
|
|
207
|
+
return this
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async then(resolve, reject) {
|
|
213
|
+
try {
|
|
214
|
+
let buffer = await read(this.input)
|
|
215
|
+
|
|
216
|
+
for (const step of this.steps) {
|
|
217
|
+
if (step.op === 'decode') {
|
|
218
|
+
buffer = await decode(buffer, step.opts)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (step.op === 'resize') {
|
|
222
|
+
buffer = await resize(buffer, step.opts)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (step.op === 'crop') {
|
|
226
|
+
buffer = await crop(buffer, step.opts)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (step.op === 'slice') {
|
|
230
|
+
buffer = slice(buffer, step.opts)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (step.op === 'encode') {
|
|
234
|
+
buffer = await encode(buffer, step.opts)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
resolve(buffer)
|
|
239
|
+
} catch (err) {
|
|
240
|
+
reject(err)
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async save(filename, opts) {
|
|
245
|
+
const buffer = await this
|
|
246
|
+
return save(filename, buffer, opts)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function image(input) {
|
|
251
|
+
return new ImagePipeline(input)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
image.read = read
|
|
255
|
+
image.save = save
|
|
256
|
+
image.decode = decode
|
|
257
|
+
image.resize = resize
|
|
258
|
+
image.crop = crop
|
|
259
|
+
image.slice = slice
|
|
260
|
+
image.encode = encode
|
|
261
|
+
|
|
262
|
+
export { image }
|
package/src/util.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import getMimeType from 'get-mime-type'
|
|
2
|
+
import getFileFormat from 'get-file-format'
|
|
3
|
+
|
|
4
|
+
export function detectMimeType(buffer) {
|
|
5
|
+
return getMimeType(getFileFormat(buffer))
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
|
|
9
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
10
|
+
return { width, height }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const widthRatio = maxWidth / width
|
|
14
|
+
const heightRatio = maxHeight / height
|
|
15
|
+
const ratio = Math.min(widthRatio, heightRatio)
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
width: Math.round(width * ratio),
|
|
19
|
+
height: Math.round(height * ratio)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isHttpUrl(value) {
|
|
24
|
+
try {
|
|
25
|
+
const url = new URL(value)
|
|
26
|
+
return url.protocol === 'http:' || url.protocol === 'https:'
|
|
27
|
+
} catch {
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
}
|