From 02a8c3a9294fa7f8517f1efa4aa70536c571865e Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Tue, 22 Oct 2024 10:44:29 -0600 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=86=20Add=20webp=20-->=20png=20convers?= =?UTF-8?q?ion=20for=20typst/pdf/word=20(#1598)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changeset/chatty-mayflies-yell.md | 5 ++ packages/myst-cli/src/transforms/images.ts | 26 +++++- packages/myst-cli/src/utils/imagemagick.ts | 96 ++++++++++++++++------ packages/myst-cli/src/utils/inkscape.ts | 4 +- 4 files changed, 101 insertions(+), 30 deletions(-) create mode 100644 .changeset/chatty-mayflies-yell.md diff --git a/.changeset/chatty-mayflies-yell.md b/.changeset/chatty-mayflies-yell.md new file mode 100644 index 000000000..6a90b5c26 --- /dev/null +++ b/.changeset/chatty-mayflies-yell.md @@ -0,0 +1,5 @@ +--- +"myst-cli": patch +--- + +Add webp-->png conversion diff --git a/packages/myst-cli/src/transforms/images.ts b/packages/myst-cli/src/transforms/images.ts index 1b4399b8a..ad98e5fd1 100644 --- a/packages/myst-cli/src/transforms/images.ts +++ b/packages/myst-cli/src/transforms/images.ts @@ -244,6 +244,7 @@ type ConversionOpts = { file: string; inkscapeAvailable: boolean; imagemagickAvailable: boolean; + dwebpAvailable: boolean; }; type ConversionFn = ( @@ -334,6 +335,22 @@ async function gifToPng( return null; } +/** + * webp -> png using dwebp + */ +async function webpToPng( + session: ISession, + source: string, + writeFolder: string, + opts: ConversionOpts, +) { + const { dwebpAvailable } = opts; + if (dwebpAvailable) { + return imagemagick.convertWebpToPng(session, source, writeFolder); + } + return null; +} + /** * These are all the available image conversion functions * @@ -353,6 +370,9 @@ const conversionFnLookup: Record> = { [ImageExtensions.gif]: { [ImageExtensions.png]: gifToPng, }, + [ImageExtensions.webp]: { + [ImageExtensions.png]: webpToPng, + }, [ImageExtensions.eps]: { // Currently the inkscape CLI has a bug which prevents EPS conversions; // once that is fixed, we may uncomment the rest of this section to @@ -418,8 +438,9 @@ export async function transformImageFormats( }); if (Object.keys(invalidImages).length === 0) return; - const inkscapeAvailable = !!inkscape.isInkscapeAvailable(); - const imagemagickAvailable = !!imagemagick.isImageMagickAvailable(); + const inkscapeAvailable = inkscape.isInkscapeAvailable(); + const imagemagickAvailable = imagemagick.isImageMagickAvailable(); + const dwebpAvailable = imagemagick.isDwebpAvailable(); /** * convert runs the input conversion functions on the image @@ -437,6 +458,7 @@ export async function transformImageFormats( file, inkscapeAvailable, imagemagickAvailable, + dwebpAvailable, }); } } diff --git a/packages/myst-cli/src/utils/imagemagick.ts b/packages/myst-cli/src/utils/imagemagick.ts index 2f646cfbd..14a8b4cfe 100644 --- a/packages/myst-cli/src/utils/imagemagick.ts +++ b/packages/myst-cli/src/utils/imagemagick.ts @@ -7,12 +7,12 @@ import { RuleId } from 'myst-common'; import type { ISession } from '../session/types.js'; import { addWarningForFile } from './addWarningForFile.js'; -function magickCommandAvailable() { - return which.sync('magick', { nothrow: true }); +function magickCommandAvailable(): boolean { + return !!which.sync('magick', { nothrow: true }); } -function convertCommandAvailable() { - return which.sync('convert', { nothrow: true }); +function convertCommandAvailable(): boolean { + return !!which.sync('convert', { nothrow: true }); } export function isImageMagickAvailable() { @@ -26,12 +26,16 @@ export function imageMagickCommand() { return 'magick'; } -export function isWebpAvailable() { - return which.sync('cwebp', { nothrow: true }); +export function isWebpAvailable(): boolean { + return !!which.sync('cwebp', { nothrow: true }); } -export function isGif2webpAvailable() { - return which.sync('gif2webp', { nothrow: true }); +export function isDwebpAvailable(): boolean { + return !!which.sync('dwebp', { nothrow: true }); +} + +export function isGif2webpAvailable(): boolean { + return !!which.sync('gif2webp', { nothrow: true }); } const LARGE_IMAGE = 1024 * 1024 * 1.5; @@ -66,24 +70,64 @@ export async function extractFirstFrameOfGif(session: ISession, gif: string, wri const png = path.join(writeFolder, pngFile); if (fs.existsSync(png)) { session.log.debug(`Cached file found for extracted GIF: ${gif}`); - } else { - const executable = `${imageMagickCommand()} -density 600 -colorspace RGB ${gif}[0] ${png}`; - session.log.debug(`Executing: ${executable}`); - const exec = makeExecutable(executable, session.log); - try { - await exec(); - } catch (err) { - addWarningForFile( - session, - gif, - `Could not extract an image from gif: ${gif} - ${err}`, - 'error', - { - ruleId: RuleId.imageFormatConverts, - }, - ); - return null; - } + return pngFile; + } + const executable = `${imageMagickCommand()} -density 600 -colorspace RGB ${gif}[0] ${png}`; + session.log.debug(`Executing: ${executable}`); + const exec = makeExecutable(executable, session.log); + try { + await exec(); + } catch (err) { + addWarningForFile( + session, + gif, + `Could not extract an image from gif: ${gif} - ${err}`, + 'error', + { + ruleId: RuleId.imageFormatConverts, + }, + ); + return null; + } + return pngFile; +} + +/** + * webp -> png using dwebp + */ +export async function convertWebpToPng(session: ISession, source: string, writeFolder: string) { + if (!fs.existsSync(source)) return null; + const { name, ext } = path.parse(source); + if (ext !== '.webp') return null; + const pngFile = `${name}.png`; + const png = path.join(writeFolder, pngFile); + if (fs.existsSync(png)) { + session.log.debug(`Cached file found for converted PNG: ${source}`); + return pngFile; + } + const debugLogger = { + // We cannot destructure the logger here, bunyan complains + debug(...args: any[]) { + session.log.debug(...args); + }, + error(...args: any[]) { + session.log.debug(...args); + }, + }; + const exec = makeExecutable(`dwebp "${source}" -o "${png}"`, debugLogger); + try { + await exec(); + } catch (err) { + addWarningForFile( + session, + source, + `Could not extract a PNG from WEBP: ${source} - ${err}`, + 'error', + { + ruleId: RuleId.imageFormatConverts, + }, + ); + return null; } return pngFile; } diff --git a/packages/myst-cli/src/utils/inkscape.ts b/packages/myst-cli/src/utils/inkscape.ts index 23c9d9592..ddbe73375 100644 --- a/packages/myst-cli/src/utils/inkscape.ts +++ b/packages/myst-cli/src/utils/inkscape.ts @@ -27,8 +27,8 @@ function createInkscpapeLogger(session: ISession): LoggerDE { return logger; } -export function isInkscapeAvailable() { - return which.sync('inkscape', { nothrow: true }); +export function isInkscapeAvailable(): boolean { + return !!which.sync('inkscape', { nothrow: true }); } export async function convert(