Content Management and Capture

Content Management and Capture

Come for answers. Stay for best practices. All we’re missing is you.

 View Only

ViewONE Plugin for Document Workflows

By RAMA LINGAM posted 04/30/26 12:50 AM

  

Introduction

Document annotation is a critical feature in many enterprise content management systems. Business users often need to review, mark up, redact, approve, or collaborate on documents without changing the original source file. In IBM Content Navigator and Daeja ViewONE Virtual Viewer environments, annotations provide a powerful way to support these workflows while preserving document integrity.

This blog post walks through the implementation of a custom ViewONE Plugin. The plugin demonstrates how to load annotations, save annotations, burn annotations permanently into documents, and support export workflows for PDF and TIFF outputs.

The goal of this implementation is to provide a practical, production-ready foundation for teams that need deeper control over annotation storage, formatting, persistence, and export behavior in a Daeja ViewONE-based document viewing solution.


What the Plugin Does

The ViewONE Plugin provides a complete annotation lifecycle around documents displayed in the ViewONE Virtual Viewer and exporting document.

At a high level, it supports:

  • Loading existing annotations for a document
  • Creating and editing annotations in the viewer
  • Saving annotations to the server
  • Preserving ViewONE annotation file formatting requirements
  • Burning annotations permanently into the document output
  • Exporting annotated documents as PDF or TIFF
  • Tracking viewer events for debugging and diagnostics
  • Providing a configurable foundation for enterprise deployment

This makes the plugin useful for different processes.


Architecture Overview

The implementation is structured around a simple web application pattern. The browser hosts the ViewONE Virtual Viewer, while JSP handlers manage annotation retrieval, persistence, and burn/export operations on the server side.

User Browser
|
|-- Virtual-plugin.html
| |
| |-- Loads document in ViewONE Virtual Viewer
| |-- Loads annotation file through load.jsp
| |-- Saves annotation updates through save.jsp
| |-- Sends burn/export requests through burn.jsp
|
Application Server
|
|-- load.jsp
| |-- Retrieves annotation content
|
|-- save.jsp
| |-- Persists annotation content
| |-- Preserves blank-line formatting
|
|-- burn.jsp
|-- Burns annotations into output document
|-- Supports PDF/TIFF export behavior
Storage Layer
|
|-- Source documents
|-- Annotation files
|-- Exported or burned output files

The design intentionally separates the viewer interface from the server-side annotation operations. This makes the plugin easier to maintain, debug, and extend.


Technology Stack

The plugin uses a lightweight stack that fits naturally into existing Java-based enterprise environments.

Frontend:
- HTML
- JavaScript
- Daeja ViewONE Virtual Viewer
Backend:
- JSP
- Java servlet container or application server
Storage:
- File-based annotation storage, or another enterprise repository depending on deployment needs
Viewer Platform:
- IBM Daeja ViewONE Virtual Viewer
- IBM Content Navigator-compatible environments

Although the sample implementation uses JSP files, the same pattern can be adapted to servlets, REST APIs, Spring Boot services, or repository-backed persistence layers.


Components:

Virtual-plugin.html is the main viewer interface. It is responsible for launching the ViewONE Virtual Viewer and passing the correct parameters to enable annotation behavior.

This page typically handles:

  • Viewer initialization
  • Document URL configuration
  • Annotation URL configuration
  • Save handler configuration
  • Burn/export handler configuration
  • Viewer event tracking
  • Debug console output

A simplified conceptual structure looks like this:

<html>
<head>
<title>ViewONE Annotation Plugin</title>
</head>
<body>
<div id="viewerContainer">
<!-- ViewONE Virtual Viewer loads here -->
</div>
<script>
// Configure document URL
// Configure annotation load URL
// Configure annotation save URL
// Configure viewer callbacks
// Initialize ViewONE viewer
</script>
</body>
</html>

The most important responsibility of this file is to connect the viewer to the correct server-side handlers. In a production environment, this page would usually receive document identifiers, repository identifiers, or case identifiers through request parameters or application context.


load.jsp retrieves the annotation file associated with the document being viewed.

When the viewer opens a document, it needs to know whether annotations already exist. The load handler receives the document reference, locates the matching annotation content, and returns it in the format expected by ViewONE.

Typical responsibilities include:

  • Reading the document identifier from the request
  • Resolving the annotation file path or repository object
  • Validating access to the annotation resource
  • Returning annotation content to the viewer
  • Handling the case where no annotation exists yet

A simplified flow is shown below:

Viewer requests annotations
|
load.jsp receives document identifier
|
Server checks annotation storage
|
If annotation exists:
return annotation content
Else:
return empty annotation response

This component should be kept simple and predictable. The viewer depends on a valid response, even when no annotations exist.


save.jsp persists annotation updates from the viewer.

This is one of the most important parts of the plugin because ViewONE annotation files have formatting requirements that must be preserved. In particular, blank-line formatting can matter. If annotation content is saved incorrectly, the viewer may fail to reload annotations or may display them incorrectly.

The save handler is responsible for:

  • Receiving annotation content from the viewer
  • Validating the document identifier
  • Validating annotation payload size and structure
  • Preserving required line breaks and blank lines
  • Writing the annotation content to storage
  • Returning a success or failure response

