[desktop] im => vips (#4906)

More cleanup is pending, once we give this nightly a go.
This commit is contained in:
Manav Rathi
2025-01-30 08:22:27 +05:30
committed by GitHub
7 changed files with 117 additions and 97 deletions

6
desktop/.gitignore vendored
View File

@@ -21,5 +21,9 @@ out
# electron-builder
dist/
# We download it on demand, if needed for the particular OS/arch.
# Legacy, people who checked out main for a brief while in Jan 2025 may have
# this. Can be removed in the future.
build/magick*
# We download it on demand, if needed for the particular OS/arch.
build/vips*

View File

@@ -111,6 +111,8 @@ For video conversions and metadata extraction, we use ffmpeg. To bundle a
> ffmpeg binary and using the wasm one (that our renderer process already has).
> Which is why we bundle it to speed up operations on the desktop app.
TODO: Cleanup im - update this
On Linux and Windows, we use ImageMagick for thumbnail generation and JPEG
conversion of unpreviewable images. A static OS/architecture specific binary of
this is bundled in our extra resources (`build`) folder by `scripts/magick.sh`

View File

@@ -19,7 +19,7 @@
"postinstall": "electron-builder install-app-deps",
"lint": "yarn prettier --check --log-level warn . && yarn eslint && yarn tsc",
"lint-fix": "yarn prettier --write --log-level warn . && yarn eslint && yarn tsc",
"prepare": "node scripts/magick.js"
"prepare": "node scripts/vips.js"
},
"resolutions": {
"jackspeak": "2.1.1"

View File

@@ -3,8 +3,8 @@ const fsp = require("fs/promises");
/**
* This hook is invoked during the initial build (e.g. when triggered by "yarn
* build"), and importantly, on each rebuild for a different architecture during
* the build. We use it to ensure that the magick binary is for the current
* architecture being built. See "[Note: ImageMagick]" for more details.
* the build. We use it to ensure that the vips binary is for the current
* architecture being built. See "[Note: vips]" for more details.
*
* The documentation for this hook is at:
* https://www.electron.build/app-builder-lib.interface.configuration#beforebuild
@@ -22,8 +22,10 @@ const fsp = require("fs/promises");
* },
* arch: 'arm64'
*
* Note that we must not return falsey from this function, because
* > Resolving to false will skip dependencies install or rebuild.
* Note that we must not return falsey from this function, because:
*
* > Resolving to false will skip dependencies install or rebuild.
*
*/
module.exports = async (context) => {
const { appDir, platform, arch } = context;
@@ -42,7 +44,7 @@ module.exports = async (context) => {
const download = async (downloadName, outputName) => {
const out = `${appDir}/build/${outputName}`;
console.log(`Downloading ${downloadName}`);
const downloadPath = `https://github.com/ente-io/ImageMagick/releases/download/2025-01-21/${downloadName}`;
const downloadPath = `https://github.com/ente-io/libvips-packaging/releases/tag/v8.16.0/${downloadName}`;
return fetch(downloadPath)
.then((res) => res.blob())
.then((blob) => fsp.writeFile(out, blob.stream()))
@@ -51,13 +53,13 @@ module.exports = async (context) => {
switch (`${platform.nodeName}-${arch}`) {
case "linux-x64":
await download("magick-x86_64", "magick");
return download("vips-x64", "vips");
case "linux-arm64":
await download("magick-aarch64", "magick");
return download("vips-arm64", "vips");
case "win32-x64":
await download("magick-x64.exe", "magick.exe");
return download("vips-x86_64.exe", "vips.exe");
case "linux-arm64":
await download("magick-arm64.exe", "magick.exe");
return download("vips-aarch64.exe", "vips.exe");
}
return true;

View File

@@ -1,78 +0,0 @@
/**
* [Note: ImageMagick]
*
* We need static builds for Linux and Windows for both x64 and ARM. For this,
* we need a custom workflow because (as of writing):
*
* 1. Upstream doesn't publish ARM64 binaries for Linux
*
* 2. The Windows portable releases are not part of the artifacts attached to
* the upstream GitHub release.
*
* Our custom workflow is an adaption of the upstream release.yml - its goal is
* to have 4 standalone binaries - Linux x64, Linux ARM, Win x64, Win ARM -
* attached to a GitHub release from which we can pull them when building the
* desktop app.
*
* This is our custom workflow, which runs on a fork of upstream:
* https://github.com/ente-io/ImageMagick/commit/df895cce13d6a3f874a716c05ff2babeb33351b9
* (For reference, we also include a copy of it in this repo - `magick.yml`).
*
* The binaries it creates are available at
* https://github.com/ente-io/ImageMagick/releases/tag/2025-01-21.
*
* To integrate this ImageMagick binary, we need to modify two places:
*
* 1. This script, `magick.js`, runs during "yarn install" (it is set as the
* "prepare" step in our `package.json`). It downloads the relevant binary
* for the current OS/arch combination and places it in the `build` folder,
* allowing it to be used during development.
*
* 2. The sibling script, `beforeBuild.js`, runs during "yarn build" (it is set
* as the beforeBuild script in `electrons-builder.yml`). It downloads the
* relevant binary for the OS/arch combination being built.
*
* Note that `magick.js` would've already run once `beforeBuild.js` is run, but
* on our CI we prepare builds for multiple architectures in one go, so we need
* to unconditonally replace the binary with the relevant one for the current
* architecture being built (which might be different from the one we're running
* on). `beforeBuild.js` runs for each architecture being built.
*
* On macOS, we don't need ImageMagick since there we use the native `sips`.
*/
const fs = require("fs");
const fsp = require("fs/promises");
const main = () => {
switch (`${process.platform}-${process.arch}`) {
case "linux-x64":
return downloadIfNeeded("magick-x86_64", "magick");
case "linux-arm64":
return downloadIfNeeded("magick-aarch64", "magick");
case "win32-x64":
return downloadIfNeeded("magick-x64.exe", "magick.exe");
case "linux-arm64":
return downloadIfNeeded("magick-arm64.exe", "magick.exe");
}
};
const downloadIfNeeded = (downloadName, outputName) => {
const out = `build/${outputName}`;
try {
// Making the file executable is the last step, so if the file exists at
// this path and is executable, we assume it is the correct one.
fs.accessSync(out, fs.constants.X_OK);
return;
} catch {}
console.log(`Downloading ${downloadName}`);
const downloadPath = `https://github.com/ente-io/ImageMagick/releases/download/2025-01-21/${downloadName}`;
return fetch(downloadPath)
.then((res) => res.blob())
.then((blob) => fsp.writeFile(out, blob.stream()))
.then(() => fsp.chmod(out, "744"));
};
main();

68
desktop/scripts/vips.js Executable file
View File

@@ -0,0 +1,68 @@
/**
* [Note: vips]
*
* For use within our Electron app we need static builds for Linux and Windows
* for both x64 and ARM. For this, we need a custom workflow because (as of
* writing) upstream doesn't publish these.
*
* This is our custom workflow, which runs on a fork of upstream:
* https://github.com/ente-io/libvips-packaging/commit/a298aff3e1f25f713508d31d0c3a55a4f828fdd3
*
* The binaries it creates are available at
* https://github.com/ente-io/libvips-packaging/releases/tag/v8.16.0
*
* To integrate this binary, we need to modify two places:
*
* 1. This script, `vips.js`, runs during "yarn install" (it is set as the
* "prepare" step in our `package.json`). It downloads the relevant binary
* for the current OS/arch combination and places it in the `build` folder,
* allowing it to be used during development.
*
* 2. The sibling script, `beforeBuild.js`, runs during "yarn build" (it is set
* as the beforeBuild script in `electrons-builder.yml`). It downloads the
* relevant binary for the OS/arch combination being built.
*
* Note that `vips.js` would've already run once `beforeBuild.js` is run, but on
* our CI we prepare builds for multiple architectures in one go, so we need to
* unconditonally replace the binary with the relevant one for the current
* architecture being built (which might be different from the one we're running
* on). `beforeBuild.js` runs for each architecture being built.
*
* On macOS, we don't need `vips` since there we use the native `sips`.
*/
const fs = require("fs");
const fsp = require("fs/promises");
const main = () => {
switch (`${process.platform}-${process.arch}`) {
case "linux-x64":
return downloadIfNeeded("vips-x64", "vips");
case "linux-arm64":
return downloadIfNeeded("vips-arm64", "vips");
case "win32-x64":
return downloadIfNeeded("vips-x86_64.exe", "vips.exe");
case "linux-arm64":
return downloadIfNeeded("vips-aarch64.exe", "vips.exe");
}
};
const downloadIfNeeded = (downloadName, outputName) => {
const out = `build/${outputName}`;
try {
// Making the file executable is the last step, so if the file exists at
// this path and is executable, we assume it is the correct one.
fs.accessSync(out, fs.constants.X_OK);
return;
} catch {}
console.log(`Downloading ${downloadName}`);
const downloadPath = `https://github.com/ente-io/libvips-packaging/releases/tag/v8.16.0/${downloadName}`;
return fetch(downloadPath)
.then((res) => res.blob())
.then((blob) => fsp.writeFile(out, blob.stream()))
.then(() => fsp.chmod(out, "744"));
};
main();

View File

@@ -45,14 +45,17 @@ const convertToJPEGCommand = (
case "linux":
case "win32":
return [
imageMagickPath(),
"convert",
inputFilePath,
"-quality",
"100%",
outputFilePath,
];
return [vipsPath(), "copy", inputFilePath, outputFilePath];
// TODO: Cleanup im
// return [
// imageMagickPath(),
// "convert",
// inputFilePath,
// "-quality",
// "100%",
// outputFilePath,
// ];
default:
throw new Error("Not available on the current OS/arch");
@@ -68,6 +71,15 @@ const imageMagickPath = () =>
process.platform == "win32" ? "magick.exe" : "magick",
);
/**
* Path to the vips executable bundled with our app on Linux and Windows.
*/
const vipsPath = () =>
path.join(
isDev ? "build" : process.resourcesPath,
process.platform == "win32" ? "vips.exe" : "vips",
);
export const generateImageThumbnail = async (
dataOrPathOrZipItem: Uint8Array | string | ZipItem,
maxDimension: number,
@@ -138,6 +150,16 @@ const generateImageThumbnailCommand = (
case "linux":
case "win32":
return [
vipsPath(),
"thumbnail",
inputFilePath,
`${outputFilePath}[Q=${quality}]`,
`${maxDimension}`,
];
case "aix" /* dummy case to hold TODO */:
/* TODO: Cleanup im */
return [
imageMagickPath(),
"convert",