UNPKG

ng2-pdfjs-viewer

Version:

The most comprehensive Angular PDF viewer, powered by Mozilla PDF.js 6 — view, annotate, sign, fill forms, search, and read aloud from one component. 8.3M+ downloads, mobile-first, production-ready.

3,179 lines 145 kB
import * as i0 from '@angular/core';
import { EventEmitter, isDevMode, Input, Output, ViewChild, Component, NgModule } from '@angular/core';
import * as i1 from '@angular/common';
import { CommonModule } from '@angular/common';
import { PdfAiAssistant } from 'ng2-pdfjs-viewer/ai';
export { PdfAiAssistant } from 'ng2-pdfjs-viewer/ai';

// Single action queue with readiness-gated execution
class ActionQueueManager {
    // Bounded history for getActionStatus()/getQueueStatus(); prevents unbounded
    // growth in long-lived viewers (every dispatched action has a unique id).
    static MAX_EXECUTED_RESULTS = 100;
    actionQueue = [];
    executedActions = new Map();
    isDocumentLoaded = false;
    postMessageReadiness = 0;
    diagnosticLogs = false;
    postMessageExecutor;
    constructor(diagnosticLogs = false) {
        this.diagnosticLogs = diagnosticLogs;
    }
    setDiagnosticLogs(enabled) {
        this.diagnosticLogs = enabled;
    }
    updateReadiness(readiness) {
        this.postMessageReadiness = readiness;
    }
    queueAction(action, readinessLevel) {
        this.actionQueue.push({ action, readinessLevel });
    }
    onDocumentLoaded() {
        this.isDocumentLoaded = true;
        this.processQueuedActions();
    }
    // Execute every queued action whose readiness requirement is now met
    processQueuedActions() {
        const ready = [];
        this.actionQueue = this.actionQueue.filter((item) => {
            const canExecute = this.postMessageReadiness >= item.readinessLevel &&
                (item.readinessLevel < 5 || this.isDocumentLoaded);
            if (canExecute) {
                ready.push(item.action);
            }
            return !canExecute;
        });
        ready.forEach((action) => this.executeAction(action));
    }
    async executeAction(action) {
        const result = {
            actionId: action.id,
            success: false,
            timestamp: Date.now(),
        };
        try {
            const response = await this.executeActionViaPostMessage(action);
            result.success = true;
            // Query actions return a payload in the wrapper's control response
            if (response && typeof response === "object" && "data" in response) {
                result.data = response.data;
            }
            action.resolver?.(result);
        }
        catch (error) {
            result.error = error instanceof Error ? error.message : String(error);
            if (this.diagnosticLogs) {
                console.error(`ActionQueueManager: Error executing action ${action.action}:`, error);
            }
            // A requeued action retries later with the same id and resolver - don't
            // settle the caller's promise on the transient failure.
            if (action.requeued) {
                action.requeued = false;
            }
            else {
                action.resolver?.(result);
            }
        }
        this.recordResult(action.id, result);
        return result;
    }
    recordResult(id, result) {
        if (this.executedActions.size >= ActionQueueManager.MAX_EXECUTED_RESULTS &&
            !this.executedActions.has(id)) {
            const oldest = this.executedActions.keys().next().value;
            if (oldest !== undefined) {
                this.executedActions.delete(oldest);
            }
        }
        this.executedActions.set(id, result);
    }
    async executeActionViaPostMessage(action) {
        if (!this.postMessageExecutor) {
            throw new Error("PostMessage executor not set");
        }
        return await this.postMessageExecutor(action);
    }
    setPostMessageExecutor(executor) {
        this.postMessageExecutor = executor;
    }
    getActionStatus(actionId) {
        const result = this.executedActions.get(actionId);
        if (!result) {
            const inQueue = this.actionQueue.some((item) => item.action.id === actionId);
            return inQueue ? "pending" : "not-found";
        }
        return result.success ? "completed" : "failed";
    }
    // Settle document-gated (level-5) actions when a document load fails -
    // they can never execute against the failed document, and without this
    // consumer awaits (setAnnotations, search, getDocumentText, ...) hang
    // forever. Lower-level actions stay queued: the viewer itself is alive.
    failDocumentActions(reason) {
        this.actionQueue = this.actionQueue.filter((item) => {
            if (item.readinessLevel < 5) {
                return true;
            }
            item.action.resolver?.({
                actionId: item.action.id,
                success: false,
                error: reason,
                timestamp: Date.now(),
            });
            return false;
        });
    }
    // Drop queued actions (settling their callers' promises) and clear history
    clearQueues() {
        for (const item of this.actionQueue) {
            item.action.resolver?.({
                actionId: item.action.id,
                success: false,
                error: "Action discarded: queue cleared",
                timestamp: Date.now(),
            });
        }
        this.actionQueue = [];
        this.executedActions.clear();
    }
    // Full reset for a new document load (pdfSrc change / refresh)
    reset() {
        this.clearQueues();
        this.isDocumentLoaded = false;
        this.postMessageReadiness = 0;
    }
    getQueueStatus() {
        return {
            queuedActions: this.actionQueue.length,
            executedActions: this.executedActions.size,
        };
    }
}

// Property normalization between component inputs and PDF.js viewer values
// Mode/name lists shared by the to- and from-viewer transforms. PDF.js encodes
// scroll and spread modes as integer enums, so for those the array index is the
// enum value and the order here doubles as the numeric->name map (keep it in
// sync with PDF.js). Declaring each list once keeps the input whitelist and the
// index map from drifting apart, and hoisting them to module scope avoids
// re-allocating the array on every viewer state-sync event.
const ZOOM_NAMES = [
    "auto",
    "page-fit",
    "page-width",
    "page-actual",
];
const CURSOR_MODES = ["select", "hand", "zoom"];
const SCROLL_MODES = [
    "vertical",
    "horizontal",
    "wrapped",
    "page",
];
const SPREAD_MODES = ["none", "odd", "even"];
const PAGE_MODES = [
    "none",
    "thumbs",
    "bookmarks",
    "attachments",
];
// Lowercase + whitelist with fallback
const pick = (value, allowed, fallback) => {
    const v = value ? value.toLowerCase() : "";
    return allowed.includes(v) ? v : fallback;
};
class PropertyTransformers {
    static transformZoom = {
        toViewer: (zoom) => {
            if (!zoom)
                return "auto";
            const v = zoom.toLowerCase();
            // Named zooms normalize to lowercase; numeric strings pass through
            return ZOOM_NAMES.includes(v) ? v : zoom;
        },
        fromViewer: (viewerZoom) => {
            if (typeof viewerZoom === "string")
                return viewerZoom;
            // Numeric scale as a plain string ("1.25") - PDF.js accepts it directly
            if (typeof viewerZoom === "number")
                return viewerZoom.toString();
            return "auto";
        },
    };
    static transformRotation = {
        toViewer: (rotation) => ((rotation % 360) + 360) % 360,
        fromViewer: (viewerRotation) => typeof viewerRotation === "number" ? viewerRotation : 0,
    };
    static transformCursor = {
        toViewer: (cursor) => pick(cursor, CURSOR_MODES, "select"),
        fromViewer: (viewerCursor) => typeof viewerCursor === "string" ? viewerCursor : "select",
    };
    static transformScroll = {
        toViewer: (scroll) => pick(scroll, SCROLL_MODES, "vertical"),
        fromViewer: (viewerScroll) => {
            if (typeof viewerScroll === "number") {
                return SCROLL_MODES[viewerScroll] || "vertical";
            }
            return typeof viewerScroll === "string" ? viewerScroll : "vertical";
        },
    };
    static transformSpread = {
        toViewer: (spread) => pick(spread, SPREAD_MODES, "none"),
        fromViewer: (viewerSpread) => {
            if (typeof viewerSpread === "number") {
                return SPREAD_MODES[viewerSpread] || "none";
            }
            return typeof viewerSpread === "string" ? viewerSpread : "none";
        },
    };
    static transformPageMode = {
        toViewer: (pageMode) => pick(pageMode, PAGE_MODES, "none"),
        fromViewer: (viewerPageMode) => typeof viewerPageMode === "string" ? viewerPageMode : "none",
    };
}

// The iframe sandbox shipped on every viewer embed. allow-popups (+ escape)
// lets external PDF links open in a new, unsandboxed tab - the only link
// behavior that works without granting the document navigation rights over
// the host page.
//
// Honest scope: because the viewer is same-origin and needs allow-scripts +
// allow-same-origin, this sandbox is NOT a containment boundary against a
// compromised PDF.js (same-origin content can reach the parent document).
// It hardens link/navigation behavior of COOPERATIVE viewer code; defenses
// against hostile documents are PDF.js's own parsing/rendering isolation.
const BASE_IFRAME_SANDBOX = "allow-forms allow-scripts allow-same-origin allow-modals allow-downloads " +
    "allow-popups allow-popups-to-escape-sandbox";
