bare-media 1.0.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/LICENSE.txt +202 -0
- package/README.md +59 -0
- package/client/cross-spawn/bare-kit.js +16 -0
- package/client/cross-spawn/index.js +9 -0
- package/client/cross-spawn/pear.js +11 -0
- package/client/index.js +52 -0
- package/index.js +5 -0
- package/package.json +51 -0
- package/shared/codecs.js +20 -0
- package/shared/spec/build.js +34 -0
- package/shared/spec/constants.js +2 -0
- package/shared/spec/hrpc/hrpc.json +44 -0
- package/shared/spec/hrpc/index.js +132 -0
- package/shared/spec/hrpc/messages.js +422 -0
- package/shared/spec/schema/index.js +422 -0
- package/shared/spec/schema/schema.json +272 -0
- package/shared/spec/schema.js +196 -0
- package/worker/index.js +25 -0
- package/worker/media.js +105 -0
- package/worker/util.js +16 -0
|
@@ -0,0 +1,196 @@
|
|
|
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
|
+
name: 'width',
|
|
12
|
+
type: 'uint',
|
|
13
|
+
required: true
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'height',
|
|
17
|
+
type: 'uint',
|
|
18
|
+
required: true
|
|
19
|
+
}]
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
media.register({
|
|
23
|
+
name: 'metadata',
|
|
24
|
+
fields: [{
|
|
25
|
+
name: 'mimetype',
|
|
26
|
+
type: 'string'
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
name: 'dimensions',
|
|
30
|
+
type: '@media/dimensions'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: 'duration',
|
|
34
|
+
type: 'uint'
|
|
35
|
+
}]
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
media.register({
|
|
39
|
+
name: 'file',
|
|
40
|
+
fields: [{
|
|
41
|
+
name: 'metadata',
|
|
42
|
+
type: '@media/metadata'
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'inlined',
|
|
46
|
+
type: 'string'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'buffer',
|
|
50
|
+
type: 'buffer'
|
|
51
|
+
}]
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
media.register({
|
|
55
|
+
name: 'preview-by-size',
|
|
56
|
+
fields: [{
|
|
57
|
+
name: 'small',
|
|
58
|
+
type: '@media/file'
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'medium',
|
|
62
|
+
type: '@media/file'
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
name: 'large',
|
|
66
|
+
type: '@media/file'
|
|
67
|
+
}]
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
media.register({
|
|
71
|
+
name: 'sizePreview',
|
|
72
|
+
fields: [{
|
|
73
|
+
name: 'small',
|
|
74
|
+
type: 'uint',
|
|
75
|
+
required: true
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'medium',
|
|
79
|
+
type: 'uint',
|
|
80
|
+
required: true
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
name: 'large',
|
|
84
|
+
type: 'uint',
|
|
85
|
+
required: true
|
|
86
|
+
}]
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
media.register({
|
|
90
|
+
name: 'create-preview-request',
|
|
91
|
+
fields: [{
|
|
92
|
+
name: 'path',
|
|
93
|
+
type: 'string',
|
|
94
|
+
required: true
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
name: 'mimetype',
|
|
98
|
+
type: 'string'
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: 'maxWidth',
|
|
102
|
+
type: 'uint'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'maxHeight',
|
|
106
|
+
type: 'uint'
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: 'format',
|
|
110
|
+
type: 'string'
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
name: 'encoding',
|
|
114
|
+
type: 'string'
|
|
115
|
+
}]
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
media.register({
|
|
119
|
+
name: 'create-preview-response',
|
|
120
|
+
fields: [{
|
|
121
|
+
name: 'metadata',
|
|
122
|
+
type: '@media/metadata',
|
|
123
|
+
required: true
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
name: 'preview',
|
|
127
|
+
type: '@media/file',
|
|
128
|
+
required: true
|
|
129
|
+
}]
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
media.register({
|
|
133
|
+
name: 'create-preview-all-request',
|
|
134
|
+
fields: [{
|
|
135
|
+
name: 'path',
|
|
136
|
+
type: 'string',
|
|
137
|
+
required: true
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'mimetype',
|
|
141
|
+
type: 'string'
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'maxWidth',
|
|
145
|
+
type: '@media/sizePreview',
|
|
146
|
+
required: true
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'maxHeight',
|
|
150
|
+
type: '@media/sizePreview',
|
|
151
|
+
required: true
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
name: 'format',
|
|
155
|
+
type: 'string'
|
|
156
|
+
}]
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
media.register({
|
|
160
|
+
name: 'create-preview-all-response',
|
|
161
|
+
fields: [{
|
|
162
|
+
name: 'metadata',
|
|
163
|
+
type: '@media/metadata',
|
|
164
|
+
required: true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
name: 'preview',
|
|
168
|
+
type: '@media/preview-by-size',
|
|
169
|
+
required: true
|
|
170
|
+
}]
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
media.register({
|
|
174
|
+
name: 'decode-image-request',
|
|
175
|
+
fields: [{
|
|
176
|
+
name: 'httpLink',
|
|
177
|
+
type: 'string',
|
|
178
|
+
required: true
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
name: 'mimetype',
|
|
182
|
+
type: 'string'
|
|
183
|
+
}]
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
media.register({
|
|
187
|
+
name: 'decode-image-response',
|
|
188
|
+
fields: [{
|
|
189
|
+
name: 'metadata',
|
|
190
|
+
type: '@media/metadata'
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
name: 'data',
|
|
194
|
+
type: 'buffer'
|
|
195
|
+
}]
|
|
196
|
+
})
|
package/worker/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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.onCreatePreviewAll(media.createPreviewAll)
|
|
21
|
+
rpc.onDecodeImage(media.decodeImage)
|
|
22
|
+
|
|
23
|
+
uncaughts.on((err) => {
|
|
24
|
+
log('Uncaught error:', err)
|
|
25
|
+
})
|
package/worker/media.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import b4a from 'b4a'
|
|
2
|
+
import fs from 'bare-fs'
|
|
3
|
+
import fetch from 'bare-fetch'
|
|
4
|
+
|
|
5
|
+
import { importCodec } from '../shared/codecs.js'
|
|
6
|
+
import { calculateFitDimensions } from './util'
|
|
7
|
+
|
|
8
|
+
const DEFAULT_PREVIEW_FORMAT = 'image/webp'
|
|
9
|
+
|
|
10
|
+
const animatableMimetypes = ['image/webp']
|
|
11
|
+
|
|
12
|
+
export async function createPreview ({ path, mimetype, maxWidth, maxHeight, format, encoding }) {
|
|
13
|
+
format = format || DEFAULT_PREVIEW_FORMAT
|
|
14
|
+
|
|
15
|
+
const buffer = fs.readFileSync(path)
|
|
16
|
+
const rgba = await decodeImageToRGBA(buffer, mimetype)
|
|
17
|
+
const { width, height } = rgba
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
metadata: {
|
|
21
|
+
dimensions: { width, height }
|
|
22
|
+
},
|
|
23
|
+
preview: await createPreviewFromRGBA(rgba, maxWidth, maxHeight, format, encoding)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function createPreviewAll ({ path, mimetype, maxWidth, maxHeight, format }) {
|
|
28
|
+
format = format || DEFAULT_PREVIEW_FORMAT
|
|
29
|
+
|
|
30
|
+
const buffer = fs.readFileSync(path)
|
|
31
|
+
const rgba = await decodeImageToRGBA(buffer, mimetype)
|
|
32
|
+
const { width, height } = rgba
|
|
33
|
+
|
|
34
|
+
const [small, medium, large] = await Promise.all([
|
|
35
|
+
createPreviewFromRGBA(rgba, maxWidth.small, maxHeight.small, format, 'base64'),
|
|
36
|
+
createPreviewFromRGBA(rgba, maxWidth.medium, maxHeight.medium, format, 'base64'),
|
|
37
|
+
createPreviewFromRGBA(rgba, maxWidth.large, maxHeight.large, format)
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
metadata: {
|
|
42
|
+
dimensions: { width, height }
|
|
43
|
+
},
|
|
44
|
+
preview: { small, medium, large }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function decodeImage ({ httpLink, mimetype }) {
|
|
49
|
+
const response = await fetch(httpLink)
|
|
50
|
+
const buffer = await response.buffer()
|
|
51
|
+
|
|
52
|
+
const rgba = await decodeImageToRGBA(buffer, mimetype)
|
|
53
|
+
const { width, height, data } = rgba
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
metadata: {
|
|
57
|
+
dimensions: { width, height }
|
|
58
|
+
},
|
|
59
|
+
data
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function decodeImageToRGBA (buffer, mimetype) {
|
|
64
|
+
let rgba
|
|
65
|
+
|
|
66
|
+
const codec = await importCodec(mimetype)
|
|
67
|
+
|
|
68
|
+
if (animatableMimetypes.includes(mimetype)) {
|
|
69
|
+
const { frames, width, height } = codec.decodeAnimated(buffer)
|
|
70
|
+
const { data } = frames.next().value
|
|
71
|
+
rgba = { width, height, data }
|
|
72
|
+
} else {
|
|
73
|
+
rgba = codec.decode(buffer)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return rgba
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function createPreviewFromRGBA (rgba, maxWidth, maxHeight, format, encoding) {
|
|
80
|
+
format = format || DEFAULT_PREVIEW_FORMAT
|
|
81
|
+
|
|
82
|
+
const { width, height } = rgba
|
|
83
|
+
let maybeResized, dimensions
|
|
84
|
+
|
|
85
|
+
if (maxWidth && maxHeight && (width > maxWidth || height > maxHeight)) {
|
|
86
|
+
const { resize } = await import('bare-image-resample')
|
|
87
|
+
dimensions = calculateFitDimensions(width, height, maxWidth, maxHeight)
|
|
88
|
+
maybeResized = resize(rgba, dimensions.width, dimensions.height)
|
|
89
|
+
} else {
|
|
90
|
+
dimensions = { width, height }
|
|
91
|
+
maybeResized = rgba
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const codec = await importCodec(format)
|
|
95
|
+
const encoded = codec.encode(maybeResized)
|
|
96
|
+
|
|
97
|
+
const result = encoding === 'base64'
|
|
98
|
+
? { inlined: b4a.toString(encoded, 'base64') }
|
|
99
|
+
: { buffer: encoded }
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
...result,
|
|
103
|
+
metadata: { mimetype: format, dimensions }
|
|
104
|
+
}
|
|
105
|
+
}
|
package/worker/util.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const log = (...args) => console.log('[bare-media]', ...args)
|
|
2
|
+
|
|
3
|
+
export function calculateFitDimensions (width, height, maxWidth, maxHeight) {
|
|
4
|
+
if (width <= maxWidth && height <= maxHeight) {
|
|
5
|
+
return { width, height }
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const widthRatio = maxWidth / width
|
|
9
|
+
const heightRatio = maxHeight / height
|
|
10
|
+
const ratio = Math.min(widthRatio, heightRatio)
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
width: Math.round(width * ratio),
|
|
14
|
+
height: Math.round(height * ratio)
|
|
15
|
+
}
|
|
16
|
+
}
|