A conceptual save flow looks like this:

User creates or edits annotations
|
Viewer sends annotation content to save.jsp
|
save.jsp validates request
|
save.jsp preserves required formatting
|
save.jsp writes annotation data to storage
|
Viewer receives save confirmation

The formatting behavior is especially important. Annotation persistence is not just a matter of writing raw text to a file. The handler must preserve the annotation file structure expected by the viewer.


burn.jsp handles permanent annotation burning.

Normal annotations are separate from the original document. This is useful because users can edit, hide, or remove them later. However, some business workflows require annotations or redactions to become part of the final exported document.

That is where annotation burning is used.

The burn handler typically supports:

  • Taking a source document
  • Taking its associated annotation file
  • Invoking ViewONE export or burn behavior
  • Producing an output document with annotations applied
  • Returning the generated PDF or TIFF result

A simplified burn flow looks like this:

User requests burned output
|
burn.jsp receives document and annotation references
|
Server validates document and annotation access
|
Burn/export operation is triggered
|
Annotated output file is generated
|
User receives final PDF or TIFF

This is especially useful for redaction workflows where the final document must permanently include redaction marks before being shared, archived, or sent downstream.


Understanding the ViewONE Annotation File Format

ViewONE uses a proprietary annotation format. The exact content depends on annotation type, page number, coordinates, color, author, timestamp, and other metadata.

A simplified annotation file may contain information such as:

[RECTANGLE]
X = 605
Y = 1081
WIDTH = 500
HEIGHT = 201
PAGE = 1
PAGEURL = http://localhost:8080/Test.tif
LINEWIDTH = 8
COLOR = 255, 0, 0
TRANSPARENT = 0
LABEL = Rectangle
PAGESIZE = 1654, 2339
EDIT = 1
CREATEDATE = 29 APR 2026, 11:54:15, UTC
MODIFIEDDATE = 29 APR 2026, 11:54:15, UTC
 
[LINE]
X1 = 833
Y1 = 845
X2 = 837
Y2 = 1594
PAGE = 1
PAGEURL = http://localhost:8080/Test.tif
LINEWIDTH = 8
COLOR = 255, 0, 0
LABEL = Line
PAGESIZE = 1654, 2339
EDIT = 1
CREATEDATE = 29 Apr 2026, 20:48:19, UTC-7
MODIFIEDDATE = 29 Apr 2026, 20:48:19, UTC-7

The real format can be more detailed, but the key point is that ViewONE expects the annotation content to follow a specific structure. Line breaks, section boundaries, and blank lines may affect how the viewer parses the file.

For this reason, the save operation should avoid unnecessary transformations such as trimming all whitespace, collapsing blank lines, changing encodings incorrectly, or rewriting content in a way that breaks the expected structure.


Exporting Annotated Documents

A major feature of the plugin is support for exporting documents with annotations.

Depending on business requirements, export behavior may include:

  • Exporting the original document without annotations
  • Exporting the document with editable annotations
  • Exporting the document with annotations burned in
  • Exporting PDF output
  • Exporting TIFF output
  • Applying special behavior for redactions

For example, a document review workflow may allow users to save editable annotations during review. After approval, the same document may be exported with annotations burned into the final PDF.

In another workflow, TIFF output may be needed for compatibility with downstream imaging systems.

The plugin structure makes this behavior easier to manage because export and burn logic is isolated in burn.jsp rather than mixed directly into the viewer page.


Debug Console Integration

A practical annotation plugin should be easy to troubleshoot. Viewer integrations can fail for several reasons, including incorrect document URLs, missing annotation files, invalid server responses, CORS issues, permissions problems, or malformed annotation content.

The implementation includes debug console integration to help diagnose these problems.

Useful debug information may include:

  • Document URL being loaded
  • Annotation load URL
  • Annotation save URL
  • Burn/export URL
  • Viewer initialization status
  • Save response status
  • Viewer events
  • Error messages returned from server-side handlers

A debug console is especially useful during development and customer issue reproduction because it makes viewer behavior visible without requiring full server-side tracing for every issue.


Viewer Event Tracking

Viewer event tracking provides additional visibility into user and viewer behavior.

Events that may be useful to track include:

  • Viewer loaded
  • Document opened
  • Page changed
  • Annotation created
  • Annotation modified
  • Annotation deleted
  • Annotation save started
  • Annotation save completed
  • Export requested
  • Burn operation completed
  • Viewer error occurred

This event information can be written to the browser console, displayed in a debug panel, or sent to a server-side logging endpoint.

For enterprise environments, event tracking can also help support audit requirements, performance analysis, and user behavior diagnostics.



Deployment Guide

A typical deployment process may look like this:

Step 1: Package the plugin files

Include the main viewer page and server-side handlers:

Virtual-plugin.html
load.jsp
save.jsp
burn.jsp

Step 2: Deploy to the application server

Place the files in the appropriate web application directory or package them into a deployable application archive, depending on your environment.

Step 3: Configure document storage

Make sure the plugin can resolve document URLs or repository document references correctly.

