AAE-45391 Lazy loading and improvements for pdfJS (#11869)

* [AAE-45391] - Lazy loading some of the big external libs

* [AAE-45391] - Added some extra units to pdfjs viewer

* [AAE-45391] - Some improement on viewer component

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - Fixed the way it was redered to a newer angular system

* [AAE-45391] - fixed schematic

* [AAE-45391] - Added some docs for the schematics as well

* [AAE-37328] - Fixed comment on missing provider
This commit is contained in:
Vito Albano
2026-05-11 14:17:39 +01:00
committed by GitHub
parent 62fb51e910
commit 9fe4006a0d
42 changed files with 939 additions and 65 deletions
+1 -7
View File
@@ -2,11 +2,6 @@
"$schema": "../../node_modules/ng-packagr/ng-package.schema.json",
"dest": "../../dist/libs/core",
"assets": [
{
"glob": "pdf.worker.mjs",
"input": "./assets/pdfjs",
"output": "assets/pdfjs"
},
{
"glob": "assets/**/*",
"input": "./src/lib",
@@ -38,7 +33,6 @@
"allowedNonPeerDependencies": [
"cropperjs",
"angular-oauth2-oidc",
"date-fns",
"rxjs"
"date-fns"
]
}
+8 -3
View File
@@ -19,8 +19,7 @@
"dependencies": {
"cropperjs": "^1.6.2",
"angular-oauth2-oidc": "19.0.0",
"date-fns": "^2.30.0",
"rxjs": "7.8.2"
"date-fns": "^2.30.0"
},
"peerDependencies": {
"@angular/animations": ">=16.0.0",
@@ -37,7 +36,13 @@
"@alfresco/js-api": ">=9.4.1",
"@alfresco/adf-extensions": ">=8.4.1",
"minimatch": ">=10.0.0",
"pdfjs-dist": ">=3.3.122"
"pdfjs-dist": ">=3.3.122",
"rxjs": ">=7.8.0"
},
"peerDependenciesMeta": {
"pdfjs-dist": {
"optional": true
}
},
"keywords": [
"core",
@@ -0,0 +1,91 @@
# Migration Schematic: migrate-pdf-viewer-imports (v9.0.0)
Automatically migrates consumer projects from the monolithic `@alfresco/adf-core` PDF viewer to the new `@alfresco/adf-core/viewer/pdf` secondary entrypoint.
## What it does
### 1. Rewrites imports
Moves PDF-related symbols from `@alfresco/adf-core` to `@alfresco/adf-core/viewer/pdf`:
**Before:**
```typescript
import { AppConfigService, PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core';
```
**After:**
```typescript
import { AppConfigService } from '@alfresco/adf-core';
import { PdfViewerComponent, RenderingQueueServices } from '@alfresco/adf-core/viewer/pdf';
```
Affected symbols:
- `PdfViewerComponent`
- `PdfPasswordDialogComponent`
- `PdfThumbListComponent`
- `PdfThumbComponent`
- `PDFJS_MODULE`
- `PDFJS_VIEWER_MODULE`
- `RenderingQueueServices`
### 2. Adds `providePdfViewer()` to app providers
The PDF viewer now requires explicit registration via the `providePdfViewer()` provider function. The schematic automatically adds it to the consumer's application configuration.
**Standalone app (`app.config.ts`):**
```typescript
import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf';
export const appConfig: ApplicationConfig = {
providers: [providePdfViewer(), /* ...existing providers */]
};
```
**NgModule app (`app.module.ts`):**
```typescript
import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf';
@NgModule({
providers: [providePdfViewer()]
})
export class AppModule {}
```
## How it runs
Triggered automatically via `ng update @alfresco/adf-core` when upgrading to v9.0.0+.
Can also be run manually:
```bash
ng generate @alfresco/adf-core:migrate-pdf-viewer-imports
```
## File discovery
The schematic scans all `.ts` files in the project (excluding `node_modules`, `.git`, `.angular`, `.nxcache`) and processes any file containing one of the PDF symbols listed above.
For provider injection, it searches for the app config file in this order:
1. `src/app/app.config.ts`
2. `src/app/app.module.ts`
3. `src/main.ts`
4. Falls back to scanning for any file containing `bootstrapApplication` or `ApplicationConfig`
## Edge cases handled
| Scenario | Behavior |
|----------|----------|
| Existing `@alfresco/adf-core/viewer/pdf` import | Merges moved symbols into it (deduplicates) |
| All symbols in the core import are PDF symbols | Removes the entire `@alfresco/adf-core` import |
| `providePdfViewer` already present in the project | Skips provider injection |
| No app config file found | Skips provider injection (consumer must add manually) |
| File has no PDF symbols | Skipped entirely (no modifications) |
| Empty providers array | Inserts `providePdfViewer()` as the first element |
## Why this migration exists
In v9.0.0, the PDF viewer was extracted into a secondary entrypoint (`@alfresco/adf-core/viewer/pdf`) so that:
- `pdfjs-dist` becomes an optional peer dependency
- Applications that don't use PDF viewing avoid bundling ~500KB of PDF processing code
- The primary `@alfresco/adf-core` bundle stays lean
Without this schematic, consumers upgrading to v9 would see PDF files rendered as "unknown format" because the `PDF_VIEWER_COMPONENT` injection token defaults to `null` when `providePdfViewer()` is not called.
@@ -0,0 +1,274 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import * as ts from 'typescript';
import { Rule, Tree } from '@angular-devkit/schematics';
const PDF_SYMBOLS = [
'PdfViewerComponent',
'PdfPasswordDialogComponent',
'PdfThumbListComponent',
'PdfThumbComponent',
'PDFJS_MODULE',
'PDFJS_VIEWER_MODULE',
'RenderingQueueServices'
];
const OLD_SOURCE = '@alfresco/adf-core';
const NEW_SOURCE = '@alfresco/adf-core/viewer/pdf';
/**
* @returns Schematic rule for migrating PDF viewer imports to the secondary entrypoint
*/
export function migratePdfViewerImports(): Rule {
return (tree: Tree) => {
let providerAdded = false;
tree.visit((filePath: string) => {
if (
!filePath.includes('/.git/') &&
!filePath.includes('/node_modules/') &&
!filePath.includes('/.angular/') &&
!filePath.includes('/.nxcache/') &&
/\.ts$/.test(filePath)
) {
const bufferContent = tree.read(filePath);
if (!bufferContent) {
return;
}
const fileContent = bufferContent.toString();
if (PDF_SYMBOLS.some((sym) => fileContent.includes(sym))) {
const sourceFile = ts.createSourceFile(filePath, fileContent, ts.ScriptTarget.Latest, true);
const updatedContent = movePdfImports(sourceFile, fileContent);
if (updatedContent !== fileContent) {
tree.overwrite(filePath, updatedContent);
}
}
if (!providerAdded && fileContent.includes('providePdfViewer')) {
providerAdded = true;
}
}
});
if (!providerAdded) {
addProvidePdfViewerToAppConfig(tree);
}
return tree;
};
}
/**
* @param sourceFile - the parsed TypeScript source file
* @param fileContent - the raw file content string
* @returns updated file content with PDF imports moved to the secondary entrypoint
*/
function movePdfImports(sourceFile: ts.SourceFile, fileContent: string): string {
const importDeclarations = sourceFile.statements.filter(ts.isImportDeclaration);
const coreImport = importDeclarations.find((decl) => {
const moduleSpecifier = decl.moduleSpecifier.getText().replace(/['"]/g, '');
return moduleSpecifier === OLD_SOURCE;
});
if (!coreImport?.importClause?.namedBindings || !ts.isNamedImports(coreImport.importClause.namedBindings)) {
return fileContent;
}
const namedImports = coreImport.importClause.namedBindings.elements;
const pdfImports = namedImports.filter((el) => PDF_SYMBOLS.includes(el.name.text));
const remainingImports = namedImports.filter((el) => !PDF_SYMBOLS.includes(el.name.text));
if (pdfImports.length === 0) {
return fileContent;
}
const pdfImportNames = pdfImports.map((el) => el.name.text);
const existingNewImport = importDeclarations.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
const existingNewImportNames: string[] = [];
if (existingNewImport?.importClause?.namedBindings && ts.isNamedImports(existingNewImport.importClause.namedBindings)) {
existingNewImportNames.push(...existingNewImport.importClause.namedBindings.elements.map((el) => el.name.text));
}
const mergedNames = [...new Set([...existingNewImportNames, ...pdfImportNames])];
const mergedImportStatement = `import { ${mergedNames.join(', ')} } from '${NEW_SOURCE}';`;
const edits: { start: number; end: number; replacement: string }[] = [];
if (remainingImports.length === 0) {
edits.push({ start: coreImport.getFullStart(), end: coreImport.getEnd(), replacement: '' });
} else {
const remainingNames = remainingImports.map((el) => el.getText()).join(', ');
const updatedImport = `import { ${remainingNames} } from '${OLD_SOURCE}';`;
edits.push({ start: coreImport.getStart(), end: coreImport.getEnd(), replacement: updatedImport });
}
if (existingNewImport) {
edits.push({ start: existingNewImport.getStart(), end: existingNewImport.getEnd(), replacement: mergedImportStatement });
}
edits.sort((a, b) => b.start - a.start);
let updatedContent = fileContent;
for (const edit of edits) {
updatedContent = updatedContent.slice(0, edit.start) + edit.replacement + updatedContent.slice(edit.end);
}
if (!existingNewImport) {
const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true);
const firstNonImport = updatedSource.statements.find((stmt) => !ts.isImportDeclaration(stmt));
const insertPos = firstNonImport ? firstNonImport.getFullStart() : updatedContent.length;
updatedContent = updatedContent.slice(0, insertPos).trimEnd() + '\n' + mergedImportStatement + '\n' + updatedContent.slice(insertPos);
}
return updatedContent.replace(/\n{3,}/g, '\n\n');
}
/**
* @param tree - the schematic file tree
*/
function addProvidePdfViewerToAppConfig(tree: Tree): void {
const candidates = ['src/app/app.config.ts', 'src/app/app.module.ts', 'src/main.ts'];
let targetPath: string | null = null;
for (const candidate of candidates) {
if (tree.exists(`/${candidate}`)) {
targetPath = `/${candidate}`;
break;
}
}
if (!targetPath) {
tree.visit((filePath: string) => {
if (targetPath) {
return;
}
if (!filePath.includes('/node_modules/') && !filePath.includes('/.git/') && /\.ts$/.test(filePath)) {
const content = tree.read(filePath)?.toString() ?? '';
if (content.includes('bootstrapApplication') || content.includes('ApplicationConfig')) {
targetPath = filePath;
}
}
});
}
if (!targetPath) {
return;
}
const buffer = tree.read(targetPath);
if (!buffer) {
return;
}
const content = buffer.toString();
if (content.includes('providePdfViewer')) {
return;
}
const sourceFile = ts.createSourceFile(targetPath, content, ts.ScriptTarget.Latest, true);
const result = insertProvidePdfViewer(sourceFile, content);
if (result !== content) {
tree.overwrite(targetPath, result);
}
}
/**
* @param sourceFile - the parsed TypeScript source file
* @param content - the raw file content
* @returns updated content with providePdfViewer() added to providers
*/
function insertProvidePdfViewer(sourceFile: ts.SourceFile, content: string): string {
const providersArray = findProvidersArray(sourceFile);
if (!providersArray) {
return content;
}
const lastElement = providersArray.elements[providersArray.elements.length - 1];
let insertPos: number;
let prefix: string;
if (lastElement) {
insertPos = lastElement.getEnd();
prefix = ', providePdfViewer()';
} else {
insertPos = providersArray.getStart() + 1;
prefix = 'providePdfViewer()';
}
let updatedContent = content.slice(0, insertPos) + prefix + content.slice(insertPos);
const importStatement = `import { providePdfViewer } from '${NEW_SOURCE}';\n`;
const existingPdfImport = sourceFile.statements
.filter(ts.isImportDeclaration)
.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
if (existingPdfImport) {
if (!content.includes('providePdfViewer')) {
const updatedSource = ts.createSourceFile('temp.ts', updatedContent, ts.ScriptTarget.Latest, true);
const existingDecl = updatedSource.statements
.filter(ts.isImportDeclaration)
.find((decl) => decl.moduleSpecifier.getText().replace(/['"]/g, '') === NEW_SOURCE);
if (existingDecl?.importClause?.namedBindings && ts.isNamedImports(existingDecl.importClause.namedBindings)) {
const names = existingDecl.importClause.namedBindings.elements.map((el) => el.name.text);
if (!names.includes('providePdfViewer')) {
names.push('providePdfViewer');
const newImport = `import { ${names.join(', ')} } from '${NEW_SOURCE}';`;
updatedContent = updatedContent.slice(0, existingDecl.getStart()) + newImport + updatedContent.slice(existingDecl.getEnd());
}
}
}
} else {
const firstImport = sourceFile.statements.find(ts.isImportDeclaration);
const importInsertPos = firstImport ? firstImport.getFullStart() : 0;
updatedContent = updatedContent.slice(0, importInsertPos) + importStatement + updatedContent.slice(importInsertPos);
}
return updatedContent;
}
/**
* @param sourceFile - the TypeScript source file to search
* @returns the providers ArrayLiteralExpression if found
*/
function findProvidersArray(sourceFile: ts.SourceFile): ts.ArrayLiteralExpression | null {
let result: ts.ArrayLiteralExpression | null = null;
const visit = (node: ts.Node): void => {
if (result) {
return;
}
if (ts.isPropertyAssignment(node) && node.name.getText() === 'providers' && ts.isArrayLiteralExpression(node.initializer)) {
result = node.initializer;
return;
}
ts.forEachChild(node, visit);
};
visit(sourceFile);
return result;
}
export default migratePdfViewerImports;
@@ -5,6 +5,11 @@
"description": "Update alfresco-api imports",
"version": "7.0.0",
"factory": "./7_0_0/index#updateAlfrescoApiImports"
},
"migrate-pdf-viewer-imports": {
"description": "Move PDF viewer imports to @alfresco/adf-core/viewer/pdf secondary entrypoint",
"version": "9.0.0",
"factory": "./9_0_0/index#migratePdfViewerImports"
}
},
"packageJsonUpdates": {
@@ -32,17 +32,13 @@
}
@case ('pdf') {
<adf-pdf-viewer
[thumbnailsTemplate]="thumbnailsTemplate"
[allowThumbnails]="allowThumbnails"
[blobFile]="blobFile"
[urlFile]="urlFile"
[fileName]="internalFileName"
[cacheType]="cacheTypeForContent"
(pagesLoaded)="markAsLoaded()"
(close)="onClose()"
(error)="onUnsupportedFile()"
/>
@if (pdfViewerComponent) {
<ng-container
[ngComponentOutlet]="pdfViewerComponent"
[ngComponentOutletInputs]="pdfViewerInputs" />
} @else {
<adf-viewer-unknown-format />
}
}
@case ('image') {
@@ -18,13 +18,30 @@
import { AppExtensionService, ViewerExtensionRef } from '@alfresco/adf-extensions';
import { Location } from '@angular/common';
import { SpyLocation } from '@angular/common/testing';
import { Component, DebugElement, TemplateRef, ViewChild } from '@angular/core';
import { Component, DebugElement, EventEmitter, Input, Output, TemplateRef, ViewChild } from '@angular/core';
import { ComponentFixture, DeferBlockBehavior, TestBed } from '@angular/core/testing';
import { MatDialog, MatDialogModule } from '@angular/material/dialog';
import { UnitTestingUtils } from '../../../testing';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { ViewerRenderComponent } from './viewer-render.component';
import { ImgViewerComponent, MediaPlayerComponent, PdfViewerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token';
import { PdfViewerRef } from '../../tokens/pdf-viewer-ref';
import { ImgViewerComponent, MediaPlayerComponent, ViewerExtensionDirective } from '@alfresco/adf-core';
@Component({
selector: 'adf-pdf-viewer',
template: '<div class="adf-pdf-viewer-mock"></div>'
})
class MockPdfViewerComponent implements PdfViewerRef {
@Input() urlFile = '';
@Input() blobFile: Blob;
@Input() fileName = '';
@Input() allowThumbnails = false;
@Input() thumbnailsTemplate: TemplateRef<unknown> = null;
@Input() cacheType = '';
@Output() pagesLoaded = new EventEmitter<void>();
@Output() error = new EventEmitter<void>();
@Output() close = new EventEmitter<void>();
}
@Component({
selector: 'adf-double-viewer',
@@ -69,7 +86,7 @@ describe('ViewerComponent', () => {
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [MatDialogModule, ViewerRenderComponent, DoubleViewerComponent],
providers: [RenderingQueueServices, { provide: Location, useClass: SpyLocation }, MatDialog],
providers: [{ provide: Location, useClass: SpyLocation }, MatDialog, { provide: PDF_VIEWER_COMPONENT, useValue: MockPdfViewerComponent }],
deferBlockBehavior: DeferBlockBehavior.Playthrough
});
fixture = TestBed.createComponent(ViewerRenderComponent);
@@ -532,7 +549,7 @@ describe('ViewerComponent', () => {
component.ngOnChanges();
fixture.detectChanges();
const imgViewer = testingUtils.getByDirective(PdfViewerComponent);
const imgViewer = testingUtils.getByDirective(MockPdfViewerComponent);
imgViewer.triggerEventHandler('pagesLoaded', null);
fixture.detectChanges();
@@ -16,9 +16,10 @@
*/
import { AppExtensionService, ExtensionsModule, ViewerExtensionRef, PreviewExtensionComponent } from '@alfresco/adf-extensions';
import { NgForOf, NgTemplateOutlet } from '@angular/common';
import { NgComponentOutlet, NgForOf, NgTemplateOutlet } from '@angular/common';
import {
Component,
effect,
EventEmitter,
Injector,
Input,
@@ -26,18 +27,22 @@ import {
OnInit,
Output,
TemplateRef,
Type,
ViewChild,
ViewEncapsulation,
inject
inject,
viewChild
} from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { TranslatePipe } from '@ngx-translate/core';
import { Subscription } from 'rxjs';
import { Track } from '../../models/viewer.model';
import { ViewUtilService } from '../../services/view-util.service';
import { PDF_VIEWER_COMPONENT } from '../../tokens/pdf-viewer.token';
import { PdfViewerRef } from '../../tokens/pdf-viewer-ref';
import { ImgViewerComponent } from '../img-viewer/img-viewer.component';
import { MediaPlayerComponent } from '../media-player/media-player.component';
import { PdfViewerComponent } from '../pdf-viewer/pdf-viewer.component';
import { TxtViewerComponent } from '../txt-viewer/txt-viewer.component';
import { UnknownFormatComponent } from '../unknown-format/unknown-format.component';
@@ -50,7 +55,7 @@ import { UnknownFormatComponent } from '../unknown-format/unknown-format.compone
imports: [
TranslatePipe,
MatProgressSpinnerModule,
PdfViewerComponent,
NgComponentOutlet,
ImgViewerComponent,
MediaPlayerComponent,
TxtViewerComponent,
@@ -68,6 +73,30 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
dialog = inject(MatDialog);
readonly injector = inject(Injector);
readonly pdfViewerComponent: Type<PdfViewerRef> | null = inject(PDF_VIEWER_COMPONENT, { optional: true });
pdfViewerInputs: Record<string, unknown> = {};
private readonly pdfOutlet = viewChild(NgComponentOutlet);
constructor() {
effect((onCleanup) => {
const outlet = this.pdfOutlet();
const instance = outlet?.componentInstance as PdfViewerRef | null;
if (!instance) {
return;
}
const subs: Subscription[] = [
instance.pagesLoaded.subscribe(() => this.markAsLoaded()),
instance.close.subscribe(() => this.onClose()),
instance.error.subscribe(() => this.onUnsupportedFile())
];
onCleanup(() => subs.forEach((s) => s.unsubscribe()));
});
}
/**
* If you want to load an external file that does not come from ACS you
* can use this URL to specify where to load the file from.
@@ -197,6 +226,13 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
} else if (this.urlFile) {
this.setUpUrlFile();
}
if (this.viewerType === 'pdf' && !this.pdfViewerComponent) {
console.error(
'@alfresco/adf-core: PDF viewer is not configured. ' +
'Add providePdfViewer() from @alfresco/adf-core/viewer/pdf to your application providers.'
);
}
this.updatePdfViewerInputs();
}
markAsLoaded() {
@@ -250,4 +286,15 @@ export class ViewerRenderComponent implements OnChanges, OnInit {
onClose() {
this.close.next(true);
}
private updatePdfViewerInputs(): void {
this.pdfViewerInputs = {
urlFile: this.urlFile,
blobFile: this.blobFile,
fileName: this.internalFileName,
allowThumbnails: this.allowThumbnails,
thumbnailsTemplate: this.thumbnailsTemplate,
cacheType: this.cacheTypeForContent
};
}
}
+2 -5
View File
@@ -16,13 +16,10 @@
*/
export * from './services/view-util.service';
export * from './tokens/pdf-viewer.token';
export * from './tokens/pdf-viewer-ref';
export * from './components/img-viewer/img-viewer.component';
export * from './components/media-player/media-player.component';
export * from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog';
export * from './components/pdf-viewer/pdf-viewer.component';
export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
export * from './components/pdf-viewer-thumb/pdf-viewer-thumb.component';
export * from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
export * from './components/txt-viewer/txt-viewer.component';
export * from './components/unknown-format/unknown-format.component';
export * from './components/viewer-more-actions.component';
@@ -0,0 +1,30 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter, TemplateRef } from '@angular/core';
export interface PdfViewerRef {
urlFile: string;
blobFile: Blob;
fileName: string;
allowThumbnails: boolean;
thumbnailsTemplate: TemplateRef<unknown>;
cacheType: string;
pagesLoaded: EventEmitter<void>;
error: EventEmitter<void>;
close: EventEmitter<void>;
}
@@ -0,0 +1,24 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { InjectionToken, Type } from '@angular/core';
import { PdfViewerRef } from './pdf-viewer-ref';
export const PDF_VIEWER_COMPONENT = new InjectionToken<Type<PdfViewerRef> | null>('PDF_VIEWER_COMPONENT', {
providedIn: 'root',
factory: () => null
});
-8
View File
@@ -19,10 +19,6 @@ import { NgModule } from '@angular/core';
import { DownloadPromptDialogComponent } from './components/download-prompt-dialog/download-prompt-dialog.component';
import { ImgViewerComponent } from './components/img-viewer/img-viewer.component';
import { MediaPlayerComponent } from './components/media-player/media-player.component';
import { PdfPasswordDialogComponent } from './components/pdf-viewer-password-dialog/pdf-viewer-password-dialog';
import { PdfThumbComponent } from './components/pdf-viewer-thumb/pdf-viewer-thumb.component';
import { PdfThumbListComponent } from './components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component';
import { TxtViewerComponent } from './components/txt-viewer/txt-viewer.component';
import { UnknownFormatComponent } from './components/unknown-format/unknown-format.component';
import { ViewerMoreActionsComponent } from './components/viewer-more-actions.component';
@@ -36,14 +32,10 @@ import { ViewerComponent } from './components/viewer.component';
import { ViewerExtensionDirective } from './directives/viewer-extension.directive';
export const VIEWER_DIRECTIVES = [
PdfPasswordDialogComponent,
ViewerRenderComponent,
ImgViewerComponent,
TxtViewerComponent,
MediaPlayerComponent,
PdfViewerComponent,
PdfThumbComponent,
PdfThumbListComponent,
ViewerExtensionDirective,
UnknownFormatComponent,
ViewerToolbarComponent,
+77
View File
@@ -0,0 +1,77 @@
# @alfresco/adf-core/viewer/pdf
Secondary entrypoint that provides the PDF viewer component, extracted from `@alfresco/adf-core` to make `pdfjs-dist` an optional dependency.
## Setup
1. Install `pdfjs-dist` (if not already present):
```bash
npm install pdfjs-dist
```
2. Add `providePdfViewer()` to your app providers:
```typescript
import { providePdfViewer } from '@alfresco/adf-core/viewer/pdf';
// Standalone app (app.config.ts)
export const appConfig: ApplicationConfig = {
providers: [
providePdfViewer(),
// ...other providers
]
};
// NgModule-based app (app.module.ts)
@NgModule({
providers: [providePdfViewer()]
})
export class AppModule {}
```
That's it. The viewer will render PDFs using the real `PdfViewerComponent`.
## Migration from v8.x
If upgrading from a version where `PdfViewerComponent` was part of `@alfresco/adf-core` directly, run:
```bash
ng update @alfresco/adf-core
```
The migration schematic will:
- Rewrite imports of PDF symbols (`PdfViewerComponent`, `PdfPasswordDialogComponent`, `PdfThumbListComponent`, `PdfThumbComponent`, `PDFJS_MODULE`, `PDFJS_VIEWER_MODULE`, `RenderingQueueServices`) from `@alfresco/adf-core` to `@alfresco/adf-core/viewer/pdf`
- Add `providePdfViewer()` to your app's providers array
## Opting out of PDF support
If your application does not need to render PDFs, you can skip installing `pdfjs-dist` and omit `providePdfViewer()`. The viewer will display an "unknown format" placeholder for PDF files.
## Exports
| Symbol | Description |
|--------|-------------|
| `providePdfViewer()` | Provider function that registers the PDF viewer |
| `PdfViewerComponent` | The PDF viewer component |
| `PdfPasswordDialogComponent` | Password dialog for protected PDFs |
| `PdfThumbListComponent` | PDF thumbnail list |
| `PdfThumbComponent` | Individual PDF thumbnail |
| `PDFJS_MODULE` | Injection token for pdfjs-dist library |
| `PDFJS_VIEWER_MODULE` | Injection token for pdfjs viewer module |
| `RenderingQueueServices` | Service managing PDF page render queue |
## Architecture
```
@alfresco/adf-core (primary entrypoint)
- Exports: PDF_VIEWER_COMPONENT token, PdfViewerRef interface
- ViewerRenderComponent injects the token via NgComponentOutlet
- Does NOT depend on pdfjs-dist
@alfresco/adf-core/viewer/pdf (this entrypoint)
- Imports from: @alfresco/adf-core, pdfjs-dist
- Exports: PdfViewerComponent, providePdfViewer(), etc.
```
The dependency is one-way (secondary -> primary). The primary entrypoint never imports from this package, avoiding circular dependencies and keeping `pdfjs-dist` out of the main bundle.
+8
View File
@@ -0,0 +1,8 @@
{
"lib": {
"entryFile": "src/index.ts",
"styleIncludePaths": [
"../../src/lib"
]
}
}
+23
View File
@@ -0,0 +1,23 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export * from './lib/components/pdf-viewer/pdf-viewer.component';
export * from './lib/components/pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
export * from './lib/components/pdf-viewer-thumb/pdf-viewer-thumb.component';
export * from './lib/components/pdf-viewer-password-dialog/pdf-viewer-password-dialog';
export * from './lib/services/rendering-queue.services';
export * from './lib/provide-pdf-viewer';
@@ -0,0 +1,53 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export const annotations = [
{
subtype: 'Text',
name: 'NoIcon',
id: 'R13',
titleObj: {
str: 'Annotation title'
},
contentsObj: {
str: 'Annotation contents'
},
modificationDate: "D:20260202104106Z00'00",
popupRef: 'R1'
}
];
export default {
GlobalWorkerOptions: {},
getDocument: jasmine.createSpy('getDocument').and.callFake(() => ({
loadingTask: () => ({
destroy: () => Promise.resolve()
}),
promise: new Promise((resolve) => {
resolve({
numPages: 6,
getPage: () =>
Promise.resolve({
getAnnotations: () => annotations
})
});
})
})),
PasswordResponses: {
NEED_PASSWORD: 1,
INCORRECT_PASSWORD: 2
}
};
@@ -23,7 +23,7 @@ import { ReactiveFormsModule, UntypedFormControl, Validators } from '@angular/fo
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { TranslatePipe } from '@ngx-translate/core';
import { IconModule } from '../../../icon/icon.module';
import { IconModule } from '@alfresco/adf-core';
declare const pdfjsLib: { PasswordResponses: { NEED_PASSWORD: number; INCORRECT_PASSWORD: number } };
@@ -17,7 +17,7 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PdfThumbListComponent } from './pdf-viewer-thumbnails.component';
import { UnitTestingUtils } from '../../../testing';
import { UnitTestingUtils } from '@alfresco/adf-core';
import { DOWN_ARROW, ESCAPE, UP_ARROW } from '@angular/cdk/keycodes';
declare const pdfjsViewer: any;
@@ -0,0 +1,99 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PDF_VIEWER_COMPONENT, ViewerRenderComponent, UnitTestingUtils } from '@alfresco/adf-core';
import { PdfViewerComponent, PDFJS_MODULE, PDFJS_VIEWER_MODULE } from './pdf-viewer.component';
import { providePdfViewer } from '../../provide-pdf-viewer';
import pdfjsLibraryMock from '../mock/pdfjs-lib.mock';
describe('PdfViewer Integration with ViewerRenderComponent', () => {
let fixture: ComponentFixture<ViewerRenderComponent>;
let component: ViewerRenderComponent;
let testingUtils: UnitTestingUtils;
beforeEach(async () => {
TestBed.configureTestingModule({
imports: [ViewerRenderComponent],
providers: [
providePdfViewer(),
{ provide: PDFJS_MODULE, useValue: pdfjsLibraryMock },
{ provide: PDFJS_VIEWER_MODULE, useValue: class {} }
]
});
fixture = TestBed.createComponent(ViewerRenderComponent);
component = fixture.componentInstance;
testingUtils = new UnitTestingUtils(fixture.debugElement);
});
afterEach(() => {
fixture.destroy();
});
it('should provide PdfViewerComponent via token', () => {
const token = TestBed.inject(PDF_VIEWER_COMPONENT);
expect(token).toBe(PdfViewerComponent);
});
it('should render real PdfViewerComponent for PDF files', async () => {
component.urlFile = 'fake-test-file.pdf';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(component.viewerType).toBe('pdf');
expect(testingUtils.getByCSS('adf-pdf-viewer')).not.toBeNull();
});
it('should pass inputs to real PdfViewerComponent', async () => {
component.urlFile = 'fake-test-file.pdf';
component.allowThumbnails = true;
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const pdfViewer = testingUtils.getByCSS('adf-pdf-viewer');
expect(pdfViewer).not.toBeNull();
expect(pdfViewer.componentInstance.urlFile).toBe('fake-test-file.pdf');
expect(pdfViewer.componentInstance.allowThumbnails).toBe(true);
});
it('should show unknown format when token is not provided', async () => {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [ViewerRenderComponent],
providers: [{ provide: PDF_VIEWER_COMPONENT, useValue: null }]
});
fixture = TestBed.createComponent(ViewerRenderComponent);
component = fixture.componentInstance;
testingUtils = new UnitTestingUtils(fixture.debugElement);
component.urlFile = 'fake-test-file.pdf';
component.ngOnChanges();
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
expect(component.viewerType).toBe('pdf');
expect(testingUtils.getByCSS('adf-pdf-viewer')).toBeNull();
expect(testingUtils.getByCSS('adf-viewer-unknown-format')).not.toBeNull();
});
});
@@ -0,0 +1,126 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Component, ViewChild } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { provideCoreAuthTesting } from '@alfresco/adf-core';
import { PdfViewerComponent } from './pdf-viewer.component';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs';
const MINIMAL_PDF_BASE64 =
'JVBERi0xLjcKCjEgMCBvYmogICUgZW50cnkgcG9pbnQKPDwKICAvVHlwZSAvQ2F0YWxvZwog' +
'IC9QYWdlcyAyIDAgUgo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvVHlwZSAvUGFnZXMKICAv' +
'TWVkaWFCb3ggWyAwIDAgMjAwIDIwMCBdCiAgL0NvdW50IDEKICAvS2lkcyBbIDMgMCBSIF0K' +
'Pj4KZW5kb2JqCgozIDAgb2JqCjw8CiAgL1R5cGUgL1BhZ2UKICAvUGFyZW50IDIgMCBSCiAg' +
'L1Jlc291cmNlcyA8PAogICAgL0ZvbnQgPDwKICAgICAgL0YxIDQgMCBSIAogICAgPj4KICA+' +
'PgogIC9Db250ZW50cyA1IDAgUgo+PgplbmRvYmoKCjQgMCBvYmoKPDwKICAvVHlwZSAvRm9u' +
'dAogIC9TdWJ0eXBlIC9UeXBlMQogIC9CYXNlRm9udCAvVGltZXMtUm9tYW4KPj4KZW5kb2Jq' +
'Cgo1IDAgb2JqICAlIHBhZ2UgY29udGVudAo8PAogIC9MZW5ndGggNDQKPj4Kc3RyZWFtCkJU' +
'CjcwIDUwIFRECi9GMSAxMiBUZgooSGVsbG8sIHdvcmxkISkgVGoKRVQKZW5kc3RyZWFtCmVu' +
'ZG9iagoKeHJlZgowIDYKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDEwIDAwMDAwIG4g' +
'CjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE3MyAwMDAwMCBuIAowMDAwMDAwMzAxIDAw' +
'MDAwIG4gCjAwMDAwMDAzODAgMDAwMDAgbiAKdHJhaWxlcgo8PAogIC9TaXplIDYKICAvUm9v' +
'dCAxIDAgUgo+PgpzdGFydHhyZWYKNDkyCiUlRU9G';
/** @returns a Blob containing a minimal valid 1-page PDF */
function createRealPdfBlob(): Blob {
const binaryString = atob(MINIMAL_PDF_BASE64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return new Blob([bytes], { type: 'application/pdf' });
}
@Component({
imports: [PdfViewerComponent],
template: `<adf-pdf-viewer [blobFile]="blobFile" />`
})
class RealPdfTestHostComponent {
@ViewChild(PdfViewerComponent, { static: true })
pdfViewerComponent: PdfViewerComponent;
blobFile = createRealPdfBlob();
}
/**
* @param predicate - condition to wait for
* @param timeout - max wait time in ms
* @returns a promise that resolves when the predicate returns true
*/
function waitFor(predicate: () => boolean, timeout = 5000): Promise<void> {
const start = Date.now();
return new Promise((resolve, reject) => {
const check = () => {
if (predicate()) {
resolve();
} else if (Date.now() - start > timeout) {
reject(new Error('waitFor timed out'));
} else {
setTimeout(check, 50);
}
};
check();
});
}
describe('PdfViewerComponent with real pdfjs-dist', () => {
let fixture: ComponentFixture<RealPdfTestHostComponent>;
let component: PdfViewerComponent;
beforeAll(async () => {
const workerModule = await import('pdfjs-dist/build/pdf.worker.min.mjs');
(globalThis as any).pdfjsWorker = { WorkerMessageHandler: workerModule.WorkerMessageHandler };
});
afterAll(() => {
delete (globalThis as any).pdfjsWorker;
});
beforeEach(async () => {
pdfjsLib.GlobalWorkerOptions.workerSrc = '';
TestBed.configureTestingModule({
imports: [RealPdfTestHostComponent],
providers: [provideCoreAuthTesting(), { provide: MatDialog, useValue: { open: () => {} } }, RenderingQueueServices]
});
fixture = TestBed.createComponent(RealPdfTestHostComponent);
component = fixture.componentInstance.pdfViewerComponent;
spyOn(component as any, 'setupPdfJsWorker').and.resolveTo();
});
afterEach(() => {
fixture.destroy();
});
it('should load a real PDF blob and detect page count', async () => {
fixture.detectChanges();
await waitFor(() => component.totalPages > 0);
expect(component.totalPages).toBe(1);
});
it('should emit pagesLoaded when a real PDF is rendered', async () => {
const pagesLoadedSpy = spyOn(component.pagesLoaded, 'emit');
fixture.detectChanges();
await waitFor(() => pagesLoadedSpy.calls.count() > 0);
expect(pagesLoadedSpy).toHaveBeenCalled();
});
});
@@ -1,4 +1,4 @@
@use '../../../styles/mat-selectors' as ms;
@use 'styles/mat-selectors' as ms;
.adf-pdf-viewer {
width: 100%;
@@ -21,9 +21,7 @@ import { ComponentFixture, fakeAsync, flush, TestBed, tick } from '@angular/core
import { MatDialog } from '@angular/material/dialog';
import { By } from '@angular/platform-browser';
import { of } from 'rxjs';
import { AppConfigService } from '../../../app-config';
import { EventMock } from '../../../mock';
import { UnitTestingUtils, provideCoreAuthTesting } from '../../../testing';
import { AppConfigService, EventMock, UnitTestingUtils, provideCoreAuthTesting } from '@alfresco/adf-core';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
import { PDFJS_MODULE, PDFJS_VIEWER_MODULE, PdfViewerComponent } from './pdf-viewer.component';
@@ -461,7 +459,7 @@ describe('Test PdfViewer - User interaction', () => {
component.urlFile = './fake-test-file.pdf';
fixture.detectChanges();
component.ngOnChanges({
component.ngOnChanges({
urlFile: new SimpleChange(null, './fake-test-file.pdf', true)
});
@@ -40,8 +40,7 @@ import { MatProgressBarModule } from '@angular/material/progress-bar';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { from, Subject, switchMap } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { AppConfigService } from '../../../app-config';
import { ToolbarComponent, ToolbarDividerComponent } from '../../../toolbar';
import { AppConfigService, IconModule, ToolbarComponent, ToolbarDividerComponent } from '@alfresco/adf-core';
import { RenderingQueueServices } from '../../services/rendering-queue.services';
import { PdfPasswordDialogComponent } from '../pdf-viewer-password-dialog/pdf-viewer-password-dialog';
import { PdfThumbListComponent } from '../pdf-viewer-thumbnails/pdf-viewer-thumbnails.component';
@@ -49,7 +48,6 @@ import * as pdfjsLib from 'pdfjs-dist/build/pdf.min.mjs';
import { PDFDateString } from 'pdfjs-dist/build/pdf.min.mjs';
import { EventBus, PDFViewer } from 'pdfjs-dist/web/pdf_viewer.mjs';
import { OnProgressParameters, PDFDocumentLoadingTask, PDFDocumentProxy, PDFPageProxy } from 'pdfjs-dist/types/src/display/api';
import { IconModule } from '../../../icon/icon.module';
export type PdfScaleMode = 'init' | 'page-actual' | 'page-width' | 'page-height' | 'page-fit' | 'auto';
@@ -630,7 +628,7 @@ export class PdfViewerComponent implements OnChanges, OnDestroy {
* @param event.source.container.id - the container id
*/
onPageChange(event: PageChangingEvent) {
if (event.source && event.source.container.id === `${this.randomPdfId}-viewer-pdf-viewer`) {
if (event.source?.container.id === `${this.randomPdfId}-viewer-pdf-viewer`) {
this.page = event.pageNumber;
this.displayPage = event.pageNumber;
}
@@ -0,0 +1,25 @@
/*!
* @license
* Copyright © 2005-2025 Hyland Software, Inc. and its affiliates. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { Provider } from '@angular/core';
import { PDF_VIEWER_COMPONENT } from '@alfresco/adf-core';
import { PdfViewerComponent } from './components/pdf-viewer/pdf-viewer.component';
/** @returns Provider that registers the PdfViewerComponent for the viewer */
export function providePdfViewer(): Provider {
return { provide: PDF_VIEWER_COMPONENT, useValue: PdfViewerComponent };
}
-3
View File
@@ -21,9 +21,6 @@
]
},
"allowedNonPeerDependencies": [
"@alfresco/adf-core",
"@alfresco/adf-content-services",
"@ngx-translate/core",
"chart.js",
"ng2-charts",
"raphael"
+4 -4
View File
@@ -11,9 +11,6 @@
"url": "https://github.com/Alfresco/alfresco-ng2-components/issues"
},
"dependencies": {
"@alfresco/adf-core": ">=8.4.1",
"@alfresco/adf-content-services": ">=8.4.1",
"@ngx-translate/core": ">=17.0.0",
"chart.js": "^4.3.0",
"ng2-charts": "^4.1.1",
"raphael": ">=2.3.0"
@@ -23,7 +20,10 @@
"@angular/compiler": ">=14.1.3",
"@angular/core": ">=14.1.3",
"@angular/forms": ">=14.1.3",
"@angular/material": ">=14.1.2"
"@angular/material": ">=14.1.2",
"@alfresco/adf-core": ">=8.4.1",
"@alfresco/adf-content-services": ">=8.4.1",
"@ngx-translate/core": ">=17.0.0"
},
"keywords": [
"analytics",
+1 -4
View File
@@ -26,8 +26,5 @@
"../core/src/lib"
]
},
"allowedNonPeerDependencies": [
"@apollo/client",
"apollo-angular"
]
"allowedNonPeerDependencies": []
}
+1
View File
@@ -30,6 +30,7 @@
"@alfresco/adf-core/breadcrumbs": ["lib/core/breadcrumbs/src/index.ts"],
"@alfresco/adf-core/feature-flags": ["lib/core/feature-flags/public-api.ts"],
"@alfresco/adf-core/shell": ["lib/core/shell/src/index.ts"],
"@alfresco/adf-core/viewer/pdf": ["lib/core/viewer/pdf/src/index.ts"],
"@alfresco/adf-extensions": ["lib/extensions/src/public-api.ts"],
"@alfresco/adf-insights": ["lib/insights/src/public-api.ts"],
"@alfresco/adf-process-services": ["lib/process-services/src/public-api.ts"],