From 8ff1632b490110ee11e45946fc7f2e3b29d8d5a3 Mon Sep 17 00:00:00 2001 From: yangkangkang Date: Mon, 8 Jun 2026 09:50:43 +0800 Subject: [PATCH] fix: revoke generated object URLs --- .../functional/VertdErrorDetails.svelte | 22 ++-- src/lib/store/index.svelte.ts | 100 ++++++++++-------- 2 files changed, 72 insertions(+), 50 deletions(-) diff --git a/src/lib/components/functional/VertdErrorDetails.svelte b/src/lib/components/functional/VertdErrorDetails.svelte index 52513586..1bbcc29b 100644 --- a/src/lib/components/functional/VertdErrorDetails.svelte +++ b/src/lib/components/functional/VertdErrorDetails.svelte @@ -2,6 +2,7 @@ import { m } from "$lib/paraglide/messages"; import type { DialogProps } from "$lib/store/DialogProvider"; import { link, sanitize } from "$lib/store/index.svelte"; + import { onMount } from "svelte"; interface VertdErrorDetailsProps { jobId: string; @@ -13,6 +14,19 @@ type Props = DialogProps; let { additional }: Props = $props(); + let errorMessageUrl = $state("#"); + + onMount(() => { + const url = URL.createObjectURL( + new Blob([additional.errorMessage], { + type: "text/plain", + }), + ); + + errorMessageUrl = url; + + return () => URL.revokeObjectURL(url); + });
@@ -41,13 +55,7 @@ {@html sanitize(link( ["view_link"], m["convert.errors.vertd_details_error_message"](), - [ - URL.createObjectURL( - new Blob([additional.errorMessage], { - type: "text/plain", - }), - ), - ], + [errorMessageUrl], [true], ["text-blue-500 font-normal"], ))} diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 318c6bda..6fccff72 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -90,53 +90,67 @@ class Files { const mediaElement = isVideo ? document.createElement("video") : new Image(); - mediaElement.src = URL.createObjectURL(file); - - await new Promise((resolve, reject) => { - if (isVideo) { - const video = mediaElement as HTMLVideoElement; - // seek to 10% of video time or 2 seconds in - video.onloadeddata = () => { - const seekTime = Math.min(video.duration * 0.1, 2); - video.currentTime = seekTime; - }; - video.onseeked = resolve; - video.onerror = reject; - } else { - (mediaElement as HTMLImageElement).onload = resolve; - (mediaElement as HTMLImageElement).onerror = reject; + const mediaUrl = URL.createObjectURL(file); + mediaElement.src = mediaUrl; + + try { + await new Promise((resolve, reject) => { + if (isVideo) { + const video = mediaElement as HTMLVideoElement; + // seek to 10% of video time or 2 seconds in + video.onloadeddata = () => { + const seekTime = Math.min(video.duration * 0.1, 2); + video.currentTime = seekTime; + }; + video.onseeked = resolve; + video.onerror = reject; + } else { + (mediaElement as HTMLImageElement).onload = resolve; + (mediaElement as HTMLImageElement).onerror = reject; + } + }); + + const canvas = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + if (!ctx) return undefined; + + const width = isVideo + ? (mediaElement as HTMLVideoElement).videoWidth + : (mediaElement as HTMLImageElement).width; + const height = isVideo + ? (mediaElement as HTMLVideoElement).videoHeight + : (mediaElement as HTMLImageElement).height; + + const scale = Math.max(maxSize / width, maxSize / height); + canvas.width = width * scale; + canvas.height = height * scale; + ctx.drawImage(mediaElement, 0, 0, canvas.width, canvas.height); + + // check if completely transparent + const imageData = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height, + ); + const isTransparent = Array.from(imageData.data).every( + (value, index) => { + return (index + 1) % 4 !== 0 || value === 0; + }, + ); + if (isTransparent) { + canvas.remove(); + return undefined; } - }); - const canvas = document.createElement("canvas"); - const ctx = canvas.getContext("2d"); - if (!ctx) return undefined; - - const width = isVideo - ? (mediaElement as HTMLVideoElement).videoWidth - : (mediaElement as HTMLImageElement).width; - const height = isVideo - ? (mediaElement as HTMLVideoElement).videoHeight - : (mediaElement as HTMLImageElement).height; - - const scale = Math.max(maxSize / width, maxSize / height); - canvas.width = width * scale; - canvas.height = height * scale; - ctx.drawImage(mediaElement, 0, 0, canvas.width, canvas.height); - - // check if completely transparent - const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); - const isTransparent = Array.from(imageData.data).every((value, index) => { - return (index + 1) % 4 !== 0 || value === 0; - }); - if (isTransparent) { + const url = canvas.toDataURL(); canvas.remove(); - return undefined; + return url; + } finally { + URL.revokeObjectURL(mediaUrl); + mediaElement.removeAttribute("src"); + if (isVideo) (mediaElement as HTMLVideoElement).load(); } - - const url = canvas.toDataURL(); - canvas.remove(); - return url; } private async _handleZipFile(file: File): Promise {