Step 4: Configure annotation storage

Choose where annotation files will be stored. This may be a file system, database, object store, or enterprise content repository.

Step 5: Configure viewer parameters

Set the ViewONE parameters required for document loading, annotation loading, annotation saving, and export behavior.

Step 6: Validate permissions

Confirm that users can only access documents and annotations they are authorized to view or modify.

Step 7: Test

Test the full lifecycle:

Open document
Create annotation
Save annotation
Close viewer
Reopen document
Confirm annotation reloads
Modify annotation
Save again
Export or burn annotations
Validate output file

Step 8: Enable logging and debugging

Enable enough logging to troubleshoot issues during rollout. Debug logging can be reduced after the implementation is stable.


Troubleshooting Common Issues

Issue 1: Annotations do not load

Possible causes:

  • Incorrect annotation load URL
  • Missing annotation file
  • Invalid document identifier
  • User does not have permission
  • Server returns an unexpected response format

Recommended checks:

  • Open browser developer tools
  • Confirm the request to load.jsp succeeds
  • Check the response body
  • Validate the annotation file exists
  • Review server logs for access or path errors

Issue 2: Annotations save but do not reload

Possible causes:

  • Annotation file formatting was changed during save
  • Blank lines were removed
  • Incorrect encoding was used
  • Annotation was saved under the wrong document identifier

Recommended checks:

  • Compare the saved annotation file with a known working file
  • Confirm blank-line formatting is preserved
  • Check whether the document ID used during save matches the document ID used during load

Issue 3: Burned output does not include annotations

Possible causes:

  • Burn handler is not receiving the annotation reference
  • Annotation file is empty or inaccessible
  • Export parameters are incorrect
  • Burn operation is using the source document only

Recommended checks:

  • Confirm burn.jsp receives both document and annotation inputs
  • Check server logs during export
  • Validate the annotation file content before burn
  • Test with a simple annotation first

Issue 4: Export format is incorrect

Possible causes:

  • Output format parameter is missing
  • Viewer or server defaults to PDF
  • TIFF-specific behavior is not enabled
  • PDF input is handled differently from image input

Recommended checks:

  • Confirm the requested output format
  • Validate export parameters
  • Test PDF and TIFF inputs separately
  • Document expected behavior for each input/output combination

Issue 5: Viewer works locally but fails in production

Possible causes:

  • Different context root
  • Reverse proxy or load balancer path changes
  • Authentication differences
  • CORS or CSP restrictions
  • Missing application server permissions

Recommended checks:

  • Compare local and production URLs
  • Check browser console errors
  • Review network requests
  • Validate proxy headers and context paths
  • Confirm production security policies allow viewer resources to load

Conclusion

The ViewONE Annotation Plugin provides a complete foundation for managing annotations in a Daeja ViewONE Virtual Viewer environment. It supports the core annotation lifecycle: loading, editing, saving, exporting, and permanently burning annotations into output documents.

By separating the viewer interface from server-side handlers, the implementation remains understandable and extensible. The use of Virtual-plugin.html, load.jsp, save.jsp, and burn.jsp creates a clean structure that can be adapted for file-based storage, repository-backed storage, or larger enterprise content management systems.

For teams working with IBM Content Navigator, Daeja ViewONE, or document-heavy enterprise workflows, this plugin is a practical starting point for building controlled, secure, and auditable annotation capabilities.

With additional enhancements such as audit logging, repository persistence, versioning, and role-based permissions, the same foundation can evolve into a robust production annotation service for enterprise document processing.