// Tokens consumers may add via [iframeSandbox]. Deliberately excludes anything
// that would let viewer content reach outside a user-initiated navigation.
// Prefer 'allow-top-navigation-by-user-activation' over 'allow-top-navigation':
// the latter lets a hostile document redirect the whole host page without any
// user gesture (frame-phishing) - only use it with fully trusted documents.
const ALLOWED_EXTRA_SANDBOX_TOKENS = new Set([
    "allow-top-navigation",
    "allow-top-navigation-by-user-activation",
    "allow-presentation",
]);
const PROPERTY_REGISTRY = [
    // Control visibility (DOM toggles)
    { prop: "showOpenFile", action: "show-openfile", level: 3, init: "always" },
    { prop: "showDownload", action: "show-download", level: 3, init: "always" },
    { prop: "showPrint", action: "show-print", level: 3, init: "always" },
    { prop: "showFullScreen", action: "show-fullscreen", level: 3, init: "always" },
    { prop: "showFind", action: "show-find", level: 3, init: "always" },
    { prop: "showViewBookmark", action: "show-bookmark", level: 3, init: "always" },
    { prop: "showAnnotations", action: "show-annotations", level: 3, init: "always" },
    // Toolbar/sidebar group visibility
    { prop: "showToolbarLeft", action: "show-toolbar-left", level: 3, init: "always" },
    { prop: "showToolbarMiddle", action: "show-toolbar-middle", level: 3, init: "always" },
    { prop: "showToolbarRight", action: "show-toolbar-right", level: 3, init: "always" },
    { prop: "showSecondaryToolbarToggle", action: "show-secondary-toolbar-toggle", level: 3, init: "always" },
    // chromeless forces both hidden while leaving the consumer's own
    // showToolbar/showSidebar bindings untouched (see the chromeless @Input).
    { prop: "showToolbar", action: "show-toolbar", level: 3, init: "always", get: (c) => c.showToolbar && !c.chromeless },
    { prop: "showSidebar", action: "show-sidebar", level: 3, init: "always", get: (c) => c.showSidebar && !c.chromeless },
    { prop: "showSidebarLeft", action: "show-sidebar-left", level: 3, init: "always" },
    { prop: "showSidebarRight", action: "show-sidebar-right", level: 3, init: "always" },
    // Layout & responsive customization
    { prop: "toolbarDensity", action: "set-toolbar-density", level: 4, init: "always" },
    { prop: "sidebarWidth", action: "set-sidebar-width", level: 4, init: "truthy" },
    { prop: "toolbarPosition", action: "set-toolbar-position", level: 4, init: "always" },
    { prop: "sidebarPosition", action: "set-sidebar-position", level: 4, init: "always" },
    { prop: "responsiveBreakpoint", action: "set-responsive-breakpoint", level: 4, init: "defined" },
    // Modes & navigation
    { prop: "cursor", action: "set-cursor", level: 4, init: "truthy" },
    { prop: "scroll", action: "set-scroll", level: 4, init: "truthy" },
    { prop: "spread", action: "set-spread", level: 4, init: "truthy" },
    { prop: "zoom", action: "set-zoom", level: 4, init: "truthy" },
    { prop: "pageMode", action: "update-page-mode", level: 4, init: "truthy" },
    { prop: "page", action: "set-page", level: 5, init: "truthy", get: (c) => c._page },
    { prop: "rotation", action: "set-rotation", level: 5, init: false },
    { prop: "namedDest", action: "go-to-named-dest", level: 5, init: "nonempty" },
    { prop: "rotateCW", action: "trigger-rotate-cw", level: 5, init: "true" },
    { prop: "rotateCCW", action: "trigger-rotate-ccw", level: 5, init: "true" },
    // Theme & visual customization (DOM-only, applies as soon as viewer loads)
    { prop: "theme", action: "set-theme", level: 1, init: "always", payload: (v) => v || "auto" },
    { prop: "primaryColor", action: "set-primary-color", level: 1, init: "truthy" },
    { prop: "backgroundColor", action: "set-background-color", level: 1, init: "truthy" },
    { prop: "pageBorderColor", action: "set-page-border-color", level: 1, init: "truthy" },
    { prop: "pageSpacing", action: "set-page-spacing", level: 3, init: "truthy" },
    { prop: "toolbarColor", action: "set-toolbar-color", level: 1, init: "truthy" },
    { prop: "textColor", action: "set-text-color", level: 1, init: "truthy" },
    { prop: "borderRadius", action: "set-border-radius", level: 1, init: "truthy" },
    {
        prop: "customCSS",
        action: "set-custom-css",
        level: 1,
        init: "truthy",
        payload: (v, c) => (c.cspNonce ? { css: v, nonce: c.cspNonce } : v),
    },
    // Misc configuration. downloadFileName is level 5 because PDF.js overwrites
    // its _contentDispositionFilename during document load.
    { prop: "useOnlyCssZoom", action: "set-css-zoom", level: 3, init: "defined" },
    // 'always': the embedded PDF.js default ('top') is sandbox-blocked, so the
    // component's 'blank' default must reach the viewer on every load.
    { prop: "externalLinkTarget", action: "set-external-link-target", level: 3, init: "always" },
    { prop: "rememberLastView", action: "set-remember-last-view", level: 3, init: "defined" },
    { prop: "downloadFileName", action: "set-download-filename", level: 5, init: "truthy" },
    { prop: "urlValidation", action: "set-url-validation", level: 3, init: false },
    { prop: "diagnosticLogs", action: "set-diagnostic-logs", level: 3, init: false },
    { prop: "highlightEditorColors", action: "set-highlight-editor-colors", level: 3, init: "nonempty" },
    // The hook object stays host-side; the wrapper only needs the on/off bit
    { prop: "signatureStorage", action: "set-signature-storage", level: 3, init: "truthy", payload: (v) => !!v },
    // Setter-dispatched at runtime; registry entry re-applies the active editor
    // after iframe reloads (pdfSrc change / refresh). 'none' is the PDF.js
    // default and never needs sending.
    {
        prop: "annotationEditor",
        action: "set-annotation-editor-mode",
        level: 5,
        init: "truthy",
        get: (c) => c._annotationEditor === "none"
            ? undefined
            : c._annotationEditor,
    },
];
const REGISTRY_BY_PROP = {};
const ACTION_READINESS = {
    // On-demand triggers without an owning property
    "trigger-download": 5,
    "trigger-print": 5,
    "go-to-last-page": 5,
    // Annotation editing + document queries need a loaded document
    "set-annotation-editor-mode": 5,
    "get-annotations": 5,
    "set-annotations": 5,
    "save-document": 5,
    "search": 5,
    "search-next": 5,
    "search-previous": 5,
    "clear-search": 5,
    // Forms need field objects from the loaded document
    "get-form-data": 5,
    "set-form-data": 5,
    "set-form-field": 5,
    // Content protection is DOM-level
    "set-content-protection": 3,
    "set-watermark": 3,
    // Text extraction + read-aloud need the document
    "get-document-text": 5,
    "read-aloud": 5,
};
for (const entry of PROPERTY_REGISTRY) {
    REGISTRY_BY_PROP[entry.prop] = entry;
    ACTION_READINESS[entry.action] = entry.level;
}
// Wrapper-side event notifications enabled at init. All @Output emitters exist
// unconditionally, so these are enabled unconditionally; 'enable-idle' is the
// exception (it installs document-wide activity listeners in the iframe) and
// is only sent when the consumer actually subscribed to (onIdle).
const ENABLE_EVENT_ACTIONS = [
    "enable-before-print",
    "enable-after-print",
    "enable-pages-loaded",
    "enable-page-change",
    "enable-document-error",
    "enable-document-init",
    "enable-pages-init",
    "enable-presentation-mode-changed",
    "enable-open-file",
    "enable-find",
    "enable-update-find-matches-count",
    "enable-metadata-loaded",
    "enable-outline-loaded",
    "enable-page-rendered",
    "enable-annotation-layer-rendered",
    "enable-bookmark-click",
    "enable-sidebar-view-changed",
    "enable-layers-changed",
    "enable-named-action",
    "enable-document-properties",
];
// Properties whose setters dispatch on their own - skipped in ngOnChanges to
// avoid a second postMessage per change
const SETTER_DISPATCHED_PROPS = new Set([
    "zoom",
    "rotation",
    "cursor",
    "scroll",
    "spread",
    "pageMode",
    "page",
    "diagnosticLogs",
    "annotationEditor",
    "formData",
    "contentProtection",
]);
// Auto-actions are read at the next document load; changing them mid-session
// dispatches nothing
const DOCUMENT_LOAD_PROPS = new Set([
    "downloadOnLoad",
    "printOnLoad",
    "showLastPageOnLoad",
]);
// Config-object inputs fan out to the individual properties their setters
// populate, so post-init changes propagate to the viewer (they used to be
// silently dropped after init)
const CONFIG_FANOUT = {
    controlVisibility: [
        "showDownload", "showPrint", "showFind", "showFullScreen",
        "showOpenFile", "showViewBookmark", "showAnnotations",
    ],
    groupVisibility: [
        "showToolbarLeft", "showToolbarMiddle", "showToolbarRight",
        "showSecondaryToolbarToggle", "showSidebar", "showSidebarLeft", "showSidebarRight",
    ],
    layoutConfig: [
        "toolbarDensity", "sidebarWidth", "toolbarPosition",
        "sidebarPosition", "responsiveBreakpoint",
    ],
    themeConfig: [
        "theme", "primaryColor", "backgroundColor", "pageBorderColor",
        "pageSpacing", "toolbarColor", "textColor", "borderRadius", "customCSS",
    ],
    viewerConfig: ["useOnlyCssZoom", "externalLinkTarget", "rememberLastView"],
    autoActions: ["rotateCW", "rotateCCW"],
    errorHandling: [],
    // chromeless is a preset, not a config object, but it reuses the fanout so a
    // runtime toggle re-dispatches the (get-overridden) toolbar/sidebar actions.
    chromeless: ["showToolbar", "showSidebar"],
};
function hasObservers(emitter) {
    return (emitter.observed === true ||
        (emitter.observers?.length ?? 0) > 0);
}
// Config-object inputs are commonly bound to getters that return a FRESH
// object every change-detection cycle. Reference identity then flags a
// "change" each cycle - only the content matters.
// (Exported for unit tests; not part of the public package API.)
function shallowEquals(a, b) {
    if (a === b)
        return true;
    if (!a || !b || typeof a !== "object" || typeof b !== "object")
        return false;
    const aKeys = Object.keys(a);
    const bKeys = Object.keys(b);
    return (aKeys.length === bKeys.length && aKeys.every((k) => a[k] === b[k]));
}
// #endregion
class PdfJsViewerComponent {
    // #region Component Properties
    iframe;
    static lastID = 0;
    viewerId = `ng2-pdfjs-viewer-ID${++PdfJsViewerComponent.lastID}`;
    // #region Event Outputs
    onBeforePrint = new EventEmitter();
    onAfterPrint = new EventEmitter();
    onDocumentLoad = new EventEmitter();
    onPageChange = new EventEmitter();
    onScaleChange = new EventEmitter();
    onRotationChange = new EventEmitter();
    // New high-value events for enhanced PDF viewer functionality
    onDocumentError = new EventEmitter();
    onDocumentInit = new EventEmitter();
    onPagesInit = new EventEmitter();
    onPresentationModeChanged = new EventEmitter();
    onOpenFile = new EventEmitter();
    onFind = new EventEmitter();
    onUpdateFindMatchesCount = new EventEmitter();
    onMetadataLoaded = new EventEmitter();
    onOutlineLoaded = new EventEmitter();
    onPageRendered = new EventEmitter();
    // New high-value events
    onAnnotationLayerRendered = new EventEmitter();
    onBookmarkClick = new EventEmitter();
    onIdle = new EventEmitter();
    // Fired when PDF.js shows its password dialog for a protected document.
    // The loading spinner is dropped automatically so the dialog is usable.
    onPasswordPrompt = new EventEmitter();
    // Annotation editor undo/redo/empty state - drives "unsaved changes" UX
    onAnnotationEditorStateChange = new EventEmitter();
    // Page organization events (reorder/delete/extract/merge in the sidebar)
    onPagesEdited = new EventEmitter();
    // Read-aloud progress: reading | paused | stopped | finished | error
    onReadAloudStateChange = new EventEmitter();
    // Sidebar panel switches (thumbnails/outline/attachments/layers)
    onSidebarViewChanged = new EventEmitter();
    // Optional-content layers: loaded for the document / visibility toggled
    onLayersChanged = new EventEmitter();
    // Named actions triggered from inside the document (GoToPage, Print, ...)
    onNamedAction = new EventEmitter();
    // User opened the document-properties dialog
    onDocumentProperties = new EventEmitter();
    // #endregion
    // #region Basic Configuration Properties
    viewerFolder;
    externalWindow = false;
    target = "_blank";
    showSpinner = true;
    downloadFileName;
    locale;
    useOnlyCssZoom = false;
    // Where external PDF links open. The embedded PDF.js default ('top') is
    // blocked by the iframe sandbox, leaving links dead (issue #304); 'blank'
    // works with the sandbox's allow-popups and is the safe default.
    externalLinkTarget = "blank";
    // Restore the previous reading position (page/zoom/sidebar) on reload.
    // Set false to always open documents at page 1 / initial view (issue #299).
    rememberLastView = true;
    // Active annotation editor tool. Two-way bindable: user toolbar clicks emit
    // annotationEditorChange. Editing requires a loaded document (level 5).
    set annotationEditor(mode) {
        if (mode === this._annotationEditor) {
            return;
        }
        this._annotationEditor = mode ?? "none";
        this.dispatchAction("set-annotation-editor-mode", this._annotationEditor, "property-change");
    }
    get annotationEditor() {
        return this._annotationEditor;
    }
    _annotationEditor = "none";
    annotationEditorChange = new EventEmitter();
    // Highlight palette for the highlight editor, PDF.js format:
    // 'yellow=#FFFF98,green=#53FFBC,...'. Applied before the document opens.
    highlightEditorColors;
    // Opt-in PDF.js signature editor (draw / type / upload image). Saved as
    // stamp-style annotations - an eSign convenience, NOT cryptographic signing.
    // Init-time option: changing it after load requires a reload.
    enableSignatureEditor = false;
    // Host-side persistence for the signature editor's saved signatures.
    // When set, the viewer's "save signature" feature round-trips through these
    // callbacks (e.g. to a server, per user) instead of the iframe's
    // localStorage. Use with [enableSignatureEditor].
    signatureStorage;
    // Re-render page CONTENT with custom colors (true dark mode for pages, not
    // just viewer chrome). Example: { background: '#1e1e1e', foreground: '#e8e8e8' }.
    // Init-time option: changing it after load requires a reload.
    pageColors;
    // Raw allowlisted PDF.js AppOptions passthrough for init-time options
    // (e.g. { printResolution: 300, sidebarViewOnLoad: 1, enableComment: true }).
    // Keys outside the wrapper's allowlist are ignored with a console warning.
    // Init-time: changing it after load requires a reload.
    pdfJsOptions;
    // Opt-in PDF.js comment editor: threaded comment popups on highlights with
    // edit/delete and undo/redo. Init-time option: changing requires a reload.
    enableCommentEditor = false;
    // Opt-in in-viewer page organization: drag-drop reorder, delete, cut/copy/
    // paste, extract and merge pages from the sidebar's views manager.
    // Init-time option: changing requires a reload.
    enablePageEditing = false;
    // Show/hide the entire viewer toolbar (pair with customToolbarTpl to ship a
    // fully custom host-side toolbar)
    showToolbar = true;
    // Chromeless / embedded mode: hide the toolbar and sidebar in one switch so
    // the iframe shows just the scrolling pages. Shorthand for showToolbar=false
    // + showSidebar=false; it overrides them without mutating those bindings, so
    // flipping it back restores whatever they were. There is still an iframe and
    // its own scroll container - use pageOverlayTpl if you need per-page host DOM.
    chromeless = false;
    // Host-side replacement toolbar rendered ABOVE the viewer iframe. Template
    // context: let-viewer (the component instance) for driving the public API,
    // e.g. <ng-template #tb let-viewer><button (click)="viewer.setPage(1)">...
    customToolbarTpl;
    // Host-side sidebar panel rendered BESIDE the viewer iframe (left). Same
    // template context as customToolbarTpl: let-viewer (the component
    // instance). Pair with [groupVisibility]="{ sidebar: false }" to replace
    // the built-in sidebar entirely. Size it with your own CSS width.
    customSidebarTpl;
    // Built-in chat-with-the-document panel (floating, bottom-right). The
    // library only calls the endpoint configured here - never any AI service
    // of its own. Answers cite pages as [p.3]; citations are clickable and
    // jump the viewer to that page. For fully custom UI use PdfAiAssistant +
    // getDocumentText() instead.
    aiAssistantConfig;
    // AI panel state (template-bound)
    aiPanelOpen = false;
    aiBusy = false;
    aiMessages = [];
    aiClient;
    aiClientConfig;
    aiDocText;
    // Bumped whenever the document context changes; in-flight aiAsk
    // continuations compare against it and abandon stale answers
    aiGeneration = 0;
    aiAbort;
    // Drop AI panel state tied to the current document and abandon any
    // in-flight request, so a slow answer about the OLD document can't land
    // in (or re-enable) the new document's chat.
    invalidateAiState(clearChat) {
        this.aiGeneration++;
        this.aiAbort?.abort();
        this.aiAbort = undefined;
        this.aiDocText = undefined;
        if (clearChat) {
            this.aiMessages = [];
        }
        this.aiBusy = false;
    }
    // Angular template rendered as an overlay on every page (watermark badges,
    // stamps, review UI). Context: let-page (1-based page number). The overlay
    // wrapper is pointer-events:none; re-enable on your own elements as needed.
    // Setter so clearing/replacing the template also unmounts existing overlays.
    _pageOverlayTpl;
    set pageOverlayTpl(value) {
        if (value === this._pageOverlayTpl) {
            return;
        }
        this._pageOverlayTpl = value;
        this.destroyPageOverlays();
        if (value) {
            this.mountOverlaysOnRenderedPages();
        }
    }
    get pageOverlayTpl() {
        return this._pageOverlayTpl;
    }
    // Request headers sent when the component fetches a string pdfSrc URL
    // (JWT bearer tokens, API keys). When set, the component downloads the
    // document itself and hands the viewer a local blob - the URL never needs
    // to be reachable by the viewer iframe directly.
    httpHeaders;
    // Send cookies/credentials with the component-side fetch of pdfSrc.
    withCredentials = false;
    // Download progress while the component fetches pdfSrc (only emitted for
    // the httpHeaders/withCredentials fetch path). total is 0 when the server
    // sends no content-length.
    onProgress = new EventEmitter();
    // Monotonic token so a pdfSrc change mid-fetch abandons the stale download
    authLoadToken = 0;
    // AcroForm field values, two-way bindable: [(formData)]. Setting writes the
    // fields into the viewer; user edits in the viewer emit formDataChange.
    set formData(value) {
        this._formData = value ?? {};
        this.dispatchAction("set-form-data", this._formData, "property-change");
    }
    get formData() {
        return this._formData;
    }
    _formData = {};
    formDataChange = new EventEmitter();
    // Client-side content protection (deterrence, not DRM): block print/save
    // shortcuts, disable text selection, render a per-page watermark.
    set contentProtection(config) {
        this._contentProtection = config ?? {};
        this.dispatchAction("set-content-protection", {
            blockPrint: this._contentProtection.blockPrint === true,
            blockDownload: this._contentProtection.blockDownload === true,
            disableTextSelection: this._contentProtection.disableTextSelection === true,
        }, "property-change");
        if (this._contentProtection.blockPrint !== undefined) {
            this.dispatchAction("show-print", !this._contentProtection.blockPrint, "property-change");
        }
        if (this._contentProtection.blockDownload !== undefined) {
            this.dispatchAction("show-download", !this._contentProtection.blockDownload, "property-change");
        }
        this.dispatchAction("set-watermark", this._contentProtection.watermark ?? null, "property-change");
    }
    get contentProtection() {
        return this._contentProtection;
    }
    _contentProtection = {};
    // Additional iframe sandbox permissions, validated against a fixed
    // allowlist; anything else is ignored (issue #304 asked for
    // allow-top-navigation for trusted documents).
    set iframeSandbox(value) {
        const requested = (value || "").split(/\s+/).filter(Boolean);
        const accepted = requested.filter((token) => ALLOWED_EXTRA_SANDBOX_TOKENS.has(token));
        const rejected = requested.filter((token) => !ALLOWED_EXTRA_SANDBOX_TOKENS.has(token));
        if (rejected.length > 0) {
            console.warn(`ng2-pdfjs-viewer: ignoring sandbox tokens not in the allowlist: ${rejected.join(", ")}`);
        }
        this._extraSandboxTokens = accepted;
    }
    get iframeSandbox() {
        return this._extraSandboxTokens.join(" ");
    }
    _extraSandboxTokens = [];
    get effectiveSandbox() {
        return this._extraSandboxTokens.length === 0
            ? BASE_IFRAME_SANDBOX
            : `${BASE_IFRAME_SANDBOX} ${this._extraSandboxTokens.join(" ")}`;
    }
    set diagnosticLogs(value) {
        this._diagnosticLogs = value;
        // Update action queue manager
        this.actionQueueManager.setDiagnosticLogs(value);
        // Send to wrapper
        this.dispatchAction("set-diagnostic-logs", value, "property-change");
    }
    get diagnosticLogs() {
        return this._diagnosticLogs;
    }
    _diagnosticLogs = false;
    // #endregion
    // #region Control Visibility Properties
    showOpenFile = true;
    // Default true since PDF.js 5.7: the editor toolbar (highlight/text/draw/
    // stamp, plus the opt-in signature/comment editors) is core viewer UI.
    // Set false to hide the editing buttons entirely.
    showAnnotations = true;
    showDownload = true;
    showViewBookmark = true;
    showPrint = true;
    showFullScreen = true;
    showFind = true;
    // #endregion
    // #region Auto-Action Properties
    downloadOnLoad = false;
    printOnLoad = false;
    rotateCW = false;
    rotateCCW = false;
    showLastPageOnLoad = false;
    // #endregion
    // #region Navigation Properties
    namedDest;
    // #endregion
    // #region Error Handling Properties
    errorOverride = false;
    errorAppend = true;
    errorMessage;
    urlValidation = true;
    customSecurityTpl;
    // Security warning state
    securityWarning = null;
    // #endregion
    // #region Theme & Visual Customization Properties
    theme = "auto";
    primaryColor;
    backgroundColor;
    pageBorderColor;
    pageSpacing;
    toolbarColor;
    textColor;
    borderRadius;
    customCSS;
    cspNonce;
    iframeTitle; // CSP nonce for customCSS (optional)
    // #endregion
    // #region Loading & Spinner Customization
    customSpinnerTpl;
    spinnerClass;
    // #endregion
    // #region Error Display Customization
    customErrorTpl;
    errorClass;
    // #endregion
    // #region Toolbar/Sidebar Group Visibility
    showToolbarLeft = true;
    showToolbarMiddle = true;
    showToolbarRight = true;
    showSecondaryToolbarToggle = true;
    showSidebar = true;
    showSidebarLeft = true;
    showSidebarRight = true;
    // #endregion
    // #region Layout & Responsive Customization
    toolbarDensity = "default";
    sidebarWidth; // e.g., '280px'
    toolbarPosition = "top";
    sidebarPosition = "left";
    responsiveBreakpoint;
    // #endregion
    // Internal loading state for overlay control
    isLoading = true;
    // Internal error state for error display
    hasError = false;
    currentErrorMessage = "";
    errorTemplateData = {};
    // Kept for API compatibility; the template binds errorTemplateData directly
    // so change detection sees a stable object identity.
    getErrorTemplateData() {
        return this.errorTemplateData;
    }
    updateErrorTemplateData() {
        this.errorTemplateData = {
            errorMessage: this.currentErrorMessage,
            errorClass: this.errorClass,
        };
    }
    // Helper method to get iframe CSS classes (no per-CD-cycle allocation)
    getIframeClasses() {
        return this.iframeBorder && this.iframeBorder !== "0" && this.iframeBorder !== 0
            ? "ng2-pdfjs-viewer-iframe has-border"
            : "ng2-pdfjs-viewer-iframe";
    }
    // Error template button actions
    reloadViewer() {
        this.refresh();
    }
    goBack() {
        if (window.history.length > 1) {
            window.history.back();
        }
        else {
            window.close();
        }
    }
    closeViewer() {
        window.close();
    }
    // #region Convenience Configuration Setters
    set controlVisibility(config) {
        if (config.download !== undefined)
            this.showDownload = config.download;
        if (config.print !== undefined)
            this.showPrint = config.print;
        if (config.find !== undefined)
            this.showFind = config.find;
        if (config.fullScreen !== undefined)
            this.showFullScreen = config.fullScreen;
        if (config.openFile !== undefined)
            this.showOpenFile = config.openFile;
        if (config.viewBookmark !== undefined)
            this.showViewBookmark = config.viewBookmark;
        if (config.annotations !== undefined)
            this.showAnnotations = config.annotations;
    }
    set autoActions(config) {
        if (config.downloadOnLoad !== undefined)
            this.downloadOnLoad = config.downloadOnLoad;
        if (config.printOnLoad !== undefined)
            this.printOnLoad = config.printOnLoad;
        if (config.showLastPageOnLoad !== undefined)
            this.showLastPageOnLoad = config.showLastPageOnLoad;
        if (config.rotateCW !== undefined)
            this.rotateCW = config.rotateCW;
        if (config.rotateCCW !== undefined)
            this.rotateCCW = config.rotateCCW;
    }
    set errorHandling(config) {
        if (config.override !== undefined)
            this.errorOverride = config.override;
        if (config.append !== undefined)
            this.errorAppend = config.append;
        if (config.message !== undefined)
            this.errorMessage = config.message;
    }
    set viewerConfig(config) {
        if (config.showSpinner !== undefined)
            this.showSpinner = config.showSpinner;
        if (config.useOnlyCssZoom !== undefined)
            this.useOnlyCssZoom = config.useOnlyCssZoom;
        if (config.diagnosticLogs !== undefined)
            this.diagnosticLogs = config.diagnosticLogs;
        if (config.locale !== undefined)
            this.locale = config.locale;
        if (config.externalLinkTarget !== undefined)
            this.externalLinkTarget = config.externalLinkTarget;
        if (config.rememberLastView !== undefined)
            this.rememberLastView = config.rememberLastView;
    }
    set themeConfig(config) {
        if (config.theme !== undefined)
            this.theme = config.theme;
        if (config.primaryColor !== undefined)
            this.primaryColor = config.primaryColor;
        if (config.backgroundColor !== undefined)
            this.backgroundColor = config.backgroundColor;
        if (config.pageBorderColor !== undefined)
            this.pageBorderColor = config.pageBorderColor;
        if (config.pageSpacing !== undefined)
            this.pageSpacing = config.pageSpacing;
        if (config.toolbarColor !== undefined)
            this.toolbarColor = config.toolbarColor;
        if (config.textColor !== undefined)
            this.textColor = config.textColor;
        if (config.borderRadius !== undefined)
            this.borderRadius = config.borderRadius;
        if (config.customCSS !== undefined)
            this.customCSS = config.customCSS;
        if (config.cspNonce !== undefined)
            this.cspNonce = config.cspNonce;
    }
    set groupVisibility(config) {
        if (config.toolbarLeft !== undefined)
            this.showToolbarLeft = config.toolbarLeft;
        if (config.toolbarMiddle !== undefined)
            this.showToolbarMiddle = config.toolbarMiddle;
        if (config.toolbarRight !== undefined)
            this.showToolbarRight = config.toolbarRight;
        if (config.secondaryToolbarToggle !== undefined)
            this.showSecondaryToolbarToggle = config.secondaryToolbarToggle;
        if (config.sidebar !== undefined)
            this.showSidebar = config.sidebar;
        if (config.sidebarLeft !== undefined)
            this.showSidebarLeft = config.sidebarLeft;
        if (config.sidebarRight !== undefined)
            this.showSidebarRight = config.sidebarRight;
    }
    set layoutConfig(config) {
        if (config.toolbarDensity !== undefined)
            this.toolbarDensity = config.toolbarDensity;
        if (config.sidebarWidth !== undefined)
            this.sidebarWidth = config.sidebarWidth;
        if (config.toolbarPosition !== undefined)
            this.toolbarPosition = config.toolbarPosition;
        if (config.sidebarPosition !== undefined)
            this.sidebarPosition = config.sidebarPosition;
        if (config.responsiveBreakpoint !== undefined)
            this.responsiveBreakpoint = config.responsiveBreakpoint;
    }
    // #endregion
    // #region Helper function for deprecated properties
    static warnedDeprecations = new Set();
    setDeprecatedProperty(oldName, newProperty, value) {
        if (!PdfJsViewerComponent.warnedDeprecations.has(oldName)) {
            PdfJsViewerComponent.warnedDeprecations.add(oldName);
            console.warn(`ng2-pdfjs-viewer: Property "${oldName}" is deprecated. Use "${newProperty}" instead.`);
        }
        this[newProperty] = value;
    }
    // #endregion
    // #region Deprecated Properties (Simplified)
    /** @deprecated Use `downloadOnLoad` instead. This property will be removed in a future version. */
    set startDownload(value) {
        this.setDeprecatedProperty("startDownload", "downloadOnLoad", value);
    }
    /** @deprecated Use `printOnLoad` instead. This property will be removed in a future version. */
    set startPrint(value) {
        this.setDeprecatedProperty("startPrint", "printOnLoad", value);
    }
    /** @deprecated Use `showOpenFile` instead. This property will be removed in a future version. */
    set openFile(value) {
        this.setDeprecatedProperty("openFile", "showOpenFile", value);
    }
    /** @deprecated Use `showDownload` instead. This property will be removed in a future version. */
    set download(value) {
        this.setDeprecatedProperty("download", "showDownload", value);
    }
    /** @deprecated Use `showPrint` instead. This property will be removed in a future version. */
    set print(value) {
        this.setDeprecatedProperty("print", "showPrint", value);
    }
    /** @deprecated Use `showFullScreen` instead. This property will be removed in a future version. */
    set fullScreen(value) {
        this.setDeprecatedProperty("fullScreen", "showFullScreen", value);
    }
    /** @deprecated Use `showFind` instead. This property will be removed in a future version. */
    set find(value) {
        this.setDeprecatedProperty("find", "showFind", value);
    }
    /** @deprecated Use `showViewBookmark` instead. This property will be removed in a future version. */
    set viewBookmark(value) {
        this.setDeprecatedProperty("viewBookmark", "showViewBookmark", value);
    }
    /** @deprecated Use `showLastPageOnLoad` instead. This property will be removed in a future version. */
    set lastPage(value) {
        this.setDeprecatedProperty("lastPage", "showLastPageOnLoad", value);
    }
    // #endregion
    // #region External Window Properties
    externalWindowOptions;
    viewerTab;
    // #endregion
    // #region Security Properties
    // iframe sandbox is static for security and Angular compliance
    // #endregion
    // #region iframe Properties
    iframeBorder = "0";
    // #endregion
    // #region Private Properties
    _src;
    _page;
    isPostMessageReady = false;
    postMessageReadiness = 0;
    initialConfigQueued = false;
    actionQueueManager = new ActionQueueManager(this._diagnosticLogs);
    cdr;
    appRef;
    ngZone;
    constructor(cdr, appRef, ngZone) {
        this.cdr = cdr;
        this.appRef = appRef;
        this.ngZone = ngZone;
    }
    messageIdCounter = 0;
    // Monotonic suffix keeps action ids unique even within one millisecond
    actionIdCounter = 0;
    pendingMessages = new Map();
    // Changes that arrived before the viewer was ready, coalesced per property
    // (last write wins)
    pendingChanges = {};
    releaseUrl;
    webviewerLoadedHandler;
    pdfEventHandlers;
    // #endregion
    // #region Two-Way Binding Properties
    // Private backing fields for two-way binding properties
    _zoom = "auto";
    _rotation = 0;
    _cursor = "select";
    _scroll = "vertical";
    _spread = "none";
    _pageMode = "none";
    // Two-way binding Output events
    zoomChange = new EventEmitter();
    cursorChange = new EventEmitter();
    scrollChange = new EventEmitter();
    spreadChange = new EventEmitter();
    pageModeChange = new EventEmitter();
    /**
     * Two-way binding for zoom level
     * Supports: auto, page-fit, page-width, page-actual, percentage values (e.g., "150%")
     */
    get zoom() {
        return this._zoom;
    }
    set zoom(value) {
        const normalizedValue = PropertyTransformers.transformZoom.toViewer(value);
        if (this._zoom !== normalizedValue) {
            this._zoom = normalizedValue;
            this.dispatchAction("set-zoom", this._zoom, "property-change");
            this.zoomChange.emit(this._zoom);
        }
    }
    /**
     * One-way binding for document rotation
     * Supports: 0, 90, 180, 270 degrees
     */
    set rotation(value) {
        const normalizedValue = PropertyTransformers.transformRotation.toViewer(value);
        if (this._rotation !== normalizedValue) {
            this._rotation = normalizedValue;
            this.dispatchAction("set-rotation", this._rotation, "property-change");
        }
    }
    get rotation() {
        return this._rotation;
    }
    /**
     * Two-way binding for cursor mode
     * Supports: select, hand, zoom
     */
    get cursor() {
        return this._cursor;
    }
    set cursor(value) {
        const normalizedValue = PropertyTransformers.transformCursor.toViewer(value);
        if (this._cursor !== normalizedValue) {
            this._cursor = normalizedValue;
            this.dispatchAction("set-cursor", this._cursor, "property-change");
            this.cursorChange.emit(this._cursor);
        }
    }
    /**
     * Two-way binding for scroll mode
     * Supports: vertical, horizontal, wrapped, page
     */
    get scroll() {
        return this._scroll;
    }
    set scroll(value) {
        const normalizedValue = PropertyTransformers.transformScroll.toViewer(value);
        if (this._scroll !== normalizedValue) {
            this._scroll = normalizedValue;
            this.dispatchAction("set-scroll", this._scroll, "property-change");
            this.scrollChange.emit(this._scroll);
        }
    }
    /**
     * Two-way binding for spread mode
     * Supports: none, odd, even
     */
    get spread() {
        return this._spread;
    }
    set spread(value) {
        const normalizedValue = PropertyTransformers.transformSpread.toViewer(value);
        if (this._spread !== normalizedValue) {
            this._spread = normalizedValue;
            this.dispatchAction("set-spread", this._spread, "property-change");
            this.spreadChange.emit(this._spread);
        }
    }
    /**
     * Two-way binding for page mode (sidebar state)
     * Supports: none, thumbs, bookmarks, attachments
     */
    get pageMode() {
        return this._pageMode;
    }
    set pageMode(value) {
        const normalizedValue = PropertyTransformers.transformPageMode.toViewer(value);
        if (this._pageMode !== normalizedValue) {
            this._pageMode = normalizedValue;
            this.dispatchAction("update-page-mode", this._pageMode, "property-change");
            this.pageModeChange.emit(this._pageMode);
        }
    }
    set page(_page) {
        this._page = _page;
        if (this.PDFViewerApplication && this.PDFViewerApplication.initialized) {
            this.PDFViewerApplication.page = this._page;
        }
        else {
            if (this.diagnosticLogs) {
                console.warn("Document is not loaded yet!!!. Try to set page# after full load. Ignore this warning if you are not setting page# using '.' notation. (E.g. pdfViewer.page = 5;)");
            }
        }
    }
    get page() {
        if (this.PDFViewerApplication && this.PDFViewerApplication.initialized) {
            return this.PDFViewerApplication.page;
        }
        else {
            if (this.diagnosticLogs) {
                console.warn("Document is not loaded yet!!!. Try to retrieve page# after full load.");
            }
            return this._page || 1;
        }
    }
    set pdfSrc(_src) {
        this._src = _src;
    }
    get pdfSrc() {
        return this._src;
    }
    // #endregion
    // #region PDF.js Application Access Properties
    get PDFViewerApplicationOptions() {
        let pdfViewerOptions = null;
        if (this.externalWindow) {
            if (this.viewerTab) {
                pdfViewerOptions = this.viewerTab.PDFViewerApplicationOptions;
            }
        }
        else {
            // Optional-chained: a static `page="5"` attribute runs input setters
            // before the static ViewChild is resolved
            if (this.iframe?.nativeElement?.contentWindow) {
                pdfViewerOptions =
                    this.iframe.nativeElement.contentWindow.PDFViewerApplicationOptions;
            }
        }
        return pdfViewerOptions;
    }
    get PDFViewerApplication() {
        let pdfViewer = null;
        if (this.externalWindow) {
            if (this.viewerTab) {
                pdfViewer = this.viewerTab.PDFViewerApplication;
            }
        }
        else {
            if (this.iframe?.nativeElement?.contentWindow) {
                pdfViewer =
                    this.iframe.nativeElement.contentWindow.PDFViewerApplication;
            }
        }
        if (this.diagnosticLogs)
            console.debug("PdfJsViewer: Viewer ->", pdfViewer);
        return pdfViewer;
    }
    // #endregion
    // #endregion
    // #region Lifecycle Methods
    // SSR guard: the viewer is browser-only (iframe + postMessage). During
    // server rendering the lifecycle hooks no-op; the real load happens after
    // hydration in the browser.
    get isBrowser() {
        return typeof window !== "undefined" && typeof document !== "undefined";
    }
    ngOnInit() {
        if (!this.isBrowser) {
            return;
        }
        // Connect action queue manager to PostMessage system
        // Wrap sendControlMessage to handle iframe unavailability by re-queuing actions
        this.actionQueueManager.setPostMessageExecutor((action) => this.sendControlMessageWithRequeue(action));
        // Send diagnostic logs setting to wrapper
        this.dispatchAction("set-diagnostic-logs", this._diagnosticLogs, "initial-load");
        // Send URL validation setting to wrapper
        this.dispatchAction("set-url-validation", this.urlValidation, "initial-load");
        // Set up PostMessage listener
        this.setupMessageListener();
        // Note: PDF loading moved to ngAfterViewInit() to ensure iframe is ready
        // This prevents "Cannot read properties of null (reading 'location')" error
        // when pdfSrc is a Blob (Issue #283)
        // Bind events.
        this.bindToPdfJsEventBus();
    }
    ngAfterViewInit() {
        if (!this.isBrowser) {
            return;
        }
        // Angular only allows a static sandbox attribute in templates (NG0910), so
        // extra allowlisted tokens are applied natively - safe here because the
        // iframe has not navigated yet (sandbox applies to subsequent loads).
        if (this._extraSandboxTokens.length > 0 && this.iframe?.nativeElement) {
            this.iframe.nativeElement.setAttribute("sandbox", this.effectiveSandbox);
        }
        // Load PDF after view is initialized - trust Angular's lifecycle guarantee
        // that iframe.nativeElement.contentWindow is now available
        if (!this.externalWindow) {
            this.loadPdf();
        }
    }
    ngOnChanges(changes) {
        // Handle pdfSrc changes - reload the PDF (Issue #283)
        // Trust Angular's change detection event to trigger reload when needed
        if (changes['pdfSrc'] && !changes['pdfSrc'].firstChange) {
            // pdfSrc changed after initialization - reload the PDF
            if (!this.externalWindow) {
                // Show spinner immediately when PDF source changes (Issue #275)
                this.isLoading = true;
                this.hasError = false;
                this.currentErrorMessage = "";
                // Don't wait for the new document's documentInit relay (it is
                // enablement-gated and can race a fast local load): the AI panel's
                // text/chat refer to the outgoing document - drop them now
                this.invalidateAiState(true);
                // The new document renders unrotated; without this the stale value
                // makes a consumer's [rotation] re-set a silent no-op
                this._rotation = 0;
                // The iframe reloads fresh: discard in-flight messages and queued
                // actions, and reset readiness so configuration re-applies on the
                // new load's postmessage-ready handshake.
                this.rejectPendingMessages("PDF source changed");
                this.actionQueueManager.reset();
                this.initialConfigQueued = false;
                this.isPostMessageReady = false;
                this.postMessageReadiness = 0;
                this.loadPdf();
            }
            return; // pdfSrc change requires full reload, skip other change processing
        }
        if (this.isPostMessageReady &&
            this.PDFViewerApplication?.initialized) {
            this.applyChanges(changes);
        }
        else {
            // Coalesce per property - only the latest value matters once ready.
            // Initial bindings (firstChange) are excluded: the batched 'configure'
            // snapshot reads live property values when the viewer becomes ready,
            // so replaying them here would only duplicate traffic (and an initial
            // locale binding would trigger a spurious boot-time refresh).
            for (const key of Object.keys(changes)) {
                if (!changes[key].firstChange) {
                    this.pendingChanges[key] = changes[key];
                }
            }
        }
    }
    ngOnDestroy() {
        // Abort any in-flight AI request (potentially a 100k-char prompt)
        this.aiAbort?.abort();
        // Remove the window message listener - without this every destroyed
        // instance stays rooted forever and keeps processing viewer messages
        window.removeEventListener("message", this.messageHandler);
        // Clean up PDF.js event listeners
        this.teardownPdfJsEventBindings();
        // Settle in-flight promises and queued actions so consumer awaits don't
        // hang forever
        this.rejectPendingMessages("Viewer destroyed");
        this.actionQueueManager.clearQueues();
        // Release page-overlay embedded views
        this.destroyPageOverlays();
        // Clean up URL
        this.releaseUrl?.();
    }
    // #endregion
    // #region Message Handling Methods
    generateMessageId() {
        return `msg_${++this.messageIdCounter}_${Date.now()}`;
    }
    sendControlMessage(action, payload) {
        return new Promise((resolve, reject) => {
            const messageId = this.generateMessageId();
            const message = {
                type: "control-update",
                action,
                payload,
                id: messageId,
                timestamp: Date.now(),
            };
            this.pendingMessages.set(messageId, { resolve, reject });
            // Send message to iframe - verify accessibility (event-driven check).
            // targetOrigin "/" restricts delivery to our own origin (the viewer
            // assets are same-origin), matching the wrapper's outgoing direction.
            if (this.isIframeAccessible()) {
                this.iframe.nativeElement.contentWindow.postMessage(message, "/");
            }
            else {
                this.pendingMessages.delete(messageId);
                reject(new Error("Iframe not available"));
            }
        });
    }
    // Wrapper for sendControlMessage that re-queues actions on iframe unavailability.
    // The SAME action object (id, resolver) is re-queued so its status and the
    // caller's promise track the retry, with a bounded retry count.
    async sendControlMessageWithRequeue(actionObj) {
        try {
            return await this.sendControlMessage(actionObj.action, actionObj.payload);
        }
        catch (error) {
            // If iframe is not available, re-queue the action for retry when the
            // iframe becomes available or readiness increases
            if (error instanceof Error &&
                error.message === "Iframe not available" &&
                (actionObj.retries = (actionObj.retries ?? 0) + 1) <= 3) {
                actionObj.requeued = true;
                this.actionQueueManager.queueAction(actionObj, actionObj.level ?? this.getRequiredReadinessLevel(actionObj.action));
                // Trigger processing if iframe becomes available soon (event-driven, no polling)
                Promise.resolve().then(() => {
                    if (this.isIframeAccessible() && this.isPostMessageReady) {
                        this.actionQueueManager.processQueuedActions();
                    }
                });
            }
            // Re-throw to maintain error handling in ActionQueueManager
            throw error;
        }
    }
    // Event-driven iframe accessibility check (no polling, trust-based)
    isIframeAccessible() {
        return !!(this.iframe &&
            this.iframe.nativeElement &&
            this.iframe.nativeElement.contentWindow);
    }
    // Process actions on every postmessage-ready (the wrapper announces each
    // readiness increase: levels 3, 4 and 5 of a single load)
    processPostMessageReadyActions() {
        this.actionQueueManager.updateReadiness(this.postMessageReadiness);
        this.actionQueueManager.processQueuedActions();
        // Apply the configuration snapshot ONCE per document load. The flag is
        // reset when the iframe navigates (pdfSrc change / refresh), which is
        // what makes reloads reconfigure the fresh viewer.
        if (!this.initialConfigQueued) {
            this.initialConfigQueued = true;
            this.queueAllConfigurations();
        }
        // Apply any pending changes that occurred before PostMessage API was ready
        this.applyPendingChanges();
    }
    handleControlResponse(response) {
        const pendingMessage = this.pendingMessages.get(response.id);
        if (pendingMessage) {
            this.pendingMessages.delete(response.id);
            if (response.success) {
                pendingMessage.resolve(response);
            }
            else {
                pendingMessage.reject(new Error(response.error || "Unknown error"));
            }
        }
    }
    // Named handler so ngOnDestroy can remove it. Arrow field keeps `this` bound.
    messageHandler = (event) => {
        // Only accept messages from this component's own viewer iframe. This
        // prevents cross-talk between multiple viewer instances on one page and
        // spoofed messages from unrelated windows.
        if (!this.iframe?.nativeElement?.contentWindow ||
            event.source !== this.iframe.nativeElement.contentWindow ||
            !event.data) {
            return;
        }
        switch (event.data.type) {
            case "control-response":
                this.handleControlResponse(event.data);
                return;
            case "ng2-pdfjs-viewer-security-warning":
                this.securityWarning = {
                    message: event.data.message,
                    originalUrl: event.data.originalUrl,
                    currentUrl: event.data.currentUrl,
                };
                this.cdr.markForCheck();
                return;
            case "postmessage-ready":
                this.isPostMessageReady = true;
                this.postMessageReadiness = event.data.readiness || 0;
                // Verify iframe is accessible before processing actions (event-driven readiness check)
                // This prevents "Iframe not available" errors when dialog reopens quickly
                if (this.isIframeAccessible()) {
                    this.processPostMessageReadyActions();
                }
                else {
                    // Defer to the microtask queue - handles Material Dialog lifecycle timing
                    Promise.resolve().then(() => {
                        if (this.isIframeAccessible()) {
                            this.processPostMessageReadyActions();
                        }
                    });
                }
                return;
            case "state-change":
                this.handleStateChangeNotification(event.data);
                return;
            case "event-notification":
                this.handleEventNotification(event.data);
                return;
            case "host-request":
                // Wrapper-initiated round-trip (signature storage hooks)
                void this.handleHostRequest(event.data);
                return;
        }
    };
    // Serve a wrapper-initiated request against the host-side hooks and post
    // the result back. Errors are returned (not thrown) so the wrapper's
    // pending promise always settles.
    async handleHostRequest(request) {
        const respond = (data, error) => {
            this.iframe?.nativeElement?.contentWindow?.postMessage({
                type: "host-response",
                requestId: request.requestId,
                data,
                error: error ?? null,
            }, "/");
        };
        const storage = this.signatureStorage;
        if (!storage) {
            respond(null, "No signatureStorage hook configured");
            return;
        }
        try {
            switch (request.action) {
                case "signature-storage-get-all":
                    respond((await storage.loadAll()) ?? {});
                    return;
                case "signature-storage-save":
                    await storage.save(request.payload?.uuid, request.payload?.data);
                    respond(true);
                    return;
                case "signature-storage-delete":
                    await storage.delete(request.payload?.uuid);
                    respond(true);
                    return;
                default:
                    respond(null, `Unknown host request: ${request.action}`);
            }
        }
        catch (e) {
            respond(null, e?.message || "signatureStorage hook failed");
        }
    }
    setupMessageListener() {
        window.addEventListener("message", this.messageHandler);
    }
    rejectPendingMessages(reason) {
        this.pendingMessages.forEach(({ reject }) => reject(new Error(reason)));
        this.pendingMessages.clear();
    }
    handleStateChangeNotification(notification) {
        const { property, value, source } = notification;
        // Internal loading overlay control is system-driven and must be handled unconditionally
        if (property === "loading") {
            this.isLoading = !!value;
            // Clear error state when loading starts
            if (value) {
                this.hasError = false;
                this.currentErrorMessage = "";
            }
            // Trigger change detection for OnPush scenarios (PostMessage runs outside Angular zone)
            this.cdr.markForCheck();
            return;
        }
        // Two-way [(formData)] sync from user edits in form widgets. Synthetic
        // events from our own set-form-data also land here - the equality check
        // suppresses the echo.
        if (property === "formData") {
            if (!shallowEquals(value, this._formData)) {
                this._formData = value ?? {};
                this.formDataChange.emit(this._formData);
                this.cdr.markForCheck();
            }
            return;
        }
        // Internal error state management
        if (property === "error") {
            this.hasError = !!value;
            if (value && typeof value === "string") {
                this.currentErrorMessage = this.composeErrorMessage(value);
                this.updateErrorTemplateData();
            }
            this.cdr.markForCheck();
            return;
        }
        // Only process user-initiated changes; value-equality checks below are
        // the echo suppression for our own programmatic updates
        if (source !== "user") {
            return;
        }
        switch (property) {
            case "cursor":
                if (this._cursor !== value) {
                    this._cursor = PropertyTransformers.transformCursor.fromViewer(value);
                    this.cursorChange.emit(this._cursor);
                }
                break;
            case "scroll":
                if (this._scroll !== value) {
                    this._scroll = PropertyTransformers.transformScroll.fromViewer(value);
                    this.scrollChange.emit(this._scroll);
                }
                break;
            case "spread":
                if (this._spread !== value) {
                    this._spread = PropertyTransformers.transformSpread.fromViewer(value);
                    this.spreadChange.emit(this._spread);
                }
                break;
            case "pageMode":
                if (this._pageMode !== value) {
                    this._pageMode = PropertyTransformers.transformPageMode.fromViewer(value);
                    this.pageModeChange.emit(this._pageMode);
                }
                break;
            case "zoom":
            case "rotation":
                // Intentionally ignored: the direct eventBus bindings in
                // bindToPdfJsEventBus are the single channel for zoom/rotation (the
                // wrapper's copy reports a differently-formatted value).
                break;
            default:
                if (this.diagnosticLogs) {
                    console.log(`PdfJsViewer: Unknown state change property: ${property}`);
                }
        }
    }
    // Apply the documented errorMessage/errorAppend semantics to the raw
    // viewer error before display
    composeErrorMessage(raw) {
        if (this.errorMessage) {
            return this.errorAppend ? `${raw} ${this.errorMessage}` : this.errorMessage;
        }
        return raw;
    }
    handleEventNotification(notification) {
        const { eventName, eventData } = notification;
        if (typeof eventName !== "string" || eventName.length === 0) {
            return;
        }
        // New document: the AI panel's extracted text and chat refer to the old
        // one - drop them. (Falls through to the generic emitter.)
        if (eventName === "documentInit") {
            this.invalidateAiState(true);
            this.cdr.markForCheck();
        }
        // Page add/delete/reorder invalidates extracted text and its page
        // numbers, but the chat history is still about this document.
        if (eventName === "pagesEdited") {
            this.aiDocText = undefined;
        }
        // A failed document load means queued document-gated actions can never
        // run - settle them (and fail-fast later dispatches via the latch) so
        // consumer awaits don't hang forever.
        if (eventName === "documentError") {
            this.documentLoadFailed = true;
            this.actionQueueManager.failDocumentActions("Document failed to load: " + (eventData?.message || "unknown error"));
        }
        // Any sign of a (new) document coming up lifts the latch
        if (eventName === "documentInit" || eventName === "pagesInit") {
            this.documentLoadFailed = false;
        }
        // Mount the per-page overlay template as pages (re-)render. PDF.js may
        // drop appended children on re-render; mounting is idempotent and moves
        // the same embedded-view nodes back in.
        if (eventName === "pageRendered" && this.pageOverlayTpl) {
            const pageNumber = eventData?.pageNumber;
            if (typeof pageNumber === "number") {
                this.mountPageOverlay(pageNumber);
            }
            // fall through to the generic emitter below
        }
        // Two-way [(annotationEditor)] sync: the wrapper relays every editor mode
        // switch, including echoes of our own dispatches - only real changes
        // (user toolbar clicks) update the property and emit.
        if (eventName === "annotationEditorModeChange") {
            const mode = eventData?.mode;
            if (mode && mode !== this._annotationEditor) {
                this._annotationEditor = mode;
                this.annotationEditorChange.emit(mode);
                this.cdr.markForCheck();
            }
            return;
        }
        // 'documentError' -> this.onDocumentError, etc.
        const emitter = this["on" + eventName.charAt(0).toUpperCase() + eventName.slice(1)];
        if (emitter instanceof EventEmitter) {
            emitter.emit(eventData ?? undefined);
        }
        else if (this.diagnosticLogs) {
            console.log(`PdfJsViewer: Unknown event notification: ${eventName}`);
        }
    }
    // #endregion
    // #region Property Mapping and Update Methods
    applyChanges(changes) {
        let needsRefresh = false;
        for (const propertyName of Object.keys(changes)) {
            const change = changes[propertyName];
            if (change.currentValue === change.previousValue) {
                continue;
            }
            // PDF.js applies locale only before initialization - reload to switch
            if (propertyName === "locale" ||
                (propertyName === "viewerConfig" &&
                    change.currentValue?.locale !== change.previousValue?.locale)) {
                needsRefresh = true;
                if (propertyName === "locale") {
                    continue;
                }
            }
            // Init-time PDF.js options ride the viewer URL and are read before the
            // postMessage channel exists - a change requires reloading the viewer
            if (propertyName === "pdfJsOptions" ||
                propertyName === "enableSignatureEditor" ||
                propertyName === "enableCommentEditor" ||
                propertyName === "enablePageEditing" ||
                propertyName === "pageColors") {
                needsRefresh = true;
                continue;
            }
            if (DOCUMENT_LOAD_PROPS.has(propertyName) ||
                SETTER_DISPATCHED_PROPS.has(propertyName)) {
                continue;
            }
            // Config-object inputs: their setters already copied the values onto
            // the individual properties - propagate those to the viewer. Keys the
            // config never set stay undefined and are skipped. Same-content objects
            // (fresh references from getter bindings) are not real changes.
            const fanout = CONFIG_FANOUT[propertyName];
            if (fanout) {
                if (shallowEquals(change.currentValue, change.previousValue)) {
                    continue;
                }
                for (const prop of fanout) {
                    const entry = REGISTRY_BY_PROP[prop];
                    const v = entry?.get ? entry.get(this) : this[prop];
                    if (v !== undefined) {
                        this.dispatchRegisteredProperty(prop, v);
                    }
                }
                continue;
            }
            this.dispatchRegisteredProperty(propertyName, change.currentValue);
        }
        if (needsRefresh) {
            this.refresh();
        }
    }
    // Dispatch one registry-backed property to the viewer. Without an explicit
    // value the current (setter-normalized) property value is used.
    dispatchRegisteredProperty(prop, value) {
        const entry = REGISTRY_BY_PROP[prop];
        if (!entry) {
            return; // not viewer-backed (handled component-side or via URL)
        }
        const v = value !== undefined ? value : entry.get ? entry.get(this) : this[prop];
        this.dispatchAction(entry.action, entry.payload ? entry.payload(v, this) : v, "property-change");
    }
    applyPendingChanges() {
        // Only apply pending changes if PostMessage API is ready
        if (!this.isPostMessageReady) {
            return;
        }
        const changes = this.pendingChanges;
        this.pendingChanges = {};
        if (Object.keys(changes).length > 0) {
            this.applyChanges(changes);
        }
    }
    // #endregion
    // #region PDF.js Event Binding Methods
    /**
     * Waits for the PDF.js viewer to be ready, and binds the the event bus.
     */
    bindToPdfJsEventBus() {
        // Store the event listener reference so we can remove it later
        const webviewerLoadedHandler = (event) => {
            // For same-origin embeds PDF.js dispatches 'webviewerloaded' on the
            // PARENT document, so this handler hears the event from EVERY viewer
            // iframe on the page. Only react to our own iframe's dispatch -
            // otherwise instance A re-binds (and duplicates) its eventBus handlers
            // whenever instance B loads, multiplying every relayed event and
            // auto-action.
            const source = event?.detail?.source;
            if (source && source !== this.iframe?.nativeElement?.contentWindow) {
                return;
            }
            if (this.diagnosticLogs)
                console.debug("PdfJsViewer: webviewerloaded event received");
            // Set locale immediately when PDF.js is loaded but before it initializes
            // https://github.com/mozilla/pdf.js/issues/11829#issuecomment-617668679
            if (this.locale && this.iframe?.nativeElement?.contentWindow) {
                try {
                    const iframeWindow = this.iframe.nativeElement.contentWindow;
                    if (iframeWindow.PDFViewerApplicationOptions) {
                        iframeWindow.PDFViewerApplicationOptions.set("localeProperties", {
                            lang: this.locale,
                        });
                        if (this.diagnosticLogs)
                            console.debug(`PdfJsViewer: Locale set to ${this.locale} before initialization`);
                    }
                }
                catch (error) {
                    if (this.diagnosticLogs)
                        console.debug("PdfJsViewer: Could not set locale before initialization:", error);
                }
            }
            if (!this.PDFViewerApplication) {
                if (this.diagnosticLogs)
                    console.debug("PdfJsViewer: Viewer not yet (or no longer) available, events can not yet be bound.");
                return;
            }
            // https://github.com/mozilla/pdf.js/issues/9527
            this.PDFViewerApplication.initializedPromise.then(() => {
                // Apply any pending changes that occurred before initialization
                this.applyPendingChanges();
                const eventBus = this.PDFViewerApplication.eventBus;
                const handlers = {
                    documentloaded: () => {
                        this.documentLoaded = true;
                        if (this.diagnosticLogs)
                            console.debug("PdfJsViewer: The document has now been loaded!");
                        this.onDocumentLoad.emit();
                        // Project onPagesInit here. PDF.js fires the real 'pagesinit' once,
                        // BEFORE this handler map finishes registering (it lands between
                        // 'pagesinit' and 'scalechanging'), and the postMessage wrapper's own
                        // pagesInit relay is wired on 'documentloaded' too late for the
                        // one-shot - so onPagesInit never fired and the signals entry point's
                        // loaded()/totalPages() stayed empty. 'documentloaded' is reliably
                        // caught (it drives onDocumentLoad) and pagesCount is set by now.
                        const app = this.PDFViewerApplication;
                        const pagesCount = app?.pagesCount ?? app?.pdfDocument?.numPages;
                        if (typeof pagesCount === "number" && pagesCount > 0) {
                            this.onPagesInit.emit({ pagesCount });
                        }
                        // Queue auto-actions with the property values current at THIS load
                        this.queueAutoActionsForDocumentLoad();
                        // Execute all queued auto-actions
                        this.actionQueueManager.onDocumentLoaded();
                    },
                    pagesloaded: () => {
                        // Auto-print here: the PDF is fully ready for printing
                        if (this.printOnLoad === true) {
                            this.dispatchAction("trigger-print", true, "initial-load");
                        }
                    },
                    beforeprint: () => this.onBeforePrint.emit(),
                    afterprint: () => this.onAfterPrint.emit(),
                    pagechanging: (event) => {
                        this._page = event.pageNumber;
                        this.onPageChange.emit(event.pageNumber);
                    },
                    rotationchanging: (event) => {
                        this._rotation = PropertyTransformers.transformRotation.fromViewer(event.pagesRotation);
                        const newRotation = {
                            rotation: event.pagesRotation,
                            page: event.pageNumber,
                        };
                        this.onRotationChange.emit(newRotation);
                    },
                    scalechanging: (event) => {
                        // Named zooms (page-fit, page-width, ...) arrive as presetValue
                        // alongside the resolved numeric scale; prefer the name so
                        // zoomChange round-trips named values instead of leaking numbers.
                        const normalizedZoom = typeof event.presetValue === "string" && event.presetValue
                            ? event.presetValue
                            : PropertyTransformers.transformZoom.fromViewer(event.scale);
                        // Value-echo suppression: emit only when the value actually changed
                        if (this._zoom !== normalizedZoom) {
                            this._zoom = normalizedZoom;
                            this.zoomChange.emit(normalizedZoom);
                        }
                        this.onScaleChange.emit(event.scale);
                    },
                };
                // The eventBus invokes these from the iframe realm, outside the
                // parent's NgZone - without re-entering the zone, consumer bindings
                // driven by these outputs ((onPageChange), [(zoom)], ...) never
                // schedule change detection in zone-based apps.
                const zonedHandlers = {};
                for (const eventName of Object.keys(handlers)) {
                    const handler = handlers[eventName];
                    zonedHandlers[eventName] = (event) => {
                        if (this.ngZone) {
                            this.ngZone.run(() => handler(event));
                        }
                        else {
                            handler(event);
                        }
                    };
                }
                // Store the registered (zoned) functions so teardown removes the
                // exact listeners that were added
                this.pdfEventHandlers = zonedHandlers;
                for (const eventName of Object.keys(zonedHandlers)) {
                    eventBus.on(eventName, zonedHandlers[eventName]);
                }
            });
        };
        // Store the handler reference for cleanup / re-binding on refresh()
        this.webviewerLoadedHandler = webviewerLoadedHandler;
        document.addEventListener("webviewerloaded", webviewerLoadedHandler);
    }
    teardownPdfJsEventBindings() {
        if (this.webviewerLoadedHandler) {
            document.removeEventListener("webviewerloaded", this.webviewerLoadedHandler);
            this.webviewerLoadedHandler = undefined;
        }
        if (this.pdfEventHandlers) {
            const eventBus = this.PDFViewerApplication?.eventBus;
            if (eventBus) {
                for (const eventName of Object.keys(this.pdfEventHandlers)) {
                    eventBus.off(eventName, this.pdfEventHandlers[eventName]);
                }
            }
            this.pdfEventHandlers = undefined;
        }
    }
    // #endregion
    // #region Configuration and Action Queue Methods
    // Snapshot every registry-backed property and queue it for the (re)loaded
    // viewer, then enable wrapper-side event notifications.
    queueAllConfigurations() {
        // The whole configuration snapshot ships as ONE batched 'configure'
        // message per readiness level (instead of ~40 individual messages).
        // The wrapper replays each step through its normal control dispatch.
        const batches = new Map();
        const add = (level, action, payload) => {
            let steps = batches.get(level);
            if (!steps) {
                batches.set(level, (steps = []));
            }
            steps.push({ action, payload });
        };
        for (const entry of PROPERTY_REGISTRY) {
            if (entry.init === false) {
                continue;
            }
            const value = entry.get ? entry.get(this) : this[entry.prop];
            const send = entry.init === "always" ||
                (entry.init === "truthy" && !!value) ||
                (entry.init === "defined" && value !== undefined) ||
                (entry.init === "true" && value === true) ||
                (entry.init === "nonempty" &&
                    typeof value === "string" &&
                    value.trim() !== "");
            if (send) {
                add(entry.level, entry.action, entry.payload ? entry.payload(value, this) : value);
            }
        }
        for (const action of ENABLE_EVENT_ACTIONS) {
            add(this.getRequiredReadinessLevel(action), action, true);
        }
        // Idle is opt-in: enabling it installs document-wide activity listeners
        // in the iframe, so only pay for it when someone listens
        if (hasObservers(this.onIdle)) {
            add(this.getRequiredReadinessLevel("enable-idle"), "enable-idle", true);
        }
        // Ascending level order so lower-readiness batches apply first
        for (const level of [...batches.keys()].sort((a, b) => a - b)) {
            this.dispatchAction("configure", batches.get(level), "initial-load", level);
        }
    }
    queueAutoActionsForDocumentLoad() {
        // Use universal dispatcher for auto-actions
        if (this.downloadOnLoad === true) {
            this.dispatchAction("trigger-download", true, "initial-load");
        }
        if (this.showLastPageOnLoad === true) {
            this.dispatchAction("go-to-last-page", true, "initial-load");
        }
        // Auto-print is handled in the pagesloaded handler for timing reasons
    }
    refresh() {
        // Needs to be invoked for external window or when needs to reload pdf
        // The reload swaps the document out from under the AI panel - drop its
        // extracted text/chat and abandon any in-flight request
        this.invalidateAiState(true);
        // Remove stale PDF.js bindings, then re-arm webviewerloaded so the
        // reloaded viewer's events bind again (they used to die after refresh)
        this.teardownPdfJsEventBindings();
        this.bindToPdfJsEventBus();
        // Settle in-flight messages and queued actions from the old load
        this.rejectPendingMessages("Viewer reloading");
        this.actionQueueManager.reset();
        // Reset PostMessage readiness state
        this.isPostMessageReady = false;
        this.postMessageReadiness = 0;
        this.initialConfigQueued = false;
        // Reload the PDF - this will trigger queueAllConfigurations() when PostMessage API is ready
        this.loadPdf();
    }
    // Public method for external control messages
    sendViewerControlMessage(action, payload) {
        return this.sendControlMessage(action, payload);
    }
    // #region Public Methods for On-Demand Actions
    triggerDownload() {
        // Use universal dispatcher for user interactions - now always returns Promise<ActionExecutionResult>
        return this.dispatchAction("trigger-download", true, "user-interaction");
    }
    triggerPrint() {
        // Use universal dispatcher for user interactions - now always returns Promise<ActionExecutionResult>
        return this.dispatchAction("trigger-print", true, "user-interaction");
    }
    setPage(page) {
        // Use universal dispatcher for user interactions - now always returns Promise<ActionExecutionResult>
        return this.dispatchAction("set-page", page, "user-interaction");
    }
    setZoom(zoom) {
        // Use universal dispatcher for user interactions - now always returns Promise<ActionExecutionResult>
        return this.dispatchAction("set-zoom", zoom, "user-interaction");
    }
    goToLastPage() {
        // Use universal dispatcher for user interactions - now always returns Promise<ActionExecutionResult>
        return this.dispatchAction("go-to-last-page", true, "user-interaction");
    }
    setCursor(cursor) {
        // Use universal dispatcher for user interactions
        return this.dispatchAction("set-cursor", cursor, "user-interaction");
    }
    setScroll(scroll) {
        // Use universal dispatcher for user interactions
        return this.dispatchAction("set-scroll", scroll, "user-interaction");
    }
    setSpread(spread) {
        // Use universal dispatcher for user interactions
        return this.dispatchAction("set-spread", spread, "user-interaction");
    }
    triggerRotation(direction) {
        // Use universal dispatcher for user interactions
        const action = direction === "cw" ? "trigger-rotate-cw" : "trigger-rotate-ccw";
        return this.dispatchAction(action, true, "user-interaction");
    }
    goToPage(page) {
        // Alias for setPage for backward compatibility
        return this.setPage(page);
    }
    // #endregion
    // #region Annotation & Search API
    /**
     * Serialized state of every annotation created or modified in the editor.
     * Send this to a server to persist user annotations.
     */
    async getAnnotations() {
        const result = await this.dispatchAction("get-annotations", null, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "getAnnotations failed");
        }
        return result.data ?? [];
    }
    /**
     * Restore annotations previously exported with getAnnotations() back into
     * the editor. Each annotation is rebuilt on its own page; annotations for
     * pages that haven't rendered yet apply automatically as those pages
     * render (counted in `pending`). Items with an invalid pageIndex for the
     * current document are skipped (counted in `rejected`). Calling this twice
     * with the same payload creates duplicates - restore is additive. Note:
     * stamp images cannot round-trip (their bitmaps are not serializable).
     */
    async setAnnotations(annotations) {
        const result = await this.dispatchAction("set-annotations", annotations ?? [], "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "setAnnotations failed");
        }
        return result.data ?? { restored: 0, pending: 0, rejected: 0 };
    }
    /**
     * The current document - including annotation edits and filled form
     * fields - as a Blob, ready for upload or download.
     */
    async getDocumentAsBlob() {
        const result = await this.dispatchAction("save-document", null, "user-interaction");
        if (!result.success || !result.data?.bytes) {
            throw new Error(result.error || "getDocumentAsBlob failed");
        }
        return new Blob([result.data.bytes], { type: "application/pdf" });
    }
    /**
     * Programmatic full-text search. Resolves with totals, per-page match
     * counts and the pages containing matches; matches are highlighted in the
     * viewer (highlightAll defaults to true).
     */
    async search(query, options) {
        const result = await this.dispatchAction("search", { query, ...(options ?? {}) }, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "search failed");
        }
        return result.data;
    }
    /** Move the search selection to the next match. */
    async searchNext() {
        const result = await this.dispatchAction("search-next", null, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "searchNext failed");
        }
        return result.data;
    }
    /** Move the search selection to the previous match. */
    async searchPrevious() {
        const result = await this.dispatchAction("search-previous", null, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "searchPrevious failed");
        }
        return result.data;
    }
    /** Clear search highlights and forget the active query. */
    clearSearch() {
        return this.dispatchAction("clear-search", null, "user-interaction");
    }
    // Embedded views for pageOverlayTpl, keyed by page number. Views are
    // attached to ApplicationRef so bindings inside stay live.
    overlayViews = new Map();
    mountPageOverlay(pageNumber, knownPageEl) {
        if (!this.pageOverlayTpl || this.externalWindow)
            return;
        const doc = this.iframe?.nativeElement?.contentDocument;
        // Callers iterating already-rendered pages pass the element they hold, so
        // we skip re-finding it by selector (saves one DOM query per page on large
        // documents); the per-page render path passes nothing and looks it up.
        const pageEl = knownPageEl ??
            doc?.querySelector(`.pdfViewer .page[data-page-number="${pageNumber}"]`);
        if (!pageEl || pageEl.querySelector(":scope > .ng2-page-overlay")) {
            return;
        }
        let view = this.overlayViews.get(pageNumber);
        if (!view) {
            view = this.pageOverlayTpl.createEmbeddedView({ $implicit: pageNumber });
            this.appRef?.attachView(view);
            view.detectChanges();
            this.overlayViews.set(pageNumber, view);
        }
        const wrapper = doc.createElement("div");
        wrapper.className = "ng2-page-overlay";
        for (const node of view.rootNodes) {
            wrapper.appendChild(node);
        }
        pageEl.appendChild(wrapper);
    }
    destroyPageOverlays() {
        for (const view of this.overlayViews.values()) {
            this.appRef?.detachView(view);
            view.destroy();
        }
        this.overlayViews.clear();
        // Destroying the views removes their nodes but not the wrapper divs;
        // leftover wrappers would also block the remount guard in mountPageOverlay.
        const doc = this.iframe?.nativeElement?.contentDocument;
        doc
            ?.querySelectorAll(".ng2-page-overlay")
            .forEach((el) => el.remove());
    }
    // Mount overlays on every page div that already exists (used when the
    // template input is set after pages have rendered, e.g. a toggle).
    mountOverlaysOnRenderedPages() {
        const doc = this.iframe?.nativeElement?.contentDocument;
        if (!doc)
            return;
        doc
            .querySelectorAll(".pdfViewer .page[data-page-number]")
            .forEach((el) => {
            const pageNumber = Number(el.getAttribute("data-page-number"));
            if (pageNumber > 0) {
                this.mountPageOverlay(pageNumber, el);
            }
        });
    }
    /**
     * Plain text of the document (or a 1-based page range), extracted from the
     * PDF.js text layer. The raw material for BYO-AI chat/summarize flows.
     */
    async getDocumentText(from, to) {
        // The document may not have finished loading the instant this is called
        // (e.g. an AI 'ask' fired immediately after the viewer appears). Wait for
        // 'documentloaded' so we don't extract empty text; fall back on timeout.
        if (!this.documentLoaded) {
            await this.waitForDocumentLoad(15000);
        }
        const result = await this.dispatchAction("get-document-text", { from, to }, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "getDocumentText failed");
        }
        return result.data ?? [];
    }
    /**
     * Resolve once the current document has loaded, or after timeoutMs (in which
     * case callers proceed with whatever the viewer can provide). Never rejects.
     */
    waitForDocumentLoad(timeoutMs) {
        if (this.documentLoaded)
            return Promise.resolve();
        return new Promise((resolve) => {
            let settled = false;
            const finish = () => {
                if (settled)
                    return;
                settled = true;
                sub.unsubscribe();
                clearTimeout(timer);
                resolve();
            };
            const sub = this.onDocumentLoad.subscribe(() => finish());
            const timer = setTimeout(finish, timeoutMs);
        });
    }
    /**
     * Read the document aloud from the current (or given) page using the
     * browser's speech synthesis. Progress arrives on onReadAloudStateChange.
     */
    startReadAloud(options) {
        return this.dispatchAction("read-aloud", { command: "start", ...(options ?? {}) }, "user-interaction");
    }
    pauseReadAloud() {
        return this.dispatchAction("read-aloud", { command: "pause" }, "user-interaction");
    }
    resumeReadAloud() {
        return this.dispatchAction("read-aloud", { command: "resume" }, "user-interaction");
    }
    stopReadAloud() {
        return this.dispatchAction("read-aloud", { command: "stop" }, "user-interaction");
    }
    /**
     * Ask the built-in AI panel a question programmatically (same path the
     * panel's input uses). Requires [aiAssistantConfig]. Document text is
     * extracted once per document and reused across questions.
     */
    async aiAsk(question) {
        const q = (question || "").trim();
        const config = this.aiAssistantConfig;
        if (!q || this.aiBusy || !config) {
            return;
        }
        this.aiBusy = true;
        const generation = this.aiGeneration;
        this.aiAbort = new AbortController();
        const signal = this.aiAbort.signal;
        this.aiMessages.push({ role: "user", content: q, parts: [{ text: q }] });
        // Placeholder assistant turn we stream tokens into as they arrive.
        const assistant = { role: "assistant", content: "", parts: [] };
        this.aiMessages.push(assistant);
        this.cdr.markForCheck();
        try {
            if (!this.aiClient || this.aiClientConfig !== config) {
                this.aiClient = new PdfAiAssistant(config);
                this.aiClientConfig = config;
            }
            if (!this.aiDocText) {
                this.aiDocText = await this.getDocumentText();
            }
            const history = this.aiMessages
                .slice(0, -2)
                .filter((m) => !m.error)
                .map((m) => ({ role: m.role, content: m.content }));
            const answer = await this.aiClient.ask(q, this.aiDocText, history, signal, (full) => {
                if (generation !== this.aiGeneration) {
                    return; // stale stream - ignore late tokens
                }
                assistant.content = full;
                assistant.parts = this.parseAiCitations(full);
                this.cdr.markForCheck();
            });
            if (generation !== this.aiGeneration) {
                return; // document changed mid-flight - stale answer
            }
            assistant.content = answer;
            assistant.parts = this.parseAiCitations(answer);
        }
        catch (e) {
            if (generation !== this.aiGeneration) {
                return; // aborted by invalidation - already cleaned up
            }
            assistant.error = e?.message || "AI request failed";
        }
        finally {
            if (generation === this.aiGeneration) {
                this.aiBusy = false;
            }
            this.cdr.markForCheck();
        }
    }
    // Split an answer into text runs and clickable [p.N] page citations
    parseAiCitations(text) {
        const parts = [];
        const re = /\[p\.?\s*(\d+)\]/gi;
        let last = 0;
        let m;
        while ((m = re.exec(text)) !== null) {
            if (m.index > last) {
                parts.push({ text: text.slice(last, m.index) });
            }
            parts.push({ page: parseInt(m[1], 10) });
            last = m.index + m[0].length;
        }
        if (last < text.length) {
            parts.push({ text: text.slice(last) });
        }
        return parts;
    }
    /**
     * Current AcroForm field values (field name -> value), reflecting any
     * user edits. Returns {} for documents without form fields.
     */
    async getFormData() {
        const result = await this.dispatchAction("get-form-data", null, "user-interaction");
        if (!result.success) {
            throw new Error(result.error || "getFormData failed");
        }
        return (result.data ?? {});
    }
    /** Set a single form field by name. */
    setFormField(name, value) {
        return this.dispatchAction("set-form-field", { name, value }, "user-interaction");
    }
    // #endregion
    // Action queue management methods
    getActionStatus(actionId) {
        return this.actionQueueManager.getActionStatus(actionId);
    }
    getQueueStatus() {
        return this.actionQueueManager.getQueueStatus();
    }
    clearActionQueue() {
        this.actionQueueManager.clearQueues();
    }
    /**
     * Enable or disable URL validation security feature
     * When enabled, prevents users from modifying the file parameter in the viewer URL
     * @param enabled - Whether to enable URL validation (default: true)
     * @returns Promise<ActionExecutionResult>
     */
    setUrlValidation(enabled = true) {
        return this.dispatchAction("set-url-validation", enabled, "user-interaction");
    }
    /**
     * Dismiss the security warning
     */
    dismissSecurityWarning() {
        this.securityWarning = null;
        // Public API called from outside this component's CD context - mark so
        // the overlay clears under OnPush/zoneless consumers
        this.cdr.markForCheck();
    }
    // #endregion
    // #region PDF Loading and URL Handling
    loadPdf() {
        if (!this._src)
            return;
        // Show spinner immediately when PDF loading starts (Issue #275).
        // markForCheck: loadPdf is reachable from the public refresh() API, where
        // no input-change CD pass marks this (possibly OnPush) view.
        this.isLoading = true;
        this.hasError = false;
        this.currentErrorMessage = "";
        // A new load lifts the failed-document latch (the documentInit relay
        // also clears it, but that is enablement-gated and can race fast loads)
        this.documentLoadFailed = false;
        // Text isn't extractable until the new document finishes loading.
        this.documentLoaded = false;
        this.cdr.markForCheck();
        if (!this.setupExternalWindow()) {
            return; // popup blocked - nothing to navigate
        }
        // Authenticated fetch path: the viewer iframe cannot attach headers to
        // its own request, so the component downloads the document and feeds the
        // viewer a local blob instead.
        if (typeof this._src === "string" &&
            (this.httpHeaders || this.withCredentials)) {
            void this.fetchPdfWithAuth(this._src);
            return;
        }
        const fileUrl = this.createFileUrl();
        const viewerUrl = this.buildViewerUrl(fileUrl);
        this.navigateToViewer(viewerUrl);
    }
    async fetchPdfWithAuth(url) {
        const loadToken = ++this.authLoadToken;
        try {
            const response = await fetch(url, {
                headers: this.httpHeaders ?? {},
                credentials: this.withCredentials ? "include" : "same-origin",
            });
            if (!response.ok) {
                throw new Error(`HTTP ${response.status} ${response.statusText}`);
            }
            let blob;
            if (response.body && hasObservers(this.onProgress)) {
                const total = Number(response.headers.get("content-length")) || 0;
                const reader = response.body.getReader();
                const chunks = [];
                let loaded = 0;
                for (;;) {
                    const { done, value } = await reader.read();
                    if (done)
                        break;
                    chunks.push(value);
                    loaded += value.byteLength;
                    this.onProgress.emit({ loaded, total });
                }
                blob = new Blob(chunks, { type: "application/pdf" });
            }
            else {
                blob = await response.blob();
            }
            if (loadToken !== this.authLoadToken) {
                return; // pdfSrc changed while downloading - drop the stale result
            }
            // Hand the bytes to the normal blob path without touching the
            // consumer's pdfSrc (it stays the original string URL).
            this.releaseUrl?.();
            const objectUrl = URL.createObjectURL(blob);
            this.releaseUrl = () => URL.revokeObjectURL(objectUrl);
            const viewerUrl = this.buildViewerUrl(encodeURIComponent(objectUrl));
            this.navigateToViewer(viewerUrl);
        }
        catch (error) {
            if (loadToken !== this.authLoadToken) {
                return;
            }
            const message = error instanceof Error ? error.message : String(error);
            this.isLoading = false;
            this.hasError = true;
            this.currentErrorMessage = this.composeErrorMessage(`Failed to fetch PDF: ${message}`);
            this.updateErrorTemplateData();
            this.onDocumentError.emit({
                message: this.currentErrorMessage,
                name: "FetchError",
                source: url,
            });
            this.cdr.markForCheck();
        }
    }
    // Returns false when an external window is required but could not be opened
    setupExternalWindow() {
        if (!this.externalWindow)
            return true;
        if (typeof this.viewerTab === "undefined" || this.viewerTab.closed) {
            this.viewerTab = window.open("", this.target, this.externalWindowOptions || "");
            if (this.viewerTab == null) {
                // Always surface this - it's an actionable consumer-facing failure
                console.error("ng2-pdfjs-viewer: 'externalWindow = true' requires pop-ups to be enabled.");
                return false;
            }
            if (this.showSpinner) {
                this.renderLoadingSpinner();
            }
        }
        return true;
    }
    renderLoadingSpinner() {
        this.viewerTab.document.write(`
          <style>
          .loader {
            position: fixed;
            left: 40%;
            top: 40%;
            border: 16px solid #f3f3f3;
            border-radius: 50%;
            border-top: 16px solid #3498db;
            width: 120px;
            height: 120px;
            animation: spin 2s linear infinite;
          }
          @keyframes spin {
            0% {
              transform: rotate(0deg);
            }
            100% {
              transform: rotate(360deg);
            }
          }
          </style>
          <div class="loader"></div>
        `);
    }
    createFileUrl() {
        this.releaseUrl?.();
        if (this._src instanceof Blob) {
            const url = URL.createObjectURL(this._src);
            this.releaseUrl = () => URL.revokeObjectURL(url);
            return encodeURIComponent(url);
        }
        else if (this._src instanceof Uint8Array) {
            // A typed-array view is a valid BlobPart; using it directly respects
            // byteOffset/byteLength (the raw .buffer of a subarray would not)
            const blob = new Blob([this._src], { type: "application/pdf" });
            const url = URL.createObjectURL(blob);
            this.releaseUrl = () => URL.revokeObjectURL(url);
            return encodeURIComponent(url);
        }
        else {
            return this._src;
        }
    }
    buildViewerUrl(fileUrl) {
        const base = this.viewerFolder
            ? `${this.viewerFolder}/web/viewer.html`
            : `assets/pdfjs/web/viewer.html`;
        // Control params go in the query string first; the file URL is appended
        // LAST. A consumer's file URL may end in a hash fragment that PDF.js reads
        // for navigation (e.g. doc.pdf#search=foo, #page=2). Anything appended
        // after the file param would land inside that fragment instead of the
        // query string - so file trails the whole URL, leaving the hash where
        // PDF.js looks for it. (#305)
        let viewerUrl = `${base}?urlValidation=${this.urlValidation === false ? 0 : 1}`;
        if (typeof this.viewerId !== "undefined") {
            viewerUrl += `&viewerId=${this.viewerId}`;
        }
        // Init-time PDF.js options (signature editor, page colors, passthrough).
        // These are read by PDF.js during initialize() - before the postMessage
        // channel exists - so they ride the viewer URL and are applied by the
        // wrapper at 'webviewerloaded' (after module eval, before run()). The
        // wrapper validates every key against its allowlist.
        const initOptions = this.collectInitTimeOptions();
        if (Object.keys(initOptions).length > 0) {
            viewerUrl += `&pjsOptions=${encodeURIComponent(JSON.stringify(initOptions))}`;
        }
        // Cache-bust in Angular dev mode so editing pdfjs assets takes effect.
        // (Angular's own signal, not a hostname/port heuristic - production apps
        // served from localhost keep clean, cacheable viewer URLs.)
        if (isDevMode()) {
            viewerUrl += `&_t=${Date.now()}`;
        }
        // File last (see above): its optional #hash fragment must trail the URL.
        viewerUrl += `&file=${fileUrl}`;
        return viewerUrl;
    }
    // Merge the dedicated convenience inputs with the raw pdfJsOptions
    // passthrough (dedicated inputs win on conflict).
    collectInitTimeOptions() {
        const options = {
            ...(this.pdfJsOptions ?? {}),
        };
        if (this.enableSignatureEditor) {
            options["enableSignatureEditor"] = true;
        }
        if (this.enableCommentEditor) {
            options["enableComment"] = true;
        }
        if (this.enablePageEditing) {
            options["enableMerge"] = true;
            options["enableSplitMerge"] = true;
            options["enableUpdatedAddImage"] = true;
        }
        if (this.pageColors) {
            options["forcePageColors"] = true;
            options["pageColorsBackground"] = this.pageColors.background;
            options["pageColorsForeground"] = this.pageColors.foreground;
        }
        return options;
    }
    navigateToViewer(viewerUrl) {
        if (this.externalWindow) {
            this.viewerTab.location.href = viewerUrl;
        }
        else {
            this.iframe.nativeElement.contentWindow.location.replace(viewerUrl);
        }
        if (this.diagnosticLogs) {
            console.debug("PdfJsViewer: loading viewer", {
                viewerUrl,
                pdfSrc: this.pdfSrc,
                externalWindow: this.externalWindow,
                viewerFolder: this.viewerFolder,
                viewerId: this.viewerId,
            });
        }
    }
    // #endregion
    // External-window mode has no postMessage channel (the wrapper runs in the
    // popup, not the hidden iframe) - actions queue forever. Warn once instead
    // of failing silently.
    externalWindowWarned = false;
    // Set on documentError, cleared when a new load begins: document-gated
    // actions dispatched against a failed load settle immediately instead of
    // queueing forever.
    documentLoadFailed = false;
    // True once the current document fires 'documentloaded'. getDocumentText()
    // awaits this so an AI 'ask' fired before render doesn't extract empty text.
    documentLoaded = false;
    // Universal Action Dispatcher - ALL actions go through readiness-based queuing
    dispatchAction(action, payload, source = "property-change", level) {
        if (this.externalWindow && !this.externalWindowWarned) {
            this.externalWindowWarned = true;
            console.warn("PdfJsViewer: programmatic actions and event relays are not available " +
                "in externalWindow mode - the postMessage channel only connects to " +
                "the embedded iframe viewer.");
        }
        const requiredReadiness = level ?? this.getRequiredReadinessLevel(action);
        const actionObj = {
            id: `${source}-${action}-${++this.actionIdCounter}`,
            action: action,
            payload: payload,
            level: level,
        };
        // Document-gated action against a load that already failed: it can never
        // execute - settle now instead of queueing forever.
        if (requiredReadiness === 5 &&
            this.documentLoadFailed &&
            !this.actionQueueManager.isDocumentLoaded) {
            return Promise.resolve({
                actionId: actionObj.id,
                success: false,
                error: "Document failed to load",
                timestamp: Date.now(),
            });
        }
        // Check if we have sufficient readiness to execute immediately
        if (this.hasRequiredReadiness(requiredReadiness)) {
            return this.actionQueueManager.executeAction(actionObj);
        }
        // Queue the action; the promise settles when it actually executes (or
        // resolves with success:false if the queue is cleared first)
        this.actionQueueManager.queueAction(actionObj, requiredReadiness);
        return new Promise((resolve) => {
            actionObj.resolver = resolve;
        });
    }
    getRequiredReadinessLevel(action) {
        return ACTION_READINESS[action] ?? 3; // default: EVENTBUS_READY
    }
    hasRequiredReadiness(requiredLevel) {
        if (!this.isPostMessageReady)
            return false;
        if (requiredLevel === 5 && !this.actionQueueManager.isDocumentLoaded)
            return false;
        return this.postMessageReadiness >= requiredLevel;
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ApplicationRef }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
    static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "22.0.1", type: PdfJsViewerComponent, isStandalone: false, selector: "ng2-pdfjs-viewer", inputs: { viewerId: "viewerId", viewerFolder: "viewerFolder", externalWindow: "externalWindow", target: "target", showSpinner: "showSpinner", downloadFileName: "downloadFileName", locale: "locale", useOnlyCssZoom: "useOnlyCssZoom", externalLinkTarget: "externalLinkTarget", rememberLastView: "rememberLastView", annotationEditor: "annotationEditor", highlightEditorColors: "highlightEditorColors", enableSignatureEditor: "enableSignatureEditor", signatureStorage: "signatureStorage", pageColors: "pageColors", pdfJsOptions: "pdfJsOptions", enableCommentEditor: "enableCommentEditor", enablePageEditing: "enablePageEditing", showToolbar: "showToolbar", chromeless: "chromeless", customToolbarTpl: "customToolbarTpl", customSidebarTpl: "customSidebarTpl", aiAssistantConfig: "aiAssistantConfig", pageOverlayTpl: "pageOverlayTpl", httpHeaders: "httpHeaders", withCredentials: "withCredentials", formData: "formData", contentProtection: "contentProtection", iframeSandbox: "iframeSandbox", diagnosticLogs: "diagnosticLogs", showOpenFile: "showOpenFile", showAnnotations: "showAnnotations", showDownload: "showDownload", showViewBookmark: "showViewBookmark", showPrint: "showPrint", showFullScreen: "showFullScreen", showFind: "showFind", downloadOnLoad: "downloadOnLoad", printOnLoad: "printOnLoad", rotateCW: "rotateCW", rotateCCW: "rotateCCW", showLastPageOnLoad: "showLastPageOnLoad", namedDest: "namedDest", errorOverride: "errorOverride", errorAppend: "errorAppend", errorMessage: "errorMessage", urlValidation: "urlValidation", customSecurityTpl: "customSecurityTpl", theme: "theme", primaryColor: "primaryColor", backgroundColor: "backgroundColor", pageBorderColor: "pageBorderColor", pageSpacing: "pageSpacing", toolbarColor: "toolbarColor", textColor: "textColor", borderRadius: "borderRadius", customCSS: "customCSS", cspNonce: "cspNonce", iframeTitle: "iframeTitle", customSpinnerTpl: "customSpinnerTpl", spinnerClass: "spinnerClass", customErrorTpl: "customErrorTpl", errorClass: "errorClass", showToolbarLeft: "showToolbarLeft", showToolbarMiddle: "showToolbarMiddle", showToolbarRight: "showToolbarRight", showSecondaryToolbarToggle: "showSecondaryToolbarToggle", showSidebar: "showSidebar", showSidebarLeft: "showSidebarLeft", showSidebarRight: "showSidebarRight", toolbarDensity: "toolbarDensity", sidebarWidth: "sidebarWidth", toolbarPosition: "toolbarPosition", sidebarPosition: "sidebarPosition", responsiveBreakpoint: "responsiveBreakpoint", controlVisibility: "controlVisibility", autoActions: "autoActions", errorHandling: "errorHandling", viewerConfig: "viewerConfig", themeConfig: "themeConfig", groupVisibility: "groupVisibility", layoutConfig: "layoutConfig", startDownload: "startDownload", startPrint: "startPrint", openFile: "openFile", download: "download", print: "print", fullScreen: "fullScreen", find: "find", viewBookmark: "viewBookmark", lastPage: "lastPage", externalWindowOptions: "externalWindowOptions", iframeBorder: "iframeBorder", zoom: "zoom", rotation: "rotation", cursor: "cursor", scroll: "scroll", spread: "spread", pageMode: "pageMode", page: "page", pdfSrc: "pdfSrc" }, outputs: { onBeforePrint: "onBeforePrint", onAfterPrint: "onAfterPrint", onDocumentLoad: "onDocumentLoad", onPageChange: "onPageChange", onScaleChange: "onScaleChange", onRotationChange: "onRotationChange", onDocumentError: "onDocumentError", onDocumentInit: "onDocumentInit", onPagesInit: "onPagesInit", onPresentationModeChanged: "onPresentationModeChanged", onOpenFile: "onOpenFile", onFind: "onFind", onUpdateFindMatchesCount: "onUpdateFindMatchesCount", onMetadataLoaded: "onMetadataLoaded", onOutlineLoaded: "onOutlineLoaded", onPageRendered: "onPageRendered", onAnnotationLayerRendered: "onAnnotationLayerRendered", onBookmarkClick: "onBookmarkClick", onIdle: "onIdle", onPasswordPrompt: "onPasswordPrompt", onAnnotationEditorStateChange: "onAnnotationEditorStateChange", onPagesEdited: "onPagesEdited", onReadAloudStateChange: "onReadAloudStateChange", onSidebarViewChanged: "onSidebarViewChanged", onLayersChanged: "onLayersChanged", onNamedAction: "onNamedAction", onDocumentProperties: "onDocumentProperties", annotationEditorChange: "annotationEditorChange", onProgress: "onProgress", formDataChange: "formDataChange", zoomChange: "zoomChange", cursorChange: "cursorChange", scrollChange: "scrollChange", spreadChange: "spreadChange", pageModeChange: "pageModeChange" }, viewQueries: [{ propertyName: "iframe", first: true, predicate: ["iframe"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
    <div
      class="ng2-pdfjs-viewer-container"
      [class.ng2-has-custom-toolbar]="customToolbarTpl && !externalWindow"
      [class.ng2-has-custom-sidebar]="customSidebarTpl && !externalWindow"
    >
      <div
        class="ng2-pdfjs-custom-toolbar"
        *ngIf="customToolbarTpl && !externalWindow"
      >
        <ng-container
          [ngTemplateOutlet]="customToolbarTpl"
          [ngTemplateOutletContext]="{ $implicit: this }"
        ></ng-container>
      </div>
      <div
        class="ng2-pdfjs-custom-sidebar"
        *ngIf="customSidebarTpl && !externalWindow"
      >
        <ng-container
          [ngTemplateOutlet]="customSidebarTpl"
          [ngTemplateOutletContext]="{ $implicit: this }"
        ></ng-container>
      </div>
      <iframe
        [title]="iframeTitle || 'PDF document viewer'"
        [hidden]="externalWindow || (!externalWindow && !pdfSrc)"
        sandbox="allow-forms allow-scripts allow-same-origin allow-modals allow-downloads allow-popups allow-popups-to-escape-sandbox"
        [class]="getIframeClasses()"
        #iframe
        width="100%"
        height="100%"
      ></iframe>
      
      <div
        class="ng2-pdfjs-loading-overlay"
        *ngIf="showSpinner && isLoading && !externalWindow"
        [ngClass]="spinnerClass"
      >
        <ng-container
          *ngIf="customSpinnerTpl; else defaultSpinner"
          [ngTemplateOutlet]="customSpinnerTpl"
        ></ng-container>
        <ng-template #defaultSpinner>
          <div class="ng2-pdfjs-spinner-content">
            <div class="ng2-pdfjs-spinner-icon"></div>
            <div class="ng2-pdfjs-spinner-text">
              Loading PDF...
            </div>
          </div>
        </ng-template>
      </div>

      <div
        class="ng2-pdfjs-error-overlay"
        *ngIf="errorOverride && hasError && !externalWindow"
        [ngClass]="errorClass"
      >
        <ng-container
          *ngIf="customErrorTpl; else defaultError"
          [ngTemplateOutlet]="customErrorTpl"
          [ngTemplateOutletContext]="errorTemplateData"
        ></ng-container>
        <ng-template #defaultError>
          <div class="ng2-pdfjs-error-content">
            <div class="ng2-pdfjs-error-icon">
              ⚠️
            </div>
            <div class="ng2-pdfjs-error-title">
              Error Loading PDF
            </div>
            <div class="ng2-pdfjs-error-message">
              {{ currentErrorMessage }}
            </div>
          </div>
        </ng-template>
      </div>

      <button
        type="button"
        class="ng2-ai-fab"
        *ngIf="aiAssistantConfig && !externalWindow"
        (click)="aiPanelOpen = !aiPanelOpen"
        [attr.aria-expanded]="aiPanelOpen"
        aria-label="Ask AI about this document"
      >
        ✦
      </button>

      <div
        class="ng2-ai-panel"
        *ngIf="aiAssistantConfig && aiPanelOpen && !externalWindow"
        role="complementary"
        aria-label="AI assistant"
      >
        <div class="ng2-ai-head">
          <span>{{ aiAssistantConfig.title || 'Ask this document' }}</span>
          <button
            type="button"
            (click)="aiPanelOpen = false"
            aria-label="Close AI panel"
          >
            ×
          </button>
        </div>
        <div class="ng2-ai-msgs" aria-live="polite">
          <div
            class="ng2-ai-msg"
            *ngFor="let m of aiMessages"
            [class.ng2-ai-user]="m.role === 'user'"
          >
            <ng-container *ngFor="let part of m.parts">
              <button
                type="button"
                class="ng2-ai-cite"
                *ngIf="part.page; else plainPart"
                (click)="setPage(part.page!)"
              >
                p.{{ part.page }}
              </button>
              <ng-template #plainPart>{{ part.text }}</ng-template>
            </ng-container>
            <span class="ng2-ai-error" *ngIf="m.error">{{ m.error }}</span>
          </div>
          <div class="ng2-ai-msg ng2-ai-busy" *ngIf="aiBusy">Thinking…</div>
        </div>
        <div class="ng2-ai-input">
          <input
            #aiq
            type="text"
            [placeholder]="aiAssistantConfig.placeholder || 'Ask the document…'"
            [disabled]="aiBusy"
            (keyup.enter)="aiAsk(aiq.value); aiq.value = ''"
            aria-label="Question for the AI assistant"
          />
          <button
            type="button"
            [disabled]="aiBusy"
            (click)="aiAsk(aiq.value); aiq.value = ''"
          >
            Ask
          </button>
        </div>
      </div>

      <div
        class="ng2-pdfjs-error-overlay"
        *ngIf="securityWarning && !externalWindow"
        [ngClass]="errorClass"
      >
        <ng-container
          *ngIf="customSecurityTpl; else defaultSecurity"
          [ngTemplateOutlet]="customSecurityTpl"
          [ngTemplateOutletContext]="{ $implicit: securityWarning, securityWarning: securityWarning }"
        ></ng-container>
        <ng-template #defaultSecurity>
          <div class="ng2-pdfjs-error-content">
            <div class="ng2-pdfjs-error-icon">
              ⚠️
            </div>
            <div class="ng2-pdfjs-error-title">
              Security Warning
            </div>
            <div class="ng2-pdfjs-error-message">
              {{ securityWarning?.message }}
            </div>
          </div>
        </ng-template>
      </div>
    </div>
  `, isInline: true, styles: [".ng2-pdfjs-viewer-container{position:relative;width:100%;height:100%}.ng2-pdfjs-viewer-container.ng2-has-custom-toolbar{display:flex;flex-direction:column}.ng2-pdfjs-viewer-container.ng2-has-custom-toolbar iframe{flex:1 1 auto;min-height:0}.ng2-pdfjs-custom-toolbar{flex:0 0 auto}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:\"toolbar toolbar\" \"sidebar viewer\"}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar .ng2-pdfjs-custom-toolbar{grid-area:toolbar}.ng2-pdfjs-custom-sidebar{grid-area:sidebar;min-height:0;overflow:auto}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar iframe{grid-area:viewer;min-width:0;min-height:0}.ng2-pdfjs-loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#fff9;-webkit-backdrop-filter:saturate(120%) blur(1px);backdrop-filter:saturate(120%) blur(1px)}.ng2-pdfjs-spinner-content{text-align:center}.ng2-pdfjs-spinner-icon{display:inline-block;width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #2196F3;border-radius:50%;animation:spin 1s linear infinite}.ng2-pdfjs-spinner-text{margin-top:16px;color:#666;font-size:16px}.ng2-pdfjs-error-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffffe6;-webkit-backdrop-filter:saturate(120%) blur(1px);backdrop-filter:saturate(120%) blur(1px)}.ng2-pdfjs-error-content{text-align:center;max-width:400px;padding:20px}.ng2-pdfjs-error-icon{font-size:48px;color:#f44336;margin-bottom:16px}.ng2-pdfjs-error-title{color:#333;font-size:18px;font-weight:500;margin-bottom:8px}.ng2-pdfjs-error-message{color:#666;font-size:14px;line-height:1.4}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ng2-ai-fab{position:absolute;right:16px;bottom:16px;z-index:20;width:44px;height:44px;border:none;border-radius:50%;cursor:pointer;font-size:18px;line-height:1;color:var(--ng2-ai-fab-color, #fff);background:var(--ng2-ai-accent, #4436a1);box-shadow:0 2px 8px #00000040}.ng2-ai-panel{position:absolute;right:16px;bottom:72px;z-index:20;display:flex;flex-direction:column;width:min(340px,calc(100% - 32px));max-height:min(480px,calc(100% - 96px));border-radius:10px;overflow:hidden;font-size:13px;color:var(--ng2-ai-text, #222);background:var(--ng2-ai-bg, #fff);box-shadow:0 6px 24px #00000047}.ng2-ai-head{display:flex;align-items:center;justify-content:space-between;flex:0 0 auto;padding:10px 12px;font-weight:600;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-head button{border:none;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}.ng2-ai-msgs{flex:1 1 auto;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:8px}.ng2-ai-msg{white-space:pre-wrap;word-break:break-word;padding:8px 10px;border-radius:8px;background:var(--ng2-ai-answer-bg, #f2f1f7);align-self:stretch}.ng2-ai-msg.ng2-ai-user{background:var(--ng2-ai-question-bg, #e4f0fe);align-self:flex-end;max-width:85%}.ng2-ai-msg.ng2-ai-busy{opacity:.7;font-style:italic}.ng2-ai-cite{display:inline-block;margin:0 2px;padding:0 6px;border:none;border-radius:9px;cursor:pointer;font:inherit;font-size:12px;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-error{color:#b3261e}.ng2-ai-input{display:flex;flex:0 0 auto;gap:6px;padding:10px 12px;border-top:1px solid rgba(0,0,0,.08)}.ng2-ai-input input{flex:1 1 auto;min-width:0;padding:6px 8px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:inherit}.ng2-ai-input button{flex:0 0 auto;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;font:inherit;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-input button:disabled{opacity:.6;cursor:default}.ng2-pdfjs-viewer-iframe{border:0}.ng2-pdfjs-viewer-iframe.has-border{border:1px solid #ccc}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerComponent, decorators: [{
            type: Component,
            args: [{ selector: "ng2-pdfjs-viewer", standalone: false, template: `
    <div
      class="ng2-pdfjs-viewer-container"
      [class.ng2-has-custom-toolbar]="customToolbarTpl && !externalWindow"
      [class.ng2-has-custom-sidebar]="customSidebarTpl && !externalWindow"
    >
      <div
        class="ng2-pdfjs-custom-toolbar"
        *ngIf="customToolbarTpl && !externalWindow"
      >
        <ng-container
          [ngTemplateOutlet]="customToolbarTpl"
          [ngTemplateOutletContext]="{ $implicit: this }"
        ></ng-container>
      </div>
      <div
        class="ng2-pdfjs-custom-sidebar"
        *ngIf="customSidebarTpl && !externalWindow"
      >
        <ng-container
          [ngTemplateOutlet]="customSidebarTpl"
          [ngTemplateOutletContext]="{ $implicit: this }"
        ></ng-container>
      </div>
      <iframe
        [title]="iframeTitle || 'PDF document viewer'"
        [hidden]="externalWindow || (!externalWindow && !pdfSrc)"
        sandbox="allow-forms allow-scripts allow-same-origin allow-modals allow-downloads allow-popups allow-popups-to-escape-sandbox"
        [class]="getIframeClasses()"
        #iframe
        width="100%"
        height="100%"
      ></iframe>
      
      <div
        class="ng2-pdfjs-loading-overlay"
        *ngIf="showSpinner && isLoading && !externalWindow"
        [ngClass]="spinnerClass"
      >
        <ng-container
          *ngIf="customSpinnerTpl; else defaultSpinner"
          [ngTemplateOutlet]="customSpinnerTpl"
        ></ng-container>
        <ng-template #defaultSpinner>
          <div class="ng2-pdfjs-spinner-content">
            <div class="ng2-pdfjs-spinner-icon"></div>
            <div class="ng2-pdfjs-spinner-text">
              Loading PDF...
            </div>
          </div>
        </ng-template>
      </div>

      <div
        class="ng2-pdfjs-error-overlay"
        *ngIf="errorOverride && hasError && !externalWindow"
        [ngClass]="errorClass"
      >
        <ng-container
          *ngIf="customErrorTpl; else defaultError"
          [ngTemplateOutlet]="customErrorTpl"
          [ngTemplateOutletContext]="errorTemplateData"
        ></ng-container>
        <ng-template #defaultError>
          <div class="ng2-pdfjs-error-content">
            <div class="ng2-pdfjs-error-icon">
              ⚠️
            </div>
            <div class="ng2-pdfjs-error-title">
              Error Loading PDF
            </div>
            <div class="ng2-pdfjs-error-message">
              {{ currentErrorMessage }}
            </div>
          </div>
        </ng-template>
      </div>

      <button
        type="button"
        class="ng2-ai-fab"
        *ngIf="aiAssistantConfig && !externalWindow"
        (click)="aiPanelOpen = !aiPanelOpen"
        [attr.aria-expanded]="aiPanelOpen"
        aria-label="Ask AI about this document"
      >
        ✦
      </button>

      <div
        class="ng2-ai-panel"
        *ngIf="aiAssistantConfig && aiPanelOpen && !externalWindow"
        role="complementary"
        aria-label="AI assistant"
      >
        <div class="ng2-ai-head">
          <span>{{ aiAssistantConfig.title || 'Ask this document' }}</span>
          <button
            type="button"
            (click)="aiPanelOpen = false"
            aria-label="Close AI panel"
          >
            ×
          </button>
        </div>
        <div class="ng2-ai-msgs" aria-live="polite">
          <div
            class="ng2-ai-msg"
            *ngFor="let m of aiMessages"
            [class.ng2-ai-user]="m.role === 'user'"
          >
            <ng-container *ngFor="let part of m.parts">
              <button
                type="button"
                class="ng2-ai-cite"
                *ngIf="part.page; else plainPart"
                (click)="setPage(part.page!)"
              >
                p.{{ part.page }}
              </button>
              <ng-template #plainPart>{{ part.text }}</ng-template>
            </ng-container>
            <span class="ng2-ai-error" *ngIf="m.error">{{ m.error }}</span>
          </div>
          <div class="ng2-ai-msg ng2-ai-busy" *ngIf="aiBusy">Thinking…</div>
        </div>
        <div class="ng2-ai-input">
          <input
            #aiq
            type="text"
            [placeholder]="aiAssistantConfig.placeholder || 'Ask the document…'"
            [disabled]="aiBusy"
            (keyup.enter)="aiAsk(aiq.value); aiq.value = ''"
            aria-label="Question for the AI assistant"
          />
          <button
            type="button"
            [disabled]="aiBusy"
            (click)="aiAsk(aiq.value); aiq.value = ''"
          >
            Ask
          </button>
        </div>
      </div>

      <div
        class="ng2-pdfjs-error-overlay"
        *ngIf="securityWarning && !externalWindow"
        [ngClass]="errorClass"
      >
        <ng-container
          *ngIf="customSecurityTpl; else defaultSecurity"
          [ngTemplateOutlet]="customSecurityTpl"
          [ngTemplateOutletContext]="{ $implicit: securityWarning, securityWarning: securityWarning }"
        ></ng-container>
        <ng-template #defaultSecurity>
          <div class="ng2-pdfjs-error-content">
            <div class="ng2-pdfjs-error-icon">
              ⚠️
            </div>
            <div class="ng2-pdfjs-error-title">
              Security Warning
            </div>
            <div class="ng2-pdfjs-error-message">
              {{ securityWarning?.message }}
            </div>
          </div>
        </ng-template>
      </div>
    </div>
  `, styles: [".ng2-pdfjs-viewer-container{position:relative;width:100%;height:100%}.ng2-pdfjs-viewer-container.ng2-has-custom-toolbar{display:flex;flex-direction:column}.ng2-pdfjs-viewer-container.ng2-has-custom-toolbar iframe{flex:1 1 auto;min-height:0}.ng2-pdfjs-custom-toolbar{flex:0 0 auto}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar{display:grid;grid-template-columns:auto 1fr;grid-template-rows:auto 1fr;grid-template-areas:\"toolbar toolbar\" \"sidebar viewer\"}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar .ng2-pdfjs-custom-toolbar{grid-area:toolbar}.ng2-pdfjs-custom-sidebar{grid-area:sidebar;min-height:0;overflow:auto}.ng2-pdfjs-viewer-container.ng2-has-custom-sidebar iframe{grid-area:viewer;min-width:0;min-height:0}.ng2-pdfjs-loading-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#fff9;-webkit-backdrop-filter:saturate(120%) blur(1px);backdrop-filter:saturate(120%) blur(1px)}.ng2-pdfjs-spinner-content{text-align:center}.ng2-pdfjs-spinner-icon{display:inline-block;width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #2196F3;border-radius:50%;animation:spin 1s linear infinite}.ng2-pdfjs-spinner-text{margin-top:16px;color:#666;font-size:16px}.ng2-pdfjs-error-overlay{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:#ffffffe6;-webkit-backdrop-filter:saturate(120%) blur(1px);backdrop-filter:saturate(120%) blur(1px)}.ng2-pdfjs-error-content{text-align:center;max-width:400px;padding:20px}.ng2-pdfjs-error-icon{font-size:48px;color:#f44336;margin-bottom:16px}.ng2-pdfjs-error-title{color:#333;font-size:18px;font-weight:500;margin-bottom:8px}.ng2-pdfjs-error-message{color:#666;font-size:14px;line-height:1.4}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.ng2-ai-fab{position:absolute;right:16px;bottom:16px;z-index:20;width:44px;height:44px;border:none;border-radius:50%;cursor:pointer;font-size:18px;line-height:1;color:var(--ng2-ai-fab-color, #fff);background:var(--ng2-ai-accent, #4436a1);box-shadow:0 2px 8px #00000040}.ng2-ai-panel{position:absolute;right:16px;bottom:72px;z-index:20;display:flex;flex-direction:column;width:min(340px,calc(100% - 32px));max-height:min(480px,calc(100% - 96px));border-radius:10px;overflow:hidden;font-size:13px;color:var(--ng2-ai-text, #222);background:var(--ng2-ai-bg, #fff);box-shadow:0 6px 24px #00000047}.ng2-ai-head{display:flex;align-items:center;justify-content:space-between;flex:0 0 auto;padding:10px 12px;font-weight:600;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-head button{border:none;background:transparent;color:inherit;font-size:18px;line-height:1;cursor:pointer}.ng2-ai-msgs{flex:1 1 auto;overflow-y:auto;padding:10px 12px;display:flex;flex-direction:column;gap:8px}.ng2-ai-msg{white-space:pre-wrap;word-break:break-word;padding:8px 10px;border-radius:8px;background:var(--ng2-ai-answer-bg, #f2f1f7);align-self:stretch}.ng2-ai-msg.ng2-ai-user{background:var(--ng2-ai-question-bg, #e4f0fe);align-self:flex-end;max-width:85%}.ng2-ai-msg.ng2-ai-busy{opacity:.7;font-style:italic}.ng2-ai-cite{display:inline-block;margin:0 2px;padding:0 6px;border:none;border-radius:9px;cursor:pointer;font:inherit;font-size:12px;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-error{color:#b3261e}.ng2-ai-input{display:flex;flex:0 0 auto;gap:6px;padding:10px 12px;border-top:1px solid rgba(0,0,0,.08)}.ng2-ai-input input{flex:1 1 auto;min-width:0;padding:6px 8px;border:1px solid rgba(0,0,0,.2);border-radius:6px;font:inherit}.ng2-ai-input button{flex:0 0 auto;padding:6px 12px;border:none;border-radius:6px;cursor:pointer;font:inherit;color:#fff;background:var(--ng2-ai-accent, #4436a1)}.ng2-ai-input button:disabled{opacity:.6;cursor:default}.ng2-pdfjs-viewer-iframe{border:0}.ng2-pdfjs-viewer-iframe.has-border{border:1px solid #ccc}\n"] }]
        }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.ApplicationRef }, { type: i0.NgZone }], propDecorators: { iframe: [{
                type: ViewChild,
                args: ["iframe", { static: true }]
            }], viewerId: [{
                type: Input
            }], onBeforePrint: [{
                type: Output
            }], onAfterPrint: [{
                type: Output
            }], onDocumentLoad: [{
                type: Output
            }], onPageChange: [{
                type: Output
            }], onScaleChange: [{
                type: Output
            }], onRotationChange: [{
                type: Output
            }], onDocumentError: [{
                type: Output
            }], onDocumentInit: [{
                type: Output
            }], onPagesInit: [{
                type: Output
            }], onPresentationModeChanged: [{
                type: Output
            }], onOpenFile: [{
                type: Output
            }], onFind: [{
                type: Output
            }], onUpdateFindMatchesCount: [{
                type: Output
            }], onMetadataLoaded: [{
                type: Output
            }], onOutlineLoaded: [{
                type: Output
            }], onPageRendered: [{
                type: Output
            }], onAnnotationLayerRendered: [{
                type: Output
            }], onBookmarkClick: [{
                type: Output
            }], onIdle: [{
                type: Output
            }], onPasswordPrompt: [{
                type: Output
            }], onAnnotationEditorStateChange: [{
                type: Output
            }], onPagesEdited: [{
                type: Output
            }], onReadAloudStateChange: [{
                type: Output
            }], onSidebarViewChanged: [{
                type: Output
            }], onLayersChanged: [{
                type: Output
            }], onNamedAction: [{
                type: Output
            }], onDocumentProperties: [{
                type: Output
            }], viewerFolder: [{
                type: Input
            }], externalWindow: [{
                type: Input
            }], target: [{
                type: Input
            }], showSpinner: [{
                type: Input
            }], downloadFileName: [{
                type: Input
            }], locale: [{
                type: Input
            }], useOnlyCssZoom: [{
                type: Input
            }], externalLinkTarget: [{
                type: Input
            }], rememberLastView: [{
                type: Input
            }], annotationEditor: [{
                type: Input
            }], annotationEditorChange: [{
                type: Output
            }], highlightEditorColors: [{
                type: Input
            }], enableSignatureEditor: [{
                type: Input
            }], signatureStorage: [{
                type: Input
            }], pageColors: [{
                type: Input
            }], pdfJsOptions: [{
                type: Input
            }], enableCommentEditor: [{
                type: Input
            }], enablePageEditing: [{
                type: Input
            }], showToolbar: [{
                type: Input
            }], chromeless: [{
                type: Input
            }], customToolbarTpl: [{
                type: Input
            }], customSidebarTpl: [{
                type: Input
            }], aiAssistantConfig: [{
                type: Input
            }], pageOverlayTpl: [{
                type: Input
            }], httpHeaders: [{
                type: Input
            }], withCredentials: [{
                type: Input
            }], onProgress: [{
                type: Output
            }], formData: [{
                type: Input
            }], formDataChange: [{
                type: Output
            }], contentProtection: [{
                type: Input
            }], iframeSandbox: [{
                type: Input
            }], diagnosticLogs: [{
                type: Input
            }], showOpenFile: [{
                type: Input
            }], showAnnotations: [{
                type: Input
            }], showDownload: [{
                type: Input
            }], showViewBookmark: [{
                type: Input
            }], showPrint: [{
                type: Input
            }], showFullScreen: [{
                type: Input
            }], showFind: [{
                type: Input
            }], downloadOnLoad: [{
                type: Input
            }], printOnLoad: [{
                type: Input
            }], rotateCW: [{
                type: Input
            }], rotateCCW: [{
                type: Input
            }], showLastPageOnLoad: [{
                type: Input
            }], namedDest: [{
                type: Input
            }], errorOverride: [{
                type: Input
            }], errorAppend: [{
                type: Input
            }], errorMessage: [{
                type: Input
            }], urlValidation: [{
                type: Input
            }], customSecurityTpl: [{
                type: Input
            }], theme: [{
                type: Input
            }], primaryColor: [{
                type: Input
            }], backgroundColor: [{
                type: Input
            }], pageBorderColor: [{
                type: Input
            }], pageSpacing: [{
                type: Input
            }], toolbarColor: [{
                type: Input
            }], textColor: [{
                type: Input
            }], borderRadius: [{
                type: Input
            }], customCSS: [{
                type: Input
            }], cspNonce: [{
                type: Input
            }], iframeTitle: [{
                type: Input
            }], customSpinnerTpl: [{
                type: Input
            }], spinnerClass: [{
                type: Input
            }], customErrorTpl: [{
                type: Input
            }], errorClass: [{
                type: Input
            }], showToolbarLeft: [{
                type: Input
            }], showToolbarMiddle: [{
                type: Input
            }], showToolbarRight: [{
                type: Input
            }], showSecondaryToolbarToggle: [{
                type: Input
            }], showSidebar: [{
                type: Input
            }], showSidebarLeft: [{
                type: Input
            }], showSidebarRight: [{
                type: Input
            }], toolbarDensity: [{
                type: Input
            }], sidebarWidth: [{
                type: Input
            }], toolbarPosition: [{
                type: Input
            }], sidebarPosition: [{
                type: Input
            }], responsiveBreakpoint: [{
                type: Input
            }], controlVisibility: [{
                type: Input
            }], autoActions: [{
                type: Input
            }], errorHandling: [{
                type: Input
            }], viewerConfig: [{
                type: Input
            }], themeConfig: [{
                type: Input
            }], groupVisibility: [{
                type: Input
            }], layoutConfig: [{
                type: Input
            }], startDownload: [{
                type: Input
            }], startPrint: [{
                type: Input
            }], openFile: [{
                type: Input
            }], download: [{
                type: Input
            }], print: [{
                type: Input
            }], fullScreen: [{
                type: Input
            }], find: [{
                type: Input
            }], viewBookmark: [{
                type: Input
            }], lastPage: [{
                type: Input
            }], externalWindowOptions: [{
                type: Input
            }], iframeBorder: [{
                type: Input
            }], zoomChange: [{
                type: Output
            }], cursorChange: [{
                type: Output
            }], scrollChange: [{
                type: Output
            }], spreadChange: [{
                type: Output
            }], pageModeChange: [{
                type: Output
            }], zoom: [{
                type: Input
            }], rotation: [{
                type: Input
            }], cursor: [{
                type: Input
            }], scroll: [{
                type: Input
            }], spread: [{
                type: Input
            }], pageMode: [{
                type: Input
            }], page: [{
                type: Input
            }], pdfSrc: [{
                type: Input
            }] } });

class PdfJsViewerModule {
    /** @deprecated Import PdfJsViewerModule directly; forRoot() registers no providers. */
    static forRoot() {
        return {
            ngModule: PdfJsViewerModule,
        };
    }
    static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
    static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerModule, declarations: [PdfJsViewerComponent], imports: [CommonModule], exports: [PdfJsViewerComponent] });
    static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerModule, imports: [CommonModule] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.1", ngImport: i0, type: PdfJsViewerModule, decorators: [{
            type: NgModule,
            args: [{
                    imports: [CommonModule],
                    declarations: [PdfJsViewerComponent],
                    exports: [PdfJsViewerComponent],
                }]
        }] });

/**
 * Generated bundle index. Do not edit.
 */

export { PdfJsViewerComponent, PdfJsViewerModule };