From 79345f0a6959318db925d446d26b17838b518e11 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Sun, 1 Dec 2024 11:46:19 -0800 Subject: [PATCH] Enhancement: better TIFF display support (#8087) --- src-ui/package-lock.json | 16 ++++++ src-ui/package.json | 1 + .../document-detail.component.html | 9 ++++ .../document-detail.component.scss | 1 + .../document-detail.component.spec.ts | 42 ++++++++++++++++ .../document-detail.component.ts | 50 +++++++++++++++++++ 6 files changed, 119 insertions(+) diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json index 101100f94..15451b598 100644 --- a/src-ui/package-lock.json +++ b/src-ui/package-lock.json @@ -33,6 +33,7 @@ "ngx-ui-tour-ng-bootstrap": "^15.0.0", "rxjs": "^7.8.1", "tslib": "^2.8.1", + "utif": "^3.1.0", "uuid": "^11.0.2", "zone.js": "^0.14.8" }, @@ -13758,6 +13759,12 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -16563,6 +16570,15 @@ "requires-port": "^1.0.0" } }, + "node_modules/utif": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/utif/-/utif-3.1.0.tgz", + "integrity": "sha512-WEo4D/xOvFW53K5f5QTaTbbiORcm2/pCL9P6qmJnup+17eYfKaEhDeX9PeQkuyEoIxlbGklDuGl8xwuXYMrrXQ==", + "license": "MIT", + "dependencies": { + "pako": "^1.0.5" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/src-ui/package.json b/src-ui/package.json index f607655ce..e2b2b6569 100644 --- a/src-ui/package.json +++ b/src-ui/package.json @@ -35,6 +35,7 @@ "ngx-ui-tour-ng-bootstrap": "^15.0.0", "rxjs": "^7.8.1", "tslib": "^2.8.1", + "utif": "^3.1.0", "uuid": "^11.0.2", "zone.js": "^0.14.8" }, diff --git a/src-ui/src/app/components/document-detail/document-detail.component.html b/src-ui/src/app/components/document-detail/document-detail.component.html index 6a39b13bd..486277c21 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.html +++ b/src-ui/src/app/components/document-detail/document-detail.component.html @@ -388,6 +388,15 @@ {{title}} } + @case (ContentRenderType.TIFF) { + @if (!tiffError) { +
+ {{title}} +
+ } @else { +
{{tiffError}}
+ } + } @case (ContentRenderType.Other) { } diff --git a/src-ui/src/app/components/document-detail/document-detail.component.scss b/src-ui/src/app/components/document-detail/document-detail.component.scss index f61e20e83..e3d17476b 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.scss +++ b/src-ui/src/app/components/document-detail/document-detail.component.scss @@ -61,6 +61,7 @@ textarea.rtl { width: 100%; height: 100%; object-fit: contain; + object-position: top; } .thumb-preview { diff --git a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts index 41a576f01..46b72cb4e 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.spec.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.spec.ts @@ -1270,4 +1270,46 @@ describe('DocumentDetailComponent', () => { expect(component.createDisabled(DataType.StoragePath)).toBeFalsy() expect(component.createDisabled(DataType.Tag)).toBeFalsy() }) + + it('should call tryRenderTiff when no archive and file is tiff', () => { + initNormally() + const tiffRenderSpy = jest.spyOn( + DocumentDetailComponent.prototype as any, + 'tryRenderTiff' + ) + const doc = Object.assign({}, component.document) + doc.archived_file_name = null + doc.mime_type = 'image/tiff' + jest + .spyOn(documentService, 'getMetadata') + .mockReturnValue( + of({ has_archive_version: false, original_mime_type: 'image/tiff' }) + ) + component.updateComponent(doc) + fixture.detectChanges() + expect(component.archiveContentRenderType).toEqual( + component.ContentRenderType.TIFF + ) + expect(tiffRenderSpy).toHaveBeenCalled() + }) + + it('should try to render tiff and show error if failed', () => { + initNormally() + // just the text request + httpTestingController.expectOne(component.previewUrl) + + // invalid tiff + component['tryRenderTiff']() + httpTestingController + .expectOne(component.previewUrl) + .flush(new ArrayBuffer(100)) // arraybuffer + expect(component.tiffError).not.toBeUndefined() + + // http error + component['tryRenderTiff']() + httpTestingController + .expectOne(component.previewUrl) + .error(new ErrorEvent('failed')) + expect(component.tiffError).not.toBeUndefined() + }) }) diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts index 2842509fc..f1afd95c0 100644 --- a/src-ui/src/app/components/document-detail/document-detail.component.ts +++ b/src-ui/src/app/components/document-detail/document-detail.component.ts @@ -72,6 +72,7 @@ import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/dele import { HotKeyService } from 'src/app/services/hot-key.service' import { PDFDocumentProxy } from 'ng2-pdf-viewer' import { DataType } from 'src/app/data/datatype' +import * as UTIF from 'utif' enum DocumentDetailNavIDs { Details = 1, @@ -89,6 +90,7 @@ enum ContentRenderType { Text = 'text', Other = 'other', Unknown = 'unknown', + TIFF = 'tiff', } enum ZoomSetting { @@ -136,6 +138,8 @@ export class DocumentDetailComponent downloadUrl: string downloadOriginalUrl: string previewLoaded: boolean = false + tiffURL: string + tiffError: string correspondents: Correspondent[] documentTypes: DocumentType[] @@ -244,6 +248,8 @@ export class DocumentDetailComponent ['text/plain', 'application/csv', 'text/csv'].includes(mimeType) ) { return ContentRenderType.Text + } else if (mimeType.indexOf('tiff') >= 0) { + return ContentRenderType.TIFF } else if (mimeType?.indexOf('image/') === 0) { return ContentRenderType.Image } @@ -542,6 +548,9 @@ export class DocumentDetailComponent this.document = doc this.requiresPassword = false this.updateFormForCustomFields() + if (this.archiveContentRenderType === ContentRenderType.TIFF) { + this.tryRenderTiff() + } this.documentsService .getMetadata(doc.id) .pipe( @@ -1278,4 +1287,45 @@ export class DocumentDetailComponent }) }) } + + private tryRenderTiff() { + this.http.get(this.previewUrl, { responseType: 'arraybuffer' }).subscribe({ + next: (res) => { + /* istanbul ignore next */ + try { + // See UTIF.js > _imgLoaded + const tiffIfds: any[] = UTIF.decode(res) + var vsns = tiffIfds, + ma = 0, + page = vsns[0] + if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD) + for (var i = 0; i < vsns.length; i++) { + var img = vsns[i] + if (img['t258'] == null || img['t258'].length < 3) continue + var ar = img['t256'] * img['t257'] + if (ar > ma) { + ma = ar + page = img + } + } + UTIF.decodeImage(res, page, tiffIfds) + const rgba = UTIF.toRGBA8(page) + const { width: w, height: h } = page + var cnv = document.createElement('canvas') + cnv.width = w + cnv.height = h + var ctx = cnv.getContext('2d'), + imgd = ctx.createImageData(w, h) + for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i] + ctx.putImageData(imgd, 0, 0) + this.tiffURL = cnv.toDataURL() + } catch (err) { + this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` + } + }, + error: (err) => { + this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` + }, + }) + } }