Virtual-plugin.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Daeja Virtual Viewer</title>
    <script type="text/javascript" src="v1files/viewone.js"></script>
    <style>
        body {
            margin: 0;
            font-family: Arial, sans-serif;
        }

        .topbar {
            display: none;
        }

        #status {
            padding: 8px 10px;
            font-size: 14px;
            color: #333;
            border-bottom: 1px solid #ccc;
            background: #f8f8f8;
        }

        .viewer-save-button {
            position: absolute;
            top: 8px;
            right: 470px;
            z-index: 15;
            height: 28px;
            min-width: 120px;
            padding: 0 10px;
            border: 1px solid #bfbfbf;
            border-radius: 3px;
            background: rgba(255, 255, 255, 0.96);
            color: #333;
            font-size: 12px;
            cursor: pointer;
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
        }

        .viewer-save-button:hover {
            background: #f3f3f3;
        }

        .viewer-shell {
            position: relative;
            background: white;
            overflow: hidden;
        }

        .viewer-export-button {
            position: absolute;
            top: 8px;
            right: 248px;
            z-index: 15;
            height: 28px;
            min-width: 96px;
            padding: 0 10px;
            border: 1px solid #bfbfbf;
            border-radius: 3px;
            background: rgba(255, 255, 255, 0.96);
            color: #333;
            font-size: 12px;
            cursor: pointer;
            box-shadow: 0 1px 3px rgba(0,0,0,0.12);
        }

        .viewer-export-button:hover {
            background: #f3f3f3;
        }

        .viewer-export-format {
            position: absolute;
            top: 8px;
            right: 350px;
            z-index: 15;
            height: 28px;
            border: 1px solid #bfbfbf;
            border-radius: 3px;
            background: rgba(255, 255, 255, 0.96);
            color: #333;
            font-size: 12px;
            padding: 0 6px;
        }

        #debugPanel {
            height: 180px;
            overflow: auto;
            background: #111;
            color: #9fef9f;
            font-family: Consolas, monospace;
            font-size: 12px;
            line-height: 1.4;
            padding: 8px 10px;
            border-top: 1px solid #333;
            white-space: pre-wrap;
        }

    </style>
    <script type="text/javascript">
        function getViewer() {
            return document.getElementById('viewone');
        }

        function appendDebug(message) {
            var debugPanel = document.getElementById('debugPanel');
            var timestamp = new Date().toISOString();
            var line = '[' + timestamp + '] ' + message;
            if (debugPanel) {
                debugPanel.textContent += line + '\n';
                debugPanel.scrollTop = debugPanel.scrollHeight;
            }
        }

        function setStatus(message) {
            document.getElementById('status').innerHTML = message;
            appendDebug(message);
        }

        function mirrorConsoleToDebugPanel() {
            if (window.__virtualAntConsoleMirrored) {
                return;
            }
            window.__virtualAntConsoleMirrored = true;

            var originalLog = console.log;
            var originalWarn = console.warn;
            var originalError = console.error;
            var originalClear = console.clear;

            function serializeConsoleArgs(args) {
                return Array.prototype.slice.call(args).map(function(arg) {
                    if (typeof arg === 'string') {
                        return arg;
                    }
                    try {
                        return JSON.stringify(arg, null, 2);
                    } catch (e) {
                        return String(arg);
                    }
                }).join(' ');
            }

            console.log = function() {
                var text = serializeConsoleArgs(arguments);
                appendDebug(text);
                originalLog.apply(console, arguments);
            };

            console.warn = function() {
                var text = serializeConsoleArgs(arguments);
                appendDebug('WARN: ' + text);
                originalWarn.apply(console, arguments);
            };

            console.error = function() {
                var text = serializeConsoleArgs(arguments);
                appendDebug('ERROR: ' + text);
                originalError.apply(console, arguments);
            };

            console.clear = function() {
                var debugPanel = document.getElementById('debugPanel');
                if (debugPanel) {
                    debugPanel.textContent = '';
                }
                appendDebug('Console was cleared');
                if (originalClear) {
                    originalClear.apply(console, arguments);
                }
            };
        }

        function triggerLocalDownload(downloadLocation, exportFormat) {
            if (!downloadLocation) {
                console.warn('No download location returned from exportDocument.');
                return;
            }

            try {
                var normalizedFormat = (exportFormat || 'bin').toLowerCase();
                var separator = downloadLocation.indexOf('?') === -1 ? '?' : '&';
                var forcedDownloadUrl = downloadLocation + separator + 'downloadAs=' + encodeURIComponent(normalizedFormat);

                var link = document.createElement('a');
                link.href = forcedDownloadUrl;
                link.target = '_blank';
                link.rel = 'noopener';
                link.download = 'viewone-export.' + normalizedFormat;

                document.body.appendChild(link);
                link.click();
                document.body.removeChild(link);

                console.log('Triggered local download using returned URL:', forcedDownloadUrl);
            } catch (downloadError) {
                console.error('Failed to trigger local download:', downloadError);
                window.open(downloadLocation, '_blank');
            }
        }

        function normalizeAnnotationText(annotationText) {
            if (!annotationText) {
                return '';
            }

            var text = String(annotationText).replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
            if (!text || text === '[EMPTY]') {
                return '';
            }
            return text + '\n';
        }

        function attachViewerDebugHooks() {
            var viewer = getViewer();
            if (!viewer) {
                appendDebug('Viewer debug hooks: viewer element not found.');
                return;
            }

            appendDebug('Viewer debug hooks attached.');
            appendDebug('Viewer click tracing enabled for toolbar/document interactions.');

            viewer.addEventListener('click', function(event) {
                var target = event.target;
                var label = '';

                if (target) {
                    label = target.getAttribute('aria-label') ||
                            target.getAttribute('title') ||
                            target.getAttribute('alt') ||
                            target.textContent ||
                            target.tagName;
                }

                label = String(label || '').replace(/\s+/g, ' ').trim();
                appendDebug('Viewer click: ' + (label || 'unknown viewer target'));
            }, true);

            viewer.addEventListener('mousedown', function(event) {
                appendDebug('Viewer mouse action: mousedown at x=' + event.clientX + ', y=' + event.clientY);
            }, true);

            viewer.addEventListener('mouseup', function(event) {
                appendDebug('Viewer mouse action: mouseup at x=' + event.clientX + ', y=' + event.clientY);
            }, true);

            viewer.addEventListener('keydown', function(event) {
                appendDebug('Viewer key action: key=' + event.key + ', code=' + event.code);
            }, true);
        }

        function saveAnnotationData() {
            try {
                var viewer = getViewer();
                if (!viewer) {
                    setStatus('Viewer not found');
                    return;
                }

                var annotationText = '';

                if (typeof viewer.getAnnotations === 'function') {
                    annotationText = viewer.getAnnotations();
                    console.log('Using viewer.getAnnotations()');
                } else if (typeof ViewONE !== 'undefined' && typeof ViewONE.getAnnotations === 'function') {
                    annotationText = ViewONE.getAnnotations();
                    console.log('Using ViewONE.getAnnotations()');
                } else if (typeof viewer.getAnnotationText === 'function') {
                    annotationText = viewer.getAnnotationText();
                    console.log('Using viewer.getAnnotationText()');
                } else if (typeof ViewONE !== 'undefined' && typeof ViewONE.getAnnotationText === 'function') {
                    annotationText = ViewONE.getAnnotationText();
                    console.log('Using ViewONE.getAnnotationText()');
                }

                annotationText = normalizeAnnotationText(annotationText);

                if (!annotationText) {
                    setStatus('No annotation data returned by viewer');
                    return;
                }

                var xhr = new XMLHttpRequest();
                xhr.open('POST', 'http://localhost:8080/p2s/annotations/save.jsp?filename=Test.ant', true);
                xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');

                xhr.onload = function() {
                    if (xhr.status >= 200 && xhr.status < 300) {
                        setStatus('Annotations saved to Test.ant');
                    } else {
                        setStatus('Annotation save failed: HTTP ' + xhr.status + ' ' + xhr.responseText);
                    }
                };

                xhr.onerror = function() {
                    setStatus('Annotation save failed due to network error');
                };

                xhr.send('annotationData=' + encodeURIComponent(annotationText));
            } catch (e) {
                setStatus('Annotation save failed: ' + e.message);
            }
        }

        function runExportDocumentDemo() {
            console.clear();
            console.log('=== exportDocument demo ===');

            var viewer = getViewer();
            var viewOneGlobal = (typeof ViewONE !== 'undefined') ? ViewONE : null;
            var exportTarget = null;

            console.log('Viewer object:', viewer);
            console.log('window.ViewONE type:', typeof ViewONE);

            if (viewer && typeof viewer.exportDocument === 'function') {
                exportTarget = viewer;
                console.log('Using viewer.exportDocument()');
            } else if (viewOneGlobal && typeof viewOneGlobal.exportDocument === 'function') {
                exportTarget = viewOneGlobal;
                console.log('Using window.ViewONE.exportDocument()');
            } else {
                setStatus('Export demo failed: exportDocument API unavailable. Check F12 console.');
                console.warn('No exportDocument method found on viewer element or window.ViewONE.');
                if (viewer) {
                    try {
                        console.log('Viewer keys sample:', Object.keys(viewer));
                    } catch (viewerKeysError) {
                        console.warn('Unable to inspect viewer keys:', viewerKeysError);
                    }
                }
                if (viewOneGlobal) {
                    console.log('Available ViewONE keys:', Object.keys(viewOneGlobal));
                }
                return;
            }

            var exportFormatElement = document.getElementById('exportFormat');
            var exportFormat = exportFormatElement ? exportFormatElement.value : 'tif';
            var baseDocumentUrl = 'http://localhost:8080/TestDocs/Tiff/001.tif';
            var exportDocumentUrl = baseDocumentUrl;
            var annotationUrl = baseDocumentUrl;

            if (exportFormat === 'tif') {
                appendDebug('TIFF is selected in dropdown.');
                appendDebug('Using exportDocument for TIFF flow.');
                exportDocumentUrl = baseDocumentUrl + '#export=tif';
                annotationUrl = baseDocumentUrl + '#annotation=tif';
            } else if (exportFormat === 'pdf') {
                appendDebug('PDF is selected in dropdown.');
                appendDebug('Using exportDocument for PDF flow.');
                exportDocumentUrl = baseDocumentUrl + '#export=pdf';
                annotationUrl = baseDocumentUrl + '#annotation=pdf';
            } else {
                appendDebug('Unknown dropdown value, defaulting to TIFF.');
                exportFormat = 'tif';
                exportDocumentUrl = baseDocumentUrl + '#export=tif';
                annotationUrl = baseDocumentUrl + '#annotation=tif';
            }

            appendDebug('Requested export format from dropdown: ' + exportFormat);
            appendDebug('Calling exportDocument(documentUrl, annotationUrl, callback) with documentUrl=' + exportDocumentUrl + ', annotationUrl=' + annotationUrl);

            var callback = {
                onSuccess: function(downloadLocation) {
                    appendDebug('exportDocument onSuccess downloadLocation: ' + downloadLocation);
                    setStatus('Export completed as ' + exportFormat.toUpperCase() + '. Browser download triggered; see debug panel.');
                    triggerLocalDownload(downloadLocation, exportFormat);
                },
                onError: function(errorMsg) {
                    appendDebug('exportDocument onError: ' + errorMsg);
                    setStatus('Export failed. See debug panel.');
                },
                onStatusUpdate: function(message, error, pagesProcessed) {
                    appendDebug('exportDocument onStatusUpdate: message=' + message + ', error=' + error + ', pagesProcessed=' + pagesProcessed);
                }
            };

            try {
                exportTarget.exportDocument(exportDocumentUrl, annotationUrl, callback);
                setStatus('Export request submitted for ' + exportFormat.toUpperCase() + ' using dropdown selection. Check F12 console for generated URL.');
            } catch (e) {
                console.error('Exception calling exportDocument:', e);
                setStatus('Export demo exception. Check F12 console.');
            }
        }

        window.addEventListener('load', function() {
            mirrorConsoleToDebugPanel();
            appendDebug('VirtualAnt page loaded.');
            appendDebug('Viewer initialization tracing enabled.');
            setTimeout(function() {
                attachViewerDebugHooks();
            }, 1500);
        });
    </script>
