bare-media 1.6.0 → 1.8.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 CHANGED
@@ -79,6 +79,22 @@ Decode an image to RGBA
79
79
  | `buffer` | object | Bytes of the input file |
80
80
  | `mimetype` | string | Media type of the input file. If not provided it will be detected |
81
81
 
82
+ ### cropImage()
83
+
84
+ Crop an image
85
+
86
+ | Property | Type | Description |
87
+ | ---------- | ------ | ------------------------------------------------------------------------- |
88
+ | `path` | string | Path to the input file. Either `path`, `httpLink` or `buffer` is required |
89
+ | `httpLink` | string | Http link to the input file |
90
+ | `buffer` | object | Bytes of the input file |
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 |
97
+
82
98
  ## License
83
99
 
84
100
  Apache-2.0
package/client.js CHANGED
@@ -24,7 +24,7 @@ export class WorkerClient extends ReadyResource {
24
24
  }
25
25
 
26
26
  #attachMethods() {
27
- const methods = ['createPreview', 'decodeImage']
27
+ const methods = ['createPreview', 'decodeImage', 'cropImage']
28
28
 
29
29
  for (const method of methods) {
30
30
  this[method] = async (...args) => {
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "bare-media",
3
- "version": "1.6.0",
3
+ "version": "1.8.0",
4
4
  "main": "index.js",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "build:rpc": "cd shared/spec && bare ./build.js",
8
8
  "format": "prettier --write .",
9
9
  "format:check": "prettier --check .",
10
- "lint": "lunte",
11
- "test": "npm run lint && npm run format:check && brittle-bare test/index.js"
10
+ "lint": "npm run format:check && lunte",
11
+ "test": "brittle-bare test/index.js"
12
12
  },
13
13
  "keywords": [],
14
14
  "author": "Holepunch Inc",
@@ -26,6 +26,7 @@
26
26
  "bare-tiff": "^1.0.1",
27
27
  "bare-webp": "^1.0.3",
28
28
  "cross-worker": "^1.1.0",
29
+ "get-file-format": "^1.0.1",
29
30
  "get-mime-type": "^2.0.1",
30
31
  "hrpc": "^4.0.0",
31
32
  "hyperschema": "^1.12.3",
@@ -25,4 +25,10 @@ ns.register({
25
25
  response: { name: '@media/decode-image-response', stream: false }
26
26
  })
27
27
 
28
+ ns.register({
29
+ name: 'crop-image',
30
+ request: { name: '@media/crop-image-request', stream: false },
31
+ response: { name: '@media/crop-image-response', stream: false }
32
+ })
33
+
28
34
  HRPCBuilder.toDisk(builder)
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "schema": [
4
4
  {
5
5
  "id": 0,
@@ -26,6 +26,19 @@
26
26
  "stream": false
27
27
  },
28
28
  "version": 1
29
+ },
30
+ {
31
+ "id": 2,
32
+ "name": "@media/crop-image",
33
+ "request": {
34
+ "name": "@media/crop-image-request",
35
+ "stream": false
36
+ },
37
+ "response": {
38
+ "name": "@media/crop-image-response",
39
+ "stream": false
40
+ },
41
+ "version": 2
29
42
  }
30
43
  ]
31
44
  }
@@ -9,7 +9,9 @@ const methods = new Map([
9
9
  ['@media/create-preview', 0],
10
10
  [0, '@media/create-preview'],
11
11
  ['@media/decode-image', 1],
12
- [1, '@media/decode-image']
12
+ [1, '@media/decode-image'],
13
+ ['@media/crop-image', 2],
14
+ [2, '@media/crop-image']
13
15
  ])
14
16
 
