mirror of https://github.com/VERT-sh/VERT.git
feat: encode multiple images for .ico
This commit is contained in:
parent
35ccc6f709
commit
ee81d0d88f
1
bun.lock
1
bun.lock
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "vert",
|
||||
|
|
|
|||
|
|
@ -101,7 +101,8 @@
|
|||
"quality": "Quality",
|
||||
"depth": "Color depth",
|
||||
"color_space": "Color space",
|
||||
"transparency": "Transparency"
|
||||
"transparency": "Transparency",
|
||||
"single_size": "Single size image"
|
||||
},
|
||||
"audio": {
|
||||
"bitrate": {
|
||||
|
|
|
|||
|
|
@ -136,9 +136,7 @@
|
|||
);
|
||||
|
||||
if (imageSequence) {
|
||||
// this is to allow image sequence -> video on vertd (soon:tm:)
|
||||
// though i feel like this could also be done with ffmpeg-wasm in the browser? or mediabunny?
|
||||
// TODO: image sequence locally w/ ffmpeg-wasm or mediabunny
|
||||
// TODO: image sequence -> video on vertd?
|
||||
cats.push("video");
|
||||
} else {
|
||||
// handle special cases
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@
|
|||
const showDetails = () => {
|
||||
addDialog(
|
||||
m["convert.errors.vertd.details.view"](),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
VertdErrorDetails as any,
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -32,8 +32,6 @@ export class MagickConverter extends Converter {
|
|||
new FormatInfo("avif", true, true),
|
||||
new FormatInfo("heic", true, false), // seems to be unreliable? HEIC/HEIF is very weird if it will actually work
|
||||
new FormatInfo("heif", true, false),
|
||||
// TODO: .ico files can encode multiple images at various
|
||||
// sizes, bitdepths, etc. we should support that in future
|
||||
new FormatInfo("ico", true, true),
|
||||
new FormatInfo("bmp", true, true),
|
||||
new FormatInfo("cur", true, true),
|
||||
|
|
@ -125,6 +123,33 @@ export class MagickConverter extends Converter {
|
|||
): Promise<SettingDefinition[]> {
|
||||
// images - quality/compression/quantize/interlace/depth-DPI, resize, crop, rotate, flip/flop, autoOrient?, color space/bit depth, transparency settings
|
||||
const global = Settings.instance.settings;
|
||||
const settings: SettingDefinition[] = [];
|
||||
|
||||
let supportsMetadata = true;
|
||||
let supportsTransparency = true;
|
||||
|
||||
const toIcon = input.to === ".ico";
|
||||
|
||||
// TODO: surely there's a better way to do this lol
|
||||
switch (input.from) {
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
case ".jfif":
|
||||
supportsTransparency = false;
|
||||
break;
|
||||
}
|
||||
|
||||
switch (input.to) {
|
||||
case ".ico":
|
||||
supportsMetadata = false;
|
||||
break;
|
||||
|
||||
case ".jpg":
|
||||
case ".jpeg":
|
||||
case ".jfif":
|
||||
supportsTransparency = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const quality: SettingDefinition = {
|
||||
key: "quality",
|
||||
|
|
@ -134,7 +159,33 @@ export class MagickConverter extends Converter {
|
|||
min: 0,
|
||||
max: 100,
|
||||
};
|
||||
settings.push(quality);
|
||||
|
||||
// TODO: surely there's a better way to do this as well
|
||||
const iconResolutions = [
|
||||
{ value: "16x16", label: "16x16" },
|
||||
{ value: "32x32", label: "32x32" },
|
||||
{ value: "48x48", label: "48x48" },
|
||||
{ value: "64x64", label: "64x64" },
|
||||
{ value: "128x128", label: "128x128" },
|
||||
{ value: "256x256", label: "256x256" },
|
||||
{ value: "512x512", label: "512x512" },
|
||||
];
|
||||
const commonResolutions = [
|
||||
{
|
||||
value: "custom",
|
||||
label: m["convert.settings.common.custom"](),
|
||||
},
|
||||
{ value: "426x240", label: "426x240" },
|
||||
{ value: "640x360", label: "640x360" },
|
||||
{ value: "854x480", label: "854x480" },
|
||||
{ value: "720x1280", label: "720x1280 (V)" },
|
||||
{ value: "1280x720", label: "1280x720" },
|
||||
{ value: "1080x1920", label: "1080x1920 (V)" },
|
||||
{ value: "1920x1080", label: "1920x1080" },
|
||||
{ value: "2160x3840", label: "2160x3840 (V)" },
|
||||
{ value: "3840x2160", label: "3840x2160" },
|
||||
];
|
||||
const resolution: SettingDefinition = {
|
||||
key: "resolution",
|
||||
label: m["convert.settings.video.resolution.label"](),
|
||||
|
|
@ -142,24 +193,21 @@ export class MagickConverter extends Converter {
|
|||
default: "auto",
|
||||
options: [
|
||||
{ value: "auto", label: m["convert.settings.common.auto"]() },
|
||||
{
|
||||
value: "custom",
|
||||
label: m["convert.settings.common.custom"](),
|
||||
},
|
||||
{ value: "426x240", label: "426x240" },
|
||||
{ value: "640x360", label: "640x360" },
|
||||
{ value: "854x480", label: "854x480" },
|
||||
{ value: "720x1280", label: "720x1280 (V)" },
|
||||
{ value: "1280x720", label: "1280x720" },
|
||||
{ value: "1080x1920", label: "1080x1920 (V)" },
|
||||
{ value: "1920x1080", label: "1920x1080" },
|
||||
{ value: "2160x3840", label: "2160x3840 (V)" },
|
||||
{ value: "3840x2160", label: "3840x2160" },
|
||||
...(toIcon ? iconResolutions : commonResolutions),
|
||||
],
|
||||
hasCustomInput: true,
|
||||
customInputKey: "customResolution",
|
||||
placeholder: m["convert.settings.video.resolution.placeholder"](),
|
||||
};
|
||||
settings.push(resolution);
|
||||
|
||||
const singleSize: SettingDefinition = {
|
||||
key: "singleSize",
|
||||
label: m["convert.settings.image.single_size"](),
|
||||
type: "boolean",
|
||||
default: false,
|
||||
};
|
||||
if (toIcon) settings.push(singleSize);
|
||||
|
||||
const depth: SettingDefinition = {
|
||||
key: "depth",
|
||||
|
|
@ -174,6 +222,7 @@ export class MagickConverter extends Converter {
|
|||
{ value: "32", label: "32-bit" },
|
||||
],
|
||||
};
|
||||
settings.push(depth);
|
||||
|
||||
const colorSpace: SettingDefinition = {
|
||||
key: "colorSpace",
|
||||
|
|
@ -193,21 +242,15 @@ export class MagickConverter extends Converter {
|
|||
{ value: "gray", label: "Grayscale" },
|
||||
],
|
||||
};
|
||||
settings.push(colorSpace);
|
||||
|
||||
// TODO: check other formats for transparency support
|
||||
const fromJpeg =
|
||||
input.from === ".jpg" ||
|
||||
input.from === ".jpeg" ||
|
||||
input.from === ".jfif";
|
||||
const toJpeg =
|
||||
input.to === ".jpg" || input.to === ".jpeg" || input.to === ".jfif";
|
||||
const transparency: SettingDefinition = {
|
||||
key: "transparency",
|
||||
label: m["convert.settings.image.transparency"](),
|
||||
type: "boolean",
|
||||
default: true,
|
||||
disabled: fromJpeg || toJpeg,
|
||||
};
|
||||
if (supportsTransparency) settings.push(transparency);
|
||||
|
||||
const metadata: SettingDefinition = {
|
||||
key: "metadata",
|
||||
|
|
@ -215,10 +258,11 @@ export class MagickConverter extends Converter {
|
|||
type: "boolean",
|
||||
default: global.metadata ?? true,
|
||||
};
|
||||
if (supportsMetadata) settings.push(metadata);
|
||||
|
||||
// resize, crop, rotate - prob want a ui
|
||||
|
||||
return [quality, depth, colorSpace, resolution, transparency, metadata];
|
||||
return settings;
|
||||
}
|
||||
|
||||
public async getDefaultSettings(
|
||||
|
|
|
|||
|
|
@ -340,9 +340,9 @@ const downloadFile = async (url: string, file: VertFile): Promise<Blob> => {
|
|||
};
|
||||
|
||||
// prettier-ignore
|
||||
export const videoFormats = ["mp4", "mkv", "webm", "avi", "wmv", "mov", "gif", "apng", "webp", "mts", "ts", "m2ts", "mpg", "mpeg", "flv", "f4v", "vob", "m4v", "3gp", "3g2", "mxf", "ogv", "rm", "rmvb", "h264", "divx", "swf", "amv", "asf", "nut"];
|
||||
const cantEncode = ["rm", "rmvb"];
|
||||
const cantDecode = [""];
|
||||
export const videoFormats: string[] = ["mp4", "mkv", "webm", "avi", "wmv", "mov", "gif", "apng", "webp", "mts", "ts", "m2ts", "mpg", "mpeg", "flv", "f4v", "vob", "m4v", "3gp", "3g2", "mxf", "ogv", "rm", "rmvb", "h264", "divx", "swf", "amv", "asf", "nut"];
|
||||
const cantEncode: string[] = ["rm", "rmvb"];
|
||||
const cantDecode: string[] = [];
|
||||
|
||||
export class VertdConverter extends Converter {
|
||||
public name = "vertd";
|
||||
|
|
|
|||
|
|
@ -282,9 +282,10 @@ const magickConvert = async (
|
|||
) => {
|
||||
let fmt = to.slice(1).toUpperCase();
|
||||
if (fmt === "JFIF") fmt = "JPEG";
|
||||
const singleSize = Boolean(conversionSettings?.singleSize);
|
||||
|
||||
const resolution = conversionSettings.resolution as string;
|
||||
if (resolution && resolution !== "auto") {
|
||||
if (!singleSize && resolution && resolution !== "auto") {
|
||||
const actualResolution =
|
||||
resolution === "custom"
|
||||
? (conversionSettings.customResolution as string)
|
||||
|
|
@ -299,19 +300,57 @@ const magickConvert = async (
|
|||
}
|
||||
}
|
||||
|
||||
// ICO size clamp to avoid WidthOrHeightExceedsLimit
|
||||
if (fmt === "ICO") {
|
||||
const max = 256;
|
||||
const w = img.width;
|
||||
const h = img.height;
|
||||
if (fmt === "ICO") && !singleSize) {
|
||||
const standardSizes = [16, 24, 32, 48, 64, 128, 256, 512];
|
||||
|
||||
if (w > max || h > max) {
|
||||
const scale = max / Math.max(w, h);
|
||||
const newW = Math.max(1, Math.round(w * scale));
|
||||
const newH = Math.max(1, Math.round(h * scale));
|
||||
|
||||
img.resize(newW, newH);
|
||||
let desired = 0;
|
||||
if (resolution && resolution !== "auto") {
|
||||
const actualResolution =
|
||||
resolution === "custom"
|
||||
? (conversionSettings.customResolution as string)
|
||||
: resolution;
|
||||
const [wsel, hsel] = (actualResolution || "")
|
||||
.split("x")
|
||||
.map((d: string) => parseInt(d) || 0);
|
||||
desired = Math.max(wsel || 0, hsel || 0);
|
||||
} else {
|
||||
desired = Math.max(img.width || 0, img.height || 0);
|
||||
}
|
||||
|
||||
if (desired <= 0) desired = Math.max(...standardSizes);
|
||||
if (desired > Math.max(...standardSizes)) desired = Math.max(...standardSizes);
|
||||
|
||||
const sizes = standardSizes.filter((s) => s <= desired);
|
||||
if (sizes.length === 0) sizes.push(Math.min(...standardSizes));
|
||||
|
||||
const sourcePng = await new Promise<Uint8Array>((resolve) => {
|
||||
img.write(MagickFormat.Png, (o: Uint8Array) => resolve(structuredClone(o)));
|
||||
});
|
||||
|
||||
console.log(`encoding sizes for ico: ${sizes.join(", ")}`);
|
||||
|
||||
return await new Promise<Uint8Array>((resolve) => {
|
||||
MagickImageCollection.use((collection) => {
|
||||
for (const size of sizes) {
|
||||
const variant = MagickImage.create(
|
||||
sourcePng,
|
||||
new MagickReadSettings({ format: MagickFormat.Png }),
|
||||
);
|
||||
|
||||
const scale = size / Math.max(variant.width, variant.height);
|
||||
const newW = Math.max(1, Math.round(variant.width * scale));
|
||||
const newH = Math.max(1, Math.round(variant.height * scale));
|
||||
variant.resize(newW, newH);
|
||||
|
||||
collection.push(variant);
|
||||
console.log(`added size ${size}x${size} to MagickImageCollection`);
|
||||
}
|
||||
|
||||
collection.write(fmt as unknown as MagickFormat, (o: Uint8Array) => {
|
||||
resolve(structuredClone(o));
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const result = await new Promise<Uint8Array>((resolve, reject) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue