From dfa1ce572bef6976ae82b3c5cfaa581580a447f1 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Sat, 31 Jan 2026 16:28:21 +0100 Subject: [PATCH] fix: SVG Inversion on Safari (#10712) * invert image on safari * lint * Inversion to match theme filter * cleanup * Adjust canvas dimensions for device pixel ratio when inverting on Safari * revert inversion algo & handle darkMode placeholder --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> --- packages/element/src/renderElement.ts | 94 +++++++++++++++++++++------ 1 file changed, 74 insertions(+), 20 deletions(-) diff --git a/packages/element/src/renderElement.ts b/packages/element/src/renderElement.ts index 1c5f941ebf..96bff999bc 100644 --- a/packages/element/src/renderElement.ts +++ b/packages/element/src/renderElement.ts @@ -23,6 +23,7 @@ import { getVerticalOffset, invariant, applyDarkModeFilter, + isSafari, } from "@excalidraw/common"; import type { @@ -360,8 +361,9 @@ IMAGE_ERROR_PLACEHOLDER_IMG.src = `data:${MIME_TYPES.svg},${encodeURIComponent( const drawImagePlaceholder = ( element: ExcalidrawImageElement, context: CanvasRenderingContext2D, + theme: StaticCanvasRenderConfig["theme"], ) => { - context.fillStyle = "#E7E7E7"; + context.fillStyle = theme === THEME.DARK ? "#2E2E2E" : "#E7E7E7"; context.fillRect(0, 0, element.width, element.height); const imageMinWidthOrHeight = Math.min(element.width, element.height); @@ -443,13 +445,6 @@ const drawElementOnCanvas = ( ? cacheEntry?.image : undefined; - const shouldInvertImage = - renderConfig.theme === THEME.DARK && - cacheEntry?.mimeType === MIME_TYPES.svg; - - if (shouldInvertImage) { - context.filter = DARK_THEME_FILTER; - } if (img != null && !(img instanceof Promise)) { if (element.roundness && context.roundRect) { context.beginPath(); @@ -472,19 +467,78 @@ const drawElementOnCanvas = ( height: img.naturalHeight, }; - context.drawImage( - img, - x, - y, - width, - height, - 0 /* hardcoded for the selection box*/, - 0, - element.width, - element.height, - ); + const shouldInvertImage = + renderConfig.theme === THEME.DARK && + cacheEntry?.mimeType === MIME_TYPES.svg; + + if (shouldInvertImage && isSafari) { + const devicePixelRatio = window.devicePixelRatio || 1; + const tempCanvas = document.createElement("canvas"); + tempCanvas.width = element.width * devicePixelRatio; + tempCanvas.height = element.height * devicePixelRatio; + const tempContext = tempCanvas.getContext("2d"); + + if (tempContext) { + tempContext.scale(devicePixelRatio, devicePixelRatio); + tempContext.drawImage( + img, + x, + y, + width, + height, + 0, + 0, + element.width, + element.height, + ); + + const imageData = tempContext.getImageData( + 0, + 0, + tempCanvas.width, + tempCanvas.height, + ); + + const data = imageData.data; + + for (let i = 0; i < data.length; i += 4) { + data[i] = 255 - data[i]; + data[i + 1] = 255 - data[i + 1]; + data[i + 2] = 255 - data[i + 2]; + } + + tempContext.putImageData(imageData, 0, 0); + context.drawImage( + tempCanvas, + 0, + 0, + tempCanvas.width, + tempCanvas.height, + 0, + 0, + element.width, + element.height, + ); + } + } else { + if (shouldInvertImage) { + context.filter = DARK_THEME_FILTER; + } + + context.drawImage( + img, + x, + y, + width, + height, + 0 /* hardcoded for the selection box*/, + 0, + element.width, + element.height, + ); + } } else { - drawImagePlaceholder(element, context); + drawImagePlaceholder(element, context, renderConfig.theme); } context.restore(); break;