15
17
  class HRPC {
@@ -18,11 +20,13 @@ class HRPC {
18
20
  this._handlers = []
19
21
  this._requestEncodings = new Map([
20
22
  ['@media/create-preview', getEncoding('@media/create-preview-request')],
21
- ['@media/decode-image', getEncoding('@media/decode-image-request')]
23
+ ['@media/decode-image', getEncoding('@media/decode-image-request')],
24
+ ['@media/crop-image', getEncoding('@media/crop-image-request')]
22
25
  ])
23
26
  this._responseEncodings = new Map([
24
27
  ['@media/create-preview', getEncoding('@media/create-preview-response')],
25
- ['@media/decode-image', getEncoding('@media/decode-image-response')]
28
+ ['@media/decode-image', getEncoding('@media/decode-image-response')],
29
+ ['@media/crop-image', getEncoding('@media/crop-image-response')]
26
30
  ])
27
31
  this._rpc = new RPC(stream, async (req) => {
28
32
  const command = methods.get(req.command)
@@ -125,6 +129,10 @@ class HRPC {
125
129
  return this._call('@media/decode-image', args)
126
130
  }
127
131
 
132
+ async cropImage(args) {
133
+ return this._call('@media/crop-image', args)
134
+ }
135
+
128
136
  onCreatePreview(responseFn) {
129
137
  this._handlers['@media/create-preview'] = responseFn
130
138
  }
@@ -133,6 +141,10 @@ class HRPC {
133
141
  this._handlers['@media/decode-image'] = responseFn
134
142
  }
135
143
 
144
+ onCropImage(responseFn) {
145
+ this._handlers['@media/crop-image'] = responseFn
146
+ }
147
+
136
148
  _requestIsStream(command) {
137
149
  return [].includes(command)
138
150
  }
@@ -1,11 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 1
2
+ // Schema Version: 2
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
+ /* eslint-disable space-before-function-paren */
5
6
 
6
7
  import { c } from 'hyperschema/runtime'
7
8
 
8
- const VERSION = 1
9
+ const VERSION = 2
9
10
 
10
11
  // eslint-disable-next-line no-unused-vars
11
12
  let version = VERSION
@@ -260,6 +261,107 @@ const encoding6 = {
260
261
  }
261
262
  }
262
263
 
264
+ // @media/crop-image-request
265
+ const encoding7 = {
266
+ preencode(state, m) {
267
+ const flags =
268
+ (version >= 2 && m.path ? 1 : 0) |
269
+ (version >= 2 && m.httpLink ? 2 : 0) |
270
+ (version >= 2 && m.buffer ? 4 : 0) |
271
+ (version >= 2 && m.mimetype ? 8 : 0) |
272
+ (version >= 2 && m.left ? 16 : 0) |
273
+ (version >= 2 && m.top ? 32 : 0) |
274
+ (version >= 2 && m.width ? 64 : 0) |
275
+ (version >= 2 && m.height ? 128 : 0) |
276
+ (version >= 2 && m.format ? 256 : 0)
277
+
278
+ c.uint.preencode(state, flags)
279
+
280
+ if (version >= 2 && m.path) c.string.preencode(state, m.path)
281
+ if (version >= 2 && m.httpLink) c.string.preencode(state, m.httpLink)
282
+ if (version >= 2 && m.buffer) c.buffer.preencode(state, m.buffer)
283
+ if (version >= 2 && m.mimetype) c.string.preencode(state, m.mimetype)
284
+ if (version >= 2 && m.left) c.uint.preencode(state, m.left)
285
+ if (version >= 2 && m.top) c.uint.preencode(state, m.top)
286
+ if (version >= 2 && m.width) c.uint.preencode(state, m.width)
287
+ if (version >= 2 && m.height) c.uint.preencode(state, m.height)
288
+ if (version >= 2 && m.format) c.string.preencode(state, m.format)
289
+ },
290
+ encode(state, m) {
291
+ const flags =
292
+ (version >= 2 && m.path ? 1 : 0) |
293
+ (version >= 2 && m.httpLink ? 2 : 0) |
294
+ (version >= 2 && m.buffer ? 4 : 0) |
295
+ (version >= 2 && m.mimetype ? 8 : 0) |
296
+ (version >= 2 && m.left ? 16 : 0) |
297
+ (version >= 2 && m.top ? 32 : 0) |
298
+ (version >= 2 && m.width ? 64 : 0) |
299
+ (version >= 2 && m.height ? 128 : 0) |
300
+ (version >= 2 && m.format ? 256 : 0)
301
+
302
+ c.uint.encode(state, flags)
303
+
304
+ if (version >= 2 && m.path) c.string.encode(state, m.path)
305
+ if (version >= 2 && m.httpLink) c.string.encode(state, m.httpLink)
306
+ if (version >= 2 && m.buffer) c.buffer.encode(state, m.buffer)
307
+ if (version >= 2 && m.mimetype) c.string.encode(state, m.mimetype)
308
+ if (version >= 2 && m.left) c.uint.encode(state, m.left)
309
+ if (version >= 2 && m.top) c.uint.encode(state, m.top)
310
+ if (version >= 2 && m.width) c.uint.encode(state, m.width)
311
+ if (version >= 2 && m.height) c.uint.encode(state, m.height)
312
+ if (version >= 2 && m.format) c.string.encode(state, m.format)
313
+ },
314
+ decode(state) {
315
+ const flags = c.uint.decode(state)
316
+
317
+ return {
318
+ path: version >= 2 && (flags & 1) !== 0 ? c.string.decode(state) : null,
319
+ httpLink:
320
+ version >= 2 && (flags & 2) !== 0 ? c.string.decode(state) : null,
321
+ buffer: version >= 2 && (flags & 4) !== 0 ? c.buffer.decode(state) : null,
322
+ mimetype:
323
+ version >= 2 && (flags & 8) !== 0 ? c.string.decode(state) : null,
324
+ left: version >= 2 && (flags & 16) !== 0 ? c.uint.decode(state) : 0,
325
+ top: version >= 2 && (flags & 32) !== 0 ? c.uint.decode(state) : 0,
326
+ width: version >= 2 && (flags & 64) !== 0 ? c.uint.decode(state) : 0,
327
+ height: version >= 2 && (flags & 128) !== 0 ? c.uint.decode(state) : 0,
328
+ format:
329
+ version >= 2 && (flags & 256) !== 0 ? c.string.decode(state) : null
330
+ }
331
+ }
332
+ }
333
+
334
+ // @media/crop-image-response.metadata
335
+ const encoding8_0 = encoding2_0
336
+
337
+ // @media/crop-image-response
338
+ const encoding8 = {
339
+ preencode(state, m) {
340
+ state.end++ // max flag is 2 so always one byte
341
+
342
+ if (version >= 2 && m.metadata) encoding8_0.preencode(state, m.metadata)
343
+ if (version >= 2 && m.data) c.buffer.preencode(state, m.data)
344
+ },
345
+ encode(state, m) {
346
+ const flags =
347
+ (version >= 2 && m.metadata ? 1 : 0) | (version >= 2 && m.data ? 2 : 0)
348
+
349
+ c.uint.encode(state, flags)
350
+
351
+ if (version >= 2 && m.metadata) encoding8_0.encode(state, m.metadata)
352
+ if (version >= 2 && m.data) c.buffer.encode(state, m.data)
353
+ },
354
+ decode(state) {
355
+ const flags = c.uint.decode(state)
356
+
357
+ return {
358
+ metadata:
359
+ version >= 2 && (flags & 1) !== 0 ? encoding8_0.decode(state) : null,
360
+ data: version >= 2 && (flags & 2) !== 0 ? c.buffer.decode(state) : null
361
+ }
362
+ }
363
+ }
364
+
263
365
  function setVersion(v) {
264
366
  version = v
265
367
  }
@@ -297,6 +399,10 @@ function getEncoding(name) {
297
399
  return encoding5
298
400
  case '@media/decode-image-response':
299
401
  return encoding6
402
+ case '@media/crop-image-request':
403
+ return encoding7
404
+ case '@media/crop-image-response':
405
+ return encoding8
300
406
  default:
301
407
  throw new Error('Encoder not found ' + name)
302
408
  }
@@ -1,11 +1,12 @@
1
1
  // This file is autogenerated by the hyperschema compiler
2
- // Schema Version: 1
2
+ // Schema Version: 2
3
3
  /* eslint-disable camelcase */
4
4
  /* eslint-disable quotes */
5
+ /* eslint-disable space-before-function-paren */
5
6
 
6
7
  import { c } from 'hyperschema/runtime'
7
8
 
8
- const VERSION = 1
9
+ const VERSION = 2
9
10
 
10
11
  // eslint-disable-next-line no-unused-vars
11
12
  let version = VERSION
@@ -260,6 +261,107 @@ const encoding6 = {
260
261
  }
261
262
  }
262
263
 
264
+ // @media/crop-image-request
265
+ const encoding7 = {
266
+ preencode(state, m) {
267
+ const flags =
268
+ (version >= 2 && m.path ? 1 : 0) |
269
+ (version >= 2 && m.httpLink ? 2 : 0) |
270
+ (version >= 2 && m.buffer ? 4 : 0) |
271
+ (version >= 2 && m.mimetype ? 8 : 0) |
272
+ (version >= 2 && m.left ? 16 : 0) |
273
+ (version >= 2 && m.top ? 32 : 0) |
274
+ (version >= 2 && m.width ? 64 : 0) |
275
+ (version >= 2 && m.height ? 128 : 0) |
276
+ (version >= 2 && m.format ? 256 : 0)
277
+
278
+ c.uint.preencode(state, flags)
279
+
280
+ if (version >= 2 && m.path) c.string.preencode(state, m.path)
281
+ if (version >= 2 && m.httpLink) c.string.preencode(state, m.httpLink)
282
+ if (version >= 2 && m.buffer) c.buffer.preencode(state, m.buffer)
283
+ if (version >= 2 && m.mimetype) c.string.preencode(state, m.mimetype)
284
+ if (version >= 2 && m.left) c.uint.preencode(state, m.left)
285
+ if (version >= 2 && m.top) c.uint.preencode(state, m.top)
286
+ if (version >= 2 && m.width) c.uint.preencode(state, m.width)
287
+ if (version >= 2 && m.height) c.uint.preencode(state, m.height)
288
+ if (version >= 2 && m.format) c.string.preencode(state, m.format)
289
+ },
290
+ encode(state, m) {
291
+ const flags =
292
+ (version >= 2 && m.path ? 1 : 0) |
293
+ (version >= 2 && m.httpLink ? 2 : 0) |
294
+ (version >= 2 && m.buffer ? 4 : 0) |
295
+ (version >= 2 && m.mimetype ? 8 : 0) |
296
+ (version >= 2 && m.left ? 16 : 0) |
297
+ (version >= 2 && m.top ? 32 : 0) |
298
+ (version >= 2 && m.width ? 64 : 0) |
299
+ (version >= 2 && m.height ? 128 : 0) |
300
+ (version >= 2 && m.format ? 256 : 0)
301
+
302
+ c.uint.encode(state, flags)
303
+
304
+ if (version >= 2 && m.path) c.string.encode(state, m.path)
305
+ if (version >= 2 && m.httpLink) c.string.encode(state, m.httpLink)
306
+ if (version >= 2 && m.buffer) c.buffer.encode(state, m.buffer)
307
+ if (version >= 2 && m.mimetype) c.string.encode(state, m.mimetype)
308
+ if (version >= 2 && m.left) c.uint.encode(state, m.left)
309
+ if (version >= 2 && m.top) c.uint.encode(state, m.top)
310
+ if (version >= 2 && m.width) c.uint.encode(state, m.width)
311
+ if (version >= 2 && m.height) c.uint.encode(state, m.height)
312
+ if (version >= 2 && m.format) c.string.encode(state, m.format)
313
+ },
314
+ decode(state) {
315
+ const flags = c.uint.decode(state)
316
+
317
+ return {
318
+ path: version >= 2 && (flags & 1) !== 0 ? c.string.decode(state) : null,
319
+ httpLink:
320
+ version >= 2 && (flags & 2) !== 0 ? c.string.decode(state) : null,
321
+ buffer: version >= 2 && (flags & 4) !== 0 ? c.buffer.decode(state) : null,
322
+ mimetype:
323
+ version >= 2 && (flags & 8) !== 0 ? c.string.decode(state) : null,
324
+ left: version >= 2 && (flags & 16) !== 0 ? c.uint.decode(state) : 0,
325
+ top: version >= 2 && (flags & 32) !== 0 ? c.uint.decode(state) : 0,
326
+ width: version >= 2 && (flags & 64) !== 0 ? c.uint.decode(state) : 0,
327
+ height: version >= 2 && (flags & 128) !== 0 ? c.uint.decode(state) : 0,
328
+ format:
329
+ version >= 2 && (flags & 256) !== 0 ? c.string.decode(state) : null
330
+ }
331
+ }
332
+ }
333
+
334
+ // @media/crop-image-response.metadata
335
+ const encoding8_0 = encoding2_0
336
+
337
+ // @media/crop-image-response
338
+ const encoding8 = {
339
+ preencode(state, m) {
340
+ state.end++ // max flag is 2 so always one byte
341
+
342
+ if (version >= 2 && m.metadata) encoding8_0.preencode(state, m.metadata)
343
+ if (version >= 2 && m.data) c.buffer.preencode(state, m.data)
344
+ },
345
+ encode(state, m) {
346
+ const flags =
347
+ (version >= 2 && m.metadata ? 1 : 0) | (version >= 2 && m.data ? 2 : 0)
348
+
349
+ c.uint.encode(state, flags)
350
+
351
+ if (version >= 2 && m.metadata) encoding8_0.encode(state, m.metadata)
352
+ if (version >= 2 && m.data) c.buffer.encode(state, m.data)
353
+ },
354
+ decode(state) {
355
+ const flags = c.uint.decode(state)
356
+
357
+ return {
358
+ metadata:
359
+ version >= 2 && (flags & 1) !== 0 ? encoding8_0.decode(state) : null,
360
+ data: version >= 2 && (flags & 2) !== 0 ? c.buffer.decode(state) : null
361
+ }
362
+ }
363
+ }
364
+
263
365
  function setVersion(v) {
264
366
  version = v
265
367
  }
@@ -297,6 +399,10 @@ function getEncoding(name) {
297
399
  return encoding5
298
400
  case '@media/decode-image-response':
299
401
  return encoding6
402
+ case '@media/crop-image-request':
403
+ return encoding7
404
+ case '@media/crop-image-response':
405
+ return encoding8
300
406
  default:
301
407
  throw new Error('Encoder not found ' + name)
302
408
  }
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "schema": [
4
4
  {
5
5
  "name": "dimensions",
@@ -190,6 +190,77 @@
190
190
  "version": 1
191
191
  }
192
192
  ]
193
+ },
194
+ {
195
+ "name": "crop-image-request",
196
+ "namespace": "media",
197
+ "compact": false,
198
+ "flagsPosition": 0,
199
+ "fields": [
200
+ {
201
+ "name": "path",
202
+ "type": "string",
203
+ "version": 2
204
+ },
205
+ {
206
+ "name": "httpLink",
207
+ "type": "string",
208
+ "version": 2
209
+ },
210
+ {
211
+ "name": "buffer",
212
+ "type": "buffer",
213
+ "version": 2
214
+ },
215
+ {
216
+ "name": "mimetype",
217
+ "type": "string",
218
+ "version": 2
219
+ },
220
+ {
221
+ "name": "left",
222
+ "type": "uint",
223
+ "version": 2
224
+ },
225
+ {
226
+ "name": "top",
227
+ "type": "uint",
228
+ "version": 2
229
+ },
230
+ {
231
+ "name": "width",
232
+ "type": "uint",
233
+ "version": 2
234
+ },
235
+ {
236
+ "name": "height",
237
+ "type": "uint",
238
+ "version": 2
239
+ },
240
+ {
241
+ "name": "format",
242
+ "type": "string",
243
+ "version": 2
244
+ }
245
+ ]
246
+ },
247
+ {
248
+ "name": "crop-image-response",
249
+ "namespace": "media",
250
+ "compact": false,
251
+ "flagsPosition": 0,
252
+ "fields": [
253
+ {
254
+ "name": "metadata",
255
+ "type": "@media/metadata",
256
+ "version": 2
257
+ },
258
+ {
259
+ "name": "data",
260
+ "type": "buffer",
261
+ "version": 2
262
+ }
263
+ ]
193
264
  }
194
265
  ]
