PDF Web SDK Documentation

Comprehensive Guide for Developers: UI & SDK Integration, Configuration, and API Reference

PDF Web SDK
    Preparing search index...

    Event Callbacks

    Configure callback functions to respond to editor events and customize file operations.

    Monitor loading state changes to show/hide loading indicators.

    Type signature:

    onLoadingStateChange?: (isLoading: boolean) => void
    

    Example:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onLoadingStateChange: (isLoading: boolean) => {
    const loader = document.getElementById('loading-spinner');
    if (loader) {
    loader.style.display = isLoading ? 'block' : 'none';
    }
    },
    });

    Respond to configuration changes. Triggered when layout, theme, language, branding, default font, or units & guides settings change. See ui.IConfigChangeData.

    Type signature:

    onConfigChange?: (config: IConfigChangeData) => void
    

    IConfigChangeData interface:

    interface IConfigChangeData {
    themeConfig?: IThemeConfig;
    defaultFont?: IDefaultFontConfig;
    unitsAndGuides?: IUnitsAndGuidesConfig;
    languageConfig?: ILanguageConfig;
    brandingConfig?: IBrandingConfig;
    toolsLayout?: LabelAlignment;
    ocrAdvancedConfig?: IOcrAdvancedConfig;
    openDocumentsInNewTab?: boolean;
    restoreLastViewSettings?: boolean;
    sidebarCollapsed?: boolean;
    sidebarExpandOnHover?: boolean;
    }

    Example:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onConfigChange: async (config) => {
    console.log('Configuration changed:', config);

    // Save user preferences to localStorage
    localStorage.setItem('pdfUserPreferences', JSON.stringify(config));

    // Or sync to backend
    await fetch('/api/user/preferences', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(config),
    });
    },
    });

    Handle errors that occur during PDF operations. This callback receives standard JavaScript Error objects.

    Type signature:

    onError?: (error: Error) => void
    

    Example:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onError: (error: Error) => {
    console.error('PDF Editor Error:', error);

    // Show user-friendly error message
    showNotification({
    type: 'error',
    title: 'PDF Error',
    message: error.message,
    });
    },
    });

    Integrate with error tracking services like Sentry, LogRocket, or custom logging:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';
    import * as Sentry from '@sentry/browser';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onError: (error: Error) => {
    // Log to Sentry
    Sentry.captureException(error, {
    tags: {
    component: 'pdf-editor',
    action: 'pdf-operation',
    },
    extra: {
    timestamp: new Date().toISOString(),
    userId: currentUser?.id,
    },
    });

    // Display user-friendly message
    showToast({
    type: 'error',
    message: 'An error occurred while processing the PDF. Please try again.',
    });
    },
    });

    Handle password-protected files with a custom UI. The callback receives the error details and returns the password. The viewer waits for the returned promise, so a modal can take as long as it needs.

    Type signature:

    onPasswordRequired?: IPasswordRequiredCallback;

    type IPasswordRequiredCallback = (error: IPasswordRequiredError) => Promise<string>;

    Exports:

    import type { IPasswordRequiredCallback, IPasswordRequiredError } from '@avanquest/pdf-web-viewer';
    

    IPasswordRequiredError carries documentName, message, errorCode, operation and documentId. On the first prompt message is the engine's own text; on a retry after a wrong password it is the localized "Incorrect password".

    Example: Custom password modal with error message

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onPasswordRequired: async (error: IPasswordRequiredError) => {
    const password = await showPasswordModal({
    title: 'Password required',
    fileName: error.documentName,
    message: error.message,
    });

    // Return the password to retry, or '' to cancel the open.
    return password ?? '';
    },
    });

    When onPasswordRequired is provided, the built-in password dialog is suppressed.

    Retrying is automatic. The viewer re-opens the document with whatever the callback returns; if that password is wrong it calls the callback again, with message set to "Incorrect password". Keep returning passwords to keep retrying — there is no separate retry call to make.

    To cancel, return an empty string or reject the promise. The open then fails with a PasswordDeclinedError, which is treated as a cancel rather than a failure: no error message is shown, and the file keeps its place in the recent-files list.

    The callback covers every password prompt in the viewer, not just opening: the owner-password requests raised by Compress, Convert, Organize, Document Properties and Merge route through it too.

    Opening without the UI. PdfSdk.openDocument() sits below the viewer and never prompts — onPasswordRequired does not apply to it. Pass a known password with the file, or catch the rejected PdfEditorPasswordError and handle the prompt yourself:

    import { PdfSdk } from '@avanquest/pdf-web-viewer/sdk';

    const document = await PdfSdk.openDocument({ file, password: 'secret' });

    Replace the default file picker with a custom implementation. When provided, this callback is invoked instead of the default file input behavior — from every open entry point, including "Open document" in a side-by-side pane: the file you then pass to pdfWebService.openDocument(file) lands in the pane the gesture pointed at.

    Type signature:

    onOpenFile?: () => Promise<void>
    

    Example: Custom File Picker

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onOpenFile: async () => {
    // Show custom file picker
    const file = await customFilePicker({
    accept: '.pdf',
    multiple: false,
    });

    if (file) {
    // Open the selected file using the service
    await editor.ui.pdfWebService.openDocument(file);
    }
    },
    });

    Example: Cloud Storage Integration

    Integrate with Google Drive, Dropbox, OneDrive, or other cloud storage:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onOpenFile: async () => {
    try {
    // Show cloud file picker (e.g., Google Drive Picker)
    const fileUrl = await googleDrivePicker.pick({
    mimeTypes: ['application/pdf'],
    });

    if (fileUrl) {
    // Fetch file from cloud storage
    const response = await fetch(fileUrl, {
    headers: {
    Authorization: `Bearer ${accessToken}`,
    },
    });

    const blob = await response.blob();
    const file = new File([blob], 'document.pdf', { type: 'application/pdf' });

    // Open the file
    await editor.ui.pdfWebService.openDocument(file);
    }
    } catch (error) {
    console.error('Failed to open file from cloud:', error);
    showToast({ type: 'error', message: 'Failed to open file from cloud storage' });
    }
    },
    });

    Which file formats the open UI offers. The viewer reads these itself, and they are always offered:

    Extension accept sent to the picker
    pdf application/pdf
    wwf .wwf
    png image/png
    tiff / tif image/tiff / image/tif
    jpeg / jpg image/jpeg / image/jpg
    bmp image/bmp
    gif image/gif
    txt text/plain, .txt

    Office documents are offered too: docx, dotx, doc, rtf, pptx, potx, ppt, xlsx, xltx, xls, xlt, csv, odt, ott, odp and ods (each with its MIME type plus a .ext accept token). The viewer converts them to PDF through the Developers API Office to PDF endpoint before opening, so the call goes to apiToolsUrl and is authorized per apiAuthMode — by default with your license key. Files over 100 MB are rejected by the API.

    Anything else is greyed out: the file picker filters it, and a cloud-drive listing shows it but refuses the click. openFormatsConfig.additional widens that set — for formats your code opens.

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),

    openFormatsConfig: {
    additional: [
    { ext: 'epub', accept: ['.epub', 'application/epub+zip'] },
    { ext: 'md' }, // accept defaults to ['.md']
    ],
    },

    // …and convert them before the viewer sees them.
    onOpenFile: async () => {
    const file = await myPicker();
    const pdf = /\.(epub|md)$/i.test(file.name) ? await myConverter.toPdf(file) : file;
    await editor.ui.pdfWebService.openDocument(pdf);
    },
    });

    Type signature:

    openFormatsConfig?: {
    additional?: Array<{ ext: string; accept?: string[] }>;
    };

    ext has no leading dot and matches case-insensitively; accept takes MIME types, .ext tokens, or both, exactly as <input type="file" accept> does, and defaults to ['.<ext>']. An entry without a usable ext is skipped with a console error rather than corrupting the picker's filter. The list is replaced on each config change, so dropping it drops the formats.

    Declaring a format only makes it selectable — it does not teach the viewer to read it. Convert the file in onOpenFile (or in your provider's getFileAsFile) before handing it over. One that reaches the viewer's own processing unconverted is rejected like any unreadable file, on the standard open-failure notification:

    Failed to open file "book.epub": File format is not supported
    

    Customize how files leave the viewer. The callback receives a File object with the filename already set, plus the claims of the operations that produced its contents.

    This is the single exit point for every export — the document itself (Download/Save, Ctrl+S), converted files, extracted images, split/compressed results, attachments, form data and snapshots all flow through it.

    Type signature:

    onExportFile?: (file: File, claims: readonly string[]) => void
    

    claims is the full provenance of the file, not just its last step: the claims of the document it came from plus the operation that produced this artefact. A compressed export of a merged, OCR'd document reports all three, oldest first — ['Create.CombineFiles', 'Ocr', 'Edit.Optimize.ReduceFileSize']. The order is the order things happened, so it is deliberately not sorted; sort it yourself if you need a canonical form. The same list reaches deviceConfig.saveOver / saveInto, the customProviders save methods, and the drives' isSaveAllowed guard.

    Tip: one-way on purpose — it delivers a copy and reports nothing back, so it is not a save channel. Writing the document to the machine is deviceConfig.saveOver / saveInto; the export is only what happens when neither is implemented. See Saving & Downloading.

    Example: Custom Download with Timestamp

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onExportFile: (file: File) => {
    // Add timestamp to filename
    const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
    const newFilename = `${file.name.replace('.pdf', '')}-${timestamp}.pdf`;

    // Create download link
    const url = URL.createObjectURL(file);
    const a = document.createElement('a');
    a.href = url;
    a.download = newFilename;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
    },
    });

    Example: Cloud Storage Upload

    Save files to cloud storage instead of downloading to the user's device:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onExportFile: async (file: File) => {
    try {
    // Show upload progress
    showToast({ type: 'info', message: 'Uploading to cloud...' });

    // Upload to cloud storage
    const formData = new FormData();
    formData.append('file', file);
    formData.append('userId', currentUser.id);

    const response = await fetch('/api/documents/upload', {
    method: 'POST',
    headers: {
    Authorization: `Bearer ${authToken}`,
    },
    body: formData,
    });

    if (!response.ok) {
    throw new Error('Upload failed');
    }

    const { url, documentId } = await response.json();

    // Show success message
    showToast({
    type: 'success',
    message: `File saved to cloud`,
    action: {
    label: 'View',
    onClick: () => window.open(url, '_blank'),
    },
    });

    // Update UI or navigate
    console.log('Document ID:', documentId);
    } catch (error) {
    console.error('Upload failed:', error);
    showToast({ type: 'error', message: 'Failed to upload file to cloud' });
    }
    },
    });

    Customize the print behavior. The callback receives the PDF file and an optional flag indicating whether to open in a new tab.

    Type signature:

    onPrint?: (file: File, newTab: boolean, claims: readonly string[]) => void
    

    claims lists the operations the printed contents went through, the same list onExportFile receives — a print hands a copy of the document over too. See Saving & Downloading.

    Example: Custom Print Handler

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onPrint: (file: File, newTab?: boolean) => {
    const printUrl = URL.createObjectURL(file);

    if (newTab) {
    // Open in new tab for printing
    window.open(printUrl, '_blank');
    } else {
    // Print directly using hidden iframe
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.style.position = 'absolute';
    iframe.src = printUrl;
    document.body.appendChild(iframe);

    iframe.onload = () => {
    iframe.contentWindow?.print();

    // Clean up after printing
    setTimeout(() => {
    document.body.removeChild(iframe);
    URL.revokeObjectURL(printUrl);
    }, 1000);
    };
    }
    },
    });

    Example: Print with Analytics

    Track print operations:

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onPrint: (file: File, newTab?: boolean) => {
    // Track print event
    analytics.track('pdf_print', {
    fileName: file.name,
    fileSize: file.size,
    printMode: newTab ? 'new-tab' : 'direct',
    timestamp: Date.now(),
    });

    // Perform print
    const printUrl = URL.createObjectURL(file);

    if (newTab) {
    window.open(printUrl, '_blank');
    } else {
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = printUrl;
    document.body.appendChild(iframe);

    iframe.onload = () => {
    iframe.contentWindow?.print();
    setTimeout(() => {
    document.body.removeChild(iframe);
    URL.revokeObjectURL(printUrl);
    }, 1000);
    };
    }
    },
    });

    Handle snapshot area captures with custom logic. When this callback is provided, the default snapshot preview dialog is bypassed, and the callback receives a Blob containing the PNG image data.

    Type signature:

    onSnapshotArea?: (blob: Blob, claims: readonly string[]) => void
    

    claims is always just ['View.Snapshot']. A capture is a raster of a page region, not the document, so what the document itself went through is not in the image and its claims do not travel with it. See Saving & Downloading.

    Example: Automatic Download

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onSnapshotArea: (blob: Blob) => {
    // Automatically download the snapshot
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `snapshot-${Date.now()}.png`;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);

    showToast({
    type: 'success',
    message: 'Snapshot downloaded successfully',
    });
    },
    });

    Example: Upload to Server

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onSnapshotArea: async (blob: Blob) => {
    try {
    console.log('Snapshot captured:', blob.size, 'bytes');

    // Upload to backend
    const formData = new FormData();
    formData.append('snapshot', blob, `snapshot-${Date.now()}.png`);
    formData.append('userId', currentUser.id);

    const response = await fetch('/api/snapshots/upload', {
    method: 'POST',
    headers: {
    Authorization: `Bearer ${authToken}`,
    },
    body: formData,
    });

    if (!response.ok) {
    throw new Error('Upload failed');
    }

    const { snapshotId, url } = await response.json();

    showToast({
    type: 'success',
    message: 'Snapshot uploaded successfully',
    action: {
    label: 'View',
    onClick: () => window.open(url, '_blank'),
    },
    });

    console.log('Snapshot ID:', snapshotId);
    } catch (error) {
    console.error('Failed to upload snapshot:', error);
    showToast({
    type: 'error',
    message: 'Failed to upload snapshot',
    });
    }
    },
    });

    Example: Copy to Clipboard

    import { PdfEditor } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onSnapshotArea: async (blob: Blob) => {
    try {
    // Copy image to clipboard
    await navigator.clipboard.write([
    new ClipboardItem({
    'image/png': blob,
    }),
    ]);

    showToast({
    type: 'success',
    message: 'Snapshot copied to clipboard',
    });
    } catch (error) {
    console.error('Failed to copy snapshot:', error);
    showToast({
    type: 'error',
    message: 'Failed to copy snapshot to clipboard',
    });
    }
    },
    });

    Behavior:

    • Without callback: Snapshot displays a preview dialog with options to "Save As" or "Open as PDF"
    • With callback: The callback is invoked with the PNG blob, and the preview dialog is skipped
    • Error handling: If the callback throws an error, the system falls back to showing the default dialog

    Hand the viewer's Fullscreen button to the host. Use it when the viewer is drawn inside a window it cannot fill by itself — a desktop shell (Tauri, Electron), an embedder with its own chrome. With the callback set the viewer never calls the browser Fullscreen API: it asks, you resize the window, and its layout follows what you report back through setFullscreenLayout().

    Type signature:

    onFullscreenRequested?: (fullscreen: boolean) => void
    

    Example: Desktop Shell Owning The Window

    import { PdfEditor } from '@avanquest/pdf-web-viewer';
    import { getCurrentWindow } from '@tauri-apps/api/window';

    const appWindow = getCurrentWindow();

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onFullscreenRequested: (fullscreen) => appWindow.setFullscreen(fullscreen),
    });

    // Report every change back, including the ones the viewer never asked for — a native
    // menu item, the window button, an OS shortcut — so its UI keeps matching the window.
    appWindow.onResized(async () => {
    editor.ui.pdfWebElement.setFullscreenLayout(await appWindow.isFullscreen());
    });

    Behavior:

    • Without callback: the viewer requests fullscreen on its own element and follows the browser's fullscreenchange — the web default.
    • With callback: the viewer changes nothing by itself, and setFullscreenLayout() is the only thing that moves its UI (in fullscreen the header collapses and the view-options button moves into a floating strip over the document).

    Override the built-in document context menu. When the user right-clicks (or long-presses) within the document, the callback receives the context menu type and the mouse position, so the host application can show its own menu or fall back to the default one.

    Type signature:

    onContextMenuRequested?: (data: IContextMenuRequestData) => boolean | Promise<boolean>

    interface IContextMenuRequestData {
    /** Which context menu the viewer would show (page background, selected object, text editor, …). */
    menuType: ContextMenu;
    /** Click position relative to the document render element. */
    position: { x: number; y: number };
    /** Bitmask of ContextMenuOperations flags allowed for the current selection. */
    operations: ContextMenuOperations;
    }

    enum ContextMenuOperations {
    AllowCopy = 1 << 0,
    AllowPaste = 1 << 1,
    AllowCut = 1 << 2,
    AllowUndo = 1 << 3,
    AllowRedo = 1 << 4,
    AllowToBack = 1 << 5,
    AllowToFront = 1 << 6,
    AllowOptList = 1 << 7,
    AllowMergeTextBlocks = 1 << 8,
    ESignFieldRequired = 1 << 9,
    }

    The same mask drives the enabled/disabled state of the built-in menu items — test flags with a bitwise AND to decide which items of a custom menu to enable.

    Example: Custom Context Menu

    import { PdfEditor, ContextMenu, ContextMenuOperations, IContextMenuRequestData } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),
    onContextMenuRequested: (data: IContextMenuRequestData) => {
    // Only override the plain page menu; keep the default for everything else
    if (data.menuType !== ContextMenu.View) {
    return false;
    }

    showMyContextMenu({
    x: data.position.x,
    y: data.position.y,
    items: [
    { label: 'My Action', onClick: () => doSomething() },
    { label: 'Paste', disabled: (data.operations & ContextMenuOperations.AllowPaste) === 0, onClick: () => pasteHere(data.position) },
    { label: 'Add text', onClick: () => addTextHere(data.position) },
    ],
    });

    return true; // handled — the built-in menu is suppressed
    },
    });

    Behavior:

    • Without callback: The viewer shows its built-in context menu
    • Return true: The built-in menu is suppressed — the host application is expected to show its own UI
    • Return false: The viewer falls back to the built-in context menu
    • Error handling: If the callback throws (or the returned Promise rejects), the viewer logs a warning and shows the default menu
    • The callback may be async; the viewer waits for the result before deciding
    • data.position is the click position relative to the document render element.

    Executing menu actions from a custom menu:

    Items of the built-in menu map to DocumentCommands executed on the document view. From a custom menu, run them via doCommand, passing data.position back unchanged for position-based commands:

    const view = editor.ui.pdfWebService.getPrimaryDocumentViewElement()?.documentView;

    view?.doCommand(DocumentCommand.COPY);
    view?.doCommand(DocumentCommand.PASTE, data.position); // paste at the click point
    view?.doCommand(DocumentCommand.TYPE_TEXT, data.position); // start the add-text tool at the click point
    view?.doCommand(DocumentCommand.INSERT_IMAGE, data.position); // open the image picker, insert at the click point
    await view?.print();

    Comprehensive example showing all callbacks working together:

    import { PdfEditor, IConfigChangeData } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),

    // State monitoring
    onLoadingStateChange: (isLoading: boolean) => {
    const spinner = document.getElementById('loading-spinner');
    if (spinner) {
    spinner.style.display = isLoading ? 'flex' : 'none';
    }
    },

    onConfigChange: (config: IConfigChangeData) => {
    // Persist user preferences
    localStorage.setItem('pdfUserPreferences', JSON.stringify(config));

    // Optionally sync to backend
    fetch('/api/user/preferences', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(config),
    }).catch((err) => console.error('Failed to sync preferences:', err));
    },

    // Error handling
    onError: (error: Error) => {
    console.error('PDF Editor Error:', error);

    // Log to error tracking
    if (window.Sentry) {
    window.Sentry.captureException(error);
    }

    // Show user notification
    showToast({
    type: 'error',
    message: error.message || 'An error occurred',
    });
    },

    // File operations
    onOpenFile: async () => {
    try {
    const file = await showCustomFilePicker({
    accept: '.pdf',
    multiple: false,
    });

    if (file) {
    await editor.ui.pdfWebService.openDocument(file);
    showToast({ type: 'success', message: 'File opened successfully' });
    }
    } catch (error) {
    console.error('Failed to open file:', error);
    showToast({ type: 'error', message: 'Failed to open file' });
    }
    },

    onExportFile: async (file: File) => {
    try {
    // Upload to cloud storage
    const formData = new FormData();
    formData.append('file', file);

    const response = await fetch('/api/documents/save', {
    method: 'POST',
    body: formData,
    });

    if (response.ok) {
    const { documentId } = await response.json();
    showToast({
    type: 'success',
    message: 'File saved to cloud',
    action: {
    label: 'View',
    onClick: () => window.open(`/documents/${documentId}`, '_blank'),
    },
    });
    }
    } catch (error) {
    console.error('Failed to save file:', error);
    showToast({ type: 'error', message: 'Failed to save file' });
    }
    },

    onPrint: (file: File, newTab?: boolean) => {
    const printUrl = URL.createObjectURL(file);

    if (newTab) {
    window.open(printUrl, '_blank');
    } else {
    const iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = printUrl;
    document.body.appendChild(iframe);

    iframe.onload = () => {
    iframe.contentWindow?.print();
    setTimeout(() => {
    document.body.removeChild(iframe);
    URL.revokeObjectURL(printUrl);
    }, 1000);
    };
    }
    },

    onSnapshotArea: async (blob: Blob) => {
    try {
    // Upload snapshot to server
    const formData = new FormData();
    formData.append('snapshot', blob, `snapshot-${Date.now()}.png`);

    const response = await fetch('/api/snapshots/upload', {
    method: 'POST',
    body: formData,
    });

    if (response.ok) {
    const { url } = await response.json();
    showToast({
    type: 'success',
    message: 'Snapshot saved',
    action: {
    label: 'View',
    onClick: () => window.open(url, '_blank'),
    },
    });
    }
    } catch (error) {
    console.error('Failed to save snapshot:', error);
    showToast({ type: 'error', message: 'Failed to save snapshot' });
    }
    },
    });

    Track user interactions and usage patterns:

    import { PdfEditor, IConfigChangeData } from '@avanquest/pdf-web-viewer';

    const editor = await PdfEditor({
    license: 'YOUR_LICENSE_KEY',
    container: document.getElementById('pdf-container'),

    onLoadingStateChange: (isLoading: boolean) => {
    if (isLoading) {
    analytics.track('pdf_loading_started');
    } else {
    analytics.track('pdf_loading_completed');
    }
    },

    onConfigChange: (config: IConfigChangeData) => {
    analytics.track('pdf_config_changed', {
    hasTheme: !!config.themeConfig,
    hasLanguage: !!config.languageConfig,
    hasBranding: !!config.brandingConfig,
    timestamp: Date.now(),
    });
    },

    onOpenFile: async () => {
    analytics.track('pdf_open_initiated');

    try {
    const file = await showFilePicker();
    if (file) {
    analytics.track('pdf_file_selected', {
    fileName: file.name,
    fileSize: file.size,
    fileType: file.type,
    });
    await editor.ui.pdfWebService.openDocument(file);
    }
    } catch (error) {
    analytics.track('pdf_open_failed', { error: error.message });
    }
    },

    onExportFile: (file: File) => {
    analytics.track('pdf_downloaded', {
    fileName: file.name,
    fileSize: file.size,
    timestamp: Date.now(),
    });

    // Proceed with download
    const url = URL.createObjectURL(file);
    const a = document.createElement('a');
    a.href = url;
    a.download = file.name;
    a.click();
    URL.revokeObjectURL(url);
    },

    onPrint: (file: File, newTab?: boolean) => {
    analytics.track('pdf_printed', {
    fileName: file.name,
    fileSize: file.size,
    printMode: newTab ? 'new-tab' : 'direct',
    timestamp: Date.now(),
    });

    // Proceed with print
    const url = URL.createObjectURL(file);
    if (newTab) {
    window.open(url, '_blank');
    } else {
    // ... iframe print logic
    }
    },

    onSnapshotArea: async (blob: Blob) => {
    analytics.track('pdf_snapshot_captured', {
    blobSize: blob.size,
    blobType: blob.type,
    timestamp: Date.now(),
    });

    // Handle snapshot
    try {
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `snapshot-${Date.now()}.png`;
    a.click();
    URL.revokeObjectURL(url);
    } catch (error) {
    analytics.track('pdf_snapshot_failed', { error: error.message });
    }
    },

    onError: (error: Error) => {
    analytics.track('pdf_error_occurred', {
    errorMessage: error.message,
    errorName: error.name,
    errorStack: error.stack,
    timestamp: Date.now(),
    });

    // Show error to user
    console.error('PDF Error:', error);
    },
    });