mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2025-05-03 11:29:28 -05:00
Enhancement: native TIFF display
This commit is contained in:
parent
d4a20c7e30
commit
58cb3b5d39
16
src-ui/package-lock.json
generated
16
src-ui/package-lock.json
generated
@ -33,6 +33,7 @@
|
||||
"ngx-ui-tour-ng-bootstrap": "^15.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.7.0",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zone.js": "^0.14.8"
|
||||
},
|
||||
@ -15497,6 +15498,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",
|
||||
@ -18390,6 +18397,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",
|
||||
|
@ -35,6 +35,7 @@
|
||||
"ngx-ui-tour-ng-bootstrap": "^15.0.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"tslib": "^2.7.0",
|
||||
"utif": "^3.1.0",
|
||||
"uuid": "^10.0.0",
|
||||
"zone.js": "^0.14.8"
|
||||
},
|
||||
|
@ -386,6 +386,15 @@
|
||||
<img [src]="previewUrl | safeUrl" width="100%" height="100%" alt="{{title}}" />
|
||||
</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) {
|
||||
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
|
||||
}
|
||||
|
@ -61,6 +61,7 @@ textarea.rtl {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-position: top;
|
||||
}
|
||||
|
||||
.whitespace-preserve {
|
||||
|
@ -1262,4 +1262,43 @@ 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'
|
||||
)
|
||||
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()
|
||||
})
|
||||
})
|
||||
|
@ -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 {
|
||||
@ -134,6 +136,8 @@ export class DocumentDetailComponent
|
||||
previewText: string
|
||||
downloadUrl: string
|
||||
downloadOriginalUrl: string
|
||||
tiffURL: string
|
||||
tiffError: string
|
||||
|
||||
correspondents: Correspondent[]
|
||||
documentTypes: DocumentType[]
|
||||
@ -240,6 +244,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
|
||||
}
|
||||
@ -537,6 +543,9 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.metadata = result
|
||||
if (this.archiveContentRenderType === ContentRenderType.TIFF) {
|
||||
this.tryRenderTiff()
|
||||
}
|
||||
},
|
||||
error: (error) => {
|
||||
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()}`
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user