</head>
<body>
    <div id="status">Viewer ready. Debug panel is shown below for export/save tracing.</div>

    <div class="viewer-shell">
        <button type="button" class="viewer-save-button" onclick="saveAnnotationData()">Save Annotations</button>
        <select id="exportFormat" class="viewer-export-format">
            <option value="tif">TIFF</option>
            <option value="pdf">PDF</option>
        </select>
        <button type="button" class="viewer-export-button" onclick="runExportDocumentDemo()">Export Demo</button>
        <OBJECT CLASS="com.ibm.dv.client.Viewer"
                ID="viewone"
                WIDTH="100%"
                HEIGHT="700px">

            <param name="filename" value="http://localhost:8080/TestDocs/Tiff/001.tif">

            <param name="designPackage" value="carbon">
            <param name="viewmode" value="thumbsleft">

            <param name="annotate" value="true">
            <param name="annotateEdit" value="true">

            <!-- Annotation Load/Save Configuration -->
            <param name="annotationFile" value="http://localhost:8080/p2s/annotations/load.jsp?filename=Test.ant">
            <param name="annotationSaveServlet" value="http://localhost:8080/p2s/annotations/save.jsp?filename=Test.ant">

            <param name="annotationDefaults" value="all {burnable=true}">
            <param name="annotationsToggleBurn" value="mixed">
            <param name="annotationtrace" value="true">
            <param name="annotationDefaultLineColor1" value="line: 255,0,0">
            <param name="annotationDefaultLineColor2" value="rectangle: 0,0,255">
            <param name="annotationEncoding" value="UTF8">
            <param name="annotationJavascriptExtensions" value="true">

            <param name="JavascriptExtensions" value="true">
            <param name="mayscript" value="true">

            <param name="trace" value="true">
            <param name="traceNet" value="true">
            <param name="traceFilter" value="true">
            <param name="traceRender" value="true">
            <param name="traceFont" value="true">
            <param name="traceException" value="true">

            <param name="fileButtonSave" value="true">
            <param name="fileButtonOpen" value="true">

            <param name="initialFocus" value="false">
            <param name="focusBorder" value="false">

        </OBJECT>
    </div>
    <div id="debugPanel"></div>
