Files
cbsnexora/src/app/shared/services/document-preview.service.ts
T

140 lines
3.9 KiB
TypeScript

/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import { Injectable } from '@angular/core';
import { Observable, firstValueFrom } from 'rxjs';
export type DocumentPreviewType = 'image' | 'pdf' | 'other';
export interface DocumentDescriptor {
id: string;
name?: string;
description?: string;
fileName?: string;
fileData?: string;
mimeType?: string;
}
interface CachedPreview {
url: string;
type: DocumentPreviewType;
isObjectUrl: boolean;
}
@Injectable({
providedIn: 'root'
})
export class DocumentPreviewService {
private readonly previewCache = new Map<string, CachedPreview>();
/**
* Determine whether the document can be previewed inline.
*/
isPreviewable(document: DocumentDescriptor): boolean {
const type = this.detectType(document.mimeType, document.fileName, document.fileData);
return type === 'image' || type === 'pdf';
}
/**
* Resolve a preview URL for a document, caching object URLs to avoid duplicate downloads.
*/
async resolvePreviewUrl(
document: DocumentDescriptor,
downloadFn: (descriptor: DocumentDescriptor) => Observable<Blob>
): Promise<{ url: string; type: DocumentPreviewType }> {
const cached = this.previewCache.get(document.id);
if (cached) {
return { url: cached.url, type: cached.type };
}
const inline = this.getInlineData(document);
if (inline) {
const type = this.detectType(inline.mimeType, document.fileName, document.fileData);
this.previewCache.set(document.id, { url: inline.url, type, isObjectUrl: inline.isObjectUrl });
return { url: inline.url, type };
}
const blob = await firstValueFrom(downloadFn(document));
const objectUrl = URL.createObjectURL(blob);
const type = this.detectType(blob.type || document.mimeType, document.fileName, document.fileData);
this.previewCache.set(document.id, { url: objectUrl, type, isObjectUrl: true });
return { url: objectUrl, type };
}
/**
* Revoke an object URL if it was created by this service.
*/
release(documentId: string): void {
const cached = this.previewCache.get(documentId);
if (cached?.isObjectUrl) {
URL.revokeObjectURL(cached.url);
}
this.previewCache.delete(documentId);
}
/**
* Clean up all cached URLs.
*/
clear(): void {
this.previewCache.forEach((cached) => {
if (cached.isObjectUrl) {
URL.revokeObjectURL(cached.url);
}
});
this.previewCache.clear();
}
private getInlineData(document: DocumentDescriptor): { url: string; mimeType?: string; isObjectUrl: boolean } | null {
if (!document.fileData) {
return null;
}
const mimeMatch = document.fileData.match(/^data:(.*?);/);
return {
url: document.fileData,
mimeType: mimeMatch ? mimeMatch[1] : document.mimeType,
isObjectUrl: false
};
}
private detectType(mimeType?: string, fileName?: string, fileData?: string): DocumentPreviewType {
const normalizedMime = (mimeType || this.extractMimeFromData(fileData) || '').toLowerCase();
if (normalizedMime.includes('pdf')) {
return 'pdf';
}
if (normalizedMime.startsWith('image/')) {
return 'image';
}
const extension = (fileName || '').split('.').pop()?.toLowerCase();
if (extension === 'pdf') {
return 'pdf';
}
if (extension && [
'jpg',
'jpeg',
'png',
'gif',
'bmp',
'webp',
'svg'
].includes(extension)) {
return 'image';
}
return 'other';
}
private extractMimeFromData(fileData?: string): string | undefined {
if (!fileData || !fileData.startsWith('data:')) {
return undefined;
}
const match = fileData.match(/^data:(.*?);/);
return match ? match[1] : undefined;
}
}