Skip to content

Commit

Permalink
feat: show attachments
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfriesen committed Oct 15, 2024
1 parent 3ba4808 commit 704e94b
Show file tree
Hide file tree
Showing 19 changed files with 328 additions and 18 deletions.
24 changes: 24 additions & 0 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { UploadService } from '@app/services/upload.service';
import { PreviewComponent } from '@app/components/preview/preview.component';
import { HeaderComponent } from '@app/components/header/header.component';
import { DropAreaDirective } from '@app/directives/drop-area.directive';
import { MatIconRegistry } from '@angular/material/icon';
import { DomSanitizer } from '@angular/platform-browser';

@Component({
selector: 'app-root',
Expand Down Expand Up @@ -38,4 +40,26 @@ export class AppComponent {
async onHovering(value: boolean) {
this.isHovering = value;
}

constructor() {
// const host = isPlatformServer(inject(PLATFORM_ID)) ? environment.webHost : '.';
const iconRegistry = inject(MatIconRegistry);
const sanitizer = inject(DomSanitizer);
iconRegistry.addSvgIcon(
'zip',
sanitizer.bypassSecurityTrustResourceUrl(`./assets/filetype/zip.svg`)
);
iconRegistry.addSvgIcon(
'xml',
sanitizer.bypassSecurityTrustResourceUrl(`./assets/filetype/xml.svg`)
);
iconRegistry.addSvgIcon(
'image',
sanitizer.bypassSecurityTrustResourceUrl(`./assets/filetype/image.svg`)
);
iconRegistry.addSvgIcon(
'unknown',
sanitizer.bypassSecurityTrustResourceUrl(`./assets/filetype/unknown.svg`)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<h2 mat-dialog-title>
{{ data.name }}
</h2>

<mat-dialog-content>
<p>Description: {{ data.description }}</p>
<p>MimeType: {{ data.mimeType }}</p>
</mat-dialog-content>

<mat-dialog-content>
<pre>{{ fileContent }}</pre>
</mat-dialog-content>

<mat-dialog-actions align="end">
<button mat-flat-button color="accent" mat-dialog-close>
{{ 'close' | transloco }}
</button>
</mat-dialog-actions>
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
import { DatePipe } from '@angular/common';
import { MatButtonModule } from '@angular/material/button';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { TranslocoPipe } from '@jsverse/transloco';

import { DocumentAttachment } from '@app/types/attachment';

@Component({
selector: 'app-attachment-dialog',
templateUrl: './attachment-dialog.component.html',
styleUrls: ['./attachment-dialog.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
DatePipe,
MatButtonModule,
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
TranslocoPipe,
],
})
export class AttachmentDialogComponent {
readonly data = inject<DocumentAttachment>(MAT_DIALOG_DATA);

fileContent: string | undefined;

constructor() {
this.fileContent = new TextDecoder().decode(this.data.data);
}
}
27 changes: 27 additions & 0 deletions src/app/components/attachments/attachments.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<mat-expansion-panel>
<mat-expansion-panel-header>
Attachments: {{ attachments().length }}
</mat-expansion-panel-header>

<ng-template matExpansionPanelContent>
@for (attachment of attachments(); track attachment.name) {
<div class="item">
<button class="tool" mat-mini-fab (click)="inspectAttachment(attachment)">
<mat-icon>search</mat-icon>
</button>

@if (attachment.mimeType.includes('image/')) {
<mat-icon class="icon large" svgIcon="image" />
} @else if (attachment.mimeType.includes('xml')) {
<mat-icon class="icon large" svgIcon="xml" />
} @else {
<mat-icon class="icon large" svgIcon="unknown" />
}

<div class="truncate">
{{ attachment.name }}
</div>
</div>
} @empty {}
</ng-template>
</mat-expansion-panel>
45 changes: 45 additions & 0 deletions src/app/components/attachments/attachments.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
:host {
display: block;
width: 100%;
padding-left: 1rem;
padding-bottom: 1.25rem;
padding-right: 6rem;
}

.item {
position: relative;
border-radius: 0.25rem;
border: grey solid 2px;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;

margin: 0.5rem;
max-width: 200px;

&:hover {
border-color: #3f51b5;
}

.tool {
position: absolute;
top: 0;
right: 0;
}
}

.icon.large {
font-size: 3rem;
width: 3rem;
height: 3rem;
}

.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
42 changes: 42 additions & 0 deletions src/app/components/attachments/attachments.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {
Component,
ChangeDetectionStrategy,
inject,
input,
} from '@angular/core';
import {
MatExpansionPanel,
MatExpansionPanelContent,
MatExpansionPanelHeader,
} from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { TranslocoPipe } from '@jsverse/transloco';

import { DocumentAttachment } from '@app/types/attachment';
import { LazyDialogService } from '@app/services/lazy-dialog.service';

@Component({
selector: 'app-attachments',
templateUrl: './attachments.component.html',
styleUrls: ['./attachments.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
MatExpansionPanel,
MatExpansionPanelHeader,
MatExpansionPanelContent,
MatIconModule,
MatButtonModule,
TranslocoPipe,
],
})
export class AttachmentsComponent {
private readonly lazyDialogService = inject(LazyDialogService);

readonly attachments = input.required<DocumentAttachment[]>();

inspectAttachment(attachment: DocumentAttachment) {
this.lazyDialogService.openAttachmentDialog(attachment);
}
}
23 changes: 12 additions & 11 deletions src/app/components/pages/pages.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@
(cdkDropListDropped)="onChangePosition($event)"
>
@for (page of pages(); track page) {
<div cdkDrag class="item">
<button
class="remove"
mat-mini-fab
color="warn"
(click)="onRemovePage(page)"
>
<mat-icon>delete</mat-icon>
</button>
<div cdkDrag class="item">
<button class="tool" mat-mini-fab color="warn" (click)="onRemovePage(page)">
<mat-icon>delete</mat-icon>
</button>

<app-thumb [pageIndex]="page" />
</div>
<app-thumb [pageIndex]="page" />
</div>
} @empty {}
</div>

<section class="attachments">
@defer (when hasAttachments()) {
<app-attachments [attachments]="attachments()" />
}
</section>
28 changes: 22 additions & 6 deletions src/app/components/pages/pages.component.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
:host {
display: block;
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
position: relative;
text-align: center;

height: 100%;
}

.list {
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
flex-direction: row;
box-sizing: border-box;
overflow: auto;
}

.attachments {
display: flex;
flex: 0 1 auto;
flex-wrap: wrap;
flex-direction: row;
box-sizing: border-box;
Expand All @@ -17,6 +32,7 @@
border: grey solid 2px;

display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
box-sizing: border-box;
Expand All @@ -27,12 +43,12 @@
&:hover {
border-color: #3f51b5;
}
}

button.remove {
position: absolute;
top: 0;
right: 0;
.tool {
position: absolute;
top: 0;
right: 0;
}
}

.cdk-drag-preview {
Expand Down
7 changes: 6 additions & 1 deletion src/app/components/pages/pages.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { TranslocoPipe } from '@jsverse/transloco';
import { DocumentService } from '@app/services/document.service';
import { EmptyComponent } from '../empty/empty.component';
import { ThumbnailComponent } from '../thumb/thumb.component';
import { AttachmentsComponent } from '../attachments/attachments.component';

@Component({
selector: 'app-pages',
Expand All @@ -25,13 +26,17 @@ import { ThumbnailComponent } from '../thumb/thumb.component';
DragDropModule,
TranslocoPipe,

ThumbnailComponent,
EmptyComponent,
ThumbnailComponent,
AttachmentsComponent,
],
})
export class PagesComponent {
private readonly documentService = inject(DocumentService);

readonly attachments = this.documentService.attachments;
readonly hasAttachments = computed(() => this.attachments().length > 0);

readonly pageCount = this.documentService.pageCount;
readonly pages = computed(() => {
const pageCount = this.pageCount();
Expand Down
2 changes: 2 additions & 0 deletions src/app/components/preview/preview.component.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
:host {
display: block;
height: 100%;
position: relative;
text-align: center;
}
1 change: 1 addition & 0 deletions src/app/helpers/file.helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const readFileAsDataURLAsync = (file: File) =>
};

reader.onerror = reject;
reader.onabort = reject;

reader.readAsDataURL(file);
});
Expand Down
58 changes: 58 additions & 0 deletions src/app/helpers/pdf.helper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {
decodePDFRawStream,
PDFArray,
PDFDict,
PDFDocument,
PDFHexString,
PDFName,
PDFRawStream,
PDFStream,
PDFString,
} from 'pdf-lib';

export const extractRawAttachments = (pdfDoc: PDFDocument) => {
if (!pdfDoc.catalog.has(PDFName.of('Names'))) return [];
const Names = pdfDoc.catalog.lookup(PDFName.of('Names'), PDFDict);

if (!Names.has(PDFName.of('EmbeddedFiles'))) return [];
let EmbeddedFiles = Names.lookup(PDFName.of('EmbeddedFiles'), PDFDict);

if (
!EmbeddedFiles.has(PDFName.of('Names')) &&
EmbeddedFiles.has(PDFName.of('Kids'))
)
EmbeddedFiles = EmbeddedFiles.lookup(PDFName.of('Kids'), PDFArray).lookup(
0
) as PDFDict;

if (!EmbeddedFiles.has(PDFName.of('Names'))) return [];
const EFNames = EmbeddedFiles.lookup(PDFName.of('Names'), PDFArray);

const rawAttachments = [];
for (let idx = 0, len = EFNames.size(); idx < len; idx += 2) {
const fileName = EFNames.lookup(idx) as PDFHexString | PDFString;
const fileSpec = EFNames.lookup(idx + 1, PDFDict);
rawAttachments.push({ fileName, fileSpec });
}

return rawAttachments;
};

export const extractAttachments = (pdfDoc: PDFDocument) => {
const rawAttachments = extractRawAttachments(pdfDoc);
return rawAttachments.map(({ fileName, fileSpec }) => {
const stream = fileSpec
.lookup(PDFName.of('EF'), PDFDict)
.lookup(PDFName.of('F'), PDFStream) as PDFRawStream;

const description = fileSpec.lookup(PDFName.of('Desc'), PDFString);
const subtype = stream.dict.lookup(PDFName.of('Subtype'), PDFName);

return {
name: fileName.decodeText(),
description: description.decodeText(),
mimeType: subtype.decodeText(),
data: decodePDFRawStream(stream).decode(),
};
});
};
Loading

0 comments on commit 704e94b

Please sign in to comment.