</body>
</html>

load.jsp

<%@ page language="java" contentType="text/xml; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*" %><%
    // Annotation Load Handler for ViewONE
    // Loads annotation data from a file on the server
   
    response.setContentType("text/xml");
    response.setCharacterEncoding("UTF-8");
   
    try {
        // Get filename parameter from URL
        String filename = request.getParameter("filename");
        if (filename == null || filename.trim().isEmpty()) {
            filename = "annotations.ant";
        }
       
        // Build file path
        String webappPath = application.getRealPath("/");
        String annotationsDir = webappPath + "annotations";
        String filePath = annotationsDir + File.separator + filename;
       
        File annotationFile = new File(filePath);
       
        if (annotationFile.exists() && annotationFile.isFile()) {
            // Read and return the annotation data
            BufferedReader reader = new BufferedReader(new FileReader(annotationFile));
            String line;
            StringBuilder content = new StringBuilder();
           
            while ((line = reader.readLine()) != null) {
                content.append(line).append("\n");
            }
            reader.close();
           
            // Log the load operation
            System.out.println("========================================");
            System.out.println("[ANNOTATION LOAD] SUCCESS");
            System.out.println("[ANNOTATION LOAD] Filename: " + filename);
            System.out.println("[ANNOTATION LOAD] Path: " + filePath);
            System.out.println("[ANNOTATION LOAD] Size: " + content.length() + " bytes");
            System.out.println("[ANNOTATION LOAD] Timestamp: " + new java.util.Date());
            System.out.println("========================================");
           
            // Return the annotation data
            out.print(content.toString());
           
        } else {
            // No annotations found - return empty response
            System.out.println("========================================");
            System.out.println("[ANNOTATION LOAD] No file found");
            System.out.println("[ANNOTATION LOAD] Filename: " + filename);
            System.out.println("[ANNOTATION LOAD] Path: " + filePath);
            System.out.println("[ANNOTATION LOAD] Returning empty annotations");
            System.out.println("========================================");
            out.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?><annotations></annotations>");
        }
       
    } catch (Exception e) {
        System.err.println("========================================");
        System.err.println("[ANNOTATION LOAD ERROR] " + e.getMessage());
        e.printStackTrace();
        System.err.println("========================================");
       
        // Return empty annotations on error
        out.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?><annotations></annotations>");
    }
