@openfn/language-image-utils 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 +674 -0
- package/LICENSE.LESSER +165 -0
- package/README.md +31 -0
- package/ast.json +870 -0
- package/configuration-schema.json +7 -0
- package/dist/index.cjs +314 -0
- package/dist/index.js +284 -0
- package/package.json +50 -0
- package/types/Adaptor.d.ts +110 -0
- package/types/Utils.d.ts +26 -0
- package/types/index.d.ts +3 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
21
|
+
mod
|
|
22
|
+
));
|
|
23
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
24
|
+
|
|
25
|
+
// src/index.js
|
|
26
|
+
var src_exports = {};
|
|
27
|
+
__export(src_exports, {
|
|
28
|
+
as: () => import_language_common2.as,
|
|
29
|
+
combine: () => import_language_common2.combine,
|
|
30
|
+
compress: () => compress,
|
|
31
|
+
cursor: () => import_language_common2.cursor,
|
|
32
|
+
dataPath: () => import_language_common2.dataPath,
|
|
33
|
+
dataValue: () => import_language_common2.dataValue,
|
|
34
|
+
dateFns: () => import_language_common2.dateFns,
|
|
35
|
+
default: () => src_default,
|
|
36
|
+
each: () => import_language_common2.each,
|
|
37
|
+
embedMetadata: () => embedMetadata,
|
|
38
|
+
field: () => import_language_common2.field,
|
|
39
|
+
fields: () => import_language_common2.fields,
|
|
40
|
+
fn: () => import_language_common2.fn,
|
|
41
|
+
fnIf: () => import_language_common2.fnIf,
|
|
42
|
+
getExifData: () => getExifData,
|
|
43
|
+
group: () => import_language_common2.group,
|
|
44
|
+
http: () => import_language_common2.http,
|
|
45
|
+
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
46
|
+
map: () => import_language_common2.map,
|
|
47
|
+
merge: () => import_language_common2.merge,
|
|
48
|
+
metadata: () => metadata,
|
|
49
|
+
resize: () => resize,
|
|
50
|
+
scrubEmojis: () => import_language_common2.scrubEmojis,
|
|
51
|
+
sourceValue: () => import_language_common2.sourceValue,
|
|
52
|
+
stripMetadata: () => stripMetadata,
|
|
53
|
+
util: () => import_language_common2.util
|
|
54
|
+
});
|
|
55
|
+
module.exports = __toCommonJS(src_exports);
|
|
56
|
+
|
|
57
|
+
// src/Adaptor.js
|
|
58
|
+
var Adaptor_exports = {};
|
|
59
|
+
__export(Adaptor_exports, {
|
|
60
|
+
as: () => import_language_common2.as,
|
|
61
|
+
combine: () => import_language_common2.combine,
|
|
62
|
+
compress: () => compress,
|
|
63
|
+
cursor: () => import_language_common2.cursor,
|
|
64
|
+
dataPath: () => import_language_common2.dataPath,
|
|
65
|
+
dataValue: () => import_language_common2.dataValue,
|
|
66
|
+
dateFns: () => import_language_common2.dateFns,
|
|
67
|
+
each: () => import_language_common2.each,
|
|
68
|
+
embedMetadata: () => embedMetadata,
|
|
69
|
+
field: () => import_language_common2.field,
|
|
70
|
+
fields: () => import_language_common2.fields,
|
|
71
|
+
fn: () => import_language_common2.fn,
|
|
72
|
+
fnIf: () => import_language_common2.fnIf,
|
|
73
|
+
getExifData: () => getExifData,
|
|
74
|
+
group: () => import_language_common2.group,
|
|
75
|
+
http: () => import_language_common2.http,
|
|
76
|
+
lastReferenceValue: () => import_language_common2.lastReferenceValue,
|
|
77
|
+
map: () => import_language_common2.map,
|
|
78
|
+
merge: () => import_language_common2.merge,
|
|
79
|
+
metadata: () => metadata,
|
|
80
|
+
resize: () => resize,
|
|
81
|
+
scrubEmojis: () => import_language_common2.scrubEmojis,
|
|
82
|
+
sourceValue: () => import_language_common2.sourceValue,
|
|
83
|
+
stripMetadata: () => stripMetadata,
|
|
84
|
+
util: () => import_language_common2.util
|
|
85
|
+
});
|
|
86
|
+
var import_util = require("@openfn/language-common/util");
|
|
87
|
+
var import_language_common = require("@openfn/language-common");
|
|
88
|
+
|
|
89
|
+
// src/Utils.js
|
|
90
|
+
var import_jimp = require("jimp");
|
|
91
|
+
var import_piexifjs = __toESM(require("piexifjs"), 1);
|
|
92
|
+
function decodeBase64Image(base64ImgStr) {
|
|
93
|
+
const raw = base64ImgStr.includes(",") ? base64ImgStr.split(",")[1] : base64ImgStr;
|
|
94
|
+
return Buffer.from(raw, "base64");
|
|
95
|
+
}
|
|
96
|
+
var TAG_MAP = {};
|
|
97
|
+
var REVERSE_TAG_MAP = {};
|
|
98
|
+
for (const [ifd, tags] of Object.entries({
|
|
99
|
+
"0th": import_piexifjs.default.ImageIFD,
|
|
100
|
+
Exif: import_piexifjs.default.ExifIFD,
|
|
101
|
+
GPS: import_piexifjs.default.GPSIFD,
|
|
102
|
+
Interop: import_piexifjs.default.InteropIFD
|
|
103
|
+
})) {
|
|
104
|
+
REVERSE_TAG_MAP[ifd] = {};
|
|
105
|
+
for (const [name, tagId] of Object.entries(tags)) {
|
|
106
|
+
TAG_MAP[name] = { ifd, tag: tagId };
|
|
107
|
+
REVERSE_TAG_MAP[ifd][tagId] = name;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
var POINTER_TAGS = /* @__PURE__ */ new Set(["ExifTag", "GPSTag", "InteroperabilityTag"]);
|
|
111
|
+
function buildExifBytes(kvPairs) {
|
|
112
|
+
var _a;
|
|
113
|
+
const ifdData = {};
|
|
114
|
+
for (const [key, value] of Object.entries(kvPairs)) {
|
|
115
|
+
const entry = TAG_MAP[key];
|
|
116
|
+
if (!entry)
|
|
117
|
+
throw new Error(`Unknown EXIF tag: "${key}"`);
|
|
118
|
+
ifdData[_a = entry.ifd] ?? (ifdData[_a] = {});
|
|
119
|
+
ifdData[entry.ifd][entry.tag] = key === "UserComment" ? `ASCII\0\0\0${value}` : value;
|
|
120
|
+
}
|
|
121
|
+
return import_piexifjs.default.dump(ifdData);
|
|
122
|
+
}
|
|
123
|
+
function injectExif(jpegBuffer, kvPairs) {
|
|
124
|
+
const exifBytes = buildExifBytes(kvPairs);
|
|
125
|
+
const withExif = import_piexifjs.default.insert(exifBytes, jpegBuffer.toString("binary"));
|
|
126
|
+
return Buffer.from(withExif, "binary");
|
|
127
|
+
}
|
|
128
|
+
async function resizeImage(inputBuffer, options = {}) {
|
|
129
|
+
const { width, height } = options;
|
|
130
|
+
const kb = (n) => `${(n / 1024).toFixed(1)}KB`;
|
|
131
|
+
console.log(`\u2192 input ${kb(inputBuffer.length)}, target ${width}\xD7${height}px`);
|
|
132
|
+
const image = await import_jimp.Jimp.read(inputBuffer);
|
|
133
|
+
const orientation = image.width > image.height ? "landscape" : image.width < image.height ? "portrait" : "square";
|
|
134
|
+
console.log(
|
|
135
|
+
`\u2192 source ${image.width}\xD7${image.height}px | ${kb(inputBuffer.length)} | ${orientation}`
|
|
136
|
+
);
|
|
137
|
+
image.resize({ w: width, h: height });
|
|
138
|
+
console.log(`\u2192 resized to ${width}\xD7${height}px`);
|
|
139
|
+
const buffer = await image.getBuffer("image/jpeg", { quality: 100 });
|
|
140
|
+
return { buffer, width, height };
|
|
141
|
+
}
|
|
142
|
+
function embedImageMetadata(inputBuffer, kvPairs) {
|
|
143
|
+
return { buffer: injectExif(inputBuffer, kvPairs) };
|
|
144
|
+
}
|
|
145
|
+
async function compressImage(inputBuffer, options = {}) {
|
|
146
|
+
const { maxBytes = 700 * 1024, minQuality = 20 } = options;
|
|
147
|
+
const kb = (n) => `${(n / 1024).toFixed(1)}KB`;
|
|
148
|
+
const image = await import_jimp.Jimp.read(inputBuffer);
|
|
149
|
+
let quality = 80;
|
|
150
|
+
let outBuffer;
|
|
151
|
+
while (true) {
|
|
152
|
+
outBuffer = await image.getBuffer("image/jpeg", { quality });
|
|
153
|
+
console.log(`\u2192 quality=${quality} \u2192 ${kb(outBuffer.length)}`);
|
|
154
|
+
if (outBuffer.length <= maxBytes || quality <= minQuality)
|
|
155
|
+
break;
|
|
156
|
+
quality -= 5;
|
|
157
|
+
}
|
|
158
|
+
const targetMet = outBuffer.length <= maxBytes;
|
|
159
|
+
console.log(
|
|
160
|
+
`\u2192 done \u2014 quality=${quality}, size=${kb(outBuffer.length)}` + (targetMet ? "" : ` (min quality reached, target ${kb(maxBytes)} not met)`)
|
|
161
|
+
);
|
|
162
|
+
return { buffer: outBuffer, size: outBuffer.length, quality };
|
|
163
|
+
}
|
|
164
|
+
async function stripImage(inputBuffer) {
|
|
165
|
+
const image = await import_jimp.Jimp.read(inputBuffer);
|
|
166
|
+
const buffer = await image.getBuffer("image/jpeg", { quality: 100 });
|
|
167
|
+
return { buffer };
|
|
168
|
+
}
|
|
169
|
+
async function getImageMetadata(inputBuffer) {
|
|
170
|
+
const image = await import_jimp.Jimp.read(inputBuffer);
|
|
171
|
+
const orientation = image.width > image.height ? "landscape" : image.width < image.height ? "portrait" : "square";
|
|
172
|
+
return {
|
|
173
|
+
width: image.width,
|
|
174
|
+
height: image.height,
|
|
175
|
+
orientation,
|
|
176
|
+
size: inputBuffer.length,
|
|
177
|
+
exif: getExifData(inputBuffer)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function getExifData(inputBuffer, options = {}) {
|
|
181
|
+
var _a;
|
|
182
|
+
const { mergeOutput = true } = options;
|
|
183
|
+
const exifObj = import_piexifjs.default.load(inputBuffer.toString("binary"));
|
|
184
|
+
const exifData = {};
|
|
185
|
+
for (const ifd in exifObj) {
|
|
186
|
+
if (ifd === "thumbnail" || !exifObj[ifd])
|
|
187
|
+
continue;
|
|
188
|
+
if (!mergeOutput)
|
|
189
|
+
exifData[ifd] = {};
|
|
190
|
+
for (const tag in exifObj[ifd]) {
|
|
191
|
+
const tagInfo = (_a = import_piexifjs.default.TAGS[ifd]) == null ? void 0 : _a[tag];
|
|
192
|
+
if (!tagInfo || tagInfo.type === "UNDEFINED")
|
|
193
|
+
continue;
|
|
194
|
+
if (POINTER_TAGS.has(tagInfo.name))
|
|
195
|
+
continue;
|
|
196
|
+
const target = mergeOutput ? exifData : exifData[ifd];
|
|
197
|
+
target[tagInfo.name] = exifObj[ifd][tag];
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return exifData;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/Adaptor.js
|
|
204
|
+
var import_language_common2 = require("@openfn/language-common");
|
|
205
|
+
function resolveInput(raw) {
|
|
206
|
+
return Buffer.isBuffer(raw) ? raw : decodeBase64Image(raw);
|
|
207
|
+
}
|
|
208
|
+
function applyParseAs(result, parseAs) {
|
|
209
|
+
if (parseAs !== "base64")
|
|
210
|
+
return result;
|
|
211
|
+
const { buffer, ...rest } = result;
|
|
212
|
+
return { ...rest, base64: buffer.toString("base64") };
|
|
213
|
+
}
|
|
214
|
+
function resize(base64ImgOrBuffer, options = {}) {
|
|
215
|
+
return async (state) => {
|
|
216
|
+
const [resolvedImg, resolvedOptions] = (0, import_util.expandReferences)(
|
|
217
|
+
state,
|
|
218
|
+
base64ImgOrBuffer,
|
|
219
|
+
options
|
|
220
|
+
);
|
|
221
|
+
const result = await resizeImage(
|
|
222
|
+
resolveInput(resolvedImg),
|
|
223
|
+
resolvedOptions
|
|
224
|
+
);
|
|
225
|
+
return (0, import_language_common.composeNextState)(
|
|
226
|
+
state,
|
|
227
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
228
|
+
);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function compress(base64ImgOrBuffer, options = {}) {
|
|
232
|
+
return async (state) => {
|
|
233
|
+
const [resolvedImg, resolvedOptions] = (0, import_util.expandReferences)(
|
|
234
|
+
state,
|
|
235
|
+
base64ImgOrBuffer,
|
|
236
|
+
options
|
|
237
|
+
);
|
|
238
|
+
const result = await compressImage(
|
|
239
|
+
resolveInput(resolvedImg),
|
|
240
|
+
resolvedOptions
|
|
241
|
+
);
|
|
242
|
+
return (0, import_language_common.composeNextState)(
|
|
243
|
+
state,
|
|
244
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function stripMetadata(base64ImgOrBuffer, options = {}) {
|
|
249
|
+
return async (state) => {
|
|
250
|
+
const [resolvedImg, resolvedOptions] = (0, import_util.expandReferences)(
|
|
251
|
+
state,
|
|
252
|
+
base64ImgOrBuffer,
|
|
253
|
+
options
|
|
254
|
+
);
|
|
255
|
+
const result = await stripImage(resolveInput(resolvedImg));
|
|
256
|
+
return (0, import_language_common.composeNextState)(
|
|
257
|
+
state,
|
|
258
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
259
|
+
);
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function embedMetadata(base64ImgOrBuffer, exifObj, options = {}) {
|
|
263
|
+
return async (state) => {
|
|
264
|
+
const [resolvedImg, resolvedExif, resolvedOptions] = (0, import_util.expandReferences)(
|
|
265
|
+
state,
|
|
266
|
+
base64ImgOrBuffer,
|
|
267
|
+
exifObj,
|
|
268
|
+
options
|
|
269
|
+
);
|
|
270
|
+
const result = embedImageMetadata(resolveInput(resolvedImg), resolvedExif);
|
|
271
|
+
return (0, import_language_common.composeNextState)(
|
|
272
|
+
state,
|
|
273
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
274
|
+
);
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
function metadata(base64ImgOrBuffer) {
|
|
278
|
+
return async (state) => {
|
|
279
|
+
const [resolvedImg] = (0, import_util.expandReferences)(state, base64ImgOrBuffer);
|
|
280
|
+
const result = await getImageMetadata(resolveInput(resolvedImg));
|
|
281
|
+
return (0, import_language_common.composeNextState)(state, result);
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/index.js
|
|
286
|
+
var src_default = Adaptor_exports;
|
|
287
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
288
|
+
0 && (module.exports = {
|
|
289
|
+
as,
|
|
290
|
+
combine,
|
|
291
|
+
compress,
|
|
292
|
+
cursor,
|
|
293
|
+
dataPath,
|
|
294
|
+
dataValue,
|
|
295
|
+
dateFns,
|
|
296
|
+
each,
|
|
297
|
+
embedMetadata,
|
|
298
|
+
field,
|
|
299
|
+
fields,
|
|
300
|
+
fn,
|
|
301
|
+
fnIf,
|
|
302
|
+
getExifData,
|
|
303
|
+
group,
|
|
304
|
+
http,
|
|
305
|
+
lastReferenceValue,
|
|
306
|
+
map,
|
|
307
|
+
merge,
|
|
308
|
+
metadata,
|
|
309
|
+
resize,
|
|
310
|
+
scrubEmojis,
|
|
311
|
+
sourceValue,
|
|
312
|
+
stripMetadata,
|
|
313
|
+
util
|
|
314
|
+
});
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __export = (target, all) => {
|
|
3
|
+
for (var name in all)
|
|
4
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
// src/Adaptor.js
|
|
8
|
+
var Adaptor_exports = {};
|
|
9
|
+
__export(Adaptor_exports, {
|
|
10
|
+
as: () => as,
|
|
11
|
+
combine: () => combine,
|
|
12
|
+
compress: () => compress,
|
|
13
|
+
cursor: () => cursor,
|
|
14
|
+
dataPath: () => dataPath,
|
|
15
|
+
dataValue: () => dataValue,
|
|
16
|
+
dateFns: () => dateFns,
|
|
17
|
+
each: () => each,
|
|
18
|
+
embedMetadata: () => embedMetadata,
|
|
19
|
+
field: () => field,
|
|
20
|
+
fields: () => fields,
|
|
21
|
+
fn: () => fn,
|
|
22
|
+
fnIf: () => fnIf,
|
|
23
|
+
getExifData: () => getExifData,
|
|
24
|
+
group: () => group,
|
|
25
|
+
http: () => http,
|
|
26
|
+
lastReferenceValue: () => lastReferenceValue,
|
|
27
|
+
map: () => map,
|
|
28
|
+
merge: () => merge,
|
|
29
|
+
metadata: () => metadata,
|
|
30
|
+
resize: () => resize,
|
|
31
|
+
scrubEmojis: () => scrubEmojis,
|
|
32
|
+
sourceValue: () => sourceValue,
|
|
33
|
+
stripMetadata: () => stripMetadata,
|
|
34
|
+
util: () => util
|
|
35
|
+
});
|
|
36
|
+
import { expandReferences } from "@openfn/language-common/util";
|
|
37
|
+
import { composeNextState } from "@openfn/language-common";
|
|
38
|
+
|
|
39
|
+
// src/Utils.js
|
|
40
|
+
import { Jimp } from "jimp";
|
|
41
|
+
import piexif from "piexifjs";
|
|
42
|
+
function decodeBase64Image(base64ImgStr) {
|
|
43
|
+
const raw = base64ImgStr.includes(",") ? base64ImgStr.split(",")[1] : base64ImgStr;
|
|
44
|
+
return Buffer.from(raw, "base64");
|
|
45
|
+
}
|
|
46
|
+
var TAG_MAP = {};
|
|
47
|
+
var REVERSE_TAG_MAP = {};
|
|
48
|
+
for (const [ifd, tags] of Object.entries({
|
|
49
|
+
"0th": piexif.ImageIFD,
|
|
50
|
+
Exif: piexif.ExifIFD,
|
|
51
|
+
GPS: piexif.GPSIFD,
|
|
52
|
+
Interop: piexif.InteropIFD
|
|
53
|
+
})) {
|
|
54
|
+
REVERSE_TAG_MAP[ifd] = {};
|
|
55
|
+
for (const [name, tagId] of Object.entries(tags)) {
|
|
56
|
+
TAG_MAP[name] = { ifd, tag: tagId };
|
|
57
|
+
REVERSE_TAG_MAP[ifd][tagId] = name;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
var POINTER_TAGS = /* @__PURE__ */ new Set(["ExifTag", "GPSTag", "InteroperabilityTag"]);
|
|
61
|
+
function buildExifBytes(kvPairs) {
|
|
62
|
+
var _a;
|
|
63
|
+
const ifdData = {};
|
|
64
|
+
for (const [key, value] of Object.entries(kvPairs)) {
|
|
65
|
+
const entry = TAG_MAP[key];
|
|
66
|
+
if (!entry)
|
|
67
|
+
throw new Error(`Unknown EXIF tag: "${key}"`);
|
|
68
|
+
ifdData[_a = entry.ifd] ?? (ifdData[_a] = {});
|
|
69
|
+
ifdData[entry.ifd][entry.tag] = key === "UserComment" ? `ASCII\0\0\0${value}` : value;
|
|
70
|
+
}
|
|
71
|
+
return piexif.dump(ifdData);
|
|
72
|
+
}
|
|
73
|
+
function injectExif(jpegBuffer, kvPairs) {
|
|
74
|
+
const exifBytes = buildExifBytes(kvPairs);
|
|
75
|
+
const withExif = piexif.insert(exifBytes, jpegBuffer.toString("binary"));
|
|
76
|
+
return Buffer.from(withExif, "binary");
|
|
77
|
+
}
|
|
78
|
+
async function resizeImage(inputBuffer, options = {}) {
|
|
79
|
+
const { width, height } = options;
|
|
80
|
+
const kb = (n) => `${(n / 1024).toFixed(1)}KB`;
|
|
81
|
+
console.log(`\u2192 input ${kb(inputBuffer.length)}, target ${width}\xD7${height}px`);
|
|
82
|
+
const image = await Jimp.read(inputBuffer);
|
|
83
|
+
const orientation = image.width > image.height ? "landscape" : image.width < image.height ? "portrait" : "square";
|
|
84
|
+
console.log(
|
|
85
|
+
`\u2192 source ${image.width}\xD7${image.height}px | ${kb(inputBuffer.length)} | ${orientation}`
|
|
86
|
+
);
|
|
87
|
+
image.resize({ w: width, h: height });
|
|
88
|
+
console.log(`\u2192 resized to ${width}\xD7${height}px`);
|
|
89
|
+
const buffer = await image.getBuffer("image/jpeg", { quality: 100 });
|
|
90
|
+
return { buffer, width, height };
|
|
91
|
+
}
|
|
92
|
+
function embedImageMetadata(inputBuffer, kvPairs) {
|
|
93
|
+
return { buffer: injectExif(inputBuffer, kvPairs) };
|
|
94
|
+
}
|
|
95
|
+
async function compressImage(inputBuffer, options = {}) {
|
|
96
|
+
const { maxBytes = 700 * 1024, minQuality = 20 } = options;
|
|
97
|
+
const kb = (n) => `${(n / 1024).toFixed(1)}KB`;
|
|
98
|
+
const image = await Jimp.read(inputBuffer);
|
|
99
|
+
let quality = 80;
|
|
100
|
+
let outBuffer;
|
|
101
|
+
while (true) {
|
|
102
|
+
outBuffer = await image.getBuffer("image/jpeg", { quality });
|
|
103
|
+
console.log(`\u2192 quality=${quality} \u2192 ${kb(outBuffer.length)}`);
|
|
104
|
+
if (outBuffer.length <= maxBytes || quality <= minQuality)
|
|
105
|
+
break;
|
|
106
|
+
quality -= 5;
|
|
107
|
+
}
|
|
108
|
+
const targetMet = outBuffer.length <= maxBytes;
|
|
109
|
+
console.log(
|
|
110
|
+
`\u2192 done \u2014 quality=${quality}, size=${kb(outBuffer.length)}` + (targetMet ? "" : ` (min quality reached, target ${kb(maxBytes)} not met)`)
|
|
111
|
+
);
|
|
112
|
+
return { buffer: outBuffer, size: outBuffer.length, quality };
|
|
113
|
+
}
|
|
114
|
+
async function stripImage(inputBuffer) {
|
|
115
|
+
const image = await Jimp.read(inputBuffer);
|
|
116
|
+
const buffer = await image.getBuffer("image/jpeg", { quality: 100 });
|
|
117
|
+
return { buffer };
|
|
118
|
+
}
|
|
119
|
+
async function getImageMetadata(inputBuffer) {
|
|
120
|
+
const image = await Jimp.read(inputBuffer);
|
|
121
|
+
const orientation = image.width > image.height ? "landscape" : image.width < image.height ? "portrait" : "square";
|
|
122
|
+
return {
|
|
123
|
+
width: image.width,
|
|
124
|
+
height: image.height,
|
|
125
|
+
orientation,
|
|
126
|
+
size: inputBuffer.length,
|
|
127
|
+
exif: getExifData(inputBuffer)
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function getExifData(inputBuffer, options = {}) {
|
|
131
|
+
var _a;
|
|
132
|
+
const { mergeOutput = true } = options;
|
|
133
|
+
const exifObj = piexif.load(inputBuffer.toString("binary"));
|
|
134
|
+
const exifData = {};
|
|
135
|
+
for (const ifd in exifObj) {
|
|
136
|
+
if (ifd === "thumbnail" || !exifObj[ifd])
|
|
137
|
+
continue;
|
|
138
|
+
if (!mergeOutput)
|
|
139
|
+
exifData[ifd] = {};
|
|
140
|
+
for (const tag in exifObj[ifd]) {
|
|
141
|
+
const tagInfo = (_a = piexif.TAGS[ifd]) == null ? void 0 : _a[tag];
|
|
142
|
+
if (!tagInfo || tagInfo.type === "UNDEFINED")
|
|
143
|
+
continue;
|
|
144
|
+
if (POINTER_TAGS.has(tagInfo.name))
|
|
145
|
+
continue;
|
|
146
|
+
const target = mergeOutput ? exifData : exifData[ifd];
|
|
147
|
+
target[tagInfo.name] = exifObj[ifd][tag];
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return exifData;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// src/Adaptor.js
|
|
154
|
+
import {
|
|
155
|
+
as,
|
|
156
|
+
combine,
|
|
157
|
+
cursor,
|
|
158
|
+
dataPath,
|
|
159
|
+
dataValue,
|
|
160
|
+
dateFns,
|
|
161
|
+
each,
|
|
162
|
+
field,
|
|
163
|
+
fields,
|
|
164
|
+
fn,
|
|
165
|
+
fnIf,
|
|
166
|
+
group,
|
|
167
|
+
lastReferenceValue,
|
|
168
|
+
map,
|
|
169
|
+
merge,
|
|
170
|
+
scrubEmojis,
|
|
171
|
+
sourceValue,
|
|
172
|
+
util,
|
|
173
|
+
http
|
|
174
|
+
} from "@openfn/language-common";
|
|
175
|
+
function resolveInput(raw) {
|
|
176
|
+
return Buffer.isBuffer(raw) ? raw : decodeBase64Image(raw);
|
|
177
|
+
}
|
|
178
|
+
function applyParseAs(result, parseAs) {
|
|
179
|
+
if (parseAs !== "base64")
|
|
180
|
+
return result;
|
|
181
|
+
const { buffer, ...rest } = result;
|
|
182
|
+
return { ...rest, base64: buffer.toString("base64") };
|
|
183
|
+
}
|
|
184
|
+
function resize(base64ImgOrBuffer, options = {}) {
|
|
185
|
+
return async (state) => {
|
|
186
|
+
const [resolvedImg, resolvedOptions] = expandReferences(
|
|
187
|
+
state,
|
|
188
|
+
base64ImgOrBuffer,
|
|
189
|
+
options
|
|
190
|
+
);
|
|
191
|
+
const result = await resizeImage(
|
|
192
|
+
resolveInput(resolvedImg),
|
|
193
|
+
resolvedOptions
|
|
194
|
+
);
|
|
195
|
+
return composeNextState(
|
|
196
|
+
state,
|
|
197
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
198
|
+
);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
function compress(base64ImgOrBuffer, options = {}) {
|
|
202
|
+
return async (state) => {
|
|
203
|
+
const [resolvedImg, resolvedOptions] = expandReferences(
|
|
204
|
+
state,
|
|
205
|
+
base64ImgOrBuffer,
|
|
206
|
+
options
|
|
207
|
+
);
|
|
208
|
+
const result = await compressImage(
|
|
209
|
+
resolveInput(resolvedImg),
|
|
210
|
+
resolvedOptions
|
|
211
|
+
);
|
|
212
|
+
return composeNextState(
|
|
213
|
+
state,
|
|
214
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
215
|
+
);
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function stripMetadata(base64ImgOrBuffer, options = {}) {
|
|
219
|
+
return async (state) => {
|
|
220
|
+
const [resolvedImg, resolvedOptions] = expandReferences(
|
|
221
|
+
state,
|
|
222
|
+
base64ImgOrBuffer,
|
|
223
|
+
options
|
|
224
|
+
);
|
|
225
|
+
const result = await stripImage(resolveInput(resolvedImg));
|
|
226
|
+
return composeNextState(
|
|
227
|
+
state,
|
|
228
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
229
|
+
);
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function embedMetadata(base64ImgOrBuffer, exifObj, options = {}) {
|
|
233
|
+
return async (state) => {
|
|
234
|
+
const [resolvedImg, resolvedExif, resolvedOptions] = expandReferences(
|
|
235
|
+
state,
|
|
236
|
+
base64ImgOrBuffer,
|
|
237
|
+
exifObj,
|
|
238
|
+
options
|
|
239
|
+
);
|
|
240
|
+
const result = embedImageMetadata(resolveInput(resolvedImg), resolvedExif);
|
|
241
|
+
return composeNextState(
|
|
242
|
+
state,
|
|
243
|
+
applyParseAs(result, resolvedOptions.parseAs)
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
function metadata(base64ImgOrBuffer) {
|
|
248
|
+
return async (state) => {
|
|
249
|
+
const [resolvedImg] = expandReferences(state, base64ImgOrBuffer);
|
|
250
|
+
const result = await getImageMetadata(resolveInput(resolvedImg));
|
|
251
|
+
return composeNextState(state, result);
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// src/index.js
|
|
256
|
+
var src_default = Adaptor_exports;
|
|
257
|
+
export {
|
|
258
|
+
as,
|
|
259
|
+
combine,
|
|
260
|
+
compress,
|
|
261
|
+
cursor,
|
|
262
|
+
dataPath,
|
|
263
|
+
dataValue,
|
|
264
|
+
dateFns,
|
|
265
|
+
src_default as default,
|
|
266
|
+
each,
|
|
267
|
+
embedMetadata,
|
|
268
|
+
field,
|
|
269
|
+
fields,
|
|
270
|
+
fn,
|
|
271
|
+
fnIf,
|
|
272
|
+
getExifData,
|
|
273
|
+
group,
|
|
274
|
+
http,
|
|
275
|
+
lastReferenceValue,
|
|
276
|
+
map,
|
|
277
|
+
merge,
|
|
278
|
+
metadata,
|
|
279
|
+
resize,
|
|
280
|
+
scrubEmojis,
|
|
281
|
+
sourceValue,
|
|
282
|
+
stripMetadata,
|
|
283
|
+
util
|
|
284
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@openfn/language-image-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "OpenFn image-utils adaptor",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": {
|
|
7
|
+
".": {
|
|
8
|
+
"import": "./dist/index.js",
|
|
9
|
+
"types": "./types/index.d.ts",
|
|
10
|
+
"require": "./dist/index.cjs"
|
|
11
|
+
},
|
|
12
|
+
"./package.json": "./package.json"
|
|
13
|
+
},
|
|
14
|
+
"author": "Open Function Group",
|
|
15
|
+
"license": "LGPLv3",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/",
|
|
18
|
+
"types/",
|
|
19
|
+
"ast.json",
|
|
20
|
+
"configuration-schema.json"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"jimp": "^1.6.0",
|
|
24
|
+
"piexifjs": "^1.0.6",
|
|
25
|
+
"@openfn/language-common": "3.3.4"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"assertion-error": "2.0.0",
|
|
29
|
+
"chai": "4.3.6",
|
|
30
|
+
"deep-eql": "4.1.1",
|
|
31
|
+
"mocha": "^10.7.3",
|
|
32
|
+
"rimraf": "3.0.2"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/openfn/adaptors.git"
|
|
37
|
+
},
|
|
38
|
+
"types": "types/index.d.ts",
|
|
39
|
+
"main": "dist/index.cjs",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "pnpm clean && build-adaptor image-utils",
|
|
42
|
+
"test": "mocha test/*.test.js --experimental-specifier-resolution=node --no-warnings --timeout 30000",
|
|
43
|
+
"test:watch": "mocha test/*.test.js -w --experimental-specifier-resolution=node --no-warnings --timeout 30000",
|
|
44
|
+
"test:integration": "mocha --experimental-specifier-resolution=node --no-warnings test/integration.js",
|
|
45
|
+
"clean": "rimraf dist types docs",
|
|
46
|
+
"pack": "pnpm pack --pack-destination ../../dist",
|
|
47
|
+
"lint": "eslint src",
|
|
48
|
+
"benchmark": "echo 'benchmark removed — jsquash dependency dropped'"
|
|
49
|
+
}
|
|
50
|
+
}
|