195
266
  }
@@ -154,3 +154,59 @@ media.register({
154
154
  }
155
155
  ]
156
156
  })
157
+
158
+ media.register({
159
+ name: 'crop-image-request',
160
+ fields: [
161
+ {
162
+ name: 'path',
163
+ type: 'string'
164
+ },
165
+ {
166
+ name: 'httpLink',
167
+ type: 'string'
168
+ },
169
+ {
170
+ name: 'buffer',
171
+ type: 'buffer'
172
+ },
173
+ {
174
+ name: 'mimetype',
175
+ type: 'string'
176
+ },
177
+ {
178
+ name: 'left',
179
+ type: 'uint'
180
+ },
181
+ {
182
+ name: 'top',
183
+ type: 'uint'
184
+ },
185
+ {
186
+ name: 'width',
187
+ type: 'uint'
188
+ },
189
+ {
190
+ name: 'height',
191
+ type: 'uint'
192
+ },
193
+ {
194
+ name: 'format',
195
+ type: 'string'
196
+ }
197
+ ]
198
+ })
199
+
200
+ media.register({
201
+ name: 'crop-image-response',
202
+ fields: [
203
+ {
204
+ name: 'metadata',
205
+ type: '@media/metadata'
206
+ },
207
+ {
208
+ name: 'data',
209
+ type: 'buffer'
210
+ }
211
+ ]
212
+ })
package/worker/index.js CHANGED
@@ -18,6 +18,7 @@ const rpc = new HRPC(stream)
18
18
 
