ng-pdf-renderer
Version:
A modern, zero-configuration PDF viewer for Angular applications with enhanced error handling, auto-fit, text selection, and responsive design
1 lines โข 110 kB
Source Map (JSON)
{"version":3,"file":"ng-pdf-renderer.mjs","sources":["../../../projects/ng-pdf-renderer/src/lib/ng-pdf-renderer.config.ts","../../../projects/ng-pdf-renderer/src/lib/services/pdf.service.ts","../../../projects/ng-pdf-renderer/src/lib/components/pdf-controls.component.ts","../../../projects/ng-pdf-renderer/src/lib/components/pdf-viewer.component.ts","../../../projects/ng-pdf-renderer/src/public-api.ts","../../../projects/ng-pdf-renderer/src/ng-pdf-renderer.ts"],"sourcesContent":["import { Injectable } from '@angular/core';\n\n/**\n * Configuration options for NgPdfRenderer\n */\nexport interface NgPdfRendererConfig {\n /**\n * Custom worker URL (optional, automatically detected if not provided)\n */\n workerSrc?: string;\n}\n\n/**\n * Service for global configuration of NgPdfRenderer\n * Allows application-wide settings to be applied\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class NgPdfRendererConfigService {\n private _config: NgPdfRendererConfig = {};\n\n /**\n * Get the current configuration\n */\n get config(): NgPdfRendererConfig {\n return this._config;\n }\n\n /**\n * Set the configuration for NgPdfRenderer\n * @param config Configuration options\n */\n setConfig(config: NgPdfRendererConfig): void {\n this._config = { ...this._config, ...config };\n }\n}","import { Injectable, inject } from '@angular/core';\nimport { BehaviorSubject, Observable } from 'rxjs';\n// Import PDF.js library\nimport * as pdfjsLib from 'pdfjs-dist';\n\nimport { NgPdfRendererConfigService } from '../ng-pdf-renderer.config';\n\n/**\n * Service handling PDF operations using PDF.js\n * Provides methods to load, navigate, and manipulate PDF documents\n */\n@Injectable({\n providedIn: 'root'\n})\nexport class PdfService {\n // BehaviorSubjects to track PDF state (these emit current value on subscription)\n private pdfDocumentSubject = new BehaviorSubject<any>(null); // Holds the PDF document object\n pdfDocument$ = this.pdfDocumentSubject.asObservable(); // Observable for components to subscribe to\n \n private currentPageSubject = new BehaviorSubject<number>(1); // Current page being viewed\n currentPage$ = this.currentPageSubject.asObservable();\n \n private totalPagesSubject = new BehaviorSubject<number>(0); // Total number of pages in the document\n totalPages$ = this.totalPagesSubject.asObservable();\n \n private zoomSubject = new BehaviorSubject<number>(1); // Current zoom level (1 = 100%)\n zoom$ = this.zoomSubject.asObservable();\n \n private rotationSubject = new BehaviorSubject<number>(0); // Current rotation in degrees\n rotation$ = this.rotationSubject.asObservable();\n\n // Link service for annotations (especially hyperlinks)\n private linkService: any;\n \n // Properties needed for link service\n private _pdfDocument: any = null;\n private _viewer: any = null;\n\n // Inject the configuration service\n private configService = inject(NgPdfRendererConfigService);\n\n constructor() {\n // Automatically configure the worker source\n this.configureWorkerSource();\n \n // Initialize link service\n this.linkService = {\n setDocument: (pdfDocument: any) => {\n this._pdfDocument = pdfDocument;\n },\n setViewer: (viewer: any) => {\n this._viewer = viewer;\n },\n navigateTo: (dest: any) => {\n //console.log('Navigate to:', dest);\n if (dest && typeof dest === 'object' && dest.length > 0) {\n if (dest[0] && typeof dest[0] === 'object' && 'num' in dest[0]) {\n // Navigate to page\n const pageNumber = dest[0].num + 1;\n this.setCurrentPage(pageNumber);\n if (this._viewer && this._viewer.scrollPageIntoView) {\n this._viewer.scrollPageIntoView({ pageNumber });\n }\n }\n }\n },\n getDestinationHash: (dest: any) => {\n return `page=${dest}`;\n },\n getAnchorUrl: (hash: string) => {\n return `#${hash}`;\n }\n };\n }\n \n /**\n * Gets the link service for handling annotations\n * @returns The link service instance\n */\n getLinkService(): any {\n return this.linkService;\n }\n\n /**\n * Get the current PDF document\n * @returns The current PDF document or null if none is loaded\n */\n getCurrentDocument(): any {\n return this.pdfDocumentSubject.value;\n }\n\n /**\n * Clear the current document and reset state\n * Used when switching between PDFs\n */\n clearDocument(): void {\n // console.log('Clearing PDF document from service...');\n \n // Reset all state to initial values\n this.pdfDocumentSubject.next(null);\n this.currentPageSubject.next(1);\n this.totalPagesSubject.next(0);\n this.zoomSubject.next(1);\n this.rotationSubject.next(0);\n \n // Clear link service\n this._pdfDocument = null;\n this._viewer = null;\n \n // console.log('PDF document cleared from service');\n }\n\n /**\n * Configures the PDF.js worker source automatically\n * This eliminates the need for users to manually copy worker files\n */\n private configureWorkerSource(): void {\n // First, check if workerSrc is already set\n if (pdfjsLib.GlobalWorkerOptions.workerSrc) {\n //console.log('Worker already set:', pdfjsLib.GlobalWorkerOptions.workerSrc);\n return;\n }\n \n // If workerSrc is provided in the config, use it\n if (this.configService.config.workerSrc) {\n //console.log(`Setting worker from config: ${this.configService.config.workerSrc}`);\n pdfjsLib.GlobalWorkerOptions.workerSrc = this.configService.config.workerSrc;\n return;\n }\n\n // Get the current PDF.js version\n const pdfVersion = pdfjsLib.version;\n \n // Detect major version to determine worker file name\n const majorVersion = parseInt(pdfVersion.split('.')[0]);\n \n // PDF.js v4+ uses .mjs files, v3 and below use .min.js\n const workerFile = majorVersion >= 4 ? 'pdf.worker.mjs' : 'pdf.worker.min.js';\n \n // CDN path to the worker file (using unpkg CDN)\n const cdnWorkerSrc = `https://unpkg.com/pdfjs-dist@${pdfVersion}/build/${workerFile}`;\n \n //console.log(`PDF.js version ${pdfVersion} detected (v${majorVersion})`); \n //console.log(`Using PDF.js worker from CDN: ${cdnWorkerSrc}`);\n pdfjsLib.GlobalWorkerOptions.workerSrc = cdnWorkerSrc;\n }\n\n /**\n * Loads a PDF document from a URL or binary data\n * @param src URL or binary data of the PDF\n * @returns Promise resolving to the loaded PDF document\n */\n async loadDocument(src: string | Uint8Array): Promise<any> {\n try {\n //console.log('PDF.js worker source:', pdfjsLib.GlobalWorkerOptions.workerSrc || 'NOT SET');\n //console.log(`Loading document from: ${typeof src === 'string' ? src : 'Binary data'}`);\n \n // Create a PDF loading task\n const loadingTask = pdfjsLib.getDocument(src);\n \n // Add progress tracking\n loadingTask.onProgress = (progressData: { loaded: number, total: number }) => {\n const progress = (progressData.loaded / progressData.total) * 100;\n //console.log(`Loading PDF: ${progress.toFixed(2)}%`);\n };\n \n // Wait for the document to load\n //console.log('Waiting for PDF document to load...');\n const pdfDocument = await loadingTask.promise;\n //console.log(`PDF document loaded with ${pdfDocument.numPages} pages`);\n \n // Update subjects with the loaded document info\n this.pdfDocumentSubject.next(pdfDocument);\n this.totalPagesSubject.next(pdfDocument.numPages);\n this.currentPageSubject.next(1); // Reset to first page\n \n // Configure link service with the document\n this.linkService.setDocument(pdfDocument);\n this.linkService.setViewer({\n scrollPageIntoView: ({ pageNumber }: { pageNumber: number }) => {\n this.setCurrentPage(pageNumber);\n }\n });\n \n return pdfDocument;\n } catch (error) {\n //console.error('Error loading PDF document:', error);\n throw error; // Re-throw to allow component to handle it\n }\n }\n\n /**\n * Sets the current page to display\n * @param pageNumber The page number to display (1-based index)\n */\n setCurrentPage(pageNumber: number): void {\n const totalPages = this.totalPagesSubject.value;\n // Ensure page number is within valid range\n if (pageNumber >= 1 && pageNumber <= totalPages) {\n this.currentPageSubject.next(pageNumber);\n }\n }\n\n /**\n * Navigate to the next page if available\n */\n nextPage(): void {\n const currentPage = this.currentPageSubject.value;\n const totalPages = this.totalPagesSubject.value;\n if (currentPage < totalPages) {\n this.currentPageSubject.next(currentPage + 1);\n }\n }\n\n /**\n * Navigate to the previous page if available\n */\n previousPage(): void {\n const currentPage = this.currentPageSubject.value;\n if (currentPage > 1) {\n this.currentPageSubject.next(currentPage - 1);\n }\n }\n\n /**\n * Set the zoom level for the PDF\n * @param zoom The zoom level (1 = 100%)\n */\n setZoom(zoom: number): void {\n this.zoomSubject.next(zoom);\n }\n\n /**\n * Increase zoom by 20%\n */\n zoomIn(): void {\n const currentZoom = this.zoomSubject.value;\n this.zoomSubject.next(currentZoom * 1.2);\n }\n\n /**\n * Decrease zoom by 20%\n */\n zoomOut(): void {\n const currentZoom = this.zoomSubject.value;\n this.zoomSubject.next(currentZoom / 1.2);\n }\n\n /**\n * Rotate the PDF by a specified number of degrees\n * @param degrees The degrees to rotate (positive = clockwise, negative = counterclockwise)\n */\n rotate(degrees: number): void {\n const currentRotation = this.rotationSubject.value;\n // Calculate new rotation and keep it within 0-359 degrees\n let newRotation = (currentRotation + degrees) % 360;\n if (newRotation < 0) {\n newRotation += 360;\n }\n this.rotationSubject.next(newRotation);\n }\n\n /**\n * Get the document outline (bookmarks)\n * @returns Promise resolving to the outline structure or empty array\n */\n getOutline(): Promise<any[]> {\n const pdfDocument = this.pdfDocumentSubject.value;\n if (!pdfDocument) {\n return Promise.resolve([]);\n }\n // Get outline or return empty array if not available\n return pdfDocument.getOutline() || Promise.resolve([]);\n }\n\n /**\n * Generate a thumbnail for a specific page\n * @param pageNumber The page number to generate thumbnail for\n * @param scale The scale for the thumbnail (smaller = faster)\n * @returns Promise resolving to data URL of the thumbnail\n */\n async generateThumbnail(pageNumber: number, scale: number = 0.2): Promise<string> {\n const pdfDocument = this.pdfDocumentSubject.value;\n if (!pdfDocument) {\n return '';\n }\n\n try {\n // Get the page object from PDF document\n const page = await pdfDocument.getPage(pageNumber);\n // Create a viewport with the specified scale (smaller for thumbnails)\n const viewport = page.getViewport({ scale });\n \n // Create an off-screen canvas for rendering\n const canvas = document.createElement('canvas');\n const context = canvas.getContext('2d');\n canvas.height = viewport.height;\n canvas.width = viewport.width;\n \n // Render the page to the canvas\n await page.render({\n canvasContext: context,\n viewport\n }).promise;\n \n // Convert canvas to data URL\n return canvas.toDataURL();\n } catch (error) {\n //console.error('Error generating thumbnail:', error);\n return '';\n }\n }\n\n /**\n * Search for text in the PDF document\n * @param text The text to search for\n * @returns Promise resolving to an array of search results\n */\n async search(text: string): Promise<any[]> {\n const pdfDocument = this.pdfDocumentSubject.value;\n if (!pdfDocument) {\n //console.warn('No PDF document loaded');\n return [];\n }\n\n const results: any[] = [];\n const totalPages = pdfDocument.numPages;\n\n // Search through each page\n for (let pageNum = 1; pageNum <= totalPages; pageNum++) {\n try {\n const page = await pdfDocument.getPage(pageNum);\n const textContent = await page.getTextContent();\n const textItems = textContent.items;\n\n // Search through text items on the page\n for (let i = 0; i < textItems.length; i++) {\n const item = textItems[i];\n if (item.str.toLowerCase().includes(text.toLowerCase())) {\n results.push({\n pageNumber: pageNum,\n text: item.str,\n transform: item.transform,\n width: item.width,\n height: item.height\n });\n }\n }\n } catch (error) {\n //console.error(`Error searching page ${pageNum}:`, error);\n }\n }\n\n return results;\n }\n\n /**\n * Download the PDF document\n */\n async downloadPdf(): Promise<void> {\n const pdfDocument = this.pdfDocumentSubject.value;\n if (!pdfDocument) {\n return;\n }\n \n try {\n // Get the binary data of the PDF\n const url = pdfDocument.getData ? await pdfDocument.getData() : null;\n \n if (url) {\n // Create a blob from binary data\n const blob = new Blob([url], { type: 'application/pdf' });\n const blobUrl = URL.createObjectURL(blob);\n \n // Create a temporary link element to trigger download\n const link = document.createElement('a');\n link.href = blobUrl;\n \n // Try to get filename from PDF metadata or use default\n let filename = 'document.pdf';\n try {\n const metadata = await pdfDocument.getMetadata();\n if (metadata.info && metadata.info.Title) {\n filename = `${metadata.info.Title}.pdf`;\n }\n } catch (error) {\n //console.error('Error getting PDF metadata:', error);\n }\n \n // Set download attribute and click the link\n link.download = filename;\n link.style.display = 'none';\n document.body.appendChild(link);\n link.click();\n \n // Clean up DOM and revoke blob URL\n document.body.removeChild(link);\n URL.revokeObjectURL(blobUrl);\n } else {\n //console.error('Unable to download: PDF data not available');\n }\n } catch (error) {\n //console.error('Error downloading PDF:', error);\n }\n }\n\n /**\n * Print the PDF document\n */\n async printPdf(): Promise<void> {\n const pdfDocument = this.pdfDocumentSubject.value;\n if (!pdfDocument) {\n return;\n }\n \n try {\n // Create hidden iframe to load PDF for printing\n const printIframe = document.createElement('iframe');\n printIframe.style.position = 'absolute';\n printIframe.style.top = '-1000px';\n printIframe.style.left = '-1000px';\n printIframe.style.width = '0';\n printIframe.style.height = '0';\n document.body.appendChild(printIframe);\n \n // Get PDF data and create a blob URL\n const data = await pdfDocument.getData();\n const blob = new Blob([data], { type: 'application/pdf' });\n const blobUrl = URL.createObjectURL(blob);\n \n // Load PDF into iframe\n printIframe.src = blobUrl;\n \n // Once iframe is loaded, trigger print dialog\n printIframe.onload = () => {\n try {\n if (printIframe.contentWindow) {\n // Focus and print the iframe content\n printIframe.contentWindow.focus();\n printIframe.contentWindow.print();\n }\n } catch (error) {\n //console.error('Error printing PDF:', error);\n \n // Fallback: open in new tab for user to print\n window.open(blobUrl, '_blank');\n } finally {\n // Clean up resources (after delay to allow for printing)\n setTimeout(() => {\n document.body.removeChild(printIframe);\n URL.revokeObjectURL(blobUrl);\n }, 1000);\n }\n };\n } catch (error) {\n //console.error('Error setting up PDF print:', error);\n }\n }\n}","import { Component, Input, Output, EventEmitter } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { FormsModule } from '@angular/forms';\n\n/**\n * Component for PDF controls (navigation, zoom, etc.)\n */\n@Component({\n selector: 'ng-pdf-controls',\n standalone: true, // Modern Angular standalone component\n imports: [CommonModule, FormsModule], // Import dependencies\n template: `\n <div class=\"pdf-controls\">\n <!-- Page navigation controls -->\n <div class=\"pdf-navigation\" *ngIf=\"showNavigation\">\n <button (click)=\"onFirstPage()\" [disabled]=\"currentPage <= 1\">First</button>\n <button (click)=\"onPreviousPage()\" [disabled]=\"currentPage <= 1\">Previous</button>\n <span class=\"page-info\">\n <!-- Page input with two-way binding -->\n <input type=\"number\" [ngModel]=\"currentPage\" (ngModelChange)=\"onPageInputChange($event)\" min=\"1\" [max]=\"totalPages\">\n / {{ totalPages }}\n </span>\n <button (click)=\"onNextPage()\" [disabled]=\"currentPage >= totalPages\">Next</button>\n <button (click)=\"onLastPage()\" [disabled]=\"currentPage >= totalPages\">Last</button>\n </div>\n \n <!-- Zoom controls -->\n <div class=\"pdf-zoom\" *ngIf=\"showZoomControls\">\n <button (click)=\"onZoomOut()\">-</button>\n <span>{{ (zoom * 100).toFixed(0) }}%</span>\n <button (click)=\"onZoomIn()\">+</button>\n <select [ngModel]=\"zoom\" (ngModelChange)=\"onZoomSelect($event)\">\n <option [value]=\"0.5\">50%</option>\n <option [value]=\"0.75\">75%</option>\n <option [value]=\"1\">100%</option>\n <option [value]=\"1.25\">125%</option>\n <option [value]=\"1.5\">150%</option>\n <option [value]=\"2\">200%</option>\n </select>\n </div>\n \n <!-- Rotation controls -->\n <div class=\"pdf-rotation\" *ngIf=\"showRotationControls\">\n <button (click)=\"onRotateLeft()\">โบ</button>\n <button (click)=\"onRotateRight()\">โป</button>\n </div>\n \n <!-- Action buttons -->\n <div class=\"pdf-actions\">\n <button *ngIf=\"showDownloadButton\" (click)=\"onDownload()\">Download</button>\n <button *ngIf=\"showPrintButton\" (click)=\"onPrint()\">Print</button>\n </div>\n \n <!-- Search functionality -->\n <div class=\"pdf-search\" *ngIf=\"showSearchBar\">\n <input type=\"text\" placeholder=\"Search...\" #searchInput>\n <button (click)=\"onSearch(searchInput.value)\">Search</button>\n </div>\n </div>\n `,\n styles: [`\n /* Control bar container */\n .pdf-controls {\n display: flex;\n padding: 8px;\n border-bottom: 1px solid #ddd;\n flex-wrap: wrap;\n gap: 10px;\n }\n \n /* Control groups */\n .pdf-navigation, .pdf-zoom, .pdf-rotation, .pdf-actions, .pdf-search {\n display: flex;\n align-items: center;\n gap: 5px;\n }\n \n /* Button styling */\n button {\n padding: 4px 8px;\n background: #f0f0f0;\n border: 1px solid #ccc;\n border-radius: 3px;\n cursor: pointer;\n }\n \n button:hover {\n background: #e0e0e0;\n }\n \n button:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n }\n \n /* Form control styling */\n input[type=\"number\"], input[type=\"text\"] {\n width: 50px;\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 3px;\n }\n \n input[type=\"text\"] {\n width: 150px;\n }\n \n select {\n padding: 4px;\n border: 1px solid #ccc;\n border-radius: 3px;\n }\n `]\n})\nexport class PdfControlsComponent {\n // Input properties for control configuration\n @Input() currentPage: number = 1; // Current page being displayed\n @Input() totalPages: number = 0; // Total pages in document\n @Input() zoom: number = 1; // Current zoom level\n @Input() rotation: number = 0; // Current rotation in degrees\n \n // Control visibility options\n @Input() showNavigation: boolean = true; // Show page navigation\n @Input() showZoomControls: boolean = true; // Show zoom controls\n @Input() showRotationControls: boolean = true; // Show rotation controls\n @Input() showDownloadButton: boolean = true; // Show download button\n @Input() showPrintButton: boolean = true; // Show print button\n @Input() showSearchBar: boolean = true; // Show search functionality\n @Input() showThumbnails: boolean = false; // Show thumbnails panel\n @Input() showOutline: boolean = false; // Show outline/bookmarks panel\n \n // Output events\n @Output() pageChange = new EventEmitter<number>(); // Page changed\n @Output() zoomChange = new EventEmitter<number>(); // Zoom changed\n @Output() rotationChange = new EventEmitter<number>(); // Rotation changed\n @Output() download = new EventEmitter<void>(); // Download requested\n @Output() print = new EventEmitter<void>(); // Print requested\n @Output() search = new EventEmitter<string>(); // Search requested\n @Output() toggleThumbnails = new EventEmitter<boolean>(); // Toggle thumbnails\n @Output() toggleOutline = new EventEmitter<boolean>(); // Toggle outline\n \n /**\n * Navigate to first page\n */\n onFirstPage(): void {\n this.pageChange.emit(1);\n }\n \n /**\n * Navigate to previous page\n */\n onPreviousPage(): void {\n if (this.currentPage > 1) {\n this.pageChange.emit(this.currentPage - 1);\n }\n }\n \n /**\n * Navigate to next page\n */\n onNextPage(): void {\n if (this.currentPage < this.totalPages) {\n this.pageChange.emit(this.currentPage + 1);\n }\n }\n \n /**\n * Navigate to last page\n */\n onLastPage(): void {\n this.pageChange.emit(this.totalPages);\n }\n \n /**\n * Handle direct page number input\n * @param page The new page number\n */\n onPageInputChange(page: number): void {\n if (page >= 1 && page <= this.totalPages) {\n this.pageChange.emit(page);\n }\n }\n \n /**\n * Increase zoom by 20%\n */\n onZoomIn(): void {\n this.zoomChange.emit(this.zoom * 1.2);\n }\n \n /**\n * Decrease zoom by 20%\n */\n onZoomOut(): void {\n this.zoomChange.emit(this.zoom / 1.2);\n }\n \n /**\n * Handle zoom dropdown selection\n * @param zoom The selected zoom level\n */\n onZoomSelect(zoom: number): void {\n this.zoomChange.emit(parseFloat(zoom.toString()));\n }\n \n /**\n * Rotate counterclockwise by 90 degrees\n */\n onRotateLeft(): void {\n this.rotationChange.emit(-90);\n }\n \n /**\n * Rotate clockwise by 90 degrees\n */\n onRotateRight(): void {\n this.rotationChange.emit(90);\n }\n \n /**\n * Trigger document download\n */\n onDownload(): void {\n this.download.emit();\n }\n \n /**\n * Trigger document printing\n */\n onPrint(): void {\n this.print.emit();\n }\n \n /**\n * Execute search if text is provided\n * @param text The text to search for\n */\n onSearch(text: string): void {\n if (text.trim()) {\n this.search.emit(text);\n }\n }\n}","import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, OnChanges, SimpleChanges, ElementRef, ViewChild, inject, signal } from '@angular/core';\nimport { CommonModule } from '@angular/common';\nimport { Subject, lastValueFrom, takeUntil } from 'rxjs';\n\nimport { PdfService } from '../services/pdf.service';\nimport { PdfControlsComponent } from './pdf-controls.component';\nimport { PdfOptions, PdfError } from '../models/pdf-options.model';\n\n// Import PDF.js\nimport * as pdfjsLib from 'pdfjs-dist';\n// REMOVED: import 'pdfjs-dist/web/pdf_viewer.css'; - This is handled in the CSS file\n\n/**\n * Main component for rendering PDFs\n * Uses modern Angular patterns including standalone components and signals\n */\n@Component({\n selector: 'ng-pdf-viewer',\n standalone: true, // Modern Angular standalone component (no NgModule needed)\n imports: [CommonModule, PdfControlsComponent], // Import dependencies\n template: `\n <!-- Main container with configurable dimensions -->\n <div class=\"pdf-container\" [style.width]=\"options?.width || '100%'\" [style.height]=\"options?.height || '500px'\">\n <!-- Controls bar - conditionally shown based on options -->\n <ng-pdf-controls \n *ngIf=\"options?.showControls === true\"\n [currentPage]=\"currentPage()\"\n [totalPages]=\"totalPages()\"\n [zoom]=\"zoom()\"\n [rotation]=\"rotation()\"\n [showNavigation]=\"options?.showNavigation !== false\"\n [showZoomControls]=\"options?.showZoomControls !== false\"\n [showRotationControls]=\"options?.showRotationControls !== false\"\n [showDownloadButton]=\"options?.showDownloadButton !== false\"\n [showPrintButton]=\"options?.showPrintButton !== false\"\n [showSearchBar]=\"options?.showSearchBar !== false\"\n [showThumbnails]=\"options?.showThumbnails !== false\"\n [showOutline]=\"options?.showOutline !== false\"\n (pageChange)=\"onPageChange($event)\"\n (zoomChange)=\"onZoomChange($event)\"\n (rotationChange)=\"onRotationChange($event)\"\n (download)=\"onDownload()\"\n (print)=\"onPrint()\"\n (search)=\"onSearch($event)\">\n </ng-pdf-controls>\n \n <!-- Main PDF viewing area -->\n <div class=\"pdf-viewer\">\n <!-- Loading indicator -->\n <div class=\"pdf-loading\" *ngIf=\"loading()\">Loading...</div>\n \n <!-- Error message display - ENHANCED -->\n <div class=\"pdf-error-overlay\" *ngIf=\"error()\">\n <div class=\"error-content\">\n <div class=\"error-icon\">๐</div>\n <div class=\"error-title\">Failed to Load PDF</div>\n <div class=\"error-message\">{{ error() }}</div>\n <div class=\"error-suggestion\">Please try a different PDF file.</div>\n </div>\n </div>\n \n <!-- PDF content container with rotation transform -->\n <div class=\"pdf-content\" [style.transform]=\"'rotate(' + rotation() + 'deg)'\">\n <!-- Canvas where PDF will be rendered -->\n <div #canvasContainer [style.display]=\"loading() || error() ? 'none' : 'block'\"></div>\n </div>\n </div>\n \n <!-- Optional thumbnails panel -->\n <div class=\"pdf-thumbnails\" *ngIf=\"options?.showThumbnails\">\n <!-- Thumbnails will be implemented here -->\n </div>\n \n <!-- Optional outline/bookmarks panel -->\n <div class=\"pdf-outline\" *ngIf=\"options?.showOutline\">\n <!-- Outline will be implemented here -->\n </div>\n </div>\n `,\n styles: [`\n /* Container styling */\n .pdf-container {\n display: flex;\n flex-direction: column;\n border: 1px solid #ddd;\n overflow: hidden;\n height: 100%;\n }\n \n /* PDF viewer area */\n .pdf-viewer {\n flex: 1;\n overflow: auto;\n position: relative;\n background-color: #f5f5f5;\n }\n \n /* Content container with transition for smooth rotation */\n .pdf-content {\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n transition: transform 0.3s ease;\n width: 100%;\n min-height: 100%;\n padding: 20px;\n box-sizing: border-box;\n overflow-x: auto; /* Allow horizontal scrolling if needed */\n }\n \n /* Loading and error message styling */\n .pdf-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n height: 100%;\n width: 100%;\n position: absolute;\n top: 0;\n left: 0;\n background-color: rgba(255, 255, 255, 0.9);\n z-index: 1000;\n }\n \n .pdf-error-overlay {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: #f8f9fa;\n z-index: 1000;\n }\n \n .error-content {\n text-align: center;\n padding: 40px;\n max-width: 400px;\n }\n \n .error-icon {\n font-size: 64px;\n margin-bottom: 20px;\n opacity: 0.5;\n }\n \n .error-title {\n font-size: 24px;\n font-weight: 600;\n color: #dc3545;\n margin-bottom: 12px;\n }\n \n .error-message {\n font-size: 16px;\n color: #6c757d;\n margin-bottom: 8px;\n }\n \n .error-suggestion {\n font-size: 14px;\n color: #6c757d;\n font-style: italic;\n }\n \n /* Canvas and Page styling */\n ::ng-deep .pdf-page {\n position: relative;\n margin: 10px 0;\n }\n \n ::ng-deep .pdf-page canvas {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1;\n }\n \n /* Annotation layer styling */\n ::ng-deep .annotationLayer {\n position: absolute;\n left: 0;\n top: 0;\n right: 0;\n bottom: 0;\n overflow: hidden;\n z-index: 3;\n }\n \n ::ng-deep .annotationLayer section {\n position: absolute;\n }\n \n ::ng-deep .annotationLayer .linkAnnotation > a {\n position: absolute;\n font-size: 1em;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background: rgba(0, 0, 0, 0.05);\n cursor: pointer;\n z-index: 3;\n }\n \n ::ng-deep .annotationLayer .buttonWidgetAnnotation.pushButton > a {\n background-color: #0066ff;\n background-clip: padding-box;\n border: 2px solid #000;\n border-radius: 6px;\n color: white;\n display: inline-block;\n padding: 4px 8px;\n cursor: pointer;\n position: relative;\n text-decoration: none;\n }\n\n /* ENHANCED Text layer styling - CRITICAL for proper alignment and interaction */\n ::ng-deep .pdf-page .textLayer,\n ::ng-deep div.textLayer {\n position: absolute !important;\n text-align: initial !important;\n left: 0 !important;\n top: 0 !important;\n right: 0 !important;\n bottom: 0 !important;\n overflow: hidden !important;\n /* Production settings - text invisible but selectable */\n opacity: 0.25 !important;\n line-height: 1 !important;\n -webkit-text-size-adjust: none !important;\n -moz-text-size-adjust: none !important;\n -ms-text-size-adjust: none !important;\n text-size-adjust: none !important;\n forced-color-adjust: none !important;\n transform-origin: 0 0 !important;\n /* MAXIMUM z-index to override PDF.js defaults */\n z-index: 10 !important;\n /* CRITICAL: Ensure text layer receives pointer events */\n pointer-events: auto !important;\n }\n\n ::ng-deep .textLayer span,\n ::ng-deep .textLayer br {\n /* Production settings - make text transparent */\n color: transparent !important;\n position: absolute !important;\n white-space: pre !important;\n cursor: text !important;\n transform-origin: 0% 0% !important;\n /* Ensure spans receive pointer events */\n pointer-events: auto !important;\n /* Ensure spans stay on top */\n z-index: 10 !important;\n }\n\n /* Enhanced text selection styling - CRITICAL for visible selection */\n ::ng-deep .textLayer ::selection {\n background: rgba(0, 100, 255, 0.3) !important;\n color: rgba(0, 100, 255, 0.3) !important;\n }\n\n ::ng-deep .textLayer ::-moz-selection {\n background: rgba(0, 100, 255, 0.3) !important;\n color: rgba(0, 100, 255, 0.3) !important;\n }\n \n /* Additional selection fallbacks */\n ::ng-deep .textLayer span::selection {\n background: rgba(0, 100, 255, 0.3) !important;\n }\n \n ::ng-deep .textLayer span::-moz-selection {\n background: rgba(0, 100, 255, 0.3) !important;\n }\n\n /* Ensure text layer is properly sized */\n ::ng-deep .textLayer .endOfContent {\n display: block;\n position: absolute;\n left: 0;\n top: 100%;\n right: 0;\n bottom: 0;\n z-index: -1;\n cursor: default;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n\n ::ng-deep .textLayer .highlight {\n margin: -1px;\n padding: 1px;\n background-color: rgba(180, 0, 170, 0.4);\n border-radius: 4px;\n }\n\n ::ng-deep .textLayer .highlight.selected {\n background-color: rgba(0, 100, 0, 0.4);\n }\n `]\n})\nexport class PdfViewerComponent implements OnInit, OnDestroy, OnChanges {\n // Input properties\n @Input() src!: string | Uint8Array; // Source URL or binary data for the PDF\n @Input() options?: PdfOptions; // Configuration options\n \n // Output events\n @Output() pageChange = new EventEmitter<number>(); // Emitted when page changes\n @Output() documentLoaded = new EventEmitter<any>(); // Emitted when document loads\n @Output() loadingStateChange = new EventEmitter<boolean>(); // Emitted when loading state changes\n @Output() errorOccurred = new EventEmitter<PdfError>(); // Emitted when any error occurs\n \n // Legacy events (deprecated - use errorOccurred instead)\n @Output() documentLoadError = new EventEmitter<any>(); // Emitted on load error\n @Output() errorStateChange = new EventEmitter<string | null>(); // Emitted when error state changes\n \n // Reference to the canvas container\n @ViewChild('canvasContainer', { static: true }) canvasContainer!: ElementRef<HTMLDivElement>;\n \n // Service injection using modern inject function\n private pdfService = inject(PdfService);\n \n // Subject for handling unsubscription on component destroy\n private destroy$ = new Subject<void>();\n \n // Component state using signals (reactive primitive in modern Angular)\n currentPage = signal<number>(1); // Current page number\n totalPages = signal<number>(0); // Total pages in document\n zoom = signal<number>(1); // Current zoom level\n rotation = signal<number>(0); // Current rotation in degrees\n loading = signal<boolean>(false); // Loading state\n error = signal<string | null>(null); // Error message if any\n \n // Keep track of current render task to cancel if needed\n private currentRenderTask: any = null;\n \n /**\n * Set loading state and emit event\n */\n private setLoadingState(loading: boolean): void {\n this.loading.set(loading);\n this.loadingStateChange.emit(loading);\n }\n \n /**\n * Set error state and emit event\n */\n private setErrorState(error: string | null): void {\n this.error.set(error);\n this.errorStateChange.emit(error);\n }\n \n /**\n * Handle input changes - CRITICAL for src changes\n */\n ngOnChanges(changes: SimpleChanges): void {\n // Check if src has changed\n if (changes['src']) {\n const currentSrc = changes['src'].currentValue;\n const previousSrc = changes['src'].previousValue;\n \n // Only reload if src actually changed and is not the first change\n if (!changes['src'].firstChange && currentSrc !== previousSrc) {\n // console.log('PDF src changed, cleaning up and reloading...');\n this.cleanupAndReload();\n }\n }\n }\n\n /**\n * Initialize the component\n */\n ngOnInit(): void {\n \n \n // Apply initial options if provided\n if (this.options?.initialZoom) {\n \n this.zoom.set(this.options.initialZoom);\n this.pdfService.setZoom(this.options.initialZoom);\n } else {\n // Default to 1.0 (100%) - let autoFit handle the scaling if needed\n this.zoom.set(1.0);\n this.pdfService.setZoom(1.0);\n }\n \n if (this.options?.initialPage) {\n \n this.currentPage.set(this.options.initialPage);\n this.pdfService.setCurrentPage(this.options.initialPage);\n }\n \n // Subscribe to service observables and update component state\n // The takeUntil operator automatically unsubscribes when destroy$ emits\n this.pdfService.currentPage$.pipe(takeUntil(this.destroy$))\n .subscribe(page => {\n \n this.currentPage.set(page);\n this.pageChange.emit(page);\n });\n \n this.pdfService.totalPages$.pipe(takeUntil(this.destroy$))\n .subscribe(totalPages => {\n \n this.totalPages.set(totalPages);\n });\n \n this.pdfService.zoom$.pipe(takeUntil(this.destroy$))\n .subscribe(zoom => {\n \n this.zoom.set(zoom);\n // Re-render all pages when zoom changes\n if (this.pdfService.getCurrentDocument()) {\n this.renderAllPages();\n }\n });\n \n this.pdfService.rotation$.pipe(takeUntil(this.destroy$))\n .subscribe(rotation => {\n \n this.rotation.set(rotation);\n // Re-render all pages when rotation changes\n if (this.pdfService.getCurrentDocument()) {\n this.renderAllPages();\n }\n });\n \n // Load the document\n this.loadDocument();\n }\n \n /**\n * Perform complete cleanup of all PDF rendering artifacts\n */\n private async performCompleteCleanup(): Promise<void> {\n // console.log('๐งน Performing REAL cleanup - not just covering up...');\n \n // 1. Cancel any ongoing render tasks\n if (this.currentRenderTask) {\n try {\n await this.currentRenderTask.cancel();\n // console.log('Cancelled render task');\n } catch (e) {\n // console.log('Error cancelling render task:', e);\n }\n this.currentRenderTask = null;\n }\n \n // 2. ACTUALLY REMOVE ALL PDF ARTIFACTS - not just cover them\n const container = this.canvasContainer?.nativeElement;\n const pdfContentDiv = container?.parentElement; // Get the pdf-content div\n const pdfViewerDiv = pdfContentDiv?.parentElement; // Get the pdf-viewer div\n \n // console.log('Cleaning multiple container levels...');\n \n if (container) {\n // console.log('Container contents before cleanup:', container.innerHTML.length);\n \n // Find and remove all PDF-related elements in ALL relevant containers\n const allContainers = [container, pdfContentDiv, pdfViewerDiv].filter(Boolean);\n let totalArtifacts = { pdfPages: 0, canvases: 0, textLayers: 0, annotationLayers: 0 };\n \n allContainers.forEach((cont, index) => {\n if (cont) {\n const pdfPages = cont.querySelectorAll('.pdf-page');\n const canvases = cont.querySelectorAll('canvas');\n const textLayers = cont.querySelectorAll('.textLayer');\n const annotationLayers = cont.querySelectorAll('.annotationLayer');\n \n // console.log(`Level ${index} artifacts:`, {\n // pdfPages: pdfPages.length,\n // canvases: canvases.length,\n // textLayers: textLayers.length,\n // annotationLayers: annotationLayers.length\n // });\n \n totalArtifacts.pdfPages += pdfPages.length;\n totalArtifacts.canvases += canvases.length;\n totalArtifacts.textLayers += textLayers.length;\n totalArtifacts.annotationLayers += annotationLayers.length;\n \n // Remove each type of element\n pdfPages.forEach(el => el.remove());\n canvases.forEach(el => el.remove());\n textLayers.forEach(el => el.remove());\n annotationLayers.forEach(el => el.remove());\n }\n });\n \n // console.log('Total artifacts found and removed:', totalArtifacts);\n \n // Force clear the main container\n container.innerHTML = '';\n \n // console.log('Container contents after cleanup:', container.innerHTML.length);\n \n // Reset container to clean state\n container.className = '';\n container.style.cssText = '';\n }\n \n // 3. Reset component state AND force controls to update\n this.totalPages.set(0);\n this.currentPage.set(1);\n this.zoom.set(1);\n this.rotation.set(0);\n \n // 4. Clear service state\n this.pdfService.clearDocument();\n \n // 5. Force component template to re-render by clearing error/loading states\n this.loading.set(false);\n this.error.set(null);\n \n // 5. Force Angular change detection\n await new Promise(resolve => setTimeout(resolve, 100));\n \n // console.log('โ
REAL cleanup finished - no artifacts should remain');\n }\n\n /**\n * Clean up and reload PDF when src changes\n */\n private async cleanupAndReload(): Promise<void> {\n // console.log('Starting REAL cleanup and reload process...');\n \n // 1. ACTUALLY REMOVE all PDF content from ALL levels\n const container = this.canvasContainer?.nativeElement;\n const pdfContentDiv = container?.parentElement;\n const pdfViewerDiv = pdfContentDiv?.parentElement;\n \n // console.log('Multi-level cleanup before reload...');\n \n if (container) {\n // console.log('Container before cleanup:', container.innerHTML.length);\n \n // Remove all PDF-specific elements from all container levels\n const allContainers = [container, pdfContentDiv, pdfViewerDiv].filter(Boolean);\n \n allContainers.forEach((cont, index) => {\n if (cont) {\n const pdfPages = cont.querySelectorAll('.pdf-page');\n const canvases = cont.querySelectorAll('canvas');\n const textLayers = cont.querySelectorAll('.textLayer');\n const annotationLayers = cont.querySelectorAll('.annotationLayer');\n \n // console.log(`Cleanup level ${index}:`, {\n // pdfPages: pdfPages.length,\n // canvases: canvases.length,\n // textLayers: textLayers.length,\n // annotationLayers: annotationLayers.length\n // });\n \n pdfPages.forEach(el => el.remove());\n canvases.forEach(el => el.remove());\n textLayers.forEach(el => el.remove());\n annotationLayers.forEach(el => el.remove());\n }\n });\n \n // Force clear everything\n container.innerHTML = '';\n container.className = '';\n container.style.cssText = '';\n \n // console.log('Container after cleanup:', container.innerHTML.length);\n }\n \n // 2. Cancel any pending render tasks\n if (this.currentRenderTask) {\n try {\n await this.currentRenderTask.cancel();\n // console.log('Cancelled previous render task');\n } catch (e) {\n // console.log('Error cancelling render task:', e);\n }\n this.currentRenderTask = null;\n }\n \n // 3. Reset component state\n this.setLoadingState(true);\n this.setErrorState(null);\n \n // 4. Clear service state\n this.pdfService.clearDocument();\n \n // 5. Small delay to ensure cleanup is complete\n await new Promise(resolve => setTimeout(resolve, 50));\n \n // 6. Load new document\n await this.loadDocument();\n \n // console.log('REAL cleanup and reload completed');\n }\n\n /**\n * Clean up subscriptions on component destruction\n */\n ngOnDestroy(): void {\n // Cancel any pending render tasks\n if (this.currentRenderTask) {\n try {\n this.currentRenderTask.cancel();\n } catch (e) {\n //console.log('Error cancelling render task during destroy:', e);\n }\n }\n \n // Complete the destroy subject to unsubscribe from all observables\n this.destroy$.next();\n this.destroy$.complete();\n }\n \n /**\n * Load the PDF document\n */\n private async loadDocument(): Promise<void> {\n if (!this.src) {\n this.error.set('No PDF source provided');\n return;\n }\n \n // Ensure container is clear before starting\n const container = this.canvasContainer?.nativeElement;\n if (container) {\n container.innerHTML = '';\n }\n \n this.loading.set(true);\n this.error.set(null);\n \n // console.log(`Loading PDF from source: ${typeof this.src === 'string' ? this.src : 'Binary data'}`);\n \n try {\n // Use service to load the document\n const pdfDocument = await this.pdfService.loadDocument(this.src);\n // console.log('PDF document loaded successfully!', pdfDocument);\n this.documentLoaded.emit(pdfDocument);\n \n // Set total pages ONLY on successful load\n this.totalPages.set(pdfDocument.numPages);\n // console.log(`Total pages: ${pdfDocument.numPages}`);\n \n // Set current page to 1 or initialPage ONLY on successful load\n const initialPage = this.options?.initialPage || 1;\n this.currentPage.set(initialPage);\n this.pdfService.setCurrentPage(initialPage);\n \n // Small delay to ensure DOM is ready\n await new Promise(resolve => setTimeout(resolve, 10));\n \n // Render all pages in continuous mode\n await this.renderAllPages();\n } catch (err: any) {\n // console.error('Error loading PDF:', err);\n \n // Categorize and handle the error\n const errorType = this.categorizeError(err);\n this.handlePdfError(errorType, err, { action: 'loadDocument' });\n \n // CRITICAL: Complete cleanup on load failure\n await this.performCompleteCleanup();\n } finally {\n this.loading.set(false);\n }\n }\n \n /**\n * Render a specific page of the PDF\n * @param pageNumber The page number to render\n */\n private async renderPage(pageNumber: number): Promise<void> {\n // Ensure there's a page number\n if (!pageNumber) {\n pageNumber = 1;\n }\n \n //console.log(`Attempting to render page ${pageNumber}`);\n \n // Cancel any ongoing render task\n if (this.currentRenderTask) {\n //console.log('Cancelling previous render task');\n try {\n await this.currentRenderTask.cancel();\n } catch (e) {\n //console.log('Error cancelling previous render task:', e);\n }\n this.currentRenderTask = null;\n }\n \n try {\n // Get the document directly from the service\n const pdfDocument = this.pdfService.getCurrentDocument();\n \n if (!pdfDocument) {\n //console.error('No PDF document available');\n return;\n }\n \n //console.log(`PDF document has ${pdfDocument.numPages} pages`);\n \n // Get the page from the document\n const page = await pdfDocument.getPage(pageNumber);\n //console.log('Page object retrieved:', page !== null);\n \n // Calculate scale to fit the canvas\n const scale = this.zoom();\n \n // Set up viewport based on current zoom and rotation\n const viewport = page.getViewport({ \n scale: scale, \n rotation: this.rotation() \n });\n \n // Clear the canvas container\n const container = this.canvasContainer?.nativeElement;\n if (!container) {\n // console.error('Canvas container not available');\n return;\n }\n container.innerHTML = '';\n \n // Create a new canvas element for this render operation\n const canvas = document.createElement('canvas');\n \n // Apply device pixel ratio for sharper rendering on high-DPI displays\n const pixelRatio = window.devicePixelRatio || 1;\n \n // Scale canvas by pixel ratio for sharper rendering\n const scaledWidth = Math.floor(viewport.width * pixelRatio);\n const scaledHeight = Math.floor(viewport.height * pixelRatio);\n \n // Set canvas dimensions with pixel ratio factored in\n canvas.width = scaledWidth;\n canvas.height = scaledHeight;\n \n // Set display size through CSS (original size)\n canvas.style.width = Math.floor(viewport.width) + 'px';\n canvas.style.height = Math.floor(viewport.height) + 'px';\n \n container.appendChild(canvas);\n \n // Get the canvas context\n const context = canvas.getContext('2d');\n \n if (!context) {\n //console.error('Canvas rendering context not available');\n this.error.set('Canvas rendering context not available');\n return;\n }\n \n // Scale the context to account for the device pixel ratio\n context.scale(pixelRatio, pixelRatio);\n \n //console.log(`Rendering with viewport: ${viewport.width}x${viewport.height}, scale: ${scale}, pixel ratio: ${pixelRatio}`);\n \n // Render the page to the canvas\n const renderContext = {\n canvasContext: context,\n viewport: viewport\n };\n \n //console.log('Starting page rendering...');\n // Store the render task for potential cancellation\n this.currentRenderTask = page.render(renderContext);\n \n // Wait for rendering to complete\n await this.currentRenderTask.promise;\n //console.log('Page rendered successfully');\n this.currentRenderTask = null;\n } catch (err: any) {\n // Check if this is a cancellation error, which i