2
1
mirror of https://github.com/qpdf/qpdf.git synced 2024-12-22 02:49:00 +00:00

Add emscripten build

Integrates the necessary options to build a javascript library, with tests and
documentation.

Tested with emscripten 3.1.72
This commit is contained in:
Jean-Christophe Hoelt 2024-11-21 18:07:11 +02:00
parent 3ea83e9993
commit 7fc98d710a
No known key found for this signature in database
GPG Key ID: 838AF264602A1B37
9 changed files with 845 additions and 0 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ appimage/build
.cache
/html
Doxyfile
/examples/qpdf-js/lib/

View File

@ -404,3 +404,63 @@ message(STATUS "")
if(NOT (MULTI_CONFIG OR CMAKE_BUILD_TYPE))
message(WARNING " CMAKE_BUILD_TYPE is not set; using default settings")
endif()
option(EMSCRIPTEN "Build with Emscripten" OFF)
if(EMSCRIPTEN)
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)
set(CMAKE_EXECUTABLE_SUFFIX ".js")
# Define runtime methods to export
set(EXPORTED_RUNTIME_METHODS
"callMain"
"FS"
)
# Enhanced debugging and safety flags
set(EMSCRIPTEN_LINK_FLAGS "\
-s WASM=1 \
-s ABORTING_MALLOC=1 \
-s SUPPORT_LONGJMP=1 \
-s EXPORTED_RUNTIME_METHODS='[${EXPORTED_RUNTIME_METHODS}]' \
-s EXPORT_ALL=1 \
-s FORCE_FILESYSTEM=1 \
-s ALLOW_MEMORY_GROWTH=1 \
-s MODULARIZE=1 \
-s USE_ZLIB=1 \
-s USE_LIBJPEG=1 \
-s BUILD_AS_WORKER=1 \
-s PROXY_TO_WORKER=1 \
-s ENVIRONMENT='worker' \
-s INVOKE_RUN=0 \
-s EXIT_RUNTIME=0 \
-s MAIN_MODULE=1 \
-s EXPORT_NAME='QPDF' \
-s ASSERTIONS=0 \
-s STACK_OVERFLOW_CHECK=0 \
-s DEMANGLE_SUPPORT=0 \
-s ERROR_ON_UNDEFINED_SYMBOLS=0 \
-s DISABLE_EXCEPTION_CATCHING=1 \
-s SAFE_HEAP=0 \
-s SAFE_HEAP_LOG=0 \
-s EXCEPTION_DEBUG=0 \
-s EXCEPTION_STACK_TRACES=0 \
-O2"
)
# Add sanitizer flags for additional runtime checks
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${EMSCRIPTEN_LINK_FLAGS} -fsanitize=address -fsanitize=undefined")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${EMSCRIPTEN_LINK_FLAGS} -fsanitize=address -fsanitize=undefined")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${EMSCRIPTEN_LINK_FLAGS}")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} ${EMSCRIPTEN_LINK_FLAGS}")
# Use native crypto
set(REQUIRE_CRYPTO_NATIVE ON)
set(REQUIRE_CRYPTO_OPENSSL OFF)
set(USE_IMPLICIT_CRYPTO OFF)
# Static linking
set(BUILD_SHARED_LIBS OFF)
set(BUILD_STATIC_LIBS ON)
endif()

89
README-emscripten.md Normal file
View File

