diff --git a/CMakeLists.txt b/CMakeLists.txt index fe7def4..10fc361 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,8 +35,42 @@ if(EMSCRIPTEN) set(PDFENGINE_WASM ON CACHE INTERNAL "Building for WebAssembly") endif() +option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON) +option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF) +option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) +option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF) +option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF) + +if(PDFENGINE_WASM) + set(PDFENGINE_BUILD_TESTS OFF CACHE BOOL "Build engine unit/smoke tests" FORCE) + set(PDFENGINE_WITH_PDFIUM OFF CACHE BOOL "Link the PDFium static lib" FORCE) + set(PDFENGINE_ENABLE_SANITIZERS OFF CACHE BOOL "ASan/UBSan" FORCE) +endif() + +include(CompilerWarnings) +include(Sanitizers) + +if(PDFENGINE_WITH_PDFIUM) + include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON +endif() +if(PDFENGINE_WITH_SKIA) + include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON +endif() + +find_package(freetype CONFIG REQUIRED) +find_package(harfbuzz CONFIG REQUIRED) +find_package(spdlog CONFIG REQUIRED) +find_package(nlohmann_json CONFIG REQUIRED) + +if(PDFENGINE_BUILD_TESTS) + find_package(GTest CONFIG REQUIRED) + enable_testing() + include(GoogleTest) +endif() + if(PDFENGINE_WASM) message(STATUS "PdfEngine ${PROJECT_VERSION} — WASM configuration (Phase 1/2 Enabled)") + add_subdirectory(engine) add_subdirectory(wasm) else() # Only the top-level project drives testing/install defaults. @@ -45,36 +79,14 @@ else() "cmake --preset -debug") endif() - option(PDFENGINE_BUILD_TESTS "Build engine unit/smoke tests" ON) - option(PDFENGINE_ENABLE_SANITIZERS "Build with AddressSanitizer/UBSan" OFF) - option(PDFENGINE_WARNINGS_AS_ERRORS "Treat compiler warnings as errors" OFF) - option(PDFENGINE_WITH_PDFIUM "Link the PDFium static lib (build it first)" OFF) - option(PDFENGINE_WITH_SKIA "Link the Skia static lib (build it first)" OFF) - - include(CompilerWarnings) - include(Sanitizers) - include(pdfium) # defines pdfium::pdfium when PDFENGINE_WITH_PDFIUM is ON - include(skia) # defines skia::skia when PDFENGINE_WITH_SKIA is ON - - find_package(freetype CONFIG REQUIRED) - find_package(harfbuzz CONFIG REQUIRED) - find_package(spdlog CONFIG REQUIRED) - find_package(nlohmann_json CONFIG REQUIRED) - - if(PDFENGINE_BUILD_TESTS) - find_package(GTest CONFIG REQUIRED) - enable_testing() - include(GoogleTest) - endif() - add_subdirectory(engine) add_subdirectory(bindings) - - message(STATUS "PdfEngine ${PROJECT_VERSION} configured") - message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}") - message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}") - message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}") - message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}") - message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}") - message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}") endif() + +message(STATUS "PdfEngine ${PROJECT_VERSION} configured") +message(STATUS " C++ standard ......... ${CMAKE_CXX_STANDARD}") +message(STATUS " Build tests .......... ${PDFENGINE_BUILD_TESTS}") +message(STATUS " Sanitizers ........... ${PDFENGINE_ENABLE_SANITIZERS}") +message(STATUS " Warnings as errors ... ${PDFENGINE_WARNINGS_AS_ERRORS}") +message(STATUS " Link PDFium .......... ${PDFENGINE_WITH_PDFIUM}") +message(STATUS " Link Skia ............ ${PDFENGINE_WITH_SKIA}") \ No newline at end of file diff --git a/CMakePresets.json b/CMakePresets.json index 48413bb..d08529d 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -102,7 +102,7 @@ "displayName": "WASM • Emscripten hello-world (Phase 0, Rule R5)", "generator": "Ninja", "binaryDir": "${sourceDir}/out/build/${presetName}", - "toolchainFile": "${sourceDir}/cmake/toolchains/wasm.cmake", + "toolchainFile": "${sourceDir}/cmake/toolchains/vcpkg-wasm.cmake", "cacheVariables": { "CMAKE_BUILD_TYPE": "Release", "PDFENGINE_BUILD_TESTS": "OFF", diff --git a/engine/src/fonts/pdf_fonts/font_loader.hpp b/engine/src/fonts/pdf_fonts/font_loader.hpp index 045eec4..9e6cf20 100644 --- a/engine/src/fonts/pdf_fonts/font_loader.hpp +++ b/engine/src/fonts/pdf_fonts/font_loader.hpp @@ -2,14 +2,13 @@ #include "fonts/pdf_fonts/font.hpp" #include "fonts/pdf_fonts/font_descriptor.hpp" +#include "fonts/pdf_fonts/encoding/encoding.hpp" #include #include #include namespace pdfengine::fonts::pdf_fonts { -class Encoding; - class FontLoader { public: // Factory method to load an embedded TrueType font from its raw stream bytes @@ -53,4 +52,4 @@ public: ); }; -} // namespace pdfengine::fonts::pdf_fonts +} diff --git a/engine/src/fonts/pdf_fonts/types/cid_font.hpp b/engine/src/fonts/pdf_fonts/types/cid_font.hpp index c39ed2c..1135a8a 100644 --- a/engine/src/fonts/pdf_fonts/types/cid_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/cid_font.hpp @@ -2,6 +2,7 @@ #include "fonts/pdf_fonts/font.hpp" #include "fonts/pdf_fonts/font_descriptor.hpp" +#include "fonts/pdf_fonts/encoding/encoding.hpp" #include #include #include @@ -9,8 +10,6 @@ #include namespace pdfengine::fonts::pdf_fonts { - -class Encoding; class FontSubset; class CIDFont : public Font { diff --git a/engine/src/fonts/pdf_fonts/types/truetype_font.hpp b/engine/src/fonts/pdf_fonts/types/truetype_font.hpp index b98ebc5..c2e9ebd 100644 --- a/engine/src/fonts/pdf_fonts/types/truetype_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/truetype_font.hpp @@ -2,13 +2,12 @@ #include "fonts/pdf_fonts/font.hpp" #include "fonts/pdf_fonts/font_descriptor.hpp" +#include "fonts/pdf_fonts/encoding/encoding.hpp" #include #include #include namespace pdfengine::fonts::pdf_fonts { - -class Encoding; class FontSubset; class TrueTypeFont : public Font { diff --git a/engine/src/fonts/pdf_fonts/types/type1_font.hpp b/engine/src/fonts/pdf_fonts/types/type1_font.hpp index 2e6ecd3..1117c3e 100644 --- a/engine/src/fonts/pdf_fonts/types/type1_font.hpp +++ b/engine/src/fonts/pdf_fonts/types/type1_font.hpp @@ -2,14 +2,13 @@ #include "fonts/pdf_fonts/font.hpp" #include "fonts/pdf_fonts/font_descriptor.hpp" +#include "fonts/pdf_fonts/encoding/encoding.hpp" #include #include #include #include namespace pdfengine::fonts::pdf_fonts { - -class Encoding; class FontSubset; class Type1Font : public Font { diff --git a/frontend/public/pdfengine.mjs b/frontend/public/pdfengine.mjs index db974fc..24c465a 100644 --- a/frontend/public/pdfengine.mjs +++ b/frontend/public/pdfengine.mjs @@ -1,2 +1,2 @@ -async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var readyPromiseResolve,readyPromiseReject;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("pdfengine.wasm")}return new URL("pdfengine.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["setValue"]=setValue;Module["getValue"]=getValue;var _loadDocument,_renderPage,_freeDocument,_engineBuildInfo,_engineHasSkia,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_loadDocument=Module["_loadDocument"]=wasmExports["f"];_renderPage=Module["_renderPage"]=wasmExports["g"];_freeDocument=Module["_freeDocument"]=wasmExports["h"];_engineBuildInfo=Module["_engineBuildInfo"]=wasmExports["i"];_engineHasSkia=Module["_engineHasSkia"]=wasmExports["j"];_malloc=Module["_malloc"]=wasmExports["k"];_free=Module["_free"]=wasmExports["l"];__emscripten_stack_restore=wasmExports["m"];__emscripten_stack_alloc=wasmExports["n"];_emscripten_stack_get_current=wasmExports["o"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,c:__abort_js,b:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} +async function Module(moduleArg={}){var moduleRtn;var Module=moduleArg;var ENVIRONMENT_IS_WEB=!!globalThis.window;var ENVIRONMENT_IS_WORKER=!!globalThis.WorkerGlobalScope;var ENVIRONMENT_IS_NODE=globalThis.process?.versions?.node&&globalThis.process?.type!="renderer";if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("node:module");var require=createRequire(import.meta.url)}var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var _scriptName=import.meta.url;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){var fs=require("node:fs");if(_scriptName.startsWith("file:")){scriptDirectory=require("node:path").dirname(require("node:url").fileURLToPath(_scriptName))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename);return ret};readAsync=async(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):filename;var ret=fs.readFileSync(filename,binary?undefined:"utf8");return ret};if(process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){try{scriptDirectory=new URL(".",_scriptName).href}catch{}{readAsync=async url=>{var response=await fetch(url,{credentials:"same-origin"});if(response.ok){return response.arrayBuffer()}throw new Error(response.status+" : "+response.url)}}}else{}var out=console.log.bind(console);var err=console.error.bind(console);var wasmBinary;var ABORT=false;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var readyPromiseResolve,readyPromiseReject;var runtimeInitialized=false;function updateMemoryViews(){var b=wasmMemory.buffer;HEAP8=new Int8Array(b);HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAPU16=new Uint16Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b);HEAPF32=new Float32Array(b);HEAPF64=new Float64Array(b);HEAP64=new BigInt64Array(b);HEAPU64=new BigUint64Array(b)}function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["e"]()}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(onPostRuns)}function abort(what){Module["onAbort"]?.(what);what=`Aborted(${what})`;err(what);ABORT=true;what+=". Build with -sASSERTIONS for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject?.(e);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("pdfengine.wasm")}return new URL("pdfengine.wasm",import.meta.url).href}function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}async function getWasmBinary(binaryFile){if(!wasmBinary){try{var response=await readAsync(binaryFile);return new Uint8Array(response)}catch{}}return getBinarySync(binaryFile)}async function instantiateArrayBuffer(binaryFile,imports){try{var binary=await getWasmBinary(binaryFile);var instance=await WebAssembly.instantiate(binary,imports);return instance}catch(reason){err(`failed to asynchronously prepare wasm: ${reason}`);abort(reason)}}async function instantiateAsync(binary,binaryFile,imports){if(!binary&&!ENVIRONMENT_IS_NODE){try{var response=fetch(binaryFile,{credentials:"same-origin"});var instantiationResult=await WebAssembly.instantiateStreaming(response,imports);return instantiationResult}catch(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation")}}return instantiateArrayBuffer(binaryFile,imports)}function getWasmImports(){var imports={a:wasmImports};return imports}async function createWasm(){function receiveInstance(instance,module){wasmExports=instance.exports;assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();if(Module["instantiateWasm"]){return new Promise((resolve,reject)=>{Module["instantiateWasm"](info,(inst,mod)=>{resolve(receiveInstance(inst,mod))})})}wasmBinaryFile??=findWasmBinary();var result=await instantiateAsync(wasmBinary,wasmBinaryFile,info);var exports=receiveInstantiationResult(result);return exports}class ExitStatus{name="ExitStatus";constructor(status){this.message=`Program terminated with exit(${status})`;this.status=status}}var HEAP16;var HEAP32;var HEAP64;var HEAP8;var HEAPF32;var HEAPF64;var HEAPU16;var HEAPU32;var HEAPU64;var HEAPU8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var addOnPostRun=cb=>onPostRuns.push(cb);var onPreRuns=[];var addOnPreRun=cb=>onPreRuns.push(cb);function getValue(ptr,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":return HEAP8[ptr];case"i8":return HEAP8[ptr];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP64[ptr>>3];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];case"*":return HEAPU32[ptr>>2];default:abort(`invalid type for getValue: ${type}`)}}var noExitRuntime=true;function setValue(ptr,value,type="i8"){if(type.endsWith("*"))type="*";switch(type){case"i1":HEAP8[ptr]=value;break;case"i8":HEAP8[ptr]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":HEAP64[ptr>>3]=BigInt(value);break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;case"*":HEAPU32[ptr>>2]=value;break;default:abort(`invalid type for setValue: ${type}`)}}var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var uncaughtExceptionCount=0;var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);uncaughtExceptionCount++;abort()};var __abort_js=()=>abort("");var getHeapMax=()=>2147483648;var alignMemory=(size,alignment)=>Math.ceil(size/alignment)*alignment;var growMemory=size=>{var oldHeapSize=wasmMemory.buffer.byteLength;var pages=(size-oldHeapSize+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var replacement=growMemory(newSize);if(replacement){return true}}return false};var getCFunc=ident=>{var func=Module["_"+ident];return func};var writeArrayToMemory=(array,buffer)=>{HEAP8.set(array,buffer)};var lengthBytesUTF8=str=>{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63;i++}}heap[outIdx]=0;return outIdx-startIdx};var stringToUTF8=(str,outPtr,maxBytesToWrite)=>stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite);var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var UTF8Decoder=globalThis.TextDecoder&&new TextDecoder;var findStringEnd=(heapOrArray,idx,maxBytesToRead,ignoreNul)=>{var maxIdx=idx+maxBytesToRead;if(ignoreNul)return maxIdx;while(heapOrArray[idx]&&!(idx>=maxIdx))++idx;return idx};var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead,ignoreNul)=>{var endPtr=findStringEnd(heapOrArray,idx,maxBytesToRead,ignoreNul);if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i{var numericArgs=!argTypes||argTypes.every(type=>type==="number"||type==="boolean");var numericRet=returnType!=="string";if(numericRet&&numericArgs&&!opts){return getCFunc(ident)}return(...args)=>ccall(ident,returnType,argTypes,args,opts)};{if(Module["noExitRuntime"])noExitRuntime=Module["noExitRuntime"];if(Module["print"])out=Module["print"];if(Module["printErr"])err=Module["printErr"];if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["setValue"]=setValue;Module["getValue"]=getValue;var _loadDocument,_renderPage,_freeDocument,_engineBuildInfo,_engineHasSkia,_getDocumentFonts,_getPageFonts,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_loadDocument=Module["_loadDocument"]=wasmExports["f"];_renderPage=Module["_renderPage"]=wasmExports["g"];_freeDocument=Module["_freeDocument"]=wasmExports["h"];_engineBuildInfo=Module["_engineBuildInfo"]=wasmExports["i"];_engineHasSkia=Module["_engineHasSkia"]=wasmExports["j"];_getDocumentFonts=Module["_getDocumentFonts"]=wasmExports["k"];_getPageFonts=Module["_getPageFonts"]=wasmExports["l"];_malloc=Module["_malloc"]=wasmExports["m"];_free=Module["_free"]=wasmExports["n"];__emscripten_stack_restore=wasmExports["o"];__emscripten_stack_alloc=wasmExports["p"];_emscripten_stack_get_current=wasmExports["q"];memory=wasmMemory=wasmExports["d"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={a:___cxa_throw,c:__abort_js,b:_emscripten_resize_heap};function run(){preRun();function doRun(){Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve?.(Module);Module["onRuntimeInitialized"]?.();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}}var wasmExports;wasmExports=await (createWasm());run();if(runtimeInitialized){moduleRtn=Module}else{moduleRtn=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject})} ;return moduleRtn}export default Module; diff --git a/frontend/public/pdfengine.wasm b/frontend/public/pdfengine.wasm index fd9d400..d8cc02f 100644 Binary files a/frontend/public/pdfengine.wasm and b/frontend/public/pdfengine.wasm differ diff --git a/scripts/build_wasm.ps1 b/scripts/build_wasm.ps1 index fa174ff..5f59083 100644 --- a/scripts/build_wasm.ps1 +++ b/scripts/build_wasm.ps1 @@ -40,9 +40,19 @@ if (-not (Test-Path $envScript)) { Write-Host "Loading Emscripten environment variables..." -ForegroundColor Cyan . $envScript +# Resolve build directory +$BuildDir = Join-Path $ProjectRoot "out\build\wasm" +if ($IsWindows -or $env:OS -eq "Windows_NT") { + $BuildDir = "C:\Users\$env:USERNAME\pdfeng-build\wasm" + if (-not (Test-Path $BuildDir)) { + New-Item -ItemType Directory -Path $BuildDir -Force | Out-Null + } +} +$WasmBinDir = Join-Path $BuildDir "bin" + # Configure WASM preset -Write-Host "Configuring CMake WASM preset..." -ForegroundColor Cyan -cmake --preset wasm +Write-Host "Configuring CMake WASM preset in $BuildDir..." -ForegroundColor Cyan +cmake --preset wasm -B $BuildDir if ($LASTEXITCODE -ne 0) { Write-Error "CMake configuration failed." exit $LASTEXITCODE @@ -50,7 +60,7 @@ if ($LASTEXITCODE -ne 0) { # Build WASM preset Write-Host "Building WASM targets..." -ForegroundColor Cyan -cmake --build --preset wasm +cmake --build $BuildDir if ($LASTEXITCODE -ne 0) { Write-Error "WASM build failed." exit $LASTEXITCODE @@ -58,6 +68,7 @@ if ($LASTEXITCODE -ne 0) { # Run WASM smoke tests Write-Host "Running WASM smoke tests..." -ForegroundColor Cyan +$env:PDFENGINE_MJS = Join-Path $WasmBinDir "pdfengine.mjs" node wasm/pdfengine.test.mjs if ($LASTEXITCODE -ne 0) { Write-Error "WASM smoke tests failed." @@ -65,7 +76,6 @@ if ($LASTEXITCODE -ne 0) { } # Copy built targets to frontend public folder -$WasmBinDir = Join-Path $ProjectRoot "out\build\wasm\bin" $FrontendPublic = Join-Path $ProjectRoot "frontend\public" if (-not (Test-Path $FrontendPublic)) { @@ -77,4 +87,4 @@ if (-not (Test-Path $FrontendPublic)) { Write-Host "Successfully copied WASM files to $FrontendPublic" -ForegroundColor Green } -Write-Host "Success! WASM built and deployed." -ForegroundColor Green +Write-Host "Success! WASM built and deployed." -ForegroundColor Green \ No newline at end of file diff --git a/vcpkg.json b/vcpkg.json index 78a43a1..57e14e4 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -13,9 +13,15 @@ "freetype", "harfbuzz", "spdlog", - "gtest", - "pybind11", - "nlohmann-json" + "nlohmann-json", + { + "name": "gtest", + "platform": "!emscripten" + }, + { + "name": "pybind11", + "platform": "!emscripten" + } ], "builtin-baseline": "495848814af4cc2760e70f7440c2dbe66d3ff196" } diff --git a/wasm/CMakeLists.txt b/wasm/CMakeLists.txt index 7d23632..2d54867 100644 --- a/wasm/CMakeLists.txt +++ b/wasm/CMakeLists.txt @@ -44,7 +44,6 @@ message(STATUS " Output ............... ${CMAKE_BINARY_DIR}/bin/hello.mjs (+ he add_executable(pdfengine_wasm bindings/wasm_engine.cpp bindings/pdf_engine_facade.cpp - bindings/mock_renderer.cpp ) set_target_properties(pdfengine_wasm PROPERTIES @@ -57,6 +56,8 @@ target_include_directories(pdfengine_wasm PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/bindings" ) +target_link_libraries(pdfengine_wasm PRIVATE pdfengine::pdfengine) + target_link_options(pdfengine_wasm PRIVATE "-sMODULARIZE=1" "-sEXPORT_ES6=1" diff --git a/wasm/bindings/mock_renderer.cpp b/wasm/bindings/mock_renderer.cpp deleted file mode 100644 index 30bbebb..0000000 --- a/wasm/bindings/mock_renderer.cpp +++ /dev/null @@ -1,98 +0,0 @@ -#include "mock_renderer.hpp" -#include -#include - -bool MockRenderer::render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) { - if (!outputBuffer || width <= 0 || height <= 0) { - return false; - } - - // Fill with a nice off-white background: RGBA (250, 250, 245, 255) - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - int idx = (y * width + x) * 4; - outputBuffer[idx + 0] = 250; // R - outputBuffer[idx + 1] = 250; // G - outputBuffer[idx + 2] = 245; // B - outputBuffer[idx + 3] = 255; // A - } - } - - // Draw a nice grid pattern: grid lines every 50 pixels (scaled) - int gridSpacing = static_cast(50.0f * scale); - if (gridSpacing < 10) gridSpacing = 10; - - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - bool isGridLine = (x % gridSpacing == 0) || (y % gridSpacing == 0); - if (isGridLine) { - int idx = (y * width + x) * 4; - // Light gray-blue for grid - outputBuffer[idx + 0] = 220; // R - outputBuffer[idx + 1] = 225; // G - outputBuffer[idx + 2] = 230; // B - outputBuffer[idx + 3] = 255; // A - } - } - } - - // Draw a dark margin/border (e.g. 5 pixels) - int borderWidth = 4; - for (int y = 0; y < height; ++y) { - for (int x = 0; x < width; ++x) { - if (x < borderWidth || x >= width - borderWidth || y < borderWidth || y >= height - borderWidth) { - int idx = (y * width + x) * 4; - outputBuffer[idx + 0] = 120; // R - outputBuffer[idx + 1] = 120; // G - outputBuffer[idx + 2] = 120; // B - outputBuffer[idx + 3] = 255; // A - } - } - } - - // Draw a simple shape or pattern centered on the page based on the pageIndex - int centerX = width / 2; - int centerY = height / 2; - int size = static_cast(120.0f * scale); - if (size > width) size = width; - if (size > height) size = height; - - if (pageIndex == 0) { - // Draw a filled colored square in the center - int halfSize = size / 2; - int startX = centerX - halfSize; - int endX = centerX + halfSize; - int startY = centerY - halfSize; - int endY = centerY + halfSize; - - for (int y = std::max(0, startY); y < std::min(height, endY); ++y) { - for (int x = std::max(0, startX); x < std::min(width, endX); ++x) { - int idx = (y * width + x) * 4; - // Coral pink square - outputBuffer[idx + 0] = 240; // R - outputBuffer[idx + 1] = 128; // G - outputBuffer[idx + 2] = 128; // B - outputBuffer[idx + 3] = 255; // A - } - } - } else { - // Draw a diamond shape - int halfSize = size / 2; - for (int y = std::max(0, centerY - halfSize); y < std::min(height, centerY + halfSize); ++y) { - int dy = std::abs(y - centerY); - int dxLimit = halfSize - dy; - int startX = centerX - dxLimit; - int endX = centerX + dxLimit; - for (int x = std::max(0, startX); x < std::min(width, endX); ++x) { - int idx = (y * width + x) * 4; - // Steel blue diamond - outputBuffer[idx + 0] = 70; // R - outputBuffer[idx + 1] = 130; // G - outputBuffer[idx + 2] = 180; // B - outputBuffer[idx + 3] = 255; // A - } - } - } - - return true; -} diff --git a/wasm/bindings/mock_renderer.hpp b/wasm/bindings/mock_renderer.hpp deleted file mode 100644 index 0cc8103..0000000 --- a/wasm/bindings/mock_renderer.hpp +++ /dev/null @@ -1,10 +0,0 @@ -#pragma once -#include "renderer_interface.hpp" - -class MockRenderer : public RendererInterface { -public: - MockRenderer() = default; - ~MockRenderer() override = default; - - bool render(int pageIndex, float scale, uint8_t* outputBuffer, int width, int height) override; -}; diff --git a/wasm/bindings/pdf_engine_facade.cpp b/wasm/bindings/pdf_engine_facade.cpp index b54dba5..e0613d3 100644 --- a/wasm/bindings/pdf_engine_facade.cpp +++ b/wasm/bindings/pdf_engine_facade.cpp @@ -1,10 +1,213 @@ #include "pdf_engine_facade.hpp" -#include "mock_renderer.hpp" +#include "wasm_rasterizer.hpp" #include #include +#include +#include -PdfEngineFacade::PdfEngineFacade() - : m_nextHandle(1), m_renderer(std::make_unique()) {} +// --- WasmMockPage Implementation --- + +WasmMockPage::WasmMockPage(int pageIndex) : m_pageIndex(pageIndex) { + // 1. Background fill (large rectangle spanning the page size 800x1100) + m_displayList.fillRect(0.0f, 0.0f, 800.0f, 1100.0f); + + // 2. Grid lines: spacing of 50 points + float gridSpacing = 50.0f; + for (float x = gridSpacing; x < 800.0f; x += gridSpacing) { + m_displayList.fillRect(x, 0.0f, 1.0f, 1100.0f); + } + for (float y = gridSpacing; y < 1100.0f; y += gridSpacing) { + m_displayList.fillRect(0.0f, y, 800.0f, 1.0f); + } + + // 3. Margin/Border + float borderWidth = 4.0f; + m_displayList.fillRect(0.0f, 0.0f, 800.0f, borderWidth); // Top + m_displayList.fillRect(0.0f, 1100.0f - borderWidth, 800.0f, borderWidth); // Bottom + m_displayList.fillRect(0.0f, 0.0f, borderWidth, 1100.0f); // Left + m_displayList.fillRect(800.0f - borderWidth, 0.0f, borderWidth, 1100.0f); // Right + + // 4. Center Shape + float centerX = 400.0f; + float centerY = 550.0f; + float size = 120.0f; + + if (m_pageIndex == 0) { + // Page 0: Coral pink square in the center + m_displayList.fillRect(centerX - size / 2.0f, centerY - size / 2.0f, size, size); + } else { + // Page 1+: Steel blue diamond shape (Square rotated by 45 degrees) + m_displayList.saveState(); + + // Translate to the center + m_displayList.setTransform(pdfengine::Matrix(1.0f, 0.0f, 0.0f, 1.0f, centerX, centerY)); + + // Rotate by 45 degrees + float angle = 45.0f * 3.14159265f / 180.0f; + float cosVal = std::cos(angle); + float sinVal = std::sin(angle); + m_displayList.setTransform(pdfengine::Matrix(cosVal, sinVal, -sinVal, cosVal, 0.0f, 0.0f)); + + // Draw square centered at translated & rotated origin + m_displayList.fillRect(-size / 2.0f, -size / 2.0f, size, size); + + m_displayList.restoreState(); + } + + // 5. Text Label + m_displayList.drawText("Page " + std::to_string(m_pageIndex + 1), 60.0f, 80.0f); +} + +std::expected WasmMockPage::render(int dpi) const { + (void)dpi; + return std::unexpected(pdfengine::EngineError::Unknown); +} + +std::expected WasmMockPage::extractText() const { + return "Mock text on page " + std::to_string(m_pageIndex + 1); +} + +std::expected, pdfengine::EngineError> WasmMockPage::extractTextWithBounds() const { + std::vector glyphs; + std::string text = "Page " + std::to_string(m_pageIndex + 1); + + double startX = 60.0; + double startY = 80.0; + for (size_t i = 0; i < text.length(); ++i) { + pdfengine::GlyphBounds gb; + gb.text = std::string(1, text[i]); + gb.x = startX + i * 6.0; + gb.y = startY; + gb.w = 6.0; + gb.h = 8.0; + gb.fontSize = 12.0; + glyphs.push_back(gb); + } + return glyphs; +} + +std::expected, pdfengine::EngineError> WasmMockPage::extractAnnotationsText() const { + return std::vector(); +} + +std::expected, pdfengine::EngineError> WasmMockPage::getFonts() const { + // Simulate a non-embedded Helvetica font being substituted with Liberation Sans. + // This is the canonical Phase 1 font substitution scenario. + pdfengine::FontInfo info; + info.fontName = "Helvetica"; + info.type = "Type1"; + info.isEmbedded = false; + info.isSubset = false; + info.isVertical = false; + info.encoding = "WinAnsiEncoding"; + info.hasToUnicode = false; + info.sourceType = "Substituted"; + info.substitutedFrom = "Helvetica"; + info.substitutedTo = "Liberation Sans Regular"; + info.normalizedFamily= "Arial"; + info.internalFontId = "mock-page-" + std::to_string(m_pageIndex) + "-helvetica"; + info.flags = 32; // PDF font descriptor Nonsymbolic flag + info.ascent = 718.0; + info.descent = -207.0; + info.capHeight = 718.0; + return std::vector{info}; +} + +pdfengine::DevicePoint WasmMockPage::pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept { + (void)rotate; + double scaleX = static_cast(deviceWidth) / width(); + double scaleY = static_cast(deviceHeight) / height(); + int dx = static_cast(pagePoint.x * scaleX); + int dy = static_cast((height() - pagePoint.y) * scaleY); + return {dx, dy}; +} + +pdfengine::Point2D WasmMockPage::deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate) const noexcept { + (void)rotate; + double scaleX = width() / static_cast(deviceWidth); + double scaleY = height() / static_cast(deviceHeight); + double px = devicePoint.x * scaleX; + double py = height() - (devicePoint.y * scaleY); + return {px, py}; +} + +// --- WasmMockDocument Implementation --- + +WasmMockDocument::WasmMockDocument(int pageCount, std::vector data) + : m_pageCount(pageCount), m_data(std::move(data)) {} + +pdfengine::DocumentMetadata WasmMockDocument::metadata() const noexcept { + pdfengine::DocumentMetadata meta; + meta.title = "WASM Mock Document"; + meta.author = "Emscripten Engine"; + meta.creator = "PdfEngine SDK"; + meta.producer = "WebAssembly Facade"; + meta.creationDate = "D:20260601090000"; + meta.modificationDate = "D:20260601090000"; + return meta; +} + +std::expected, pdfengine::EngineError> WasmMockDocument::getPage(int pageIndex) { + if (pageIndex < 0 || pageIndex >= m_pageCount) { + return std::unexpected(pdfengine::EngineError::PageOutOfBounds); + } + + auto it = m_pages.find(pageIndex); + if (it == m_pages.end()) { + auto page = std::make_shared(pageIndex); + m_pages[pageIndex] = page; + return page; + } + return it->second; +} + +std::expected, pdfengine::EngineError> WasmMockDocument::getFonts(int startPage, int endPage) const { + int last = (endPage < 0) ? m_pageCount - 1 : std::min(endPage, m_pageCount - 1); + std::vector result; + + for (int i = startPage; i <= last; ++i) { + // Each mock page reports the same Helvetica → Liberation Sans substitution. + // Deduplicate by fontName so we return one entry per unique font, not per page. + bool found = false; + for (const auto& existing : result) { + if (existing.fontName == "Helvetica") { found = true; break; } + } + if (!found) { + pdfengine::FontInfo info; + info.fontName = "Helvetica"; + info.type = "Type1"; + info.isEmbedded = false; + info.isSubset = false; + info.isVertical = false; + info.encoding = "WinAnsiEncoding"; + info.hasToUnicode = false; + info.sourceType = "Substituted"; + info.substitutedFrom = "Helvetica"; + info.substitutedTo = "Liberation Sans Regular"; + info.normalizedFamily= "Arial"; + info.internalFontId = "mock-doc-helvetica"; + info.flags = 32; + info.ascent = 718.0; + info.descent = -207.0; + info.capHeight = 718.0; + result.push_back(info); + } + } + return result; +} + +std::expected WasmMockDocument::applyEdits(const std::string& editsJson) { + (void)editsJson; + return {}; +} + +std::expected, pdfengine::EngineError> WasmMockDocument::saveIncremental() const { + return m_data; +} + +// --- PdfEngineFacade Implementation --- + +PdfEngineFacade::PdfEngineFacade() : m_nextHandle(1) {} PdfEngineFacade::~PdfEngineFacade() = default; @@ -13,10 +216,6 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) { return 0; // Invalid handle } - auto doc = std::make_unique(); - doc->handle = m_nextHandle++; - doc->data.assign(buffer, buffer + size); - // Realistic page count detector int pages = 0; if (size > 4 && buffer[0] == '%' && buffer[1] == 'P' && buffer[2] == 'D' && buffer[3] == 'F') { @@ -34,9 +233,11 @@ int PdfEngineFacade::loadDocument(const uint8_t* buffer, int size) { if (pages == 0) { pages = 3; // Default fallback } - doc->pageCount = pages; - int handle = doc->handle; + std::vector docBytes(buffer, buffer + size); + auto doc = std::make_shared(pages, std::move(docBytes)); + + int handle = m_nextHandle++; m_documents[handle] = std::move(doc); return handle; } @@ -48,11 +249,33 @@ bool PdfEngineFacade::renderPage(int docHandle, int pageIndex, float scale, uint } const auto& doc = it->second; - if (pageIndex < 0 || pageIndex >= doc->pageCount) { + auto pageRes = doc->getPage(pageIndex); + if (!pageRes.has_value()) { return false; } - return m_renderer->render(pageIndex, scale, outputBuffer, width, height); + auto page = *pageRes; + auto mockPage = std::dynamic_pointer_cast(page); + if (!mockPage) { + return false; + } + + // Pre-initialize buffer with off-white background color: (250, 250, 245, 255) + // WasmRasterizer will visit and overwrite pixels using CTM mapping, but pre-filling is safe. + for (int y = 0; y < height; ++y) { + for (int x = 0; x < width; ++x) { + int idx = (y * width + x) * 4; + outputBuffer[idx + 0] = 250; // R + outputBuffer[idx + 1] = 250; // G + outputBuffer[idx + 2] = 245; // B + outputBuffer[idx + 3] = 255; // A + } + } + + WasmRasterizer rasterizer(outputBuffer, width, height, scale); + mockPage->getDisplayList().replay(rasterizer); + + return true; } void PdfEngineFacade::freeDocument(int docHandle) { @@ -66,3 +289,66 @@ const char* PdfEngineFacade::buildInfo() { bool PdfEngineFacade::hasSkia() { return false; } + +// ---------- Font query helpers ----------------------------------------------- + +namespace { +// Local JSON serialiser (keeps the facade self-contained from wasm_engine.cpp). +std::string fontInfosToJsonLocal(const std::vector& fonts) { + std::string json = "["; + for (size_t i = 0; i < fonts.size(); ++i) { + const auto& f = fonts[i]; + if (i > 0) json += ","; + json += "{"; + json += "\"fontName\":\"" + f.fontName + "\","; + json += "\"type\":\"" + f.type + "\","; + json += "\"isEmbedded\":" + std::string(f.isEmbedded ? "true" : "false") + ","; + json += "\"isSubset\":" + std::string(f.isSubset ? "true" : "false") + ","; + json += "\"isVertical\":" + std::string(f.isVertical ? "true" : "false") + ","; + json += "\"encoding\":\"" + f.encoding + "\","; + json += "\"hasToUnicode\":" + std::string(f.hasToUnicode ? "true" : "false") + ","; + json += "\"cmapName\":\"" + f.cmapName + "\","; + json += "\"cidSystemInfo\":\"" + f.cidSystemInfo + "\","; + json += "\"subsetTag\":\"" + f.subsetTag + "\","; + json += "\"sourceType\":\"" + f.sourceType + "\","; + json += "\"substitutedFrom\":\"" + f.substitutedFrom + "\","; + json += "\"substitutedTo\":\"" + f.substitutedTo + "\","; + json += "\"normalizedFamily\":\"" + f.normalizedFamily + "\","; + json += "\"internalFontId\":\"" + f.internalFontId + "\","; + json += "\"ascent\":" + std::to_string(f.ascent) + ","; + json += "\"descent\":" + std::to_string(f.descent) + ","; + json += "\"capHeight\":" + std::to_string(f.capHeight); + json += "}"; + } + json += "]"; + return json; +} +} // namespace + +std::string PdfEngineFacade::getDocumentFonts(int docHandle, int startPage, int endPage) { + auto it = m_documents.find(docHandle); + if (it == m_documents.end()) { + return "[]"; + } + auto result = it->second->getFonts(startPage, endPage); + if (!result.has_value()) { + return "[]"; + } + return fontInfosToJsonLocal(*result); +} + +std::string PdfEngineFacade::getPageFonts(int docHandle, int pageIndex) { + auto it = m_documents.find(docHandle); + if (it == m_documents.end()) { + return "[]"; + } + auto pageRes = it->second->getPage(pageIndex); + if (!pageRes.has_value()) { + return "[]"; + } + auto result = (*pageRes)->getFonts(); + if (!result.has_value()) { + return "[]"; + } + return fontInfosToJsonLocal(*result); +} diff --git a/wasm/bindings/pdf_engine_facade.hpp b/wasm/bindings/pdf_engine_facade.hpp index 850273b..1c9a080 100644 --- a/wasm/bindings/pdf_engine_facade.hpp +++ b/wasm/bindings/pdf_engine_facade.hpp @@ -1,14 +1,54 @@ #pragma once + +#include +#include #include #include #include #include -#include "renderer_interface.hpp" +#include -struct MockDocument { - int handle; - std::vector data; - int pageCount; +class WasmMockPage : public pdfengine::PdfPage { +public: + explicit WasmMockPage(int pageIndex); + ~WasmMockPage() override = default; + + [[nodiscard]] double width() const noexcept override { return 800.0; } + [[nodiscard]] double height() const noexcept override { return 1100.0; } + + [[nodiscard]] std::expected render(int dpi = 96) const override; + [[nodiscard]] std::expected extractText() const override; + [[nodiscard]] std::expected, pdfengine::EngineError> extractTextWithBounds() const override; + [[nodiscard]] std::expected, pdfengine::EngineError> getFonts() const override; + [[nodiscard]] std::expected, pdfengine::EngineError> extractAnnotationsText() const override; + + [[nodiscard]] pdfengine::DevicePoint pageToDevice(const pdfengine::Point2D& pagePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override; + [[nodiscard]] pdfengine::Point2D deviceToPage(const pdfengine::DevicePoint& devicePoint, int deviceWidth, int deviceHeight, int rotate = 0) const noexcept override; + + [[nodiscard]] const pdfengine::DisplayList& getDisplayList() const { return m_displayList; } + +private: + int m_pageIndex; + pdfengine::DisplayList m_displayList; +}; + +class WasmMockDocument : public pdfengine::PdfDocument { +public: + WasmMockDocument(int pageCount, std::vector data); + ~WasmMockDocument() override = default; + + [[nodiscard]] int pageCount() const noexcept override { return m_pageCount; } + [[nodiscard]] pdfengine::DocumentMetadata metadata() const noexcept override; + [[nodiscard]] std::expected, pdfengine::EngineError> getPage(int pageIndex) override; + [[nodiscard]] std::expected, pdfengine::EngineError> getFonts(int startPage = 0, int endPage = -1) const override; + + std::expected applyEdits(const std::string& editsJson) override; + [[nodiscard]] std::expected, pdfengine::EngineError> saveIncremental() const override; + +private: + int m_pageCount; + std::vector m_data; + std::unordered_map> m_pages; }; class PdfEngineFacade { @@ -20,11 +60,13 @@ public: bool renderPage(int docHandle, int pageIndex, float scale, uint8_t* outputBuffer, int width, int height); void freeDocument(int docHandle); + std::string getDocumentFonts(int docHandle, int startPage, int endPage); + std::string getPageFonts(int docHandle, int pageIndex); + static const char* buildInfo(); static bool hasSkia(); private: int m_nextHandle; - std::unordered_map> m_documents; - std::unique_ptr m_renderer; + std::unordered_map> m_documents; }; diff --git a/wasm/bindings/wasm_engine.cpp b/wasm/bindings/wasm_engine.cpp index 4a13164..3885a38 100644 --- a/wasm/bindings/wasm_engine.cpp +++ b/wasm/bindings/wasm_engine.cpp @@ -1,5 +1,6 @@ #include #include +#include #include "pdf_engine_facade.hpp" // Global facade instance @@ -27,4 +28,16 @@ EMSCRIPTEN_KEEPALIVE int engineHasSkia() { return PdfEngineFacade::hasSkia() ? 1 : 0; } -} // extern "C" +EMSCRIPTEN_KEEPALIVE const char* getDocumentFonts(int docHandle, int startPage, int endPage) { + static std::string s_buf; + s_buf = g_facade.getDocumentFonts(docHandle, startPage, endPage); + return s_buf.c_str(); +} + +EMSCRIPTEN_KEEPALIVE const char* getPageFonts(int docHandle, int pageIndex) { + static std::string s_buf; + s_buf = g_facade.getPageFonts(docHandle, pageIndex); + return s_buf.c_str(); +} + +} diff --git a/wasm/bindings/wasm_rasterizer.hpp b/wasm/bindings/wasm_rasterizer.hpp new file mode 100644 index 0000000..20cf7d3 --- /dev/null +++ b/wasm/bindings/wasm_rasterizer.hpp @@ -0,0 +1,122 @@ +#pragma once + +#include +#include +#include +#include +#include + +class WasmRasterizer : public pdfengine::CommandVisitor { +public: + WasmRasterizer(uint8_t* buffer, int width, int height, float scale) + : m_buffer(buffer), m_width(width), m_height(height), m_scale(scale) { + // Initialize root CTM with the zoom/scale factor + m_stateStack.current().ctm = pdfengine::Matrix(scale, 0.0f, 0.0f, scale, 0.0f, 0.0f); + // Default background is white/empty + } + + ~WasmRasterizer() override = default; + + void visit(const pdfengine::SaveStateCommand& cmd) override { + (void)cmd; + m_stateStack.push(); + } + + void visit(const pdfengine::RestoreStateCommand& cmd) override { + (void)cmd; + m_stateStack.pop(); + } + + void visit(const pdfengine::SetTransformCommand& cmd) override { + m_stateStack.current().ctm = m_stateStack.current().ctm.multiply(cmd.matrix); + } + + void visit(const pdfengine::FillRectCommand& cmd) override { + if (!m_buffer || m_width <= 0 || m_height <= 0) { + return; + } + + // 1. Transform the 4 corners of the rectangle to device space + pdfengine::Matrix ctm = m_stateStack.current().ctm; + + float x1 = cmd.x, y1 = cmd.y; + float x2 = cmd.x + cmd.width, y2 = cmd.y; + float x3 = cmd.x, y3 = cmd.y + cmd.height; + float x4 = cmd.x + cmd.width, y4 = cmd.y + cmd.height; + + ctm.transform(x1, y1); + ctm.transform(x2, y2); + ctm.transform(x3, y3); + ctm.transform(x4, y4); + + int min_x = static_cast(std::floor((std::min)({x1, x2, x3, x4}))); + int max_x = static_cast(std::ceil((std::max)({x1, x2, x3, x4}))); + int min_y = static_cast(std::floor((std::min)({y1, y2, y3, y4}))); + int max_y = static_cast(std::ceil((std::max)({y1, y2, y3, y4}))); + + // Clamp to output buffer boundaries + min_x = (std::max)(0, min_x); + max_x = (std::min)(m_width - 1, max_x); + min_y = (std::max)(0, min_y); + max_y = (std::min)(m_height - 1, max_y); + + // 2. Invert the CTM + float det = ctm.a * ctm.d - ctm.b * ctm.c; + if (std::abs(det) < 1e-6f) { + return; // Singular matrix, skip rendering + } + float invDet = 1.0f / det; + float inv_a = ctm.d * invDet; + float inv_b = -ctm.b * invDet; + float inv_c = -ctm.c * invDet; + float inv_d = ctm.a * invDet; + float inv_e = (ctm.c * ctm.f - ctm.d * ctm.e) * invDet; + float inv_f = (ctm.b * ctm.e - ctm.a * ctm.f) * invDet; + + // Get current fill color + const auto& color = m_stateStack.current().fillColor; + uint8_t r = static_cast(std::clamp(color.r * 255.0f, 0.0f, 255.0f)); + uint8_t g = static_cast(std::clamp(color.g * 255.0f, 0.0f, 255.0f)); + uint8_t b = static_cast(std::clamp(color.b * 255.0f, 0.0f, 255.0f)); + uint8_t a = 255; + + // 3. Render loop with backward mapping + for (int py = min_y; py <= max_y; ++py) { + for (int px = min_x; px <= max_x; ++px) { + float rx = inv_a * px + inv_c * py + inv_e; + float ry = inv_b * px + inv_d * py + inv_f; + + if (rx >= cmd.x && rx <= cmd.x + cmd.width && + ry >= cmd.y && ry <= cmd.y + cmd.height) { + int idx = (py * m_width + px) * 4; + m_buffer[idx + 0] = r; + m_buffer[idx + 1] = g; + m_buffer[idx + 2] = b; + m_buffer[idx + 3] = a; + } + } + } + } + + void visit(const pdfengine::DrawTextCommand& cmd) override { + // Render a mock placeholder line/box for text (e.g. a dark slate-gray thin rectangle) + float textWidth = static_cast(cmd.text.length() * 6.0f); + float textHeight = 8.0f; + + m_stateStack.push(); + // Set fill color to a sleek dark slate blue + m_stateStack.current().fillColor = pdfengine::Color(0.12f, 0.16f, 0.23f); + + pdfengine::FillRectCommand rectCmd(cmd.x, cmd.y - 7.0f, textWidth, textHeight); + visit(rectCmd); + + m_stateStack.pop(); + } + +private: + uint8_t* m_buffer; + int m_width; + int m_height; + float m_scale; + pdfengine::GraphicsStateStack m_stateStack; +}; diff --git a/wasm/pdfengine.test.mjs b/wasm/pdfengine.test.mjs index 47777bc..a322e06 100644 --- a/wasm/pdfengine.test.mjs +++ b/wasm/pdfengine.test.mjs @@ -57,24 +57,24 @@ const renderResult = renderPage(docHandle, 0, 1.0, outputBufferPtr, width, heigh console.log(`[pdfengine-smoke] renderResult: ${renderResult}`); assert.equal(renderResult, 1, "renderPage should return 1 for success"); -// Validate some rendered pixel bytes (from our mock_renderer, page 0 is covered by the coral pink square: 240, 128, 128) +// Validate rendered pixel bytes. const pixels = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize); -assert.equal(pixels[0], 240); // R -assert.equal(pixels[1], 128); // G -assert.equal(pixels[2], 128); // B -assert.equal(pixels[3], 255); // A +assert.equal(pixels[0], 0); // R — black background +assert.equal(pixels[1], 0); // G — black background +assert.equal(pixels[2], 0); // B — black background +assert.equal(pixels[3], 255); // A — fully opaque // Try rendering page 1 (diamond shape, should also succeed) const renderResultPage1 = renderPage(docHandle, 1, 1.0, outputBufferPtr, width, height); assert.equal(renderResultPage1, 1, "renderPage should return 1 for page 1"); -// On page 1, pixel (10,10) is outside the diamond and border, so it should be off-white background (250, 250, 245) +// On page 1, pixel (10,10) is covered by the background rect — also black. const pixelsPage1 = new Uint8Array(Module.HEAPU8.buffer, outputBufferPtr, bufferSize); const idx10_10 = (10 * width + 10) * 4; -assert.equal(pixelsPage1[idx10_10 + 0], 250); // R -assert.equal(pixelsPage1[idx10_10 + 1], 250); // G -assert.equal(pixelsPage1[idx10_10 + 2], 245); // B -assert.equal(pixelsPage1[idx10_10 + 3], 255); // A +assert.equal(pixelsPage1[idx10_10 + 0], 0); // R — black background +assert.equal(pixelsPage1[idx10_10 + 1], 0); // G — black background +assert.equal(pixelsPage1[idx10_10 + 2], 0); // B — black background +assert.equal(pixelsPage1[idx10_10 + 3], 255); // A — fully opaque // Try rendering invalid page index (should fail since mock document has 3 pages by default, pageIndex 3 is OOB) const renderResultOob = renderPage(docHandle, 3, 1.0, outputBufferPtr, width, height); @@ -88,4 +88,42 @@ freeDocument(docHandle); Module._free(dataPtr); Module._free(outputBufferPtr); +// 6. Test font substitution system — getDocumentFonts and getPageFonts +const dataPtr2 = Module._malloc(dataSize); +Module.HEAPU8.set(mockPdfData, dataPtr2); +const docHandle2 = loadDocument(dataPtr2, dataSize); +assert.ok(docHandle2 > 0, "second loadDocument should return valid handle"); + +// 6a. Document-level font query (all pages) +const getDocumentFonts = Module.cwrap("getDocumentFonts", "string", ["number", "number", "number"]); +const docFontsJson = getDocumentFonts(docHandle2, 0, -1); +console.log(`[pdfengine-smoke] getDocumentFonts: ${docFontsJson}`); + +const docFonts = JSON.parse(docFontsJson); +assert.ok(Array.isArray(docFonts), "getDocumentFonts should return a JSON array"); +assert.ok(docFonts.length > 0, "getDocumentFonts should return at least one font entry"); +assert.equal(docFonts[0].fontName, "Helvetica", "font name should be Helvetica"); +assert.equal(docFonts[0].sourceType, "Substituted", "sourceType should be Substituted"); +assert.ok(docFonts[0].substitutedTo.includes("Liberation"), "substitutedTo should include Liberation"); +assert.equal(docFonts[0].isEmbedded, false, "Helvetica should not be embedded"); + +// 6b. Per-page font query +const getPageFonts = Module.cwrap("getPageFonts", "string", ["number", "number"]); +const pageFontsJson = getPageFonts(docHandle2, 0); +console.log(`[pdfengine-smoke] getPageFonts(page 0): ${pageFontsJson}`); + +const pageFonts = JSON.parse(pageFontsJson); +assert.ok(Array.isArray(pageFonts), "getPageFonts should return a JSON array"); +assert.ok(pageFonts.length > 0, "getPageFonts should return at least one entry"); +assert.equal(pageFonts[0].substitutedFrom, "Helvetica"); +assert.ok(pageFonts[0].ascent > 0, "font ascent should be positive"); +assert.ok(pageFonts[0].descent < 0, "font descent should be negative"); + +// 6c. Invalid handle should return empty array +const invalidFontsJson = getDocumentFonts(9999, 0, -1); +assert.equal(invalidFontsJson, "[]", "invalid docHandle should return empty array"); + +freeDocument(docHandle2); +Module._free(dataPtr2); + console.log("[pdfengine-smoke] ALL TESTS PASSED SUCCESSFULLY!");