19
19
  rpc.onCreatePreview(media.createPreview)
20
20
  rpc.onDecodeImage(media.decodeImage)
21
+ rpc.onCropImage(media.cropImage)
21
22
 
22
23
  uncaughts.on((err) => {
23
24
  log('Uncaught error:', err)
package/worker/media.js CHANGED
@@ -1,10 +1,11 @@
1
1
  import b4a from 'b4a'
2
- import fs from 'bare-fs'
3
- import fetch from 'bare-fetch'
4
- import getMimeType from 'get-mime-type'
5
2
 
6
- import { importCodec, supportsQuality } from '../shared/codecs.js'
7
- import { calculateFitDimensions } from './util'
3
+ import {
4
+ importCodec,
5
+ isCodecSupported,
6
+ supportsQuality
7
+ } from '../shared/codecs.js'
8
+ import { getBuffer, detectMimeType, calculateFitDimensions } from './util'
8
9
 
9
10
  const DEFAULT_PREVIEW_FORMAT = 'image/webp'
10
11
 
@@ -22,10 +23,15 @@ export async function createPreview({
22
23
  format,
23
24
  encoding
24
25
  }) {
25
- mimetype = mimetype || getMimeType(path)
26
26
  format = format || DEFAULT_PREVIEW_FORMAT
27
27
 
28
28
  const buff = await getBuffer({ path, httpLink, buffer })
29
+ mimetype = mimetype || detectMimeType(buff, path)
30
+
31
+ if (!isCodecSupported(mimetype)) {
32
+ throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
33
+ }
34
+
29
35
  const rgba = await decodeImageToRGBA(buff, mimetype, maxFrames)
30
36
  const { width, height } = rgba
31
37
 
@@ -118,6 +124,11 @@ export async function createPreview({
118
124
 
119
125
  export async function decodeImage({ path, httpLink, buffer, mimetype }) {
120
126
  const buff = await getBuffer({ path, httpLink, buffer })
127
+ mimetype = mimetype || detectMimeType(buff, path)
128
+
129
+ if (!isCodecSupported(mimetype)) {
130
+ throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
131
+ }
121
132
 
122
133
  const rgba = await decodeImageToRGBA(buff, mimetype)
123
134
  const { width, height, data } = rgba
@@ -130,21 +141,39 @@ export async function decodeImage({ path, httpLink, buffer, mimetype }) {
130
141
  }
131
142
  }
132
143
 
133
- async function getBuffer({ path, httpLink, buffer }) {
134
- if (buffer) return buffer
144
+ export async function cropImage({
145
+ path,
146
+ httpLink,
147
+ buffer,
148
+ mimetype,
149
+ left,
150
+ top,
151
+ width,
152
+ height,
153
+ format
154
+ }) {
155
+ const buff = await getBuffer({ path, httpLink, buffer })
156
+ mimetype = mimetype || detectMimeType(buff, path)
135
157
 
136
- if (path) {
137
- return fs.readFileSync(path)
158
+ if (!isCodecSupported(mimetype)) {
159
+ throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
138
160
  }
139
161
 
140
- if (httpLink) {
141
- const response = await fetch(httpLink)
142
- return await response.buffer()
143
- }
162
+ const rgba = await decodeImageToRGBA(buff, mimetype)
163
+
164
+ const cropped = await cropRGBA(rgba, left, top, width, height)
144
165
 
145
- throw new Error(
146
- 'At least one of "path", "httpLink" or "buffer" must be provided'
147
- )
166
+ const data = await encodeImageFromRGBA(cropped, format || mimetype)
167
+
168
+ return {
169
+ metadata: {
170
+ dimensions: {
171
+ width: rgba.width,
172
+ height: rgba.height
173
+ }
174
+ },
175
+ data
176
+ }
148
177
  }
149
178
 
150
179
  async function decodeImageToRGBA(buffer, mimetype, maxFrames) {
@@ -214,3 +243,32 @@ async function resizeRGBA(rgba, maxWidth, maxHeight) {
214
243
 
215
244
  return maybeResizedRGBA
216
245
  }
246
+
247
+ async function cropRGBA(rgba, left, top, width, height) {
248
+ if (
249
+ left < 0 ||
250
+ top < 0 ||
251
+ width <= 0 ||
252
+ height <= 0 ||
253
+ left + width > rgba.width ||
254
+ top + height > rgba.height
255
+ ) {
256
+ throw new Error('Crop rectangle out of bounds')
257
+ }
258
+
259
+ const data = Buffer.alloc(width * height * 4)
260
+
261
+ for (let y = 0; y < height; y++) {
262
+ for (let x = 0; x < width; x++) {
263
+ const srcIndex = ((y + top) * rgba.width + (x + left)) * 4
264
+ const dstIndex = (y * width + x) * 4
265
+
266
+ data[dstIndex] = rgba.data[srcIndex]
267
+ data[dstIndex + 1] = rgba.data[srcIndex + 1]
268
+ data[dstIndex + 2] = rgba.data[srcIndex + 2]
269
+ data[dstIndex + 3] = rgba.data[srcIndex + 3]
270
+ }
271
+ }
272
+
273
+ return { width, height, data }
274
+ }
package/worker/util.js CHANGED
@@ -1,5 +1,31 @@
1
+ import fs from 'bare-fs'
2
+ import fetch from 'bare-fetch'
3
+ import getMimeType from 'get-mime-type'
4
+ import getFileFormat from 'get-file-format'
5
+
1
6
  export const log = (...args) => console.log('[bare-media]', ...args)
2
7
 
8
+ export async function getBuffer({ path, httpLink, buffer }) {
9
+ if (buffer) return buffer
10
+
11
+ if (path) {
12
+ return fs.readFileSync(path)
13
+ }
14
+
15
+ if (httpLink) {
16
+ const response = await fetch(httpLink)
17
+ return await response.buffer()
18
+ }
19
+
20
+ throw new Error(
21
+ 'At least one of "path", "httpLink" or "buffer" must be provided'
22
+ )
23
+ }
24
+
25
+ export function detectMimeType(buffer, path) {
26
+ return getMimeType(getFileFormat(buffer)) || getMimeType(path)
27
+ }
28
+
3
29
  export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
4
30
  if (width <= maxWidth && height <= maxHeight) {
5
31
  return { width, height }