11import type { IImageOptions } from "docx" ;
2- import type { Image , ImageReference , IPlugin , Optional , SVG } from "@m2d/core" ;
2+ import type {
3+ Image ,
4+ ImageData ,
5+ ImageReference ,
6+ IPlugin ,
7+ Optional ,
8+ Parent ,
9+ PhrasingContent ,
10+ Root ,
11+ RootContent ,
12+ SVG ,
13+ } from "@m2d/core" ;
314import { handleSvg } from "./svg-utils" ;
15+ import { Definitions } from "@m2d/core/utils" ;
416
517/**
618 * List of image types directly supported by `docx`.
@@ -179,7 +191,7 @@ const handleNonDataUrls = async (
179191
180192 if ( / ( s v g | x m l ) / . test ( response . headers . get ( "content-type" ) ?? "" ) || url . endsWith ( ".svg" ) ) {
181193 const svgText = await response . text ( ) ;
182- return handleSvg ( { type : "svg" , value : svgText , id : `s ${ crypto . randomUUID ( ) } ` } , options ) ;
194+ return handleSvg ( { type : "svg" , value : svgText } , options ) ;
183195 }
184196
185197 const arrayBuffer = await response . arrayBuffer ( ) ;
@@ -256,6 +268,46 @@ const defaultOptions: IDefaultImagePluginOptions = {
256268 dpi : 96 ,
257269} ;
258270
271+ const cache : Record < string , Promise < IImageOptions > > = { } ;
272+
273+ /**
274+ * Generate image data
275+ *
276+ * Extracting this logic in an async function for the purpose of caching the promises
277+ */
278+ const createImageData = async (
279+ node : Image | SVG ,
280+ url : string ,
281+ options : IDefaultImagePluginOptions ,
282+ ) => {
283+ const imgOptions =
284+ node . type === "svg"
285+ ? await handleSvg ( node , options )
286+ : await options . imageResolver ( url , options ) ;
287+
288+ // apply data props
289+ const { data } = node as Image ;
290+ const { width : origW , height : origH } = imgOptions . transformation ;
291+ let { width, height } = data ?? { } ;
292+ if ( width && ! height ) {
293+ height = ( origH * width ) / origW ;
294+ } else if ( ! width && height ) {
295+ width = ( origW * height ) / origH ;
296+ } else if ( ! width && ! height ) {
297+ height = origH ;
298+ width = origW ;
299+ }
300+
301+ const scale = Math . min (
302+ ( options . maxW * options . dpi ) / width ! ,
303+ ( options . maxH * options . dpi ) / height ! ,
304+ 1 ,
305+ ) ;
306+ // @ts -expect-error -- we are mutating the immutable options.
307+ imgOptions . transformation = { width : width * scale , height : height * scale } ;
308+ return imgOptions ;
309+ } ;
310+
259311/**
260312 * Image plugin for processing inline image nodes in the Markdown AST.
261313 * Resolves both base64 and URL-based images for inclusion in DOCX.
@@ -264,48 +316,48 @@ const defaultOptions: IDefaultImagePluginOptions = {
264316 * @returns Plugin implementation for use in the `@m2d/core` pipeline.
265317 */
266318export const imagePlugin : ( options ?: IImagePluginOptions ) => IPlugin = options_ => {
267- const options : IDefaultImagePluginOptions = { ...defaultOptions , ...options_ } ;
319+ const options = { ...defaultOptions , ...options_ } ;
320+
321+ /** preprocess images */
322+ const preprocess = async ( root : Root , definitions : Definitions ) => {
323+ const promises : Promise < void > [ ] = [ ] ;
324+
325+ /** process images and create promises - use max parallel processing */
326+ const preprocessInternal = ( node : Root | RootContent | PhrasingContent ) => {
327+ ( node as Parent ) . children ?. forEach ( preprocessInternal ) ;
328+
329+ if ( / ^ ( i m a g e | s v g ) / . test ( node . type ) )
330+ promises . push (
331+ ( async ( ) => {
332+ // Only process image nodes
333+ const url =
334+ ( node as Image ) . url ??
335+ definitions [ ( node as ImageReference ) . identifier ?. toUpperCase ( ) ] ;
336+
337+ // for SVG if the value is promise, it must have mermaid. We will in future provide better type safety after considering if there are any other mermaid like tools that we might want to support
338+ const cacheKey = node . type === "svg" ? ( node . data ?. mermaid ?? String ( node . value ) ) : url ;
339+
340+ cache [ cacheKey ] ??= createImageData ( node as Image | SVG , url , options ) ;
341+ const alt = ( node as Image ) . alt ?? url ?. split ( "/" ) ?. pop ( ) ?? "" ;
342+
343+ node . data = {
344+ ...( await cache [ cacheKey ] ) ,
345+ altText : { description : alt , name : alt , title : alt } ,
346+ ...( node as Image | SVG ) . data ,
347+ } ;
348+ } ) ( ) ,
349+ ) ;
350+ } ;
351+ preprocessInternal ( root ) ;
352+ await Promise . all ( promises ) ;
353+ } ;
268354 return {
269- inline : async ( docx , node , runProps , definitions ) => {
355+ preprocess,
356+ inline : ( docx , node , runProps ) => {
270357 if ( / ^ ( i m a g e | s v g ) / . test ( node . type ) ) {
271- const alt = ( node as Image ) . alt ?? ( node as Image ) . url ?. split ( "/" ) ?. pop ( ) ?? "" ;
272- const url =
273- ( node as Image ) . url ?? definitions [ ( node as ImageReference ) . identifier ?. toUpperCase ( ) ] ;
274-
275- const imgOptions =
276- node . type === "svg"
277- ? await handleSvg ( node , options )
278- : await options . imageResolver ( url , options ) ;
279-
280- // apply data props
281- const { data } = node as Image ;
282- const { width : origW , height : origH } = imgOptions . transformation ;
283- let { width, height } = data ?? { } ;
284- if ( width && ! height ) {
285- height = ( origH * width ) / origW ;
286- } else if ( ! width && height ) {
287- width = ( origW * height ) / origH ;
288- } else if ( ! width && ! height ) {
289- height = origH ;
290- width = origW ;
291- }
292-
293- const scale = Math . min (
294- ( options . maxW * options . dpi ) / width ! ,
295- ( options . maxH * options . dpi ) / height ! ,
296- 1 ,
297- ) ;
298- // @ts -expect-error -- we are mutating the immutable options.
299- imgOptions . transformation = { width : width * scale , height : height * scale } ;
300- node . type = "" ;
301- return [
302- new docx . ImageRun ( {
303- ...imgOptions ,
304- altText : { description : alt , name : alt , title : alt } ,
305- ...runProps ,
306- ...( node as Image | SVG ) . data ,
307- } ) ,
308- ] ;
358+ const { imageOptions, ...data } = ( node as Image ) . data as ImageData ;
359+ // @ts -expect-error -- merging a lot of data types here
360+ return [ new docx . ImageRun ( { ...imageOptions , ...data , ...runProps } ) ] ;
309361 }
310362 return [ ] ;
311363 } ,
0 commit comments