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/shared/spec/schema.js
DELETED
|
@@ -1,212 +0,0 @@
|
|
|
1
|
-
import Hyperschema from 'hyperschema'
|
|
2
|
-
|
|
3
|
-
import { SCHEMA_DIR } from './constants'
|
|
4
|
-
|
|
5
|
-
export const schema = Hyperschema.from(SCHEMA_DIR)
|
|
6
|
-
const media = schema.namespace('media')
|
|
7
|
-
|
|
8
|
-
media.register({
|
|
9
|
-
name: 'dimensions',
|
|
10
|
-
fields: [
|
|
11
|
-
{
|
|
12
|
-
name: 'width',
|
|
13
|
-
type: 'uint',
|
|
14
|
-
required: true
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
name: 'height',
|
|
18
|
-
type: 'uint',
|
|
19
|
-
required: true
|
|
20
|
-
}
|
|
21
|
-
]
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
media.register({
|
|
25
|
-
name: 'metadata',
|
|
26
|
-
fields: [
|
|
27
|
-
{
|
|
28
|
-
name: 'mimetype',
|
|
29
|
-
type: 'string'
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: 'dimensions',
|
|
33
|
-
type: '@media/dimensions'
|
|
34
|
-
},
|
|
35
|
-
{
|
|
36
|
-
name: 'duration',
|
|
37
|
-
type: 'uint'
|
|
38
|
-
}
|
|
39
|
-
]
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
media.register({
|
|
43
|
-
name: 'file',
|
|
44
|
-
fields: [
|
|
45
|
-
{
|
|
46
|
-
name: 'metadata',
|
|
47
|
-
type: '@media/metadata'
|
|
48
|
-
},
|
|
49
|
-
{
|
|
50
|
-
name: 'inlined',
|
|
51
|
-
type: 'string'
|
|
52
|
-
},
|
|
53
|
-
{
|
|
54
|
-
name: 'buffer',
|
|
55
|
-
type: 'buffer'
|
|
56
|
-
}
|
|
57
|
-
]
|
|
58
|
-
})
|
|
59
|
-
|
|
60
|
-
media.register({
|
|
61
|
-
name: 'create-preview-request',
|
|
62
|
-
fields: [
|
|
63
|
-
{
|
|
64
|
-
name: 'path',
|
|
65
|
-
type: 'string'
|
|
66
|
-
},
|
|
67
|
-
{
|
|
68
|
-
name: 'httpLink',
|
|
69
|
-
type: 'string'
|
|
70
|
-
},
|
|
71
|
-
{
|
|
72
|
-
name: 'buffer',
|
|
73
|
-
type: 'buffer'
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
name: 'mimetype',
|
|
77
|
-
type: 'string'
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
name: 'maxWidth',
|
|
81
|
-
type: 'uint'
|
|
82
|
-
},
|
|
83
|
-
{
|
|
84
|
-
name: 'maxHeight',
|
|
85
|
-
type: 'uint'
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
name: 'maxFrames',
|
|
89
|
-
type: 'uint'
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'maxBytes',
|
|
93
|
-
type: 'uint'
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'format',
|
|
97
|
-
type: 'string'
|
|
98
|
-
},
|
|
99
|
-
{
|
|
100
|
-
name: 'encoding',
|
|
101
|
-
type: 'string'
|
|
102
|
-
}
|
|
103
|
-
]
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
media.register({
|
|
107
|
-
name: 'create-preview-response',
|
|
108
|
-
fields: [
|
|
109
|
-
{
|
|
110
|
-
name: 'metadata',
|
|
111
|
-
type: '@media/metadata',
|
|
112
|
-
required: true
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: 'preview',
|
|
116
|
-
type: '@media/file',
|
|
117
|
-
required: true
|
|
118
|
-
}
|
|
119
|
-
]
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
media.register({
|
|
123
|
-
name: 'decode-image-request',
|
|
124
|
-
fields: [
|
|
125
|
-
{
|
|
126
|
-
name: 'path',
|
|
127
|
-
type: 'string'
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
name: 'httpLink',
|
|
131
|
-
type: 'string'
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
name: 'buffer',
|
|
135
|
-
type: 'buffer'
|
|
136
|
-
},
|
|
137
|
-
{
|
|
138
|
-
name: 'mimetype',
|
|
139
|
-
type: 'string'
|
|
140
|
-
}
|
|
141
|
-
]
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
media.register({
|
|
145
|
-
name: 'decode-image-response',
|
|
146
|
-
fields: [
|
|
147
|
-
{
|
|
148
|
-
name: 'metadata',
|
|
149
|
-
type: '@media/metadata'
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
name: 'data',
|
|
153
|
-
type: 'buffer'
|
|
154
|
-
}
|
|
155
|
-
]
|
|
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
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import worker from 'cross-worker'
|
|
2
|
-
import uncaughts from 'uncaughts'
|
|
3
|
-
|
|
4
|
-
import HRPC from '../shared/spec/hrpc'
|
|
5
|
-
|
|
6
|
-
import * as media from './media'
|
|
7
|
-
import { log } from './util'
|
|
8
|
-
|
|
9
|
-
log('Worker started 🚀')
|
|
10
|
-
|
|
11
|
-
const stream = worker.stream()
|
|
12
|
-
|
|
13
|
-
stream.on('end', () => stream.end())
|
|
14
|
-
stream.on('error', (err) => console.error(err))
|
|
15
|
-
stream.on('close', () => Bare.exit(0))
|
|
16
|
-
|
|
17
|
-
const rpc = new HRPC(stream)
|
|
18
|
-
|
|
19
|
-
rpc.onCreatePreview(media.createPreview)
|
|
20
|
-
rpc.onDecodeImage(media.decodeImage)
|
|
21
|
-
rpc.onCropImage(media.cropImage)
|
|
22
|
-
|
|
23
|
-
uncaughts.on((err) => {
|
|
24
|
-
log('Uncaught error:', err)
|
|
25
|
-
})
|
package/worker/media.js
DELETED
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import b4a from 'b4a'
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
importCodec,
|
|
5
|
-
isCodecSupported,
|
|
6
|
-
supportsQuality
|
|
7
|
-
} from '../shared/codecs.js'
|
|
8
|
-
import { getBuffer, detectMimeType, calculateFitDimensions } from './util'
|
|
9
|
-
|
|
10
|
-
const DEFAULT_PREVIEW_FORMAT = 'image/webp'
|
|
11
|
-
|
|
12
|
-
const animatableMimetypes = ['image/webp']
|
|
13
|
-
|
|
14
|
-
export async function createPreview({
|
|
15
|
-
path,
|
|
16
|
-
httpLink,
|
|
17
|
-
buffer,
|
|
18
|
-
mimetype,
|
|
19
|
-
maxWidth,
|
|
20
|
-
maxHeight,
|
|
21
|
-
maxFrames,
|
|
22
|
-
maxBytes,
|
|
23
|
-
format,
|
|
24
|
-
encoding
|
|
25
|
-
}) {
|
|
26
|
-
format = format || DEFAULT_PREVIEW_FORMAT
|
|
27
|
-
|
|
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
|
-
|
|
35
|
-
const rgba = await decodeImageToRGBA(buff, mimetype, maxFrames)
|
|
36
|
-
const { width, height } = rgba
|
|
37
|
-
|
|
38
|
-
const maybeResizedRGBA = await resizeRGBA(rgba, maxWidth, maxHeight)
|
|
39
|
-
|
|
40
|
-
let preview = await encodeImageFromRGBA(maybeResizedRGBA, format)
|
|
41
|
-
|
|
42
|
-
// quality reduction
|
|
43
|
-
|
|
44
|
-
if (maxBytes && preview.byteLength > maxBytes && supportsQuality(format)) {
|
|
45
|
-
const MIN_QUALITY = 50
|
|
46
|
-
for (let quality = 80; quality >= MIN_QUALITY; quality -= 15) {
|
|
47
|
-
preview = await encodeImageFromRGBA(maybeResizedRGBA, format, { quality })
|
|
48
|
-
if (preview.byteLength <= maxBytes) {
|
|
49
|
-
break
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// fps reduction
|
|
55
|
-
|
|
56
|
-
if (
|
|
57
|
-
maxBytes &&
|
|
58
|
-
preview.byteLength > maxBytes &&
|
|
59
|
-
maybeResizedRGBA.frames?.length > 1
|
|
60
|
-
) {
|
|
61
|
-
const quality = 75
|
|
62
|
-
|
|
63
|
-
// drop every n frame
|
|
64
|
-
|
|
65
|
-
for (const dropEvery of [4, 3, 2]) {
|
|
66
|
-
const frames = maybeResizedRGBA.frames.filter(
|
|
67
|
-
(frame, index) => index % dropEvery !== 0
|
|
68
|
-
)
|
|
69
|
-
const filtered = { ...maybeResizedRGBA, frames }
|
|
70
|
-
preview = await encodeImageFromRGBA(filtered, format, { quality })
|
|
71
|
-
if (!maxBytes || preview.byteLength <= maxBytes) {
|
|
72
|
-
break
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// cap to 25 frames
|
|
77
|
-
|
|
78
|
-
if (preview.byteLength > maxBytes) {
|
|
79
|
-
const frames = maybeResizedRGBA.frames
|
|
80
|
-
.slice(0, 50)
|
|
81
|
-
.filter((frame, index) => index % 2 === 0)
|
|
82
|
-
const capped = { ...maybeResizedRGBA, frames }
|
|
83
|
-
preview = await encodeImageFromRGBA(capped, format, { quality })
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// take only one frame
|
|
87
|
-
|
|
88
|
-
if (preview.byteLength > maxBytes) {
|
|
89
|
-
const oneFrame = {
|
|
90
|
-
...maybeResizedRGBA,
|
|
91
|
-
frames: maybeResizedRGBA.frames.slice(0, 1)
|
|
92
|
-
}
|
|
93
|
-
preview = await encodeImageFromRGBA(oneFrame, format)
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (maxBytes && preview.byteLength > maxBytes) {
|
|
98
|
-
throw new Error(
|
|
99
|
-
`Could not create preview under maxBytes, reached ${preview.byteLength} bytes`
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const encoded =
|
|
104
|
-
encoding === 'base64'
|
|
105
|
-
? { inlined: b4a.toString(preview, 'base64') }
|
|
106
|
-
: { buffer: preview }
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
metadata: {
|
|
110
|
-
dimensions: { width, height }
|
|
111
|
-
},
|
|
112
|
-
preview: {
|
|
113
|
-
metadata: {
|
|
114
|
-
mimetype: format,
|
|
115
|
-
dimensions: {
|
|
116
|
-
width: maybeResizedRGBA.width,
|
|
117
|
-
height: maybeResizedRGBA.height
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
...encoded
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export async function decodeImage({ path, httpLink, buffer, mimetype }) {
|
|
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
|
-
}
|
|
132
|
-
|
|
133
|
-
const rgba = await decodeImageToRGBA(buff, mimetype)
|
|
134
|
-
const { width, height, data } = rgba
|
|
135
|
-
|
|
136
|
-
return {
|
|
137
|
-
metadata: {
|
|
138
|
-
dimensions: { width, height }
|
|
139
|
-
},
|
|
140
|
-
data
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
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)
|
|
157
|
-
|
|
158
|
-
if (!isCodecSupported(mimetype)) {
|
|
159
|
-
throw new Error(`Unsupported file type: No codec available for ${mimetype}`)
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const rgba = await decodeImageToRGBA(buff, mimetype)
|
|
163
|
-
|
|
164
|
-
const cropped = await cropRGBA(rgba, left, top, width, height)
|
|
165
|
-
|
|
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
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
async function decodeImageToRGBA(buffer, mimetype, maxFrames) {
|
|
180
|
-
let rgba
|
|
181
|
-
|
|
182
|
-
const codec = await importCodec(mimetype)
|
|
183
|
-
|
|
184
|
-
if (animatableMimetypes.includes(mimetype)) {
|
|
185
|
-
const { width, height, loops, frames } = codec.decodeAnimated(buffer)
|
|
186
|
-
const data = []
|
|
187
|
-
for (const frame of frames) {
|
|
188
|
-
if (maxFrames > 0 && data.length >= maxFrames) break
|
|
189
|
-
data.push(frame)
|
|
190
|
-
}
|
|
191
|
-
rgba = { width, height, loops, frames: data }
|
|
192
|
-
} else {
|
|
193
|
-
rgba = codec.decode(buffer)
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return rgba
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
async function encodeImageFromRGBA(rgba, format, opts) {
|
|
200
|
-
const codec = await importCodec(format)
|
|
201
|
-
|
|
202
|
-
let encoded
|
|
203
|
-
if (Array.isArray(rgba.frames)) {
|
|
204
|
-
encoded = codec.encodeAnimated(rgba, opts)
|
|
205
|
-
} else {
|
|
206
|
-
encoded = codec.encode(rgba, opts)
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return encoded
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
async function resizeRGBA(rgba, maxWidth, maxHeight) {
|
|
213
|
-
const { width, height } = rgba
|
|
214
|
-
|
|
215
|
-
let maybeResizedRGBA
|
|
216
|
-
|
|
217
|
-
if (maxWidth && maxHeight && (width > maxWidth || height > maxHeight)) {
|
|
218
|
-
const { resize } = await import('bare-image-resample')
|
|
219
|
-
const dimensions = calculateFitDimensions(
|
|
220
|
-
width,
|
|
221
|
-
height,
|
|
222
|
-
maxWidth,
|
|
223
|
-
maxHeight
|
|
224
|
-
)
|
|
225
|
-
if (Array.isArray(rgba.frames)) {
|
|
226
|
-
const frames = []
|
|
227
|
-
for (const frame of rgba.frames) {
|
|
228
|
-
const resized = resize(frame, dimensions.width, dimensions.height)
|
|
229
|
-
frames.push({ ...resized, timestamp: frame.timestamp })
|
|
230
|
-
}
|
|
231
|
-
maybeResizedRGBA = {
|
|
232
|
-
width: frames[0].width,
|
|
233
|
-
height: frames[0].height,
|
|
234
|
-
loops: rgba.loops,
|
|
235
|
-
frames
|
|
236
|
-
}
|
|
237
|
-
} else {
|
|
238
|
-
maybeResizedRGBA = resize(rgba, dimensions.width, dimensions.height)
|
|
239
|
-
}
|
|
240
|
-
} else {
|
|
241
|
-
maybeResizedRGBA = rgba
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
return maybeResizedRGBA
|
|
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
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
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
|
-
|
|
6
|
-
export const log = (...args) => console.log('[bare-media]', ...args)
|
|
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
|
-
|
|
29
|
-
export function calculateFitDimensions(width, height, maxWidth, maxHeight) {
|
|
30
|
-
if (width <= maxWidth && height <= maxHeight) {
|
|
31
|
-
return { width, height }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const widthRatio = maxWidth / width
|
|
35
|
-
const heightRatio = maxHeight / height
|
|
36
|
-
const ratio = Math.min(widthRatio, heightRatio)
|
|
37
|
-
|
|
38
|
-
return {
|
|
39
|
-
width: Math.round(width * ratio),
|
|
40
|
-
height: Math.round(height * ratio)
|
|
41
|
-
}
|
|
42
|
-
}
|