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 c926c82d9..42b307e58 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
@@ -54,6 +54,10 @@
Reprocess
+
+
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 ed0d2a125..97dae19b7 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
@@ -1415,4 +1415,151 @@ describe('DocumentDetailComponent', () => {
.flush('fail', { status: 500, statusText: 'Server Error' })
expect(component.previewText).toContain('An error occurred loading content')
})
+
+ it('should print document successfully', fakeAsync(() => {
+ initNormally()
+
+ const appendChildSpy = jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation((node: Node) => node)
+ const removeChildSpy = jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation((node: Node) => node)
+ const createObjectURLSpy = jest
+ .spyOn(URL, 'createObjectURL')
+ .mockReturnValue('blob:mock-url')
+ const revokeObjectURLSpy = jest
+ .spyOn(URL, 'revokeObjectURL')
+ .mockImplementation(() => {})
+
+ const mockContentWindow = {
+ focus: jest.fn(),
+ print: jest.fn(),
+ onafterprint: null,
+ }
+
+ const mockIframe = {
+ style: {},
+ src: '',
+ onload: null,
+ contentWindow: mockContentWindow,
+ }
+
+ const createElementSpy = jest
+ .spyOn(document, 'createElement')
+ .mockReturnValue(mockIframe as any)
+
+ const blob = new Blob(['test'], { type: 'application/pdf' })
+ component.printDocument()
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/${doc.id}/download/`
+ )
+ req.flush(blob)
+
+ tick()
+
+ expect(createElementSpy).toHaveBeenCalledWith('iframe')
+ expect(appendChildSpy).toHaveBeenCalledWith(mockIframe)
+ expect(createObjectURLSpy).toHaveBeenCalledWith(blob)
+
+ if (mockIframe.onload) {
+ mockIframe.onload({} as any)
+ }
+
+ expect(mockContentWindow.focus).toHaveBeenCalled()
+ expect(mockContentWindow.print).toHaveBeenCalled()
+
+ if (mockIframe.onload) {
+ mockIframe.onload(new Event('load'))
+ }
+
+ if (mockContentWindow.onafterprint) {
+ mockContentWindow.onafterprint(new Event('afterprint'))
+ }
+
+ expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
+ expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
+
+ createElementSpy.mockRestore()
+ appendChildSpy.mockRestore()
+ removeChildSpy.mockRestore()
+ createObjectURLSpy.mockRestore()
+ revokeObjectURLSpy.mockRestore()
+ }))
+
+ it('should show error toast if print document fails', () => {
+ initNormally()
+ const toastSpy = jest.spyOn(toastService, 'showError')
+ component.printDocument()
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/${doc.id}/download/`
+ )
+ req.error(new ErrorEvent('failed'))
+ expect(toastSpy).toHaveBeenCalledWith(
+ 'Error loading document for printing.'
+ )
+ })
+
+ it('should show error toast if printing throws inside iframe', fakeAsync(() => {
+ initNormally()
+
+ const appendChildSpy = jest
+ .spyOn(document.body, 'appendChild')
+ .mockImplementation((node: Node) => node)
+ const removeChildSpy = jest
+ .spyOn(document.body, 'removeChild')
+ .mockImplementation((node: Node) => node)
+ const createObjectURLSpy = jest
+ .spyOn(URL, 'createObjectURL')
+ .mockReturnValue('blob:mock-url')
+ const revokeObjectURLSpy = jest
+ .spyOn(URL, 'revokeObjectURL')
+ .mockImplementation(() => {})
+
+ const toastSpy = jest.spyOn(toastService, 'showError')
+
+ const mockContentWindow = {
+ focus: jest.fn().mockImplementation(() => {
+ throw new Error('focus failed')
+ }),
+ print: jest.fn(),
+ onafterprint: null,
+ }
+
+ const mockIframe: any = {
+ style: {},
+ src: '',
+ onload: null,
+ contentWindow: mockContentWindow,
+ }
+
+ const createElementSpy = jest
+ .spyOn(document, 'createElement')
+ .mockReturnValue(mockIframe as any)
+
+ const blob = new Blob(['test'], { type: 'application/pdf' })
+ component.printDocument()
+
+ const req = httpTestingController.expectOne(
+ `${environment.apiBaseUrl}documents/${doc.id}/download/`
+ )
+ req.flush(blob)
+
+ tick()
+
+ if (mockIframe.onload) {
+ mockIframe.onload(new Event('load'))
+ }
+
+ expect(toastSpy).toHaveBeenCalled()
+ expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
+ expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
+
+ createElementSpy.mockRestore()
+ appendChildSpy.mockRestore()
+ removeChildSpy.mockRestore()
+ createObjectURLSpy.mockRestore()
+ revokeObjectURLSpy.mockRestore()
+ }))
})
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 d139550c0..08c9a637c 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
@@ -291,6 +291,10 @@ export class DocumentDetailComponent
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
}
+ get isMobile(): boolean {
+ return this.deviceDetectorService.isMobile()
+ }
+
get archiveContentRenderType(): ContentRenderType {
return this.document?.archived_file_name
? this.getRenderType('application/pdf')
@@ -1419,6 +1423,44 @@ export class DocumentDetailComponent
})
}
+ printDocument() {
+ const printUrl = this.documentsService.getDownloadUrl(
+ this.document.id,
+ false
+ )
+ this.http
+ .get(printUrl, { responseType: 'blob' })
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe({
+ next: (blob) => {
+ const blobUrl = URL.createObjectURL(blob)
+ const iframe = document.createElement('iframe')
+ iframe.style.display = 'none'
+ iframe.src = blobUrl
+ document.body.appendChild(iframe)
+ iframe.onload = () => {
+ try {
+ iframe.contentWindow.focus()
+ iframe.contentWindow.print()
+ iframe.contentWindow.onafterprint = () => {
+ document.body.removeChild(iframe)
+ URL.revokeObjectURL(blobUrl)
+ }
+ } catch (err) {
+ this.toastService.showError($localize`Print failed.`, err)
+ document.body.removeChild(iframe)
+ URL.revokeObjectURL(blobUrl)
+ }
+ }
+ },
+ error: () => {
+ this.toastService.showError(
+ $localize`Error loading document for printing.`
+ )
+ },
+ })
+ }
+
public openShareLinks() {
const modal = this.modalService.open(ShareLinksDialogComponent)
modal.componentInstance.documentId = this.document.id
diff --git a/src-ui/src/main.ts b/src-ui/src/main.ts
index 029ec72ac..5ed4fe373 100644
--- a/src-ui/src/main.ts
+++ b/src-ui/src/main.ts
@@ -110,6 +110,7 @@ import {
playFill,
plus,
plusCircle,
+ printer,
questionCircle,
scissors,
search,
@@ -319,6 +320,7 @@ const icons = {
playFill,
plus,
plusCircle,
+ printer,
questionCircle,
scissors,
search,