mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
feat(image viewer): add LRU cache for heic converted images (#2601)
This commit is contained in:
parent
38f5114426
commit
70931462f2
|
|
@ -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<HTMLElement> {
|
|||
brokenElement?: JSX.Element | ((photoProps: BrokenElementParams) => JSX.Element);
|
||||
}
|
||||
|
||||
// Global LRU cache for HEIC conversions (capacity: 50 images)
|
||||
const heicConversionCache = new LRUCache<string, string>(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<string> => {
|
||||
// Helper function to convert HEIC to JPG with caching
|
||||
const convertHeicToJpg = async (imageUrl: string, cacheKey: string): Promise<string> => {
|
||||
// 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]);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,52 @@
|
|||
// LRU Cache implementation for HEIC image conversion
|
||||
export class LRUCache<K, V> {
|
||||
private capacity: number;
|
||||
private cache: Map<K, V>;
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue