diff --git a/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx b/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx index a9a01ef..c309139 100644 --- a/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx +++ b/src/component/Viewers/ImageViewer/react-photo-view/Photo.tsx @@ -6,6 +6,7 @@ import { getFileEntityUrl, getFileInfo } from "../../../../api/api.ts"; import { EntityType, FileResponse, Metadata } from "../../../../api/explorer.ts"; import { useAppDispatch } from "../../../../redux/hooks.ts"; import { getFileLinkedUri } from "../../../../util"; +import { LRUCache } from "../../../../util/lru.ts"; import FacebookCircularProgress from "../../../Common/CircularProgress.tsx"; import useMountedRef from "./hooks/useMountedRef"; import "./Photo.less"; @@ -28,6 +29,9 @@ export interface IPhotoProps extends React.HTMLAttributes { brokenElement?: JSX.Element | ((photoProps: BrokenElementParams) => JSX.Element); } +// Global LRU cache for HEIC conversions (capacity: 50 images) +const heicConversionCache = new LRUCache(50); + export default function Photo({ file, version, @@ -50,8 +54,14 @@ export default function Photo({ return extension === "heic" || extension === "heif"; }; - // Helper function to convert HEIC to PNG - const convertHeicToPng = async (imageUrl: string): Promise => { + // Helper function to convert HEIC to JPG with caching + const convertHeicToJpg = async (imageUrl: string, cacheKey: string): Promise => { + // Check cache first + const cachedUrl = heicConversionCache.get(cacheKey); + if (cachedUrl) { + return cachedUrl; + } + try { // Fetch the image as blob const response = await fetch(imageUrl); @@ -64,21 +74,27 @@ export default function Photo({ const isHeicBlob = await isHeic(file); if (isHeicBlob) { - // Convert HEIC to PNG - const pngBlob = await heicTo({ + // Convert HEIC to JPG + const jpgBlob = await heicTo({ blob: blob, - type: "image/png", - quality: 0.9, + type: "image/jpeg", + quality: 1, }); // Create object URL for the converted image - return URL.createObjectURL(pngBlob); + const convertedUrl = URL.createObjectURL(jpgBlob); + + // Cache the converted URL + heicConversionCache.set(cacheKey, convertedUrl); + + return convertedUrl; } else { - // If not HEIC, return original URL + // If not HEIC, cache and return original URL + heicConversionCache.set(cacheKey, imageUrl); return imageUrl; } } catch (error) { - console.error("Error converting HEIC to PNG:", error); + console.error("Error converting HEIC to JPG:", error); throw error; } }; @@ -92,11 +108,12 @@ export default function Photo({ ) .then(async (res) => { const originalUrl = res.urls[0].url; + const cacheKey = `${file.id}-${version || "default"}`; // Check if the file is HEIC/HEIF and convert if needed if (isHeicFile(file.name)) { try { - const convertedUrl = await convertHeicToPng(originalUrl); + const convertedUrl = await convertHeicToJpg(originalUrl, cacheKey); setImageSrc(convertedUrl); if (file.metadata?.[Metadata.live_photo]) { loadLivePhoto(file, convertedUrl); @@ -189,21 +206,23 @@ export default function Photo({ useEffect(() => { return () => { if (imageSrc && imageSrc.startsWith("blob:")) { - URL.revokeObjectURL(imageSrc); + // Don't revoke cached URLs, let the cache handle cleanup + // URL.revokeObjectURL(imageSrc); } }; }, [imageSrc]); - const { - onMouseDown, - onTouchStart, - style: { width, height, ...restStyle }, - ...rest - } = restProps; + const { onMouseDown, onTouchStart, style, ...rest } = restProps; + + // Extract width and height from style if available + const { width, height, ...restStyle } = style || {}; useEffect(() => { if (playerRef.current) { - playerRef.current.updateSize(width, height); + // Convert width and height to numbers, defaulting to 0 if not valid + const numWidth = typeof width === "number" ? width : 0; + const numHeight = typeof height === "number" ? height : 0; + playerRef.current.updateSize(numWidth, numHeight); } }, [width, height]); diff --git a/src/util/lru.ts b/src/util/lru.ts new file mode 100644 index 0000000..eb9f0dd --- /dev/null +++ b/src/util/lru.ts @@ -0,0 +1,52 @@ +// LRU Cache implementation for HEIC image conversion +export class LRUCache { + private capacity: number; + private cache: Map; + + constructor(capacity: number) { + this.capacity = capacity; + this.cache = new Map(); + } + + get(key: K): V | undefined { + if (this.cache.has(key)) { + // Move to end (most recently used) + const value = this.cache.get(key)!; + this.cache.delete(key); + this.cache.set(key, value); + return value; + } + return undefined; + } + + set(key: K, value: V): void { + if (this.cache.has(key)) { + // Update existing key + this.cache.delete(key); + } else if (this.cache.size >= this.capacity) { + // Remove least recently used (first item) + const firstKey = this.cache.keys().next().value; + if (firstKey !== undefined) { + const firstValue = this.cache.get(firstKey); + + // Clean up blob URL if it exists + if (typeof firstValue === "string" && firstValue.startsWith("blob:")) { + URL.revokeObjectURL(firstValue); + } + + this.cache.delete(firstKey); + } + } + this.cache.set(key, value); + } + + clear(): void { + // Clean up all blob URLs + for (const value of this.cache.values()) { + if (typeof value === "string" && value.startsWith("blob:")) { + URL.revokeObjectURL(value); + } + } + this.cache.clear(); + } +}