mirror of
https://github.com/qpdf/qpdf.git
synced 2024-12-22 10:58:58 +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:
parent
3ea83e9993
commit
7fc98d710a
1
.gitignore
vendored
1
.gitignore
vendored
@ -7,3 +7,4 @@ appimage/build
|
||||
.cache
|
||||
/html
|
||||
Doxyfile
|
||||
/examples/qpdf-js/lib/
|
||||
|
@ -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
89
README-emscripten.md
Normal 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/)
|
@ -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
|
||||
|
385
examples/qpdf-js/qpdf-worker.js
Normal file
385
examples/qpdf-js/qpdf-worker.js
Normal 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
196
examples/qpdf-js/qpdf.js
Normal 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
22
examples/qpdf-js/serve.py
Normal 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
3
examples/qpdf-js/serve.sh
Executable file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
cd "$(dirname "$0")"
|
||||
python3 serve.py
|
85
examples/qpdf-js/test.html
Normal file
85
examples/qpdf-js/test.html
Normal 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>
|
Loading…
Reference in New Issue
Block a user