image-blob-reduce
Version:
High quality image resizing for blobs in browsers (`pica` wrapper with some sugar)
642 lines (639 loc) • 21.7 kB
JavaScript
/*!
image-blob-reduce
https://github.com/nodeca/image-blob-reduce
*/
import pica, { Pica } from "pica";
//#region \0rolldown/runtime.js
var __defProp = Object.defineProperty;
var __exportAll = (all, no_symbols) => {
let target = {};
for (var name in all) __defProp(target, name, {
get: all[name],
enumerable: true
});
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
return target;
};
//#endregion
//#region src/image_traverse.ts
var image_traverse_exports = /* @__PURE__ */ __exportAll({
is_jpeg: () => is_jpeg,
jpeg_add_comment: () => jpeg_add_comment,
jpeg_exif_tags_each: () => jpeg_exif_tags_each,
jpeg_exif_tags_filter: () => jpeg_exif_tags_filter,
jpeg_segments_each: () => jpeg_segments_each,
jpeg_segments_filter: () => jpeg_segments_filter
});
function error(message, code) {
const err = new Error(message);
err.code = code;
return err;
}
function to_hex(number) {
let n = number.toString(16).toUpperCase();
for (let i = 2 - n.length; i > 0; i--) n = "0" + n;
return "0x" + n;
}
function utf8_encode(str) {
try {
return unescape(encodeURIComponent(str));
} catch (_) {
return str;
}
}
function utf8_decode(str) {
try {
return decodeURIComponent(escape(str));
} catch (_) {
return str;
}
}
function is_uint8array(bin) {
return Object.prototype.toString.call(bin) === "[object Uint8Array]";
}
var ExifParser = class {
input;
start;
big_endian;
aborted = false;
ifds_to_read = [];
output = new Uint8Array(0);
constructor(jpeg_bin, exif_start, exif_end) {
this.input = jpeg_bin.subarray(exif_start, exif_end);
this.start = exif_start;
const sig = String.fromCharCode.apply(null, this.input.subarray(0, 4));
if (sig !== "II*\0" && sig !== "MM\0*") throw error("invalid TIFF signature", "EBADDATA");
this.big_endian = sig[0] === "M";
}
each(on_entry) {
this.aborted = false;
const offset = this.read_uint32(4);
this.ifds_to_read = [{
id: 0,
offset
}];
while (this.ifds_to_read.length > 0 && !this.aborted) {
const i = this.ifds_to_read.shift();
if (!i || !i.offset) continue;
this.scan_ifd(i.id, i.offset, on_entry);
}
}
filter(on_entry) {
const ifds = {};
ifds.ifd0 = {
id: 0,
entries: []
};
this.each(function(entry) {
if (on_entry(entry) === false && !entry.is_subifd_link) return;
if (entry.is_subifd_link && entry.count !== 1 && entry.format !== 4) return;
if (!ifds["ifd" + entry.ifd]) ifds["ifd" + entry.ifd] = {
id: entry.ifd,
entries: []
};
ifds["ifd" + entry.ifd].entries.push(entry);
});
delete ifds.ifd1;
let length = 8;
Object.keys(ifds).forEach(function(ifd_no) {
length += 2;
ifds[ifd_no].entries.forEach(function(entry) {
length += 12 + (entry.data_length > 4 ? Math.ceil(entry.data_length / 2) * 2 : 0);
});
length += 4;
});
this.output = new Uint8Array(length);
this.output[0] = this.output[1] = (this.big_endian ? "M" : "I").charCodeAt(0);
this.write_uint16(2, 42);
let offset = 8;
this.write_uint32(4, offset);
Object.keys(ifds).forEach((ifd_no) => {
ifds[ifd_no].written_offset = offset;
const ifd_start = offset;
const ifd_end = ifd_start + 2 + ifds[ifd_no].entries.length * 12 + 4;
offset = ifd_end;
this.write_uint16(ifd_start, ifds[ifd_no].entries.length);
ifds[ifd_no].entries.sort(function(a, b) {
return a.tag - b.tag;
}).forEach((entry, idx) => {
const entry_offset = ifd_start + 2 + idx * 12;
this.write_uint16(entry_offset, entry.tag);
this.write_uint16(entry_offset + 2, entry.format);
this.write_uint32(entry_offset + 4, entry.count);
if (entry.is_subifd_link) {
if (ifds["ifd" + entry.tag]) ifds["ifd" + entry.tag].link_offset = entry_offset + 8;
} else if (entry.data_length <= 4) this.output.set(this.input.subarray(entry.data_offset - this.start, entry.data_offset - this.start + 4), entry_offset + 8);
else {
this.write_uint32(entry_offset + 8, offset);
this.output.set(this.input.subarray(entry.data_offset - this.start, entry.data_offset - this.start + entry.data_length), offset);
offset += Math.ceil(entry.data_length / 2) * 2;
}
});
const next_ifd = ifds["ifd" + (ifds[ifd_no].id + 1)];
if (next_ifd) next_ifd.link_offset = ifd_end - 4;
});
Object.keys(ifds).forEach((ifd_no) => {
if (ifds[ifd_no].written_offset && ifds[ifd_no].link_offset) this.write_uint32(ifds[ifd_no].link_offset, ifds[ifd_no].written_offset);
});
if (this.output.length !== offset) throw error("internal error: incorrect buffer size allocated");
return this.output;
}
read_uint16(offset) {
const d = this.input;
if (offset + 2 > d.length) throw error("unexpected EOF", "EBADDATA");
return this.big_endian ? d[offset] * 256 + d[offset + 1] : d[offset] + d[offset + 1] * 256;
}
read_uint32(offset) {
const d = this.input;
if (offset + 4 > d.length) throw error("unexpected EOF", "EBADDATA");
return this.big_endian ? d[offset] * 16777216 + d[offset + 1] * 65536 + d[offset + 2] * 256 + d[offset + 3] : d[offset] + d[offset + 1] * 256 + d[offset + 2] * 65536 + d[offset + 3] * 16777216;
}
write_uint16(offset, value) {
const d = this.output;
if (this.big_endian) {
d[offset] = value >>> 8 & 255;
d[offset + 1] = value & 255;
} else {
d[offset] = value & 255;
d[offset + 1] = value >>> 8 & 255;
}
}
write_uint32(offset, value) {
const d = this.output;
if (this.big_endian) {
d[offset] = value >>> 24 & 255;
d[offset + 1] = value >>> 16 & 255;
d[offset + 2] = value >>> 8 & 255;
d[offset + 3] = value & 255;
} else {
d[offset] = value & 255;
d[offset + 1] = value >>> 8 & 255;
d[offset + 2] = value >>> 16 & 255;
d[offset + 3] = value >>> 24 & 255;
}
}
is_subifd_link(ifd, tag) {
return ifd === 0 && tag === 34665 || ifd === 0 && tag === 34853 || ifd === 34665 && tag === 40965;
}
exif_format_length(format) {
switch (format) {
case 1:
case 2:
case 6:
case 7: return 1;
case 3:
case 8: return 2;
case 4:
case 9:
case 11: return 4;
case 5:
case 10:
case 12: return 8;
default: return 0;
}
}
exif_format_read(format, offset) {
let v;
switch (format) {
case 1:
case 2:
v = this.input[offset];
return v;
case 6:
v = this.input[offset];
return v | (v & 128) * 33554430;
case 3:
v = this.read_uint16(offset);
return v;
case 8:
v = this.read_uint16(offset);
return v | (v & 32768) * 131070;
case 4:
v = this.read_uint32(offset);
return v;
case 9:
v = this.read_uint32(offset);
return v | 0;
case 5:
case 10:
case 11:
case 12: return null;
case 7: return null;
default: return null;
}
}
scan_ifd(ifd_no, offset, on_entry) {
const entry_count = this.read_uint16(offset);
offset += 2;
for (let i = 0; i < entry_count; i++) {
const tag = this.read_uint16(offset);
const format = this.read_uint16(offset + 2);
const count = this.read_uint32(offset + 4);
const comp_length = this.exif_format_length(format);
const data_length = count * comp_length;
const data_offset = data_length <= 4 ? offset + 8 : this.read_uint32(offset + 8);
let is_subifd_link = false;
if (data_offset + data_length > this.input.length) throw error("unexpected EOF", "EBADDATA");
let value = [];
let comp_offset = data_offset;
for (let j = 0; j < count; j++, comp_offset += comp_length) {
const item = this.exif_format_read(format, comp_offset);
if (item === null) {
value = null;
break;
}
value.push(item);
}
if (Array.isArray(value) && format === 2) {
try {
value = utf8_decode(String.fromCharCode.apply(null, value));
} catch (_) {
value = null;
}
if (value && value[value.length - 1] === "\0") value = value.slice(0, -1);
}
if (this.is_subifd_link(ifd_no, tag)) {
if (Array.isArray(value) && Number.isInteger(value[0]) && value[0] > 0) {
this.ifds_to_read.push({
id: tag,
offset: value[0]
});
is_subifd_link = true;
}
}
if (on_entry({
is_big_endian: this.big_endian,
ifd: ifd_no,
tag,
format,
count,
entry_offset: offset + this.start,
data_length,
data_offset: data_offset + this.start,
value,
is_subifd_link
}) === false) {
this.aborted = true;
return;
}
offset += 12;
}
if (ifd_no === 0) this.ifds_to_read.push({
id: 1,
offset: this.read_uint32(offset)
});
}
};
function is_jpeg(jpeg_bin) {
return jpeg_bin.length >= 4 && jpeg_bin[0] === 255 && jpeg_bin[1] === 216 && jpeg_bin[2] === 255;
}
function jpeg_segments_each(jpeg_bin, on_segment) {
if (!is_uint8array(jpeg_bin)) throw error("Invalid argument (jpeg_bin), Uint8Array expected", "EINVAL");
if (typeof on_segment !== "function") throw error("Invalid argument (on_segment), Function expected", "EINVAL");
if (!is_jpeg(jpeg_bin)) throw error("Unknown file format", "ENOTJPEG");
let offset = 0, inside_scan = false;
const length = jpeg_bin.length;
for (;;) {
let segment_code;
let segment_length;
if (offset + 1 >= length) throw error("Unexpected EOF", "EBADDATA");
const byte1 = jpeg_bin[offset];
const byte2 = jpeg_bin[offset + 1];
if (byte1 === 255 && byte2 === 255) {
segment_code = 255;
segment_length = 1;
} else if (byte1 === 255 && byte2 !== 0) {
segment_code = byte2;
segment_length = 2;
if (segment_code >= 208 && segment_code <= 217 || segment_code === 1) {} else {
if (offset + 3 >= length) throw error("Unexpected EOF", "EBADDATA");
segment_length += jpeg_bin[offset + 2] * 256 + jpeg_bin[offset + 3];
if (segment_length < 2) throw error("Invalid segment length", "EBADDATA");
if (offset + segment_length - 1 >= length) throw error("Unexpected EOF", "EBADDATA");
}
if (inside_scan) if (segment_code >= 208 && segment_code <= 215) {} else inside_scan = false;
if (segment_code === 218) inside_scan = true;
} else if (inside_scan) for (let pos = offset + 1;; pos++) {
if (pos >= length) throw error("Unexpected EOF", "EBADDATA");
if (jpeg_bin[pos] === 255) {
if (pos + 1 >= length) throw error("Unexpected EOF", "EBADDATA");
if (jpeg_bin[pos + 1] !== 0) {
segment_code = 0;
segment_length = pos - offset;
break;
}
}
}
else throw error("Unexpected byte at segment start: " + to_hex(byte1) + " (offset " + to_hex(offset) + ")", "EBADDATA");
if (on_segment({
code: segment_code,
offset,
length: segment_length
}) === false) break;
if (segment_code === 217) break;
offset += segment_length;
}
}
function jpeg_segments_filter(jpeg_bin, on_segment) {
if (!is_uint8array(jpeg_bin)) throw error("Invalid argument (jpeg_bin), Uint8Array expected", "EINVAL");
if (typeof on_segment !== "function") throw error("Invalid argument (on_segment), Function expected", "EINVAL");
const ranges = [];
let out_length = 0;
jpeg_segments_each(jpeg_bin, function(segment) {
const new_segment = on_segment(segment);
if (is_uint8array(new_segment)) {
ranges.push({ data: new_segment });
out_length += new_segment.length;
} else if (Array.isArray(new_segment)) new_segment.filter(is_uint8array).forEach(function(s) {
ranges.push({ data: s });
out_length += s.length;
});
else if (new_segment !== false) {
const new_range = {
start: segment.offset,
end: segment.offset + segment.length
};
if (ranges.length > 0 && ranges[ranges.length - 1].end === new_range.start) ranges[ranges.length - 1].end = new_range.end;
else ranges.push(new_range);
out_length += segment.length;
}
});
const result = new Uint8Array(out_length);
let offset = 0;
ranges.forEach(function(range) {
const data = range.data || jpeg_bin.subarray(range.start, range.end);
result.set(data, offset);
offset += data.length;
});
return result;
}
function jpeg_exif_tags_each(jpeg_bin, on_exif_entry) {
if (!is_uint8array(jpeg_bin)) throw error("Invalid argument (jpeg_bin), Uint8Array expected", "EINVAL");
if (typeof on_exif_entry !== "function") throw error("Invalid argument (on_exif_entry), Function expected", "EINVAL");
jpeg_segments_each(jpeg_bin, function(segment) {
if (segment.code === 218) return false;
if (segment.code === 225 && segment.length >= 10 && jpeg_bin[segment.offset + 4] === 69 && jpeg_bin[segment.offset + 5] === 120 && jpeg_bin[segment.offset + 6] === 105 && jpeg_bin[segment.offset + 7] === 102 && jpeg_bin[segment.offset + 8] === 0 && jpeg_bin[segment.offset + 9] === 0) {
new ExifParser(jpeg_bin, segment.offset + 10, segment.offset + segment.length).each(on_exif_entry);
return false;
}
});
}
function jpeg_exif_tags_filter(jpeg_bin, on_exif_entry) {
if (!is_uint8array(jpeg_bin)) throw error("Invalid argument (jpeg_bin), Uint8Array expected", "EINVAL");
if (typeof on_exif_entry !== "function") throw error("Invalid argument (on_exif_entry), Function expected", "EINVAL");
let stop_search = false;
return jpeg_segments_filter(jpeg_bin, function(segment) {
if (stop_search) return;
if (segment.code === 218) stop_search = true;
if (segment.code === 225 && segment.length >= 10 && jpeg_bin[segment.offset + 4] === 69 && jpeg_bin[segment.offset + 5] === 120 && jpeg_bin[segment.offset + 6] === 105 && jpeg_bin[segment.offset + 7] === 102 && jpeg_bin[segment.offset + 8] === 0 && jpeg_bin[segment.offset + 9] === 0) {
const new_exif = new ExifParser(jpeg_bin, segment.offset + 10, segment.offset + segment.length).filter(on_exif_entry);
if (!new_exif) return false;
const header = new Uint8Array(10);
header.set(jpeg_bin.slice(segment.offset, segment.offset + 10));
header[2] = new_exif.length + 8 >>> 8 & 255;
header[3] = new_exif.length + 8 & 255;
stop_search = true;
return [header, new_exif];
}
});
}
function jpeg_add_comment(jpeg_bin, comment) {
let comment_inserted = false, segment_count = 0;
return jpeg_segments_filter(jpeg_bin, function(segment) {
segment_count++;
if (segment_count === 1 && segment.code === 216) return;
if (segment_count === 2 && segment.code === 224) return;
if (comment_inserted) return;
comment = utf8_encode(comment);
const csegment = new Uint8Array(5 + comment.length);
let offset = 0;
csegment[offset++] = 255;
csegment[offset++] = 254;
csegment[offset++] = comment.length + 3 >>> 8 & 255;
csegment[offset++] = comment.length + 3 & 255;
comment.split("").forEach(function(c) {
csegment[offset++] = c.charCodeAt(0) & 255;
});
csegment[offset++] = 0;
comment_inserted = true;
return [csegment, jpeg_bin.subarray(segment.offset, segment.offset + segment.length)];
});
}
//#endregion
//#region src/jpeg_plugins.ts
async function jpeg_patch_exif(env) {
const data = await this._getUint8Array(env.blob);
env.is_jpeg = is_jpeg(data);
if (!env.is_jpeg) return env;
env.orig_blob = env.blob;
try {
let exif_is_big_endian;
let orientation_offset;
jpeg_exif_tags_each(data, function(entry) {
if (entry.ifd === 0 && entry.tag === 274 && Array.isArray(entry.value)) {
env.orientation = entry.value[0] || 1;
exif_is_big_endian = entry.is_big_endian;
orientation_offset = entry.data_offset;
return false;
}
});
if (orientation_offset) {
const orientation_patch = exif_is_big_endian ? new Uint8Array([0, 1]) : new Uint8Array([1, 0]);
env.blob = new Blob([
data.slice(0, orientation_offset),
orientation_patch,
data.slice(orientation_offset + 2)
], { type: "image/jpeg" });
}
} catch (_) {}
return env;
}
async function jpeg_rotate_canvas(env) {
if (!env.is_jpeg) return env;
const orientation = (env.orientation || 1) - 1;
if (!orientation) return env;
let canvas;
if (orientation & 4) canvas = this.pica.createCanvas(env.out_canvas.height, env.out_canvas.width);
else canvas = this.pica.createCanvas(env.out_canvas.width, env.out_canvas.height);
const ctx = canvas.getContext("2d");
ctx.save();
if (orientation & 1) ctx.transform(-1, 0, 0, 1, canvas.width, 0);
if (orientation & 2) ctx.transform(-1, 0, 0, -1, canvas.width, canvas.height);
if (orientation & 4) ctx.transform(0, 1, 1, 0, 0, 0);
ctx.drawImage(env.out_canvas, 0, 0);
ctx.restore();
env.out_canvas.width = env.out_canvas.height = 0;
env.out_canvas = canvas;
return env;
}
async function jpeg_attach_orig_segments(env) {
if (!env.is_jpeg) return env;
const [data, data_out] = await Promise.all([this._getUint8Array(env.blob), this._getUint8Array(env.out_blob)]);
if (!is_jpeg(data)) return env;
const segments = [];
jpeg_segments_each(data, function(segment) {
if (segment.code === 218) return false;
segments.push(segment);
});
const segment_data = segments.filter(function(segment) {
if (segment.code === 226) return false;
if (segment.code >= 224 && segment.code < 240) return true;
if (segment.code === 254) return true;
return false;
}).map(function(segment) {
return data.slice(segment.offset, segment.offset + segment.length);
});
env.out_blob = new Blob([data_out.slice(0, 2)].concat(segment_data).concat([data_out.slice(20)]), { type: "image/jpeg" });
return env;
}
function assign(reducer) {
reducer.before("_blob_to_image", jpeg_patch_exif);
reducer.after("_transform", jpeg_rotate_canvas);
reducer.after("_create_blob", jpeg_attach_orig_segments);
}
//#endregion
//#region src/index.ts
var ImageBlobReduce = class {
pica;
initialized;
_initPromise;
constructor(options) {
options = options || {};
this.pica = options.pica || pica({});
this.initialized = false;
}
use(plugin, ...params) {
plugin(this, ...params);
return this;
}
setup() {
this.use(assign);
}
async _ensureInitialized() {
if (!this._initPromise) this._initPromise = Promise.resolve().then(async () => {
this.setup();
await this.pica.init();
this.initialized = true;
});
return this._initPromise;
}
async toBlob(blob, options) {
let env = {
blob,
opts: {
max: Infinity,
...options
}
};
await this._ensureInitialized();
env = await this._blob_to_image(env);
env = await this._calculate_size(env);
env = await this._transform(env);
env = await this._cleanup(env);
env = await this._create_blob(env);
env.out_canvas.width = env.out_canvas.height = 0;
return env.out_blob;
}
async toCanvas(blob, options) {
let env = {
blob,
opts: {
max: Infinity,
...options
}
};
await this._ensureInitialized();
env = await this._blob_to_image(env);
env = await this._calculate_size(env);
env = await this._transform(env);
env = await this._cleanup(env);
return env.out_canvas;
}
before(method_name, fn) {
if (!this[method_name]) throw new Error("Method \"" + method_name + "\" does not exist");
if (typeof fn !== "function") throw new Error("Invalid argument \"fn\", function expected");
const old_fn = this[method_name];
this[method_name] = (async (env) => {
const _env = await fn.call(this, env);
return old_fn.call(this, _env);
});
return this;
}
after(method_name, fn) {
if (!this[method_name]) throw new Error("Method \"" + method_name + "\" does not exist");
if (typeof fn !== "function") throw new Error("Invalid argument \"fn\", function expected");
const old_fn = this[method_name];
this[method_name] = (async (env) => {
const _env = await old_fn.call(this, env);
return fn.call(this, _env);
});
return this;
}
_blob_to_image(env) {
const win = window;
const URL = win.URL || win.webkitURL || win.mozURL || win.msURL;
env.image = document.createElement("img");
env.image_url = URL.createObjectURL(env.blob);
env.image.src = env.image_url;
return new Promise(function(resolve, reject) {
env.image.onerror = function() {
reject(/* @__PURE__ */ new Error("ImageBlobReduce: failed to create Image() from blob"));
};
env.image.onload = function() {
resolve(env);
};
});
}
async _calculate_size(env) {
let scale_factor = env.opts.max / Math.max(env.image.width, env.image.height);
if (scale_factor > 1) scale_factor = 1;
env.transform_width = Math.max(Math.round(env.image.width * scale_factor), 1);
env.transform_height = Math.max(Math.round(env.image.height * scale_factor), 1);
env.scale_factor = scale_factor;
return env;
}
async _transform(env) {
env.out_canvas = this.pica.createCanvas(env.transform_width, env.transform_height);
env.transform_width = null;
env.transform_height = null;
const { max, ...pica_opts } = env.opts;
await this.pica.resize(env.image, env.out_canvas, pica_opts);
return env;
}
async _cleanup(env) {
env.image.src = "";
env.image = null;
const win = window;
const URL = win.URL || win.webkitURL || win.mozURL || win.msURL;
if (URL.revokeObjectURL) URL.revokeObjectURL(env.image_url);
env.image_url = null;
return env;
}
async _create_blob(env) {
env.out_blob = await this.pica.toBlob(env.out_canvas, env.blob.type);
return env;
}
async _getUint8Array(blob) {
if (blob.arrayBuffer) return new Uint8Array(await blob.arrayBuffer());
return new Promise(function(resolve, reject) {
const fr = new FileReader();
fr.readAsArrayBuffer(blob);
fr.onload = function() {
resolve(new Uint8Array(fr.result));
};
fr.onerror = function() {
reject(/* @__PURE__ */ new Error("ImageBlobReduce: failed to load data from input blob"));
fr.abort();
};
fr.onabort = function() {
reject(/* @__PURE__ */ new Error("ImageBlobReduce: failed to load data from input blob (aborted)"));
};
});
}
};
function imageBlobReduce(options) {
return new ImageBlobReduce(options);
}
//#endregion
export { ImageBlobReduce, Pica, imageBlobReduce as default, image_traverse_exports as image_traverse, pica };
//# sourceMappingURL=image-blob-reduce.mjs.map