diff --git a/admin/scripts/resizeImage.js b/admin/scripts/resizeImage.js
index 09bf4f4ee535..45fe0a30aac0 100644
--- a/admin/scripts/resizeImage.js
+++ b/admin/scripts/resizeImage.js
@@ -11,7 +11,7 @@ import {fileURLToPath} from 'url';
import {program} from 'commander';
import {logger} from '@docusaurus/logger';
import sharp from 'sharp';
-import {imageSizeFromFile} from 'image-size/fromFile';
+import {imageDimensionsFromData} from 'image-dimensions';
// You can use it as:
//
@@ -64,7 +64,11 @@ program
await Promise.all(
images.map(async (imgPath) => {
- const {width, height} = await imageSizeFromFile(imgPath);
+ const dimensions = imageDimensionsFromData(await fs.readFile(imgPath));
+ if (!dimensions) {
+ throw new Error(`Could not parse image size for ${imgPath}`);
+ }
+ const {width, height} = dimensions;
const targetWidth =
options.width ?? (imgPath.includes(showcasePath) ? 640 : 1000);
const targetHeight =
diff --git a/package.json b/package.json
index 261e57d8722c..a05745935b0d 100644
--- a/package.json
+++ b/package.json
@@ -105,7 +105,7 @@
"execa": "^10.0.0",
"globals": "^17.6.0",
"husky": "^9.1.7",
- "image-size": "^2.0.2",
+ "image-dimensions": "^2.5.1",
"jest-serializer-ansi-escapes": "^5.0.0",
"jest-serializer-react-helmet-async": "^1.0.21",
"jiti": "^2.7.0",
diff --git a/packages/docusaurus-mdx-loader/package.json b/packages/docusaurus-mdx-loader/package.json
index 52476b7c3bdb..3d2714dc1fe8 100644
--- a/packages/docusaurus-mdx-loader/package.json
+++ b/packages/docusaurus-mdx-loader/package.json
@@ -27,7 +27,7 @@
"estree-util-value-to-estree": "^3.5.0",
"file-loader": "^6.2.0",
"fs-extra": "^11.2.0",
- "image-size": "^2.0.2",
+ "image-dimensions": "^2.5.1",
"mdast-util-mdx": "^3.0.0",
"mdast-util-to-string": "^4.0.0",
"rehype-raw": "^7.0.0",
diff --git a/packages/docusaurus-mdx-loader/src/processor.ts b/packages/docusaurus-mdx-loader/src/processor.ts
index 0035ab66c799..bf369e1913f0 100644
--- a/packages/docusaurus-mdx-loader/src/processor.ts
+++ b/packages/docusaurus-mdx-loader/src/processor.ts
@@ -127,6 +127,7 @@ async function createProcessorFactory() {
siteDir: options.siteDir,
onBrokenMarkdownImages:
options.markdownConfig.hooks.onBrokenMarkdownImages,
+ measureImage: options.markdownConfig.hooks.measureImage,
} satisfies TransformImageOptions,
],
// TODO merge this with transformLinks?
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/icon.svg b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/icon.svg
new file mode 100644
index 000000000000..b4785418a9d1
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/icon.svg
@@ -0,0 +1,3 @@
+
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts
index 4602e27c76d9..1b39e790af22 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/index.test.ts
@@ -365,4 +365,18 @@ describe('transformImage plugin', () => {
});
});
});
+
+ it('adds width/height for SVG images', async () => {
+ const result = await processContent(``);
+ expect(result).toContain('width="24"');
+ expect(result).toContain('height="16"');
+ });
+
+ it('uses markdown.hooks.measureImage when provided', async () => {
+ const result = await processContent(``, {
+ measureImage: async () => ({width: 11, height: 22}),
+ });
+ expect(result).toContain('width="11"');
+ expect(result).toContain('height="22"');
+ });
});
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/measureImage.test.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/measureImage.test.ts
new file mode 100644
index 000000000000..e9d192bc043b
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/measureImage.test.ts
@@ -0,0 +1,29 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import {describe, expect, it} from 'vitest';
+import {measureSvg} from '../measureImage';
+
+describe('measureSvg', () => {
+ it('reads width and height attributes', () => {
+ const svg = Buffer.from(
+ ``,
+ );
+ expect(measureSvg(svg)).toEqual({width: 24, height: 16});
+ });
+
+ it('falls back to viewBox when width/height are percentages', () => {
+ const svg = Buffer.from(
+ ``,
+ );
+ expect(measureSvg(svg)).toEqual({width: 100, height: 50});
+ });
+
+ it('returns null for non-svg content', () => {
+ expect(measureSvg(Buffer.from('not an image'))).toBeNull();
+ });
+});
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts
index ff88403eefb1..540a10a18aa2 100644
--- a/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/index.ts
@@ -17,19 +17,20 @@ import {
parseLocalURLPath,
} from '@docusaurus/utils';
import escapeHtml from 'escape-html';
-import {imageSizeFromFile} from 'image-size/fromFile';
import logger from '@docusaurus/logger';
import {
assetRequireAttributeValue,
formatNodePositionExtraMessage,
transformNode,
} from '../utils';
+import {readImageSize} from './measureImage';
import type {Plugin, Transformer} from 'unified';
import type {MdxJsxTextElement} from 'mdast-util-mdx';
import type {Image, Root} from 'mdast';
import type {Parent} from 'unist';
import type {
MarkdownConfig,
+ MeasureImageFunction,
OnBrokenMarkdownImagesFunction,
} from '@docusaurus/types';
@@ -37,12 +38,14 @@ export type PluginOptions = {
staticDirs: string[];
siteDir: string;
onBrokenMarkdownImages: MarkdownConfig['hooks']['onBrokenMarkdownImages'];
+ measureImage?: MeasureImageFunction;
};
type Context = {
staticDirs: PluginOptions['staticDirs'];
siteDir: PluginOptions['siteDir'];
onBrokenMarkdownImages: OnBrokenMarkdownImagesFunction;
+ measureImage?: MeasureImageFunction;
filePath: string;
inlineMarkdownImageFileLoader: string;
};
@@ -124,30 +127,20 @@ async function toImageRequireNode(
});
}
- try {
- const size = (await imageSizeFromFile(imagePath))!;
- if (size.width) {
- attributes.push({
- type: 'mdxJsxAttribute',
- name: 'width',
- value: String(size.width),
- });
- }
- if (size.height) {
- attributes.push({
- type: 'mdxJsxAttribute',
- name: 'height',
- value: String(size.height),
- });
- }
- } catch (err) {
- console.error(err);
- // Workaround for https://github.com/yarnpkg/berry/pull/3889#issuecomment-1034469784
- // TODO remove this check once fixed in Yarn PnP
- if (!process.versions.pnp) {
- logger.warn`The image at path=${imagePath} can't be read correctly. Please ensure it's a valid image.
-${(err as Error).message}`;
- }
+ const size = await readImageSize(imagePath, context.measureImage);
+ if (size?.width) {
+ attributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'width',
+ value: String(size.width),
+ });
+ }
+ if (size?.height) {
+ attributes.push({
+ type: 'mdxJsxAttribute',
+ name: 'height',
+ value: String(size.height),
+ });
}
transformNode(jsxNode, {
diff --git a/packages/docusaurus-mdx-loader/src/remark/transformImage/measureImage.ts b/packages/docusaurus-mdx-loader/src/remark/transformImage/measureImage.ts
new file mode 100644
index 000000000000..96d57c7945fd
--- /dev/null
+++ b/packages/docusaurus-mdx-loader/src/remark/transformImage/measureImage.ts
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import path from 'path';
+import fs from 'fs-extra';
+import {imageDimensionsFromData} from 'image-dimensions';
+import logger from '@docusaurus/logger';
+import type {MeasureImageFunction, MeasureImageSize} from '@docusaurus/types';
+
+function parseLength(value: string | undefined): number | undefined {
+ if (!value) {
+ return undefined;
+ }
+ const trimmed = value.trim();
+ if (trimmed.endsWith('%')) {
+ return undefined;
+ }
+ const parsed = Number.parseFloat(trimmed);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
+}
+
+function getSvgAttribute(attributes: string, name: string): string | undefined {
+ const match = attributes.match(
+ new RegExp(`\\b${name}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s>]+))`, 'i'),
+ );
+ return match?.[1] ?? match?.[2] ?? match?.[3];
+}
+
+export function measureSvg(buffer: Uint8Array): MeasureImageSize | null {
+ const text = Buffer.from(buffer).toString('utf8');
+ const svgTag = text.match(/