HEIC: - lib/image.js — openImage(): HEIC (HEVC) через heic-decode (libheif WASM), т.к. sharp декодирует только AV1-HEIF; непрозрачная альфа отбрасывается - compress: общий encodeImage(); формат «оригинала» по содержимому (metadata), а не по MIME; HEIC/BMP в режиме «Оригинал» → JPEG с расширением .jpg Картинки → PDF (/pdf): - /pdf/fromImages переписан: любые форматы вкл. HEIC, порядок/повторы/поворот страниц, размер листа (по картинке/A4/A3/A5/Letter), ориентация, поля, качество, имя файла; повторы встраиваются один раз; нетронутые JPEG/PNG — байт-в-байт - /pdf/thumb — серверные миниатюры для HEIC/TIFF (свой мягкий лимитер) - UI-конструктор: сетка с превью, drag&drop, сортировка, массовый поворот, прогресс загрузки - старая вкладка была нерабочей (поле files вместо images) и требовала загрузки PDF - SVG не принимается (librsvg + внешние ссылки) - фикс: потерянная обёртка panel-watermark ломала вложенность stepConfig - фикс: pdfIcon недоступен из inline onerror UI: белый фон (--surface-800) у зон загрузки, неактивных табов, опций и карточек (compress, pdf, video, converter, formatter, sanitizer, parser)
40 lines
1.9 KiB
JavaScript
40 lines
1.9 KiB
JavaScript
/**
|
||
* Открытие загруженных картинок через sharp с поддержкой HEIC.
|
||
* HEIC (фото с iPhone) — HEIF с кодеком HEVC. Сборка libvips в sharp декодирует только
|
||
* AV1-HEIF (AVIF), поэтому HEIC разбираем через libheif-js (WASM) и отдаём sharp сырые пиксели.
|
||
*/
|
||
|
||
const fs = require('fs');
|
||
const sharp = require('sharp');
|
||
const heicDecode = require('heic-decode');
|
||
|
||
const HEVC_BRANDS = ['heic', 'heix', 'hevc', 'hevx', 'heim', 'heis', 'hevm', 'hevs'];
|
||
|
||
// file — объект multer ({ path, originalname, mimetype })
|
||
function isHeic(file) {
|
||
let brand = '';
|
||
try {
|
||
const fd = fs.openSync(file.path, 'r');
|
||
const head = Buffer.alloc(12);
|
||
fs.readSync(fd, head, 0, 12, 0);
|
||
fs.closeSync(fd);
|
||
if (head.toString('latin1', 4, 8) === 'ftyp') brand = head.toString('latin1', 8, 12);
|
||
} catch {}
|
||
if (HEVC_BRANDS.includes(brand)) return true;
|
||
if (brand === 'avif' || brand === 'avis') return false;
|
||
return /\.(heic|heif)$/i.test(file.originalname) || /^image\/hei[cf]/.test(file.mimetype);
|
||
}
|
||
|
||
async function openImage(file) {
|
||
if (!isHeic(file)) return sharp(file.path);
|
||
const { width, height, data } = await heicDecode({ buffer: await fs.promises.readFile(file.path) });
|
||
const image = sharp(Buffer.from(data.buffer, data.byteOffset, data.byteLength), { raw: { width, height, channels: 4 } });
|
||
// libheif всегда отдаёт RGBA; у фото альфа сплошь 255 — без неё не будет лишнего PNG/альфа-канала
|
||
// (пересобираем буфер: metadata() описывает вход, и removeAlpha() в конвейере его hasAlpha не сбросит)
|
||
for (let i = 3; i < data.length; i += 4) if (data[i] !== 255) return image;
|
||
const rgb = await image.removeAlpha().raw().toBuffer();
|
||
return sharp(rgb, { raw: { width, height, channels: 3 } });
|
||
}
|
||
|
||
module.exports = { isHeic, openImage };
|