UNPKG

@reforged/maker-appimage

Version:

An AppImage maker implementation for the Electron Forge.

171 lines 6.97 kB
import { copyFile, mkdir, readFile, symlink } from "node:fs/promises"; import { extname, join, relative } from "node:path"; const getFixedDims = (icon) => Object.keys(icon) // Pick NxN .filter(key => key.includes("x", 1)) // Convert types .map(key => key.split("x", 2).map(n => Number(n))) // Verify .filter(key => key.every(n => !isNaN(n))); export default async function storeIcons(conf, out, work, app) { const icons = await normalizeIcon(conf); const sizes = getFixedDims(icons) .map(dim => `${dim[0]}x${dim[1]}`); if (icons.scalable) sizes.push("scalable"); const jobs = []; for (const imgKey of sizes) if (imgKey in icons && icons[imgKey]) { const iconDir = join(out, imgKey, "apps"); const iconFile = icons[imgKey]; // Create icon + icon path. jobs.push(mkdir(iconDir, { mode: 0o755, recursive: true }).then(() => copyFile(iconFile, join(iconDir, app + extname(iconFile))))); } await Promise.all(jobs); if (icons.default && icons[icons.default]) { const iconFile = app + extname(icons[icons.default]); const iconOut = relative(work, join(out, icons.default, "apps", iconFile)); await Promise.all([ symlink(iconOut, join(work, ".DirIcon")), symlink(iconOut, join(work, iconFile)) ]); } } /** * Normalizes icon property, i.e. converts it to {@linkcode IconSet} and * ensures all necessary fallback logic has been applied. * * @param conf raw icon property from options * @returns A {@linkcode IconSet} that has been normalized. */ async function normalizeIcon(conf) { switch (typeof conf) { // 1. Unset parameters are empty icon sets case "undefined": return {}; // 2. Strings depend on automatic discovery of metadata. //@ts-expect-error(TS7029) fallthrough case "string": { const meta = await readFile(conf).then(getImageMetadata); const result = { strict: false }; if (meta.width && meta.height) result[result.default = `${meta.width}x${meta.height}`] = conf; if (meta.type === "SVG") result[result.default = "scalable"] = conf; conf = result; } // 3. Icon sets have validated structure if necessary default: if ((!conf.default && !conf.scalable) || conf.strict) { const dim = getFixedDims(conf); // Set default for non-present scalable if (!conf.default) { const predicate = dim.sort((x, y) => (y[0] * y[1]) - (x[0] * x[1]))[0]?.join('x'); if (predicate) conf.default = predicate; } // Additional (optional) validation step if (conf.strict) for (const img of dim.map(dim => dim.join('x'))) if (img in conf && conf[img]) { const meta = readFile(conf[img]).then(getImageMetadata); if (img !== `${(await meta).width}x${(await meta).height}`) throw Error("Object icon validation failed"); } } else if (!conf.default) conf.default = "scalable"; } return conf; } /** * A function to validate if the type of any value is like the one in * {@link ImageMetadata} interface. * * @param meta Any value to validate the type of. * @returns Whenever `meta` is an {@link ImageMetadata}-like object. */ function validateImageMetadata(meta) { if (typeof meta !== "object" || meta === null) return false; if (!("type" in meta)) return false; else switch (meta.type) { case "PNG": case "SVG": case "XPM3": case "XPM2": break; default: return false; } if (!("width" in meta) || (typeof meta.width !== "number" && meta.width !== null)) return false; if (!("height" in meta) || (typeof meta.height !== "number" && meta.height !== null)) return false; return true; } /** * A function to fetch metadata from buffer in PNG or SVG format. * * @remarks * * For PNGs, it gets required information (like image width or height) * from IHDR header (if it is correct according to spec), otherwise it sets * dimension values to `null`. * * For SVGs, it gets information about the dimensions from `<svg>` tag. If it is * missing, this function will return `null` for `width` and/or `height`. * * This function will also recognize file formats based on *MAGIC* headers – for * SVGs, it looks for existence of `<svg>` tag, for PNGs it looks if file starts * from the specific bytes. * * @param image PNG/SVG/XPM image buffer. */ function getImageMetadata(image) { const svgMagic = { file: /<svg ?[^>]*>/, width: /<svg (?!width).*.width=["']?(\d+)(?:px)?["']?[^>]*>/, height: /<svg (?!height).*.height=["']?(\d+)(?:px)?["']?[^>]*>/ }; const partialMeta = {}; if (image.readUInt32BE() === 2303741511 /* FileHeader.PNG */) partialMeta["type"] = "PNG"; else if (image.readUInt32BE(2) === 1481657650 /* FileHeader.XPM2 */) partialMeta["type"] = "XPM2"; else if (image.readUInt32BE(3) === 1481657632 /* FileHeader.XPM3 */) partialMeta["type"] = "XPM3"; else if (svgMagic.file.test(image.toString("utf8"))) partialMeta["type"] = "SVG"; else throw Error("Unsupported file format"); switch (partialMeta.type) { // Based on specification by W3C: https://www.w3.org/TR/PNG/ case "PNG": { const prefixIHDR = 4 + image.indexOf("IHDR"); const rawMeta = { width: prefixIHDR === 3 ? null : image.readInt32BE(prefixIHDR), height: prefixIHDR === 3 ? null : image.readInt32BE(prefixIHDR + 4) }; partialMeta["width"] = (rawMeta.width ?? 0) === 0 ? null : rawMeta.width; partialMeta["height"] = (rawMeta.height ?? 0) === 0 ? null : rawMeta.height; break; } case "SVG": { const svgImage = image.toString("utf8"); const rawMeta = { width: parseInt(svgImage.match(svgMagic.width)?.[1] ?? ""), height: parseInt(svgImage.match(svgMagic.height)?.[1] ?? ""), }; partialMeta["width"] = isNaN(rawMeta["width"]) ? null : rawMeta["width"]; partialMeta["height"] = isNaN(rawMeta["height"]) ? null : rawMeta["height"]; break; } default: // Not yet supported assignment partialMeta["width"] = null; partialMeta["height"] = null; } if (validateImageMetadata(partialMeta)) return partialMeta; throw TypeError("Malformed function return type! (" + JSON.stringify(partialMeta) + ")."); } //# sourceMappingURL=image.js.map