%>

save.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*,java.util.*" %><%
    // Annotation Save Handler for ViewONE
    // Saves annotation data to a file on the server
   
    response.setContentType("text/html");
    response.setCharacterEncoding("UTF-8");
    response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
    response.setHeader("Pragma", "no-cache");
    response.setDateHeader("Expires", 0);
   
    try {
        // Get filename parameter from URL
        String filename = request.getParameter("filename");
        if (filename == null || filename.trim().isEmpty()) {
            filename = "annotations.ant";
        }
       
        // Read annotation data from the request.
        // Match the working WAR behavior more closely:
        // 1) save raw posted body when present
        // 2) support direct annotationData posts from VirtualAnt.html
        // 3) support segmented form posts such as annotationData, annotationData01..NN + annotationDataCount
        StringBuilder annotationData = new StringBuilder();

        String contentType = request.getContentType();
        if (contentType == null) {
            contentType = "";
        }

        String dataSource = "none";
        String rawPayload = null;

        // Direct parameter used by custom page save
        String directAnnotationData = request.getParameter("annotationData");
        if (directAnnotationData != null && !directAnnotationData.trim().isEmpty()) {
            annotationData.append(directAnnotationData);
            dataSource = "request parameter 'annotationData'";
        }

        // Segmented payload similar to TestingWebApp save.servlet handling
        if (annotationData.length() == 0) {
            String[] countCandidates = { "annotationDataCount", "dataCount", "annotationCount", "numannotations", "numdata" };
            String countValue = null;
            for (String countCandidate : countCandidates) {
                countValue = request.getParameter(countCandidate);
                if (countValue != null && !countValue.trim().isEmpty()) {
                    break;
                }
            }

            if (countValue != null && !countValue.trim().isEmpty()) {
                try {
                    int count = Integer.parseInt(countValue.trim());
                    String[] baseCandidates = { "annotationData", "data", "annotations" };

                    for (String baseName : baseCandidates) {
                        StringBuilder combined = new StringBuilder();
                        boolean foundAny = false;
                        boolean missingPart = false;

                        for (int i = 1; i <= count; i++) {
                            String indexedName = (i < 10) ? (baseName + "0" + i) : (baseName + i);
                            String part = request.getParameter(indexedName);
                            if (part == null) {
                                missingPart = true;
                                break;
                            }
                            combined.append(part);
                            foundAny = true;
                        }

                        if (foundAny && !missingPart) {
                            String encoding = request.getParameter("encoding");
                            if (encoding == null || encoding.trim().isEmpty()) {
                                encoding = "UTF-8";
                            }
                            try {
                                annotationData.append(java.net.URLDecoder.decode(combined.toString(), encoding));
                            } catch (Exception decodeException) {
                                annotationData.append(combined.toString());
                            }
                            dataSource = "segmented request parameters base '" + baseName + "' count " + count;
                            break;
                        }
                    }
                } catch (Exception parseException) {
                    System.out.println("[ANNOTATION SAVE] Unable to parse segmented annotation count: " + countValue);
                }
            }
        }

        // Fallback to common single parameters
        if (annotationData.length() == 0) {
            String[] parameterCandidates = {
                "annotations", "data", "xml", "content"
            };

            for (String paramName : parameterCandidates) {
                String value = request.getParameter(paramName);
                if (value != null && !value.trim().isEmpty()) {
                    annotationData.append(value);
                    dataSource = "request parameter '" + paramName + "'";
                    break;
                }
            }
        }

        // Final fallback to raw request body
        if (annotationData.length() == 0) {
            BufferedReader reader = request.getReader();
            String line;
            while ((line = reader.readLine()) != null) {
                annotationData.append(line).append("\n");
            }
            if (annotationData.length() > 0) {
                dataSource = "raw request body";
            }
        }

        // Preserve full parameter dump for diagnostics when save is empty
        Map<String, String[]> paramMap = request.getParameterMap();
        if (paramMap != null && !paramMap.isEmpty()) {
            StringBuilder formDump = new StringBuilder();
            for (Map.Entry<String, String[]> entry : paramMap.entrySet()) {
                formDump.append(entry.getKey()).append("=");
                String[] values = entry.getValue();
                if (values != null) {
                    for (int i = 0; i < values.length; i++) {
                        if (i > 0) formDump.append(",");
                        formDump.append(values[i]);
                    }
                }
                formDump.append("\n");
            }
            rawPayload = formDump.toString();
        }
       
        String normalizedAnnotationText = annotationData.toString();
        if (normalizedAnnotationText != null && !normalizedAnnotationText.trim().isEmpty()) {
            normalizedAnnotationText = normalizedAnnotationText
                .replace("\r\n", "\n")
                .replace("\r", "\n")
                .replace("<P>", "\n")
                .trim();

            // Add blank line after each annotation block
            // ViewONE annotations end with MODIFIEDDATE line
            normalizedAnnotationText = normalizedAnnotationText
                .replaceAll("(MODIFIEDDATE = [^\n]+)", "$1\n");
           
            // Clean up excessive blank lines (more than 2 consecutive)
            normalizedAnnotationText = normalizedAnnotationText.replaceAll("\n{3,}", "\n\n");

            if (!normalizedAnnotationText.endsWith("\n")) {
                normalizedAnnotationText = normalizedAnnotationText + "\n";
            }
        }

        // Create annotations directory if it doesn't exist
        String webappPath = application.getRealPath("/");
        String annotationsDir = webappPath + "annotations";
        File dir = new File(annotationsDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
       
        // Save annotation file
        String filePath = annotationsDir + File.separator + filename;
       
        FileWriter writer = new FileWriter(filePath);
        writer.write(normalizedAnnotationText);
        writer.close();

        annotationData.setLength(0);
        annotationData.append(normalizedAnnotationText);
       
        // Log the save operation
        System.out.println("========================================");
        System.out.println("[ANNOTATION SAVE] SUCCESS");
        System.out.println("[ANNOTATION SAVE] Filename: " + filename);
        System.out.println("[ANNOTATION SAVE] Path: " + filePath);
        System.out.println("[ANNOTATION SAVE] Content-Type: " + contentType);
        System.out.println("[ANNOTATION SAVE] Data source: " + dataSource);
        System.out.println("[ANNOTATION SAVE] Size: " + annotationData.length() + " bytes");
        if (annotationData.length() > 0) {
            int previewLength = Math.min(annotationData.length(), 500);
            System.out.println("[ANNOTATION SAVE] Preview: " + annotationData.substring(0, previewLength));
        } else {
            System.out.println("[ANNOTATION SAVE] WARNING: No annotation data received");
            if (rawPayload != null && !rawPayload.isEmpty()) {
                System.out.println("[ANNOTATION SAVE] Form parameters received:");
                System.out.println(rawPayload);
            }
        }
        System.out.println("[ANNOTATION SAVE] Timestamp: " + new java.util.Date());
        System.out.println("========================================");
       
        // Return success response with HTML format for ViewONE to display message
        out.println("<html>");
        out.println("<head><title>Annotation Save</title></head>");
        out.println("<body>");
        out.println("<h3>Annotations saved successfully</h3>");
        out.println("<p>File: " + filename + "</p>");
        out.println("<p>Size: " + annotationData.length() + " bytes</p>");
        out.println("<p>Status: OK</p>");
        out.println("</body>");
        out.println("</html>");
        out.flush();
       
    } catch (Exception e) {
        System.err.println("========================================");
        System.err.println("[ANNOTATION SAVE ERROR] " + e.getMessage());
        e.printStackTrace();
        System.err.println("========================================");
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        out.print("ERROR: " + e.getMessage());
    }
%>

burn.jsp

<%@ page language="java" contentType="text/plain; charset=UTF-8" pageEncoding="UTF-8"%><%@ page import="java.io.*,java.nio.file.*" %><%
    // Annotation Burn Handler
    // Receives the burned (annotated) document and saves it back to disk
   
    response.setContentType("text/plain");
    response.setCharacterEncoding("UTF-8");
   
    try {
        // Get the document filename from request
        String filename = request.getParameter("filename");
        if (filename == null || filename.trim().isEmpty()) {
            filename = "annotated-document.tif";
        }
       
        // Extract just the filename without path
        if (filename.contains("/")) {
            filename = filename.substring(filename.lastIndexOf("/") + 1);
        }
        if (filename.contains("\\")) {
            filename = filename.substring(filename.lastIndexOf("\\") + 1);
        }
       
        // Create output directory if it doesn't exist
        String outputDir = "C:/tmp/daeja_annotated";
        File dir = new File(outputDir);
        if (!dir.exists()) {
            dir.mkdirs();
        }
       
        // Generate output filename with timestamp
        String timestamp = String.valueOf(System.currentTimeMillis());
        String baseName = filename.substring(0, filename.lastIndexOf('.'));
        String extension = filename.substring(filename.lastIndexOf('.'));
        String outputFilename = baseName + "_annotated_" + timestamp + extension;
        String outputPath = outputDir + "/" + outputFilename;
       
        // Read the burned document from request input stream
        InputStream inputStream = request.getInputStream();
        FileOutputStream outputStream = new FileOutputStream(outputPath);
       
        byte[] buffer = new byte[8192];
        int bytesRead;
        long totalBytes = 0;
       
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            outputStream.write(buffer, 0, bytesRead);
            totalBytes += bytesRead;
        }
       
        outputStream.close();
        inputStream.close();
       
        // Log the burn operation
        System.out.println("[ANNOTATION BURN] Saved annotated document");
        System.out.println("[ANNOTATION BURN] Original: " + filename);
        System.out.println("[ANNOTATION BURN] Output: " + outputPath);
        System.out.println("[ANNOTATION BURN] Size: " + totalBytes + " bytes");
       
        // Return the path to the saved file
        out.print("OK:" + outputPath);
       
    } catch (Exception e) {
        System.err.println("[ANNOTATION BURN ERROR] " + e.getMessage());
        e.printStackTrace();
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        out.print("ERROR: " + e.getMessage());
    }
%>

0 comments
13 views

Permalink