@ -0,0 +1,89 @@
# Building QPDF with Emscripten
This guide explains how to compile QPDF to JavaScript/WebAssembly using Emscripten.
> **Note**: For pre-built bundles and a convenient wrapper around the WebAssembly build, check out [qpdf.js](https://github.com/j3k0/qpdf.js).
## Prerequisites
Install Emscripten by following the [official installation instructions](https://emscripten.org/docs/getting_started/downloads.html).
## Quick Start
1. Create and enter build directory:
```bash
mkdir build-emscripten && cd build-emscripten
```
2. Build and copy to examples:
```bash
# Configure and build
emcmake cmake .. \
-DEMSCRIPTEN=ON \
-DCMAKE_BUILD_TYPE=Release \
-DBUILD_SHARED_LIBS=OFF && \
emmake make -j8 qpdf && \
cp qpdf/qpdf.* ../examples/qpdf-js/lib/
```
3. Test the build:
```bash
cd ../examples/qpdf-js
python3 serve.py
```
4. Open `http://localhost:8523/test.html` in your browser
## Build Output
The build produces these files in `build-emscripten/qpdf/`:
- `qpdf.js`: JavaScript glue code
- `qpdf.wasm`: WebAssembly binary
## Browser Usage Example
Here's a minimal example to get started:
```html
<!DOCTYPE html>
<html>
<head>
<title>QPDF Test</title>
</head>
<body>
<pre id="output"></pre>
<script src="qpdf.js"></script>
<script>
function print(text) {
document.getElementById('output').textContent += text + '\n';
console.log(text); // Also log to console for debugging
}
QPDF({
keepAlive: true,
logger: print,
ready: function(qpdf) {
// Test QPDF is working
qpdf.execute(['--copyright'], function(err) {
if (err) {
print('Error: ' + err.message);
return;
}
print('QPDF loaded successfully!');
});
}
});
</script>
</body>
</html>
```
For a complete working example with PDF operations like checking and encryption, see [examples/qpdf-js/test.html](examples/qpdf-js/test.html) in the repository.
## Additional Resources
- [qpdf.js](https://github.com/j3k0/qpdf.js) - Pre-built WebAssembly bundles with JavaScript wrapper
- [Emscripten Documentation](https://emscripten.org/docs/getting_started/index.html)
- [WebAssembly](https://webassembly.org/)
- [QPDF Documentation](https://qpdf.readthedocs.io/)

View File

@ -142,6 +142,10 @@ qpdf is known to build and pass its test suite with mingw and Microsoft Visual C
work. In addition to the manual, see [README-windows.md](README-windows.md) for more details on how to build under
Windows.
## WebAssembly / JavaScript Support
QPDF can be compiled to WebAssembly for use in browsers and JavaScript environments. For most users, we recommend using [qpdf.js](https://github.com/j3k0/qpdf.js) which provides pre-built bundles and a convenient wrapper around the WebAssembly build. If you need to build the WebAssembly version yourself, see [README-emscripten.md](README-emscripten.md) for instructions.
# Building Documentation
The qpdf manual is written in reStructured Text format and is build with [sphinx](https://www.sphinx-doc.org). The

View File

@ -0,0 +1,385 @@
/* eslint-env worker */
/* global FS, callMain */
const scriptURL = self.location.href;
const basePath = scriptURL.substring(0, scriptURL.lastIndexOf('/') + 1);
let errorOrWarning = '';
function stdout(txt) {
postMessage({
type: 'stdout',
line: txt
});
}
function debugLog(type, ...args) {
const msg = args.map(arg => {
if (arg instanceof Error) {
return arg.message + '\n' + arg.stack;
}
if (typeof arg === 'object') {
return JSON.stringify(arg, null, 2);
}
return String(arg);
}).join(' ');
// stdout(`[DEBUG:${type}] ${msg}`);
}
var Module = {
thisProgram: 'qpdf',
noInitialRun: true,
print: function(text) {
stdout(text);
},
printErr: function(text) {
if (text.startsWith('WARNING: ')) {
errorOrWarning = text.slice(8);
}
stdout('[stderr] ' + text);
},
onRuntimeInitialized: function() {
debugLog('init', 'QPDF runtime initialized');
postMessage({
type: 'ready'
});
},
locateFile: function(path, prefix) {
if (path.endsWith('.wasm')) {
return basePath + 'lib/' + path;
}
return prefix + path;
},
quit: function(status, toThrow) {
debugLog('quit', `Program quit with status: ${status}`, toThrow);
if (toThrow) {
throw status;
}
}
};
importScripts(basePath + 'lib/qpdf.js');
function getFileData(fileName) {
try {
if (!FS.analyzePath(fileName).exists) {
debugLog('getFileData', `File ${fileName} does not exist`);
return null;
}
const file = FS.root.contents[fileName];
if (!file) {
debugLog('getFileData', `Could not access ${fileName} in FS`);
return null;
}
debugLog('getFileData', `Successfully read ${fileName}, size: ${file.contents.length}`);
return file.contents;
} catch (error) {
debugLog('getFileData', 'Error reading file:', error);
return null;
}
}
onmessage = function(event) {
var message = event.data;
switch (message.type) {
case 'save': {
const filename = message.filename;
const arrayBuffer = message.arrayBuffer;
try {
debugLog('save', `Saving ${filename}, size: ${arrayBuffer.byteLength}`);
if (!(arrayBuffer instanceof ArrayBuffer) && !(ArrayBuffer.isView(arrayBuffer))) {
throw new Error(`Invalid data type. Expected ArrayBuffer, got ${Object.prototype.toString.call(arrayBuffer)}`);
}
// Clean up any existing file
if (FS.analyzePath(filename).exists) {
debugLog('save', `Removing existing ${filename}`);
FS.unlink(filename);
}
const data = ArrayBuffer.isView(arrayBuffer) ? arrayBuffer : new Uint8Array(arrayBuffer);
FS.createDataFile('/', filename, data, true, false);
debugLog('save', `Successfully saved ${filename}`);
postMessage({
type: 'saved',
filename
});
} catch (error) {
debugLog('save', 'Error saving file:', error);
postMessage({
type: 'error',
message: error.message
});
}
break;
}
case 'load': {
const filename = message.filename;
try {
debugLog('load', `Loading ${filename}`);
const data = getFileData(filename);
if (data) {
debugLog('load', `Successfully loaded ${filename}`);
postMessage({
type: 'loaded',
filename,
arrayBuffer: data
});
} else {
throw new Error(`Failed to load ${filename}`);
}
} catch (error) {
debugLog('load', 'Error loading file:', error);
postMessage({
type: 'error',
message: error.message
});
}
break;
}
case 'execute': {
const args = message.args;
try {
debugLog('execute', `Running qpdf with args: ${args.join(' ')}`);
let exitStatus = undefined;
let exitMessage = undefined;
try {
exitStatus = callMain(args);
} catch (e) {
const decoded = decodeQPDFExc(e);
if (decoded) {
stdout('QPDFExc thrown');
stdout('file: ' + decoded.filename);
stdout('message: ' + decoded.message);
stdout('dump:');
stdout(decoded.dump);
exitMessage = decoded.message;
}
else if (e > 100 && e < HEAP8.length) {
stdout(hexdump(HEAP8.slice(e, e + 512), e));
}
else {
stdout('Error thrown: ' + e);
}
}
debugLog('execute', `Command completed with status: ${exitStatus === undefined ? EXITSTATUS : exitStatus}`);
if (exitStatus !== 0) {
postMessage({
type: 'executed',
status: exitStatus,
error: exitMessage || errorOrWarning || 'Command failed',
});
} else {
postMessage({
type: 'executed',
status: exitStatus,
});
}
} catch (error) {
debugLog('execute', 'Error during execution:', error);
postMessage({
type: 'executed',
status: 1,
error: error.message || String(error)
});
}
break;
}
}
};
function decodeQPDFExc(addr) {
try {
if (typeof addr !== 'number') {
debugLog('QPDFExc', 'Invalid address type:', typeof addr);
return null;
}
debugLog('QPDFExc', `Attempting to decode exception at address 0x${addr.toString(16)}`);
// Helper to read a null-terminated string
const readCString = (offset) => {
let result = '';
let i = 0;
while (i < 1000) { // Safety limit
const char = HEAP8[offset + i] & 0xFF;
if (char === 0) break;
result += String.fromCharCode(char);
i++;
}
if (result) {
debugLog('QPDFExc', `Read C string at 0x${offset.toString(16)}: "${result}"`);
}
return result;
};
// Helper to read a 32-bit little endian value
const readU32 = (offset) => {
const value = HEAP8[offset] & 0xFF |
((HEAP8[offset + 1] & 0xFF) << 8) |
((HEAP8[offset + 2] & 0xFF) << 16) |
((HEAP8[offset + 3] & 0xFF) << 24);
debugLog('QPDFExc', `Read U32 at 0x${offset.toString(16)}: 0x${value.toString(16)}`);
return value;
};
// First field might be vtable
const vtable = readU32(addr);
debugLog('QPDFExc', `Potential vtable pointer: 0x${vtable.toString(16)}`);
// Second field
const field2 = readU32(addr + 4);
debugLog('QPDFExc', `Second field: 0x${field2.toString(16)}`);
// Next fields appear to be lengths or flags
const field3 = readU32(addr + 8); // 0 in arg error case
const field4 = readU32(addr + 12); // 0x3b in arg error case
const field5 = readU32(addr + 16); // 0x22 in arg error case
const field6 = readU32(addr + 20); // 0x22 in arg error case
const field7 = readU32(addr + 24); // 0 in arg error case
debugLog('QPDFExc', `Additional fields: ${field3}, ${field4}, ${field5}, ${field6}, ${field7}`);
// Look for error patterns in a more structured way
const errorPatterns = [
// File errors
{
type: 'file_error',
markers: ["can't find", "error", "failed", "unrecognized", "invalid"]
},
// Object errors (new pattern)
{
type: 'object_error',
markers: ["expected", "object", "offset"]
}
];
let errorInfo = {
filename: null,
object: null,
offset: null,
message: null
};
// Look for structured error components
for (let i = 0; i < 200; i++) {
const testStr = readCString(addr + i);
if (!testStr) continue;
// Find filename
if (testStr.endsWith('.pdf')) {
errorInfo.filename = testStr;
debugLog('QPDFExc', `Found filename: "${testStr}"`);
i += testStr.length;
}
// Find object reference (e.g., "object 2 0")
const objectMatch = testStr.match(/object (\d+) (\d+)/);
if (objectMatch) {
errorInfo.object = `${objectMatch[1]} ${objectMatch[2]}`;
debugLog('QPDFExc', `Found object reference: "${errorInfo.object}"`);
}
// Find offset (e.g., "offset 60")
const offsetMatch = testStr.match(/offset (\d+)/);
if (offsetMatch) {
errorInfo.offset = parseInt(offsetMatch[1]);
debugLog('QPDFExc', `Found offset: ${errorInfo.offset}`);
}
// Look for error message
for (const pattern of errorPatterns) {
if (pattern.markers.some(marker => testStr.includes(marker))) {
errorInfo.message = testStr;
debugLog('QPDFExc', `Found error message (${pattern.type}): "${testStr}"`);
i += testStr.length;
break;
}
}
}
// Format the complete error message
let formattedMessage = '';
if (errorInfo.filename) {
formattedMessage += errorInfo.filename;
if (errorInfo.object || errorInfo.offset) {
formattedMessage += ' (';
if (errorInfo.object) formattedMessage += `object ${errorInfo.object}`;
if (errorInfo.object && errorInfo.offset) formattedMessage += ', ';
if (errorInfo.offset) formattedMessage += `offset ${errorInfo.offset}`;
formattedMessage += ')';
}
formattedMessage += ': ';
}
formattedMessage += errorInfo.message || 'Unknown error';
const result = {
type: 'QPDFExc',
addr: '0x' + addr.toString(16),
vtable: '0x' + readU32(addr).toString(16),
field2: '0x' + readU32(addr + 4).toString(16),
lengths: [
readU32(addr + 8),
readU32(addr + 12),
readU32(addr + 16),
readU32(addr + 20),
readU32(addr + 24)
],
...errorInfo,
formattedMessage,
dump: hexdump(HEAP8.slice(addr, addr + 128), addr)
};
debugLog('QPDFExc', 'Successfully decoded exception:', result);
return result;
} catch (e) {
debugLog('QPDFExc', 'Error while decoding:', e);
return null;
}
}
function hexdump(array, offset = 0, length = array.length) {
const bytesPerLine = 16;
let output = '';
for (let i = 0; i < length; i += bytesPerLine) {
// Address column
output += (offset + i).toString(16).padStart(8, '0');
output += ' ';
// Hex columns
for (let j = 0; j < bytesPerLine; j++) {
if (j === 8) output += ' ';
if (i + j < length) {
// Convert signed byte to unsigned byte (0-255)
const unsignedByte = array[i + j] & 0xFF;
output += unsignedByte.toString(16).padStart(2, '0');
output += ' ';
} else {
output += ' ';
}
}
// ASCII column
output += ' |';
for (let j = 0; j < bytesPerLine && i + j < length; j++) {
// Convert signed byte to unsigned byte (0-255)
const unsignedByte = array[i + j] & 0xFF;
// Print as ASCII if printable, otherwise print a dot
output += (unsignedByte >= 32 && unsignedByte <= 126) ?
String.fromCharCode(unsignedByte) : '.';
}
output += '|\n';
}
return output;
}

196
examples/qpdf-js/qpdf.js Normal file
View File

@ -0,0 +1,196 @@
/* eslint-env browser */
(function () {
// The QPDF Module
function QPDF (options) {
const {
logger = console.log.bind(console),
ready,
path = QPDF.path || '',
keepAlive = false,
} = options;
let worker = new Worker(path + 'qpdf-worker.js');
const listeners = {};
let nListeners = 0;
const addListener = function (id, fn) {
listeners[id] = fn;
nListeners += 1;
};
const callListener = function (id, err, arg) {
const fn = listeners[id];
if (fn) {
delete listeners[id];
fn(err, arg);
}
nListeners -= 1;
if (!keepAlive && nListeners === 0) {
setTimeout(function () {
// No new commands after 1 second?
// Then we terminate the worker (unless keepAlive is true).
if (worker !== null && nListeners === 0) {
worker.terminate();
worker = null;
}
}, 1000);
}
};
const qpdf = {
save (filename, arrayBuffer, callback) {
if (!worker) { return callback(new Error('worker terminated')); }
if (callback) {
addListener(filename, callback);
}
worker.postMessage({
type: 'save',
filename,
arrayBuffer
});
},
load (filename, callback) {
if (!worker) { return callback(new Error('worker terminated')); }
if (callback) {
addListener(filename, callback);
}
worker.postMessage({
type: 'load',
filename
});
},
execute (args, callback) {
if (!worker) { return callback(new Error('worker terminated')); }
if (callback) {
addListener('execute', callback);
}
worker.postMessage({
type: 'execute',
args
});
},
terminate() {
if (worker) {
worker.terminate();
worker = null;
}
}
};
worker.onmessage = function (event) {
const message = event.data;
switch (message.type) {
case 'ready': {
logger('[qpdf] ready');
if (ready) {
ready(qpdf);
}
break;
}
case 'stdout':
logger('[qpdf.worker] ' + message.line);
break;
case 'saved': {
const filename = message.filename;
logger('[qpdf] ' + filename + ' saved');
callListener(filename, null);
break;
}
case 'loaded': {
const { filename, arrayBuffer } = message;
logger('[qpdf] ' + filename + ' loaded (' + arrayBuffer.length + ')');
if (arrayBuffer) {
callListener(filename, null, arrayBuffer);
} else {
callListener(filename, new Error('File not found'));
}
break;
}
case 'executed': {
const { status, error, output } = message;
logger('[qpdf] exited with status ' + status);
if (output) {
if (output.stdout) logger('[qpdf] stdout: ' + output.stdout);
if (output.stderr) logger('[qpdf] stderr: ' + output.stderr);
}
if (status !== 0) {
callListener('execute', new Error(error || 'QPDF exited with status ' + status));
} else {
callListener('execute', null);
}
break;
}
}
};
}
QPDF.encrypt = function ({
logger,
arrayBuffer,
userPassword,
ownerPassword,
keyLength,
callback,
}) {
const safeCallback = function (err, arg) {
if (callback) {
if (err || arg) {
callback(err, arg);
callback = null;
}
}
};
QPDF({
logger,
ready: function (qpdf) {
qpdf.save('input.pdf', arrayBuffer, safeCallback);
qpdf.execute([
'--encrypt',
userPassword || '',
ownerPassword || '',
String(keyLength || 256),
'--',
'input.pdf',
'output.pdf'
], safeCallback);
qpdf.load('output.pdf', safeCallback);
}
});
};
QPDF.help = function (logger) {
QPDF({
logger,
ready: function (qpdf) {
qpdf.execute(['--help']);
}
});
};
QPDF.base64ToArrayBuffer = function (base64) {
const binary = window.atob(base64);
const len = binary.length;
const bytes = new Uint8Array(len);
for (let i = 0; i < len; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes.buffer;
};
QPDF.arrayBufferToBase64 = function (buffer) {
let binary = '';
const bytes = new Uint8Array(buffer);
const len = bytes.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(bytes[i]);
}
return window.btoa(binary);
};
window.QPDF = QPDF;
})();

22
examples/qpdf-js/serve.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python3
import http.server
import socketserver
class WASMHandler(http.server.SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def end_headers(self):
# Add CORS headers
self.send_header('Access-Control-Allow-Origin', '*')
# Add proper MIME type for .wasm files
if self.path.endswith('.wasm'):
self.send_header('Content-Type', 'application/wasm')
super().end_headers()
PORT = 8523
Handler = WASMHandler
with socketserver.TCPServer(("", PORT), Handler) as httpd:
print(f"Serving at http://localhost:{PORT}")
httpd.serve_forever()

3
examples/qpdf-js/serve.sh Executable file
View File

@ -0,0 +1,3 @@
#!/bin/bash
cd "$(dirname "$0")"
python3 serve.py

View File

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html>
<head>
<title>QPDF Test</title>
</head>
<body>
<h1>QPDF Test</h1>
<pre id="output"></pre>
<script src="./qpdf.js"></script>
<script>
function print(text) {
document.getElementById('output').textContent += text + '\n';
console.log(text); // Also log to console for debugging
}
QPDF.path = '/';
function testCheck(done) {
QPDF({
keepAlive: true,
logger: function (text) {
print(text);
},
ready: function (qpdf) {
// First test just the copyright notice
qpdf.execute(['--copyright'], function (err) {
if (err) {
print('Error showing copyright: ' + err.message);
return;
}
// Create and test a minimal PDF
qpdf.save('input.pdf', myPDF(), function (err) {
if (err) {
print('Error saving PDF: ' + err.message);
return;
}
});
// Check with verbose output
qpdf.execute(['--verbose', '--check', 'input.pdf'], function (err) {
if (err) {
print('Error checking PDF: ' + err.message);
return;
}
print('PDF check completed successfully');
done();
});
});
}
});
}
function testEncrypt(done) {
QPDF.encrypt({
arrayBuffer: myPDF(),
userPassword: 'test',
ownerPassword: 'test',
keyLength: 256,
logger: print,
callback: function (err, content) {
print('callback: ' + err + ' ' + QPDF.arrayBufferToBase64(content));
done();
}
});
}
function myPDF() {
// generated with "cat my-pdf.pdf | base64"
const pageWithA = 'JVBERi0xLjMKJb/3ov4KJVFERi0xLjAKCjEgMCBvYmoKPDwKICAvUGFnZXMgMiAwIFIKICAvVHlwZSAvQ2F0YWxvZwo+PgplbmRvYmoKCjIgMCBvYmoKPDwKICAvQ291bnQgMQogIC9LaWRzIFsKICAgIDMgMCBSCiAgXQogIC9UeXBlIC9QYWdlcwo+PgplbmRvYmoKCiUlIFBhZ2UgMQozIDAgb2JqCjw8CiAgL0NvbnRlbnRzIDQgMCBSCiAgL01lZGlhQm94IFsKICAgIDAKICAgIDAKICAgIDYxMgogICAgNzkyCiAgXQogIC9QYXJlbnQgMiAwIFIKICAvUmVzb3VyY2VzIDw8CiAgICAvRm9udCA8PAogICAgICAvRjEgNiAwIFIKICAgID4+CiAgICAvUHJvY1NldCA3IDAgUgogID4+CiAgL1R5cGUgL1BhZ2UKPj4KZW5kb2JqCgolJSBDb250ZW50cyBmb3IgcGFnZSAxCjQgMCBvYmoKPDwKICAvTGVuZ3RoIDUgMCBSCj4+CnN0cmVhbQpCVAogIC9GMSAyNCBUZgogIDcyIDcyMCBUZAogIChBKSBUagpFVAplbmRzdHJlYW0KZW5kb2JqCgo1IDAgb2JqCjM5CmVuZG9iagoKNiAwIG9iago8PAogIC9CYXNlRm9udCAvSGVsdmV0aWNhCiAgL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcKICAvTmFtZSAvRjEKICAvU3VidHlwZSAvVHlwZTEKICAvVHlwZSAvRm9udAo+PgplbmRvYmoKCjcgMCBvYmoKWwogIC9QREYKICAvVGV4dApdCmVuZG9iagoKeHJlZgowIDgKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDI1IDAwMDAwIG4gCjAwMDAwMDAwNzkgMDAwMDAgbiAKMDAwMDAwMDE2MSAwMDAwMCBuIAowMDAwMDAwMzc2IDAwMDAwIG4gCjAwMDAwMDA0NzAgMDAwMDAgbiAKMDAwMDAwMDQ4OSAwMDAwMCBuIAowMDAwMDAwNjA3IDAwMDAwIG4gCnRyYWlsZXIgPDwKICAvUm9vdCAxIDAgUgogIC9TaXplIDgKICAvSUQgWzwzNmIwNzIzMmQ3NjU3NjU4YzU0ODAwNjE1MWQ0YzU3Yz48MzZiMDcyMzJkNzY1NzY1OGM1NDgwMDYxNTFkNGM1N2M+XQo+PgpzdGFydHhyZWYKNjQyCiUlRU9GCg==';
return QPDF.base64ToArrayBuffer(pageWithA);
}
testCheck(function () {
testEncrypt(function () {
print('ALL TESTS PASSED');
});
});
</script>
</body>
</html>