Enhancement: native TIFF display

This commit is contained in:
shamoon 2024-10-28 12:44:02 -07:00
parent d4a20c7e30
commit 58cb3b5d39
6 changed files with 116 additions and 0 deletions

View File

@ -33,6 +33,7 @@
"ngx-ui-tour-ng-bootstrap": "^15.0.0", "ngx-ui-tour-ng-bootstrap": "^15.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"utif": "^3.1.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"zone.js": "^0.14.8" "zone.js": "^0.14.8"
}, },
@ -15497,6 +15498,12 @@
"node": "^16.14.0 || >=18.0.0" "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": { "node_modules/parent-module": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@ -18390,6 +18397,15 @@
"requires-port": "^1.0.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -35,6 +35,7 @@
"ngx-ui-tour-ng-bootstrap": "^15.0.0", "ngx-ui-tour-ng-bootstrap": "^15.0.0",
"rxjs": "^7.8.1", "rxjs": "^7.8.1",
"tslib": "^2.7.0", "tslib": "^2.7.0",
"utif": "^3.1.0",
"uuid": "^10.0.0", "uuid": "^10.0.0",
"zone.js": "^0.14.8" "zone.js": "^0.14.8"
}, },

View File

@ -386,6 +386,15 @@
<img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" /> <img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" />
</div> </div>
} }
@case (ContentRenderType.TIFF) {
@if (!tiffError) {
<div class="preview-sticky">
<img [src]="tiffURL" width="100%" height="100%" alt="{{title}}" />
</div>
} @else {
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{tiffError}}</div>
}
}
@case (ContentRenderType.Other) { @case (ContentRenderType.Other) {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object> <object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
} }

View File

@ -61,6 +61,7 @@ textarea.rtl {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
object-position: top;
} }
.whitespace-preserve { .whitespace-preserve {

View File

@ -1262,4 +1262,43 @@ describe('DocumentDetailComponent', () => {
expect(component.createDisabled(DataType.StoragePath)).toBeFalsy() expect(component.createDisabled(DataType.StoragePath)).toBeFalsy()
expect(component.createDisabled(DataType.Tag)).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'
)
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()
})
}) })

View File

@ -72,6 +72,7 @@ import { DeletePagesConfirmDialogComponent } from '../common/confirm-dialog/dele
import { HotKeyService } from 'src/app/services/hot-key.service' import { HotKeyService } from 'src/app/services/hot-key.service'
import { PDFDocumentProxy } from 'ng2-pdf-viewer' import { PDFDocumentProxy } from 'ng2-pdf-viewer'
import { DataType } from 'src/app/data/datatype' import { DataType } from 'src/app/data/datatype'
import * as UTIF from 'utif'
enum DocumentDetailNavIDs { enum DocumentDetailNavIDs {
Details = 1, Details = 1,
@ -89,6 +90,7 @@ enum ContentRenderType {
Text = 'text', Text = 'text',
Other = 'other', Other = 'other',
Unknown = 'unknown', Unknown = 'unknown',
TIFF = 'tiff',
} }
enum ZoomSetting { enum ZoomSetting {
@ -134,6 +136,8 @@ export class DocumentDetailComponent
previewText: string previewText: string
downloadUrl: string downloadUrl: string
downloadOriginalUrl: string downloadOriginalUrl: string
tiffURL: string
tiffError: string
correspondents: Correspondent[] correspondents: Correspondent[]
documentTypes: DocumentType[] documentTypes: DocumentType[]
@ -240,6 +244,8 @@ export class DocumentDetailComponent
['text/plain', 'application/csv', 'text/csv'].includes(mimeType) ['text/plain', 'application/csv', 'text/csv'].includes(mimeType)
) { ) {
return ContentRenderType.Text return ContentRenderType.Text
} else if (mimeType.indexOf('tiff') >= 0) {
return ContentRenderType.TIFF
} else if (mimeType?.indexOf('image/') === 0) { } else if (mimeType?.indexOf('image/') === 0) {
return ContentRenderType.Image return ContentRenderType.Image
} }
@ -537,6 +543,9 @@ export class DocumentDetailComponent
.subscribe({ .subscribe({
next: (result) => { next: (result) => {
this.metadata = result this.metadata = result
if (this.archiveContentRenderType === ContentRenderType.TIFF) {
this.tryRenderTiff()
}
}, },
error: (error) => { error: (error) => {
this.metadata = {} // allow display to fallback to <object> tag this.metadata = {} // allow display to fallback to <object> tag
@ -1250,4 +1259,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()}`
},
})
}
} }