From a4494a846efa3df101d15a0706679c34462f94d8 Mon Sep 17 00:00:00 2001 From: fk128 Date: Tue, 11 Mar 2025 11:00:34 +0000 Subject: [PATCH 1/3] feat: support page parameter for includegraphics pdf --- packages/myst-cli/src/transforms/images.ts | 13 ++++++-- packages/myst-cli/src/utils/imagemagick.ts | 9 ++--- packages/tex-to-myst/src/figures.ts | 38 +++++++++++++++------- packages/tex-to-myst/src/utils.ts | 17 ++++++++++ packages/tex-to-myst/tests/figures.yml | 34 +++++++++++++++++++ 5 files changed, 93 insertions(+), 18 deletions(-) diff --git a/packages/myst-cli/src/transforms/images.ts b/packages/myst-cli/src/transforms/images.ts index f91382090..cebe35aa6 100644 --- a/packages/myst-cli/src/transforms/images.ts +++ b/packages/myst-cli/src/transforms/images.ts @@ -250,6 +250,7 @@ type ConversionOpts = { imagemagickAvailable: boolean; dwebpAvailable: boolean; ffmpegAvailable: boolean; + page?: number; }; type ConversionFn = ( @@ -270,7 +271,8 @@ function imagemagickConvert( return async (session: ISession, source: string, writeFolder: string, opts: ConversionOpts) => { const { imagemagickAvailable } = opts; if (imagemagickAvailable) { - return imagemagick.convert(from, to, session, source, writeFolder, options); + const optsWithPage = opts.page !== undefined ? { ...options, page: opts.page } : options; + return imagemagick.convert(from, to, session, source, writeFolder, optsWithPage); } return null; }; @@ -479,13 +481,18 @@ export async function transformImageFormats( let outputFile: string | null = null; for (const conversionFn of conversionFns) { if (!outputFile) { - outputFile = await conversionFn(session, inputFile, writeFolder, { + const conversionOpts: ConversionOpts = { file, inkscapeAvailable, imagemagickAvailable, dwebpAvailable, ffmpegAvailable, - }); + }; + if (image.page !== undefined) { + conversionOpts.page = image.page; + } + + outputFile = await conversionFn(session, inputFile, writeFolder, conversionOpts); } } if (outputFile) { diff --git a/packages/myst-cli/src/utils/imagemagick.ts b/packages/myst-cli/src/utils/imagemagick.ts index 14a8b4cfe..5d1cd2783 100644 --- a/packages/myst-cli/src/utils/imagemagick.ts +++ b/packages/myst-cli/src/utils/imagemagick.ts @@ -138,12 +138,12 @@ export async function convert( session: ISession, input: string, writeFolder: string, - options?: { trim?: boolean }, + options?: { trim?: boolean; page?: number }, ) { if (!fs.existsSync(input)) return null; const { name, ext } = path.parse(input); if (ext !== inputExtension) return null; - const filename = `${name}${outputExtension}`; + const filename = `${name}${options?.page ? '-' + options.page : ''}${outputExtension}`; const output = path.join(writeFolder, filename); const inputFormatUpper = inputExtension.slice(1).toUpperCase(); const outputFormatUpper = outputExtension.slice(1).toUpperCase(); @@ -151,10 +151,11 @@ export async function convert( session.log.debug(`Cached file found for converted ${inputFormatUpper}: ${input}`); return filename; } else { - const executable = `${imageMagickCommand()} -density 600 -colorspace RGB ${input}${ + const executable = `${imageMagickCommand()} -density 600 -colorspace RGB ${input}${options?.page ? '[' + options.page + ']' : ''}${ options?.trim ? ' -trim' : '' } ${output}`; - session.log.debug(`Executing: ${executable}`); + + session.log.info(`Executing: ${executable}`); const exec = makeExecutable(executable, createImagemagikLogger(session)); try { await exec(); diff --git a/packages/tex-to-myst/src/figures.ts b/packages/tex-to-myst/src/figures.ts index 94730e04d..aa13de039 100644 --- a/packages/tex-to-myst/src/figures.ts +++ b/packages/tex-to-myst/src/figures.ts @@ -1,7 +1,8 @@ import type { GenericNode } from 'myst-common'; import { u } from 'unist-builder'; import type { Handler, ITexParser } from './types.js'; -import { getArguments, texToText } from './utils.js'; +import { getArguments, extractParams, texToText } from './utils.js'; +import { group } from 'console'; function renderCaption(node: GenericNode, state: ITexParser) { state.closeParagraph(); @@ -45,18 +46,33 @@ const FIGURE_HANDLERS: Record = { state.closeParagraph(); const url = texToText(getArguments(node, 'group')); const args = getArguments(node, 'argument')?.[0]?.content ?? []; + const params = extractParams(args); + + // Only support width and page for now + for (const key in params) { + if (key !== 'width' && key !== 'page') { + delete params[key]; + } + } + // TODO: better width, placement, etc. - if ( - args.length === 4 && - args[0].content === 'width' && - args[1].content === '=' && - Number.isFinite(Number.parseFloat(args[2].content)) - ) { - const width = `${Math.round(Number.parseFloat(args[2].content) * 100)}%`; - state.pushNode(u('image', { url, width })); - } else { - state.pushNode(u('image', { url })); + + // Convert width to percentage if present + if (params.width) { + if (typeof params.width === 'number') { + params.width = `${Math.round(params.width * 100)}%`; + } else { + delete params.width; // If width is a string, we don't know what it is, so we ignore it + } + } + if (params.page) { + if (typeof params.page === 'number') { + params.page = Number(params.page) - 1; // Convert to 0-based for imagemagick + } else { + delete params.page; + } } + state.pushNode(u('image', { url: url, ...params })); }, macro_caption: renderCaption, macro_captionof: renderCaption, diff --git a/packages/tex-to-myst/src/utils.ts b/packages/tex-to-myst/src/utils.ts index 276b2a6c0..3921f7f67 100644 --- a/packages/tex-to-myst/src/utils.ts +++ b/packages/tex-to-myst/src/utils.ts @@ -138,6 +138,23 @@ export function getArguments( ); } +export function extractParams(args: { content: string }[]): Record { + const params: Record = {}; + + for (let i = 0; i < args.length - 2; i++) { + const param = args[i].content; + const equalsSign = args[i + 1].content; + const value = args[i + 2].content; + + if (equalsSign === '=' && (Number.isFinite(Number.parseFloat(value)) || value)) { + params[param] = Number.isFinite(Number.parseFloat(value)) ? Number.parseFloat(value) : value; + i += 2; // Skip the processed elements + } + } + + return params; +} + export function renderInfoIndex(node: GenericNode, name: string): number { return node._renderInfo?.namedArguments?.findIndex((a: string) => a === name); } diff --git a/packages/tex-to-myst/tests/figures.yml b/packages/tex-to-myst/tests/figures.yml index 3a2851b86..9e9234637 100644 --- a/packages/tex-to-myst/tests/figures.yml +++ b/packages/tex-to-myst/tests/figures.yml @@ -225,3 +225,37 @@ cases: value: link to notebook - type: text value: '.' + - title: includegraphics specifies the page + tex: |- + \begin{figure}[htbp] + \centering + \includegraphics[width=1.0\textwidth,page=3]{figures/my_pic.pdf} + \caption{ This is the caption, \href{computations.ipynb}{link to notebook}.} + \label{fig:picture} + \end{figure} + tree: + type: root + children: + - type: container + kind: figure + identifier: fig:picture + label: fig:picture + align: center + children: + - type: image + url: figures/my_pic.pdf + width: 100% + page: 2 # start from 0 + - type: caption + children: + - type: paragraph + children: + - type: text + value: 'This is the caption, ' + - type: link + url: computations.ipynb + children: + - type: text + value: link to notebook + - type: text + value: '.' \ No newline at end of file From 5d93741608278e87129cd940b380bcb6419b3b9f Mon Sep 17 00:00:00 2001 From: Rowan Cockett Date: Sun, 16 Mar 2025 12:20:46 -0600 Subject: [PATCH 2/3] Create spicy-dingos-talk.md --- .changeset/spicy-dingos-talk.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 .changeset/spicy-dingos-talk.md diff --git a/.changeset/spicy-dingos-talk.md b/.changeset/spicy-dingos-talk.md new file mode 100644 index 000000000..a76ea8f62 --- /dev/null +++ b/.changeset/spicy-dingos-talk.md @@ -0,0 +1,11 @@ +--- +"myst-cli": patch +"tex-to-myst": patch +--- + +Support page parameter for includegraphics with multi-page pdf +In LaTeX, when including a multi-page PDF as a graphic, it's possible to specify a page number: +``` +\includegraphics[width=1.0\textwidth,page=3]{figures/my_pic.pdf} +``` +ImageMagick now extracts the correct page when converting from LaTeX. From 56921a58895281ee806896e674016c36968ec216 Mon Sep 17 00:00:00 2001 From: Fahdi Kanavati Date: Sun, 16 Mar 2025 19:42:23 +0000 Subject: [PATCH 3/3] fix: update types, check finite and round page to int --- packages/myst-cli/src/utils/imagemagick.ts | 2 +- packages/myst-spec-ext/src/types.ts | 2 ++ packages/tex-to-myst/src/figures.ts | 7 +++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/myst-cli/src/utils/imagemagick.ts b/packages/myst-cli/src/utils/imagemagick.ts index 5d1cd2783..05f631477 100644 --- a/packages/myst-cli/src/utils/imagemagick.ts +++ b/packages/myst-cli/src/utils/imagemagick.ts @@ -155,7 +155,7 @@ export async function convert( options?.trim ? ' -trim' : '' } ${output}`; - session.log.info(`Executing: ${executable}`); + session.log.debug(`Executing: ${executable}`); const exec = makeExecutable(executable, createImagemagikLogger(session)); try { await exec(); diff --git a/packages/myst-spec-ext/src/types.ts b/packages/myst-spec-ext/src/types.ts index b58ef8f9b..3397ef53f 100644 --- a/packages/myst-spec-ext/src/types.ts +++ b/packages/myst-spec-ext/src/types.ts @@ -115,6 +115,8 @@ export type Image = SpecImage & { urlOptimized?: string; height?: string; placeholder?: boolean; + /** Optional page number for PDF images, this ensure the correct page is extracted when converting to web and translated to LaTeX */ + page?: boolean; }; export type Iframe = Target & { diff --git a/packages/tex-to-myst/src/figures.ts b/packages/tex-to-myst/src/figures.ts index aa13de039..8b5435297 100644 --- a/packages/tex-to-myst/src/figures.ts +++ b/packages/tex-to-myst/src/figures.ts @@ -2,7 +2,6 @@ import type { GenericNode } from 'myst-common'; import { u } from 'unist-builder'; import type { Handler, ITexParser } from './types.js'; import { getArguments, extractParams, texToText } from './utils.js'; -import { group } from 'console'; function renderCaption(node: GenericNode, state: ITexParser) { state.closeParagraph(); @@ -59,15 +58,15 @@ const FIGURE_HANDLERS: Record = { // Convert width to percentage if present if (params.width) { - if (typeof params.width === 'number') { + if (typeof params.width === 'number' && Number.isFinite(params.width)) { params.width = `${Math.round(params.width * 100)}%`; } else { delete params.width; // If width is a string, we don't know what it is, so we ignore it } } if (params.page) { - if (typeof params.page === 'number') { - params.page = Number(params.page) - 1; // Convert to 0-based for imagemagick + if (typeof params.page === 'number' && Number.isFinite(params.page)) { + params.page = Math.round(Number(params.page)) - 1; // Convert to 0-based for imagemagick } else { delete params.page; }