Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 100 additions & 15 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,8 @@
"demomacro",
"29217321",
"FultonG",
"odex21"
"odex21",
"elefevre"
],
"license": "MIT",
"bugs": {
Expand Down Expand Up @@ -178,12 +179,12 @@
"html-entities": "^2.3.3",
"html-minifier-terser": "^7.2.0",
"htmlparser2": "^10.0.0",
"image-size": "^2.0.2",
"jszip": "^3.7.1",
"lodash": "^4.17.21",
"lru-cache": "^10.4.3",
"mime-types": "^2.1.35",
"nanoid": "^3.1.25",
"probe-image-size": "^7.2.3",
"xmlbuilder2": "2.1.2"
},
"overrides": {
Expand Down
11 changes: 5 additions & 6 deletions src/helpers/xml-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,8 @@
import { fragment } from 'xmlbuilder2';
import colorNames from 'color-name';
import { cloneDeep } from 'lodash';
import sizeOf from 'image-size';
import { isVNode, isVText } from '../vdom/index';
import { parseDataUrl, downloadAndCacheImage, buildImage } from '../utils/image';
import { parseDataUrl, downloadAndCacheImage, buildImage, measureImage } from '../utils/image';

import namespaces from '../namespaces';
import {
Expand Down Expand Up @@ -1139,7 +1138,7 @@ const buildRun = async (vNode, attributes, docxDocumentInstance) => {

const imageBuffer = Buffer.from(response.fileContent, 'base64');

// Validate buffer before calling sizeOf
// Validate buffer before measuring
if (!imageBuffer || imageBuffer.length === 0) {
console.warn(`[BUILDRUN] Empty image buffer for: ${imageSource}`);
return runFragment;
Expand All @@ -1154,7 +1153,7 @@ const buildRun = async (vNode, attributes, docxDocumentInstance) => {

let imageProperties;
try {
imageProperties = sizeOf(imageBuffer);
imageProperties = measureImage(imageBuffer);
if (!imageProperties || !imageProperties.width || !imageProperties.height) {
console.warn(`[BUILDRUN] Invalid image properties for: ${imageSource}`);
return runFragment;
Expand Down Expand Up @@ -1714,15 +1713,15 @@ const processImageSource = async (docxDocumentInstance, vNode, imageSource, logC

const imageBuffer = Buffer.from(decodeURIComponent(base64String), 'base64');

// Validate buffer before calling sizeOf
// Validate buffer before measuring
if (!imageBuffer || imageBuffer.length === 0) {
console.warn(`[${logContext}] Empty image buffer for: ${imageSource}`);
return null;
}

let imageProperties;
try {
imageProperties = sizeOf(imageBuffer);
imageProperties = measureImage(imageBuffer);
if (!imageProperties || !imageProperties.width || !imageProperties.height) {
console.warn(`[${logContext}] Invalid image properties for: ${imageSource}`);
return null;
Expand Down
42 changes: 36 additions & 6 deletions src/utils/image.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import axios from 'axios';
import mimeTypes from 'mime-types';
import sizeOf from 'image-size';
// `probe-image-size/sync` (not the package root) keeps `needle` — and with it
// http/https/zlib/iconv-lite — out of the bundle; we only ever probe Buffers.
import sizeOf from 'probe-image-size/sync';

import { isValidUrl } from './url';
import * as xmlBuilder from '../helpers/xml-builder';
Expand Down Expand Up @@ -141,6 +143,34 @@ function convertSVGUnitToPixels(value, unit) {
return Math.round(value * factor);
}

/**
* Measures an image buffer and returns its dimensions in pixels.
*
* `probe-image-size` reports the raw declared number alongside the unit it was
* written in (`wUnits`/`hUnits`), so an SVG declared `width="72pt"` comes back as
* `72` rather than the 96 pixels it actually occupies. `image-size` used to do that
* conversion internally and every caller still treats the result as pixels, so
* normalize here instead of at each call site.
*
* Raster formats always report `px`, so they pass through untouched.
*
* @param {Buffer} imageBuffer - The raw image bytes
* @returns {Object|null} Measurement with `width`/`height` in pixels, or null if unrecognised
*/
export function measureImage(imageBuffer) {
const measured = sizeOf(imageBuffer);

if (!measured) {
return null;
}

return {
...measured,
width: convertSVGUnitToPixels(measured.width, measured.wUnits || 'px'),
height: convertSVGUnitToPixels(measured.height, measured.hUnits || 'px'),
};
}

/**
* Parses SVG dimensions from SVG string, supporting various formats.
* Handles: integers, decimals, units (px, cm, mm, in, pt, pc, em, rem, %), and viewBox fallback.
Expand Down Expand Up @@ -483,7 +513,7 @@ export const buildImage = async (
internalRelationship
);

// Add validation before calling sizeOf
// Add validation before measuring
if (!imageBuffer || imageBuffer.length === 0) {
// eslint-disable-next-line no-console
console.error(`[ERROR] buildImage: Empty image buffer for ${vNode.properties.src}`);
Expand All @@ -502,16 +532,16 @@ export const buildImage = async (

let imageProperties;

// For SVG files, use dimensions from vNode properties instead of sizeOf
// (sizeOf doesn't work on SVG XML content)
// For SVG files, prefer the dimensions declared on the vNode over the measured
// intrinsic size — the author's width/height attributes are the intent
if (response.isSVG) {
imageProperties = {
width: vNode.properties.width || 100,
height: vNode.properties.height || 100,
};
} else {
try {
imageProperties = sizeOf(imageBuffer);
imageProperties = measureImage(imageBuffer);
if (!imageProperties || !imageProperties.width || !imageProperties.height) {
// eslint-disable-next-line no-console
console.error(
Expand All @@ -523,7 +553,7 @@ export const buildImage = async (
} catch (sizeError) {
// eslint-disable-next-line no-console
console.error(
`[ERROR] buildImage: sizeOf failed for ${vNode.properties.src}:`,
`[ERROR] buildImage: failed to measure ${vNode.properties.src}:`,
sizeError.message
);
return null;
Expand Down
Loading
Loading