From 6eb7c23cc4322c0ba178df212f8b44ad50aed10c Mon Sep 17 00:00:00 2001 From: Furqan-14 Date: Mon, 1 Jun 2026 15:28:32 +0530 Subject: [PATCH] feat: wasm build along with c++ facade --- CMakeLists.txt | 72 ++-- CMakePresets.json | 2 +- engine/src/fonts/pdf_fonts/font_loader.hpp | 5 +- engine/src/fonts/pdf_fonts/types/cid_font.hpp | 3 +- .../fonts/pdf_fonts/types/truetype_font.hpp | 3 +- .../src/fonts/pdf_fonts/types/type1_font.hpp | 3 +- frontend/public/pdfengine.mjs | 2 +- frontend/public/pdfengine.wasm | Bin 14617 -> 47884 bytes scripts/build_wasm.ps1 | 20 +- vcpkg.json | 12 +- wasm/CMakeLists.txt | 3 +- wasm/bindings/mock_renderer.cpp | 98 ------ wasm/bindings/mock_renderer.hpp | 10 - wasm/bindings/pdf_engine_facade.cpp | 308 +++++++++++++++++- wasm/bindings/pdf_engine_facade.hpp | 56 +++- wasm/bindings/wasm_engine.cpp | 15 +- wasm/bindings/wasm_rasterizer.hpp | 122 +++++++ wasm/pdfengine.test.mjs | 58 +++- 18 files changed, 605 insertions(+), 187 deletions(-) delete mode 100644 wasm/bindings/mock_renderer.cpp delete mode 100644 wasm/bindings/mock_renderer.hpp create mode 100644 wasm/bindings/wasm_rasterizer.hpp 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 fd9d400b5cab4d2380e0790af311a6b11310b630..d8cc02f28c1c44863ffa3b744ee62f69e1130d8a 100644 GIT binary patch literal 47884 zcmdtL3z!|(Rp+~_>O8wnw~ii`TWTe)Q<5Fomg1JwYPA$6?k&r*l z>(x?Qafobnu#-3tCW#S{$rp(8xMNSgcratn#4uqZNMHZH4?VTmnY?bJ!V zSi0gNKB?T4{D4Ko%adp<8gu%++r|9ub3Mt)ce_?4PU=xoah2Y-QqNGu8G#Y); zk~ry!Yt@9*?Id2htP#hRxT0poaa472wbCG0!^LT&ztqK~uZEl~j|cQL8C7 zc2Qib)#~-6UaeLu&h=EP(b4GUo2&IG`r&wHCayJTuz&vXN?PCE7wsp*xO*UahZ~RX zac__Ab>9~qa5qO2?)qrbT^k*AqtPLENpzSd9dU=EqwZid* z`?@`sulITDvdezx=<#>8rl#+29Xs*P){(16u5LwrJ%8~+{O&(UqsFOtG;`PWy3F~; zWS(%VOs^UbvLs8CqC#^2#9AKr66mtZM90OW`sY8V z&@8#WGODDlPLx&r+303o$-O>qn-5r= zde@24tPVoK8&IvuD?XmiFajL9YohB(g zh!~FBEtRUL6=@dH3H)aD;kdtOxnUOEv;#o>C4$tkhO`y45n1(CLanhCI@Ul9{YE#? z!4qC{>58l=9CK|7a<0N!Xq04_#F^Lqx8Aya>+0XnK8tFnkWWEDstkX6;libO_M zaaR2f$ttE^(0VTYbXy7C1(In5lA*?VB%?m~Hc6(MH42iUkp;=v(;Sj%h-5;m&LuIKdSh?)=Nqls~g}q7%ob0)xW?5%I~1>D(E8eH7NpDsU1) zMU)MDG*Y@b7Ogdie~9no(wpUa0!kM0>gDr6nE| zOA{>IXO7Bf_ELug6>qF_=~GdZXf%lx*-$^7txo}v@5$T-mqG+?Po50q$WC%*G;|UoV1{wCmNGzV*9GlrZ$PiSCz>u-Wxy;7+SZq;#N~C z1Zi1sqe;kR_CV;5;nKKoJ{1BpXeufx+_U4s= zT%t-eBMjHF#sp(#G#OqS9yp3f*hzu(pHGTtG>C4(dN(1|jM7mS(6n&6sW!<_oW~Ok z9`r3Td9@vk+m4mUUuwkfl%=sxv#pVpK`Wp!BG_aDpYo7iV6xBBGH_sq=9v>)s=GsbDi*1EH~ zE$Qc-8!|~ZjYiz)NuQC3Iy04V$&E+~`VF!oAD961?Yeb<`r>R9;)Um=U6%?o6N>{M zkD{tLFlrB?G11u#$mcvhMBy<$te6xHT$c=V5;97m3XIp~Ix$-n7Vkfio++6Qv0N}+ z++{kkm@!@8M$n$oE5LwU5vMzP6zVqCGr44RANwAIB8%Z%<&6FRE4gti%oF?iF^wD( z?OF#qhbXNnlid-*3N*c}Bk@2=G(zF41b#^>W_>Ymsvt2rWI|w1UPTr$Ac*FLsd;#k zWCHwiWm6R_WfiK>r3Onv1>q!c14C9O_6~%{TLCr~dR>Kd52z z6k5es^PuKO>1y!jEA7`ot1o4eGR{BM@zu6->4)^T!Tl&Bv5hRfLx#Ei7W;A~c4-s%yh^GjUXB5HzYXF3J;) zagdqoROwBdD4;wWGU?O`Lr-Oi<~PQ446kS=KE5xni+LG!_^VKYF(Z76jE4%M`|<{Z zSS4%h9e@a5p^xjg4&=s&ADQZUL4mvHWN1K^|7^Tv|gmqF)5?KXIF;q3M zuSBT|06wXo5s>CmEqJz!;^Ej@ZW;%IWra)IZI?Ej|BFU@n@gXlGM8p7MSxe=xuA4~ zF}3P1VhlqdR}u1gGjG5>0`3-WvEK+!z|7Gk92fWuc^;p#@ePbLZvq={1h$MU(Xo3b z{ckl7`ql~L6Kw+G_3=|{_D+JnJ-yNDl#G%8;=^y0J|lW4>QR#bhO zOiOExrmu=#T=NDenBoadha>+drf4hn>{({T-*Do|BCA#Z4F`i-T$x>sNG!X=(*TMJ zxj*q^wX)(5|7Iv;LZU$RTRDUtoD8XzO=^)U{iUy672rSnx&LmXl|VW3@EZ+``6Uoa z39D6~N?28t@Z*2>`$<{#%rAfKss(Z?n5>pV<(J9I76pVDm}Szm(?BDaGK~5oih;)9 z4+Yp$OB3;$X~m{I=aG%BP~Rk7Y6BcZ9y5-V!(XKs{wf;tgcc|{%2c(4WxW{lph(1! zrT|PZH0Ci0gKj8@2cs%!W>RaSlB}cdplo_;6D{G*QT2h$T@kO4EWaYIplow%iDC87 z)(-^9PhLB%7gR01)K5S9=m;?k1n?qJXip(1{f#Q36EGsv%x{7CD8Mjc+;n;3P;!D| zAlYE!P`d{?!UTh9qGdz?DI;Bgk}9O~ba~v`hHXZJsDd#7p<-LcEZXQ+?{J8uEV?S{ ze?!zyVQadDA?3P-t?L%HW^Q3?R9Mz6Z2bn+pK*o1--c;s{cg?MGmvZh2IiH`+$*9rI`PMRL za@*WGZ&S`YLeAfx&B-{_&AGqK8NY3Aoo`dlyF$)Coz2O}*3Ege%$eLfx6XT&^I*vN z&DorcmED{t%bfA_+&ZU~^Zt-?M%qUp5)HuJtdGDCflQK_Xl}JLk#avAS#~{shLoQX zKS5C+kG64z=hHl&E}lQg^9OlO_cALCV_MZW?qlA~_~vr*BEX|5X~-hs+62FFJEVzD z<|Wzd=2G#SZLP- zky@@tx_wl*aQ?&5D=F#ysp0r0Zrg_Ab>Zzc636}%5##d)dpxbjI{;hreCm3hx)^tn zzf}Geb=6~N6h!;=Dv}w@dV+`J=O7^eksjio1D>jfhnPcckn(fNpOU3Oy^Cq+PFbVP zT!c)5%cWMwBp;3kxpP~S#h{Lv9I+di=b4(b!DJQKk(LRi$O^C;5;?a@1FyY-fh22M z!Oqj}FqK~&%BPyFY0WKaqi>MdMWqc&Xa3TVWHgBB4Ga;oUlBvM@-mQ;o+d`j4Ji|Z zaPJAjQbH4rlNdQ}Mte#GINdXil9A?h(ZWVI(5Vs3MAYads}TwjK-?l6o-R+?qMb^> zLq%|6knUP%64M#^HJuR^hWgY0mPECJy^8r*%rpVp(jHlcB2ynR)#NB>5E>H*3ISFd z1`BHv=--n^HbtZLJVZ8At1(9f+;|i=sA`_XSo{BI^yE=GfF^(BEEqZr!#1n=3ld37 z@ETB2{A{K(o!b=l#sOHYeoTr=?9;si9TW_h-z-$pHdWhod7WWb7fNI?^v0t;!ieNL z;FO36dBG3Uw|W?v;Wrs{DnX}|4Ub}D(`N6d6J>V{h9=WDWoJs&n-VT0OwJg-s0_)N zRn1zI1Uiylg~}`)HwNmCEQ=^pU?j}>y1fHX=n=$H-U7C>)YS9>XTn|q2mo}-p}af! zCoHAqDJ5`_^qfmS+Xf=fgN-uyf${gtQQ12YvBLakbsiL{WdgDCYL+uTauCbtU zUETz*x?)Kj!z$smx#2>0Rx1!r*HlrzCp&eLC0SGYlGbU+Pdd7%lJy`Fx;+$y9_k4_ zgvmpQZ_M@(Z)iazxAhRs6ykt`CDwDl^tvc5Y_AB#c=Y^#h~{r%s`M*@Ih&S5Z@teQMVCO18G z?_5Ysxd9Sm+H)Yu5zIz72wA~m@{%5fYYHHEGGiZ>KbbuZFi}>RKQ)v6jp*IKI-EEH=6 zsq7%>y*A-`?$X>4#o|;=d=|-e5s#{(HU<;mOAKGodT;NQ#IpQt{f(SrZh)Ywfd|j3N@417OEs z?bAdk>7jJ2AjLZwu%saHDcZG~j8zhg{)?xuEOJjF27vC=iq?xySbkyF6Iu-AB3C6; zYyL8bNUJa|tim|hchbLUf}tspJbj~Cs*nb1kcc7z-N?I&$gr-`2SeTGyfWBqjMN2J)jNJpEw=zH-$iQiEtk#Ous>cEhC~H}R)hJJE zq2xMC-9n%%J6Mw|nZD9wbZ!qqEqgoErH%}J$dEM}%)e-_VRwZDPz(=62 zSa{Vkq-i8YH=~<0ppLoS3`yE@oXVWD7HQoa)*?Yw#tKr|&1O)U)6Mo?baN$ivssD* zOIIa#Y?Z#_V#q}-$I3X;8YIDmCWZ?JL?l9aAgW~4FlJP{T#HzZ)D?y3pl?yK7%0U> z$|RYf``}j09NWB8k}Q&Al41Sb|Yf$_L5Uv#ex`MHEJ36>2m@nRM;MZscEV zY#~rUg&$HlN`>ERe*FZmDUAy4Qmd@*zM|qXC#w3&WoFk^Uzj@sD|21E zhlYveXj)#SaTXFVuEv;KOYVeFY95f#cp?z7i7x;9m)Sxr{b-b}^;hOB&VONTAVEEU zBhXO-hHR`xPHMEq82TK1q4DJ_Yl(t*oX@blr(%SPUq8wF{WR2zX)WKF(i4PHl@V4YCRCGHHRuz@e4i$m<%M)x0D+Kj)xVBO2t6?2 zuL29;O@2kFWRYiO9ii)AyzKJ0a4vWuJT?d;q5*o{c?41r1M&T6UPcqZ1xJvcANfvY zG>2h_R+=dOl3uJZN53ao6X9&_bU?k0xGfQE+g;=YjxJi-P`c zY7wY34M^3ZW%w2hsL2PTy?9X5C-Hd9Fi*t%VcwiA%nAvabe%06!@W6MMDb`mS||_@ ztTaaxhXjcV4HB!>z==@?ATx8aK-Z9y1(XLx2YsfiamB`~aP*f;^2&%3hidmu^zq}9Y>LK3KwNs0V`N=G-)Ps62!(@!Ehvo6@I8hK$8X6 zsD#4Nuu+aGrUUcZ4r`UNRit6PT@(V(paVmEmL6qHIx_F3G;4f?)g|PF66q{VAbVXX ze+_NKbq{L6bx(+bdF8riT@}xF-E(D&lvRrX=DLS_P2q=SPxxWs-9aPKc4R3DK#RLX zV9frM9M`?jHffF)v7#GM&zch`_7!d-_KJpMiI7rAlu`|w4)an`RK4hn`XU0N{%Qq8 z;1oG!6+MQah*eD?DC!7``htQ2+$L&ZS5UCd6a)qMj!QvdQfoS|mOfdGbwOkqiHbt% z2nqzD35vLwHK==C=yc6JZSuf^h-`)k3VJ)t0uUAEjfG}iXvTPsf_Xtk4`92}s~X46 z$dSTC!xmZy%0%>{q})n8V3!E`Xe`>C>&hqBv%>pxjw>HoUFM90XwQRL^vhbPIKq!@nC@G!5b3Gzizamga&g^}Xh7=JJf$Pq?XW07;X&61Z@ zQ;=5zc_j;yS6~H^m&}pkrjooOkr!wNJLOnt&VKb`1d?5Xda1Bq^pz|+5Jd?hpzH0& zC=GiC`YHw@O;j`x$#u7wV5?PaN-sFOY^0j3%4~v9?P@a;Pl%`l`m!tqJ~SB&^kuTI z8AKF$*PxF;U-UO3N`jVj5>eog-VF4W2Kwq!7gps$JT;ZhsLP%ZPbGC>ckT}&asle1 zUgnt>qApDW=1~`#>$%j03@nI`C$wVl9+VrDuY$VfS$s5$G$uEfymU)SZG2iLEBe(NIcUROtTRUSs1I(HnP&&qCpE|Vbwgr&4y1GE5{mJ z%g|Fc9A_bHovpkmjM>Hd5Vj$#y%2`)j8-RjVqtVBJdF&;BjM@l;rMFWHpmIbsKpgZkh@~+w!0mJQ#ra zt!9{9P@M2AAyUhOVb%i!Ev>oqV-Y2KK;B9>BTMK;>I*?b1Q8(bbK$`woz`VYLNO+_ zsY78HX34Lf@z9XpMvH-VSr`1 z_9gmF|4lWnJ&31mByw}UK7qC=Qj{Sd?(~?2?D_*xWf~ zK5F3MmL)r*lO0}hlc;V_ead9chcApBv93mta4l49LRezcJD;~C3i!S$ONH4 z5O6ayXWYYC6O*N9sTM<%Ek4C?QKAh#eg^DLf|xT-_9RTBNSY_#*QrvStq_^VFKWq? zdHsD*^BMGy$tOV>=A^9tKG~aqLu;WZ67q3%tPJX>|6$zwIDu8XOl`Q$9V>9dE4dQWJ3 z=(Jd!W-__N3B4)A6Y)?M6Nj`eQQvqka0Dm9dEKXV1c*!vskJig%*-h|edo~o9?s&2 zzsNdy`uU5KWo%YU_L1L0v{}Jc%7%Wd46>6+D~54c7lykADx6YCv3e_khQ#SgoW0<%M>RlUFZXHw<)o zMUgGdqn5^_`bV}Fo-^%@Ir$&Uqg}4}yBkPaS&Id7*0Lhj`8TM_WBEn?>rVDY`nLNn zbxJ8aHUQvSD>()sPrKB)E>WT z!Z!~iR6EPEi~JwE-JRt$f!M5xLnYX2(vDu*K@-s5Jq9Rk9%8}2E`l>)v)&V(mG~0$ zPP6FPSrPc~H0m-58i_8p!7sn@MS={5s^)t_5H%)!>5H$g7r`M{M>ef6cztuVfMeV8-kP?6(uwg0%p!W*IG-E=0IA504 z$+6_RtXJrx`rTpk#WHxF;kTgw(Q7+$V71=SaN5XvdYwXe=Li}NYE<`1HMs2029R-4D_0bMevHmdl|?gyFffT;i>%mG)B*M3nW&8>)E& zLGQjn(vs;)r^lbt`Z@}o7F5~BHI*;NDELk@UDd%wN=wzjOCWp5B7t6O*%IG#)CjU> z4$lgKEFj6`D9L2nfYacMv`c*qH&tAb{I5FHI?7lM+0rPuTLcEM%9fgtyyU(-IS$u8nJsPiq3K+-E7aQy&{E(;ckpEX%42 zR~o<3U#XJ!41|=+Bx+}CWt`?!Y|`#R5*6bWVOoo;%p2>>lS%{GOof507_!RtXdLs+ zWAq}F#=kziPLfU|fkY+BtL;APQwLGPPqj@fmKvvlg{h$&07Yj zhx;TnR#Zp9P>F|-Yn>heYPKe7(ylXUm>6}a9)3xzE+K_rllYLPkddl8HNThxf!K#8 zef!7}l(bd93<5@WW=rVefCRuJ9}ptB_E=k3j$ynnh8KJS3`w}qBgvf~q+!b@{4&-K zrQ|Gwyq1A!d4uN z*qR9d%u?Hw6J@o}2ldFg!BNyKn#_Qpp<+0$r7aZ>4N_V84#(P}0NlJv4LX!HV2yz<06_xz~MkzC;yjM}nz)Sz25 z*QWVV-3hWSLkgHH6UUq+*sI#>aqC+Esv(v#7l8`CpBr_4`N6qAJ;K&QP4!W`HaPfA zv(ctjfT=neg%-4;^_73|^dD<03L|U8D6@bqrzL)=zYnl>G`c zVNo%HkE-;3sQd=zAG}R;T4lm4%B!$Q-jvi;AcGS7P|c4is}{WMFO@1*)f^#fVu0cu zj&Bl+@L}0g`T^kx7pOgtI!%8m%ezP|ktxhrR-a#d1O6jUqXFb!)nHKVMM}!nBXgPx z0*ie>!7dAS@Myl?L?)TB%da#3WV%Nx=9S3pJ`Ix~@oHKt309?B@}>+V>|vJ*Rt-}e zZk}ly3tj#LzlA+DC@2Pc&ME_SXFbOdz2uL|{F&r$LsiXYRHF<} zRg^9FI}W1A`VaMMM3Czxo4VLFF21E-PmVSH8E_9m2=I{ z`_wd(41?kW+M_jWJce*tYF{C+RYI@f*kVaT-HdI3)dNSqdkQs9stlbpg=R}<^!aCH zELqYCDjXCx=imId(QK);8LeDdF~dOB{}PGo?Um_D(twbv{~foxH*H-+Q!h^jJN>|g=ak504nPtY z%ljHmgTCbRU}^R-kw- zL4QZsGQozor+SwXE8>E*mO)kYQ9sx~IMun9bkvFU7Lv10o0*)Yljho3Km}>mHoC!1 z)tsa0O+@BrTiSp?zA%bdFW7j(2GweN3vEg#A$o06$WokQ1&1~;HIb+hb-^#iqvWIO zU1DVmnd&ADY-uK~?TA>9be;!`6E=blN`PfmOjydL zQB13UF2|i&gC#0|+J3#j-_uKLfxbE^QQag4g9$^FR_1UMjIg-85$JK2*aQ1JpD}4i zH%HyBGNU#qO=40xP=?X4s0iYqI5aq`Dp+``H}*yuSSL*8tD?3Bb(=n4lwNA*s;Zjx zLK@W;+I!iVO#_K`T$v04j;65H_T_LllBF(3X-01bT!7Z#*>Wk&>lkF)_?AIOpab(Y zx~mCcoJr8~c|~vm8B&TQ?B^seCHBZ#$x0CKJ=io9MFsN;ps(gV-G3la;wa5maxpm zf*Vn-6aGcZRu)o5qrVj8%v7{B`Wm@M%&j)Dw`C{%OAF@j7C&2Hg#yiEY!x_Sj~e5; z#2f*_N>gN{7lPzF3CAS5N(AkWV=N}#1- zZVDVkpFpunAFvaBV)9e47O06wN)?>|`AdT+qLAJaU}gbP45T3i$j@+UOHj=cCU?jD z49B>zi^Fcxgh?9&??uvos|yg=Moa@uKzpfeSNl7#`3{AfuBphhwVJiQOThfs^; z4&asvkO}K8pOW{PfSgjs<)FW$x$%$s{p3ZX46A-bDOkEfTu;@4xxNayz|9 zzU#^o4lf!y*sd{iNl9z_>HZ;L*3AE=Lw@L>bgL?;kW0Jk=DAvP=gy96Rj=Hdd-q}HJoLz_Zfz=eX@ z;Ef$_Xa13CH}>Zayp&F=X*KMhqge{Mo}sm5>q@g37Iru zC3IDzRcb3YtW&p~u0NbxfB28*98Lxg)F~E$9578#vf?v^l^@s|;HV zOwO1wku9_^traVQVJb9T31^WOilb8ll~^ja#UKE?$=YH-BwHKqH8}(nD*)c9VK_07 zlaPK4q=qV$W?{l19F9L=8I?YCWwcb{0wXr!f)akuVejJ3l*YIV`000)X4?c3`yB5I zu(WQX)5ooYk)GRHjQZSOJAsK8M}ORrxN!zh4>xvM$@ZBFDF1|3SF0G>od0(ik$Hr| z*yI--WNuF#vTdhEXKqhk=VUQ@1;JjNLA6oz0y_ahd zrk%0QubDwjhd4FqQqC%XlC{~R5Ol!FoZRd~^a7g*iej7&>9lMRb-`kC;y!a+}4D7<(c3Nb(>^HA6-P?-3X zi4!2rVo9E5aF(zHvd+a)ZRe-{^p76;+E*U=>3kdLk!>p=I##RbfjRN?0!8g`1KF|# zP$+)=Ud1BAx5v!2(e>+$Rhocrh`NCB*`Mz5|AIp<@XaavU%~k-5mfxQQs3K#h>Zf| z*&QeVH{p2{P|!=@i2qIW0v#>s(oxIk$j(9OVqn#Ggn{2kefs|w46Oc+Fz~g1{4{_p z!MeG@F@#EJEhdh!s>Y*?SzU4b)T}su;=2&Xp%@d#p_s+RF(b|QzARb)9nsMrrGCjj za!!Jd21Q5PE=)%o&ZVP2nWdvY_%7%u6k~K0in#zCZTQ}$Bd~b2uz2k}EG|7)IT|?^ zi!;5vppDr=F9ix7)9HaVPJ>`ZHosb(!G~|X74Y`~|asIU4b1?4rGIAfrinxK3 z@8_YH#lrdWbtXC&zpwOO<*@1qzu%lC-ajvixA)cKHx|wluR6J^B=9XVn96e8iTO)tPLOa@De@ zqbcUn!i6drngxJ8DLob}mC&grny#B8NF}d_-6HiV&iLuI4aUI0<{MV;d^o<$%FtS# zPGFqPSThIIjIRltl1J51Hq>B{=3{*_yUr~`*2=Ps)INO0re@i_W0?%GhZ4zu#BnV1 z7}jLjAF3B%?9@FbIn-#KF zmj1+#y%MpI!%i*jCel`Ws)Ve z1BZ2x>Y}vj0k5wBsZ5_)$@ff7bC3592p1G>25o#_FlSqV)L%r=GZE)Vf5J$BuGYlq%Xd@Bb%Zt!R-N6^oS}TYHNl;K|tu1?5Xhq9j^7)l7 zWNiu#C;<-OnsHt&?DQC!vRxX;vz{qChL@@eXwAt`$HD|?wLBwmXdF+)iyjRZYG6IU zO98+Pu0*f){Wus}K<0t^!il%EpKXiAM`BFc(I~dS?=%$>mbAWQY-zx&xiA|y zQ4qC?zgQQ-e{F};LYqaUtO2#($VVMmRv0$l`EngiZDv?8Ct<)BQL(H9+=U^TFMHXl zQ~GX};B0+NE3cejiwMK3Y+E?03}>wSKpkg%@%0`F2-aHp6_|>$!@pRhQhxqJm?PeG zVpe74aZ6Zkh$55PC~R`1M@YM7Y1O4ak+34iveqc$gqF2J9!j>Ypllnhf<-o zz+gwP0f-J24~}J7RUX2L1k(yCf>cl`g&-}Q^QPnK?7$aGQkfk}_q?BfPK2Fwt#hMj0a4vvd~G)XB}jgVd5 z8+u&Pfy|QNx2jL`FwohD?r`v{J@T+-Sp0TsHA?J=xK22tt1k*m)& z8u~xiqaI$=Lpd(GdE=sbT&$pvl2jQNZO4a7fQ_tG#naqzG5BO#Y$Y#;x2cS81V!>A zr7euiyLC(}kK~^-CZ3aN-k50lVa76lcntYP>lo)mzrXhjk=9)y!$QAZNS)niJc?DTZo`}GtvbHarRl{;+qP@eChOLGa#2JIa8?L7+mf?kt zPe#ElUmcDn+e--m8;YxR=yv=-#2UP^q-0Cbg1K8-e^HuX!&?LaB8p}qn*sy4#WFZA z-GysQg;pT4Dw7q<;IW+o;y<*OJvAnS{0G<4L#zG6x*>~rmw4BP55ltgid}SfXlYJ<{@fS|1)&|SvnJ;gLdbFXlH@h|L;l>gc7{Yy z200{!eRGykn`k8{=yA|Phog;XP-i~^LwYw8D~WS6RfHLNx?vQsI z{z`5Q|E6vM;!MeJGbO)iPg6(d^#qsOSI2J*xTc2+xViks4!hvWe-6JX zy;@Cg7JDEH)-gR43{L550N*aZm6^nEXe#KSY}K>;i}G77@LR3qH`Ivodrz~>W`0cS zXGW5&!XdcgnZ#8F>0y=gy3t9+3}3*3fr-S#QDVPScq> zaW9_XgI#j!mvdbq<%AlmKDQfN0dZf`XF8-?0Fv>35N_#j)Z?C0ZM6oC#)d`fE|bbo zjSeXQ2Xte2f`ZjP(Vp7IT)4e0837ia8tmN)Pq?LMrcA>sd20l>6MJsty(1ujBY+gs zdNgm3SX&8I<47py5H`esk4KeHejcPzQ+X4H@Ek`=!bgaoY@)rA2FV?WpZ@ZV!A?+0%CV$Mb<7z4jNky>IK=X)RG}Lei1=hI9+vnN(>S9dt0^on?-!_+BYqXRHkFtFghL<@2}m9F53Hr$u4h_n16wwhh^ z_JRCjwkKX}_debK`77xBUc<8v`CtQpFpc6{;i!tG%A)HB2PkDpwv=q_u+NtM;J}ZO zjjiq(Tj}zKcH4926KL(#MXEkhwaKOXiXS|u5(HYPVNM>>a!^m!Ca>-*-flIB2&>wZ zL=BMI%r)7-PY}N{TYZ`hTB}a~_-PQ+Vh7%;)7f$Yd0#;~r^KhO%@W#ePp5NEf3c?@ z9IyxH-af#5w>dxYnhPfGnV-1-f{C?(taX~+1G*2M#-R+TxwA7=!L4KzRMno&mz-t; zd0)njvvK528;^oXRPem=biV9#w&c-A^W}kzSKcL-SWe@OcJI`i4m=>$MWkA$;6}dO zQY|B$5wWGBOW84ZFiR#FyC7fQ(W00V8DF7sPh>r|Rf%xDG^MvGk#d|xcV*0Bo?~`T zzH2Xih<(z#88-;iQO3*{W7C7vzlr!*N^7oY67d?l^@EHcngX%rPErSg3o?kC8HnIm z*$8w)a|)jf1e!qB*2JFE3U6bJBD4hxP5#87v2q%*W$$8KHff$_PMs=V9L&U1?|e?s zSXmvQ6zJvlfk6p9ZEmzeLJ+=lC9HDR#eugiIVX_=M(qg)Uhdw$I)$(UYyGH_+*ZuW zJcbaHdHrene;40K3v&d9c1Fim&`_$H5frllomO}LY(R=0-6yqjDvJ&tn&HdTQbDIgnx?2!^QDTsri z?3_$eQjo7GDa^DiKbl>cpCBHK{L*l>ONqYiwlzM$9LB%`x=Bs=gRu@p5(-DBlyxD@ zh;-3!j5VVM=dnk zY#zV~A^`YRXGV!)ux2t8cJd1BMl^#DiB#EP(~mQ zTBJ~<=?0oS+Cu<&t@;Bj;>Wz;KcU894qGnjA&iS?0omnwuw97%XpyQI|mg z!Dm-b=CkWSuc|D3b}c87#bia=oiuve?-u4AX_GVz*X^z8m9~G-KexnG8qMIke!Cpy z$eg?M+ohBSx!T)RoZIE}H~5j!?z8*AXnNG_Fa8m2AXVl7UJ^L3Wz-CDm??*;8j2{- z>Yhw>MukRK`1*(QSFIE&th7ixpU|9E8e+g z)ovchF^(5jZN9~4vv=;WDt6v5o3M|B$vVg3VZDy7QVxJs8QSPf2-~{VQFf{(#}-Uw zX~C)lW7jrT4JFKB)pIh5RXLLmR%L6j<%dyz*5`j zFqtqEh`iUKLULPgJ!;t1XdxYsl%Zk%_xt>rzVA$>K&K0%KS zT2igCTlM=PyzreK87k)QRZt3NHPKcuM$D&<=?LOOeX_i^cpIP=t67)Y7vXN1rrzJb%Jr5`5k6Wa1YVS zHm=7EPJzrZNYVQwoBhmlu<|$?ZP=c|$H`5+Gx$`}A5Y`HVE#O#$pe#Y^(IWJr5|AV z0(SxPjC2o35Tg_SsyEmlcGRYI8;U3 zGno5nUq(fnHR>iEnF2i{_W|zDMz&EaK_~!&eJfNoQV(0SeN#(3(i&7hQ;W-;+xgtD z|J>7G``~|j<`3O_v+5?-*!gw$UJg!{tpzinrhJlhu!q{f(ZhCYXLa5jrWwPsjxz*m zeF&B>s_$^ixp5rBCEWh7hf4BgI#;;j|EveQuw3ssvERSH#mf#yiT)vT6>)qT*PBNJ zw;e7=fcu9oy}QF{Cn9xe zOVBdO! zKMyb<1G&ZD$G>z2Z2{~iu*eEP!xxZD4Lyf?Cw(YBM#E#C{6#= zM?Y9&Yh1`eL)kyZTPXEG#hm>503bG6);aY{n*@7c# zGyXF5NN0tE47fl9!%aM(B^9{Bj>qT8zB|s;NKx!;5)oNF!PG_@Q zEQ}qCVd^* z%HY%ByFwYwa zG?c+7z3&Z~xhaK(Ly^Pr$xsHL?w$@&Cqq#zMGK$$LiZz3xmu%7bVJEj0AYrG-Ys46 zeQs-0@qKQ4F22ug&&Bt-jq5vv(ASWcISnxN;pCU47NV|49t5(`0Z5fBL;#8?R2z(* zF-rXcYk-vuJ01d1nD>pi#*DHriL`(G%XRN$gtckU}8?1(BBZ$oi5JiUj z7b4SBgVGOanV?sVQUL6OJRF{`&oHO|v&{z9U?!|1kvWYJ^+jfIYMA}Pd4(|mz1Jh& zZD}wuOLceBMQ9w`Z|AQnvKpOZ<-E#nHxEj?oS zh3g`JcntYdpTX@l9}KFdxM^7qr9J^oHEqnZX%nJAT&XFQElpO7;a=ZK$)nO7WY(rV zyRLOJAK2AKKoY)ZXBjzj&D`+k771T@?ZWBzEE4`hi-iB=BH=%~NceM$gnxaJ@V{O- ze5Oh3`QvAPdsi+TK9esT?yg!meC7=chleT+*}^}S7x*_&4A~2W8?qM&H)JmmZpdCB z+>pIMxFLIia6|S2;fCx5!VTHyhX-VzAMOm<=Z6PmpC2BOeSUaA_W9w?g3b@0xkIQw zKirKi96obs;c)ju3x|(Bv~YOQ`HGwwo!jppm`y(~JkaOpTzN#}E|W!?n^t{vQ7ny9g7ugmabtw|L`&?&#ZlFdMIPV>@fpcc4jm~E1^VN2*b26 z_n3h!L0x+6XC9t;@!>BcY}VVmDNx$vo`qLU8`<5C#O!C0q|GQU!KAc*`GK$g)xZCl zU;0>)R+pxwuL(g(0)GWndAx!E&nvI!$7@)r+%G)tt0j^i@iZs6@3d*u6G=$rzw+JPD)w= zxVC_^9m`;5?qrr+*y3nuIe7-;x%2MWni5ihKeKQaEi2~TjCDb4D2INhcrCnU_Qdza z6mOr@20{qm05$v+fSSQf^NV`0fnP^f=0VTdJjnUqWwJp4UqSwarV4!ca>D;*cv?5% ze=a=nb&U_SEcWW~lzyUuivo0!*@#TL&615)hlwa26&}2~gE=N8IvbZtH@b*V`4o?_ z|8n`5_+Jf=((P-eHh@p2s6N%19S0rS zX%_f-QLSMAOHRO-{zxoS5t&c<^@#6mCt|rQ=ifc)d zhhE!(KS5ep1(-3eOd1$4RATb^toG?oy;2xfDwO|e?D{>Qw4h4*Gu7b8_ad~zP+}dE zJO@hYpyW)r$6pP1_g}dCZ$uODR{p2|%hIjz5a7zdk-4zCR~e zK0$y#9Z%pA91IUhSu2<*fN`|aKSrU25rRTL-z^lPeaZ^`Tu~?u`@9PMT(?kdB)@*hv4_%M=pkV>tV%KC6V zIl(B|Vt!c>(@%^Z{KWpnShL5A;fv7lU`8woNk89+S`yCBOSp+{mFOnGWaQli5FoA4 z0R&K?1B8Bnc4YT}zA-iwDDL8RAw+o_nzca~j9r`S^nR>(oZH_L9r6`8YGC_q!V&wD z;S@;^*Roy-SJ=En-OOp3rUqxpmD!W!=c%*^=?R~eZ~|015H zI--eY2?HC=*xJpJ1*&9MZdTk8vw~Ip|MBRrt&DIxDC?*RoDoi0QgNjZtQ8LZf^#qcLMF`ywsErb z!X#`^e&ut}0m`Ib6}Vs*0ZNad!P;LlVW)2_B2X%BtxMbdpceU~Q@KNUxU|L(k?(?i zO84m?RZwnB9+wjef40{zV>%%La*l|WrH)!bLn-HMg$``kG^6VcNMq+61*4^EIph6a zGhGcBMqHTadg&u}eN4#)8+)K#b#t=>-EwLm?x5DXA+h=vfQ=hRQFd<0iZvaAUM zt-c9quR3}ks~8`#!v2RMs~Ju-WsTCBDo;b{XCeo@U<)D#wL)_kF63NKAdb{n{m81N zP9ax;bYi;~(<3=~L(EDdShf|x6JP7lWYX75Goe+sI_&8xB;!V=^`_TC^?F$s)~$K6 z4~9~QXl7VVW<1=GrjKCZ{LY}Pj9w5(fSv_X(1p|+yj(trGmJwJjhr`{7<-wLX9>!B z9Tcf2J^3-|(*sOa)QJW3${3nI+wN|w4H?B}QbViHr{GYX_EDtpDzpSK;&td{SWdrC zVR7-*Ag4@go%r{lM@-!9!_awbzz}kxzzoJ*EhBO*|0l_4%)g8+Jp8f@$0zqnEK2V$ z5E%GoSaC3oI-iQbTY<=uB|H;cE|$9kYp~6Xy%7oxA<-1)x!V+&JuWC5*zGrDwce^W zP=wyllYK$EJOy5?4b0->E+4h~Ndv#KX2gT|l^Vsb8g4`1!DftTi^!;$kbCf=!~GAJG~y#J0oT$n8`xjWK+&=}DuwCfnS0(KyYuX}0k#)d)Z- zA{#VQ7xF+IR3gq)_wSZ=VnZ$1O#FKxQBA%61YUw-{YSLll1aKYRxG}wB>iXvkK;uL zN^%!A!-Rbzv{%((6w*SF!KCeS->ne}jVN;(GB7VtXUB2ZU`{`v%G&Oz;CO1f@#nsbhuYj8EWdbm=q7FRD&k*?PG_JXKxyZ`>NyAND-{Mf#$jvhO3Vr+W+&gpj@9Y1jR9Y#8SIdFK~y=DAvfBg9Pp}P;h zE89J`Z*2d#yZyeqk53<%zHfSbzdJ;$H|{^OZ{xk=2TzPoAJ{kM_H>`#?r!n7xZC!> zBb1fhzUxM}_makK8w%jUCR8+&uxpu5tPP@#Fiz zWRAPTSodk)*wH=X2kyDof)5-QB;&>N_l+M@^#_&V-m&9%9Qmh*575qWy&pJ!-J!e3 z_wOIy&)XfKZ^n4ewEJ@4 z@bvhx!(#_`lVUGT&z%wJ^!WYL8FZ+oX6`ooh0NVa7y{o;w~T8!*9xwSxK?sq%+=5J zEKG3|>E6b*Y4cmgjvrEyD#N)C6Str6TectF{|?9x^4Uh`EAl$BTHZ&9|G4rFmwCG^ zWBETbKmVpp6i}wHLMZ1Oq*=v%HP-;w8m`xHy_V||F2VQ`X(}-HrXkhy<}q067(H{0 zXsf41+#EPJ4PATW(4n!z`@iqN@dL1HS(V#Ex{N#>uAFNv*E%lMaR+fL5E!@IKE376 zJ2!70yz|cE_l_MK-+$-P>0@_}A3Az)Y~T3a`B=K;_B*$a9Xxmh7C3Tj@9mrJypBK0 z)88|ccPaPFxL(Jly4G{4t~1|WUFTu-LiMdd@Lf*%S8!cf)U%$jJzR5fGkp8liSgT~ z5j_Qx09bu}+x&FfW@9OgzmvpY&YgK8>QmAGgq>Ud_BW3o2OH;BK@Qqol<|6=ui_Fe zzOqmmx5FRD5UlTj3C{tff^ZicgIt@6GTMZl3&+;o2M!*5Gt%dr{JTiInS5KghKl^R zEtG%Pv9WjF0dt*`KPFGYRpo*#ZJf~;2&o#z1&UFvhB-bIXDXvpo4{?2f>qohMg6pTaewypkT#s`-$@MhXzvcQO z*DrDX3fF(+`W>!_E9LrM0q?u0=UkXSMA$-b|0}}h!v61hKNtSrB20ApG7Ttrbkk6m zNB14M@9^|>BylSkYR+L5|pf0Wm&Q1BhQrh=Q&MNnOSZ&ez#od1(m z=|v(Mqa4=^VDn4VPpnR(Hs!5OFQV11q950k24i{*QU93~%P4Af`dSg6QJ`x|-ka3t zK{9pq>ebFaI*|62FBac){!0UCw1QFw(u+j1M!2r2VZsIrqW+c?E1mzqn)E-`E>f`l zG&O?QIKso=rp;T1whnI_+3t5(>?y>hpZgl4oH}jJpb?)om zs=8nQn3%x+7^^o8ZXVn+I5fDG;d|TQ$l&%(gPS&O+PrDYrlCz+Hw|yvwrOP3c82#& zn>TOXvUzCp*3HA4w{0HTynV~ymQ7nWZ`rbCXv@|u!&|m(8QHRZXmDuL(B`2nLqkJb zhlYo?4UG(K-#WN;)7H&fw`?8Sx^?UD)@@rywr)pP*fhL(c+2q6@Ydns;cdes!`rtF zZrikN^R_M9hPG|pHoR@ywvlbyM+QeWjcgv-GBPx>b!2#C+sMes_U(YUo$9w!^mejs zCrFZPi0f*4;+9Q}9YNDOv1!w==qGwd9^!vHibInh`jl5E$&dC6_-b&gL z?%TO`a0!W5b4i-KiR&6J{k>468|8U!I?ww~E=eBM{|zpcIj?>j_YN!uf*XPW1;Z|` zxn-+S>ip}uRQ4$8&M$k@op+u%v}oZ^k*7-jJzSEC;`WCLyM225`2G9Fk4oJp*vfy5 z@RK3VG-VCKW!x7CKW%ZF2NzVMm5{$bCryR>4O};J3GSB(3*~OwbnwVMXxrn*j-l}D wZw-B`^fz&p>DLpca&DjAv^gm0vl#^cF5-b-O1^cz^Iy%=+VjgJ_r&u551b34WdHyG delta 6888 zcmZ`;du$xXd7qhm+}`aSxhI`%ny1I?>S5|(*>VD@W(t%eN2F+pmLEkD1ZX8$re)p9 zqK;w>r?s<;E9YS-h>%qfw-FNkkrqZ^*exU!wyQscRtVhID3}I_^M?~Ffs+VXz^zgB-Yor`@(pOiufIU4VkxyeDp*f zhNV&z1r@3MKpv3;13^j3=hW=13M0s4X20ggrOj3Om=bbQtdft5R2~om@(Hm-9u#%i z5{Yb!NFEZNd{W#Z4~vGJ5=+a=r7$wSExWB$3j0f8rJ@IytqhyXSA@5hZ!K4)U-{Pq z>AP|9Lum$1)`yfBuSqSm-%5oV;eY1Eda+rp4vy8$v3l`o_4n#{z4*17Sax=)_D1~? zt<2e_EiJbwp=C{)FV^u_?D0;tPLZ7sKx0nTwFd%oqK-e#idH7L!qX6MdW-V4hmG)N ztQMN%b;FmKu=oDhy~;|C*^6CfAUp1u75}?#usG@ub%i|b|A5kQ=MR_etHBrkflm%?Nf?4q z&Sc6`0f6_ybAS2my7@L=8rjZ-$+c)=!#Mt!#450*>J!aWdO%srB+5ihnD-mxrnc9F zR&^z+GKPTIIq4G^#DA{f44+ja=3-KOqwk~3X6xo|LX`ugi;2}p%h(eFS8@09I~4@p zvwU~}T4k`$kUCq>)R2lYYZkox=jEd`=x>&{5ejGBQAON;)Mzf?f!cPq#E^l|+i-md7~l*Oc9 zs#(Kk0nCCui?`D~_ktsR43Apj4)7xQXtf;c(D_uR#zB1ouXWhYEbXZ}Jg=C_?p^l| zVa4nBcn7HFQe#`b73?lHw#}ZN#Xp};X){Y`A zTUl906ScAqba}j1j$x7WjXQ-fKGATIgE<=l8CPCt6c4Z3*dJud3RG$^D4wEv6_f!Oy3M4c($6Sp`JHF`z|v zuHC3``^?%+BlDszl+=d8B1ubsMABcZ-LUzs#yTaosszYfaKp&F)xcBKqRxv!o@*5M z-SMbqXo~raB@J+~f$4G)V*KYDRuz}-*s+pXOVT&HF$vJLn8~cmGHLG~`j?e9J!e9P z6G%o-3Zq5Av@QTHv-RTPb-Tx1iuXS>#g`VRc!r(L@!Br9DUBDuT=$7?_TIk!b!KnU zKy%g>8}DkqwhV4~xpG72Vs1l&S?=c>j#>r90tGD=Gg;6}%1>|nkK1o940KE_EbHRa zcmCIhVCmhA<@QZS!J>}Jg$7jZ(J_R zKGZiX6Zbfou$b)zrTL{da09^F_fLO!dpA7BDQ@U9ZE|!DTp)#hpIn< zX%u>0Y9al%w*B^lx39nY?(hG4tDrWDvV^#0p)Mop^H^6A)t3idOdtv18lGYXbf%Ol4OyEo0vZSIG)eI+Rv=f?z@zp(Ca)unu*_3S-{+_iyBu zcQ^3}lxfJ(v_fIGQ2X4?ZA}jDZA8=CEN$P;gV2Sg2FUz&+6P$tc2+i@?WFw_4N^FK z)QFCB-wqmor}a*OBnrHAHD3(Z)hj>w-B-Ty&2#x5;#ISo11@zFTv+ip(20)VK~7+f zjM|-~p+;fB-h__{wQwSjFyk$psZucGBgc$D>~fz{672=>)a3^(m_5t(_K0;E{N;3( z?6U7mI&olkWuSj9G<$zf4SH^_%R3<74S%`8b)os0@t;DK^sN_M>3MIc|(+Upwk5>bjn}(+;h*sDmqBvP8CXmp0SnIu!zd`sPZlu=V~{v zBx(?>M7ZXIp~@#0^R((8(+kU>81%iriD;!pX$>w|qLWf#>z{##h88Bq3|~R#m4B5X z!)bZ6jk>uI1k4d837BvJrsM%wt)?qFK)rwg`dS&94;=&N7<7gShr%tP(n#-26I|V| z<2ERL9fL`%m*j-Ci=1@&fA`q`JWJ#lq69D2OX;74J*6WOszs!niWjf8=q2zjpa=Is z5$|Ob3u3J6u`iG@;y{=e%JF{}?;X>(UQUoPGl{Hc-sg~mOGz>K{14RSr1-z**DE}& z`23K%k`y~W|C7P@6Gw-9$l8K^hD8-OP~G>1jU!b!27uV2s%Yz>1R8BWL!$+GaYBmt zTpiS2;vp3HBk@CNlh$gO&?Ln>|B&V^2LY;wh+kAPK$h45pOU%?u33&v75fc^;>!nl~?X;jkz=at)6cT z$V(I8+S$Y-r@h@x97dot8CiZlZ+4$5Mi6BT)~9Hh>MGt9heWVrOzF`qMoA2SFZTmr zbV@HoERnd~iCh3H4YC{&6C!{UMTaL$0eDC{v!{c}w>c0fO0MrN00-kkT5rSwhVKNzy!zvF zzfni&!D8CXe(t$rPj}pH3>zrI%YD$~plR4?xk3zlfdJci#%j=RPs-I+B+?U$_o!@$N+$; z_|+-+C!6Pl2?i=K)ker;RronhpedR_gl38Y#1A;#%*nt&iUv6#23Bpf<60erJGg`i z18X1p4WdvfButcImoPmd?=ckw9Fg8f2_=i}mmOaRHpes2rS>`+R`yx2F^yi!nw;4W zk6_YsUf89(P`ZU`H-ZK}ft1Nu9jkeZc_8gN9=XLgfj8q*0-Y26JAD&+^DGYMw-cyJ z^xeo@P1a*J?$xc4?)-w+ z=II=2?rah}XCe&RY;v`9rxTgf5R|rLr3!JP1->cZHEz3c6tiXajFXBSAup@~GVTYA z)j=a+3SwjWgOt{BKF9FI1WqyJ!ypE*YQ&)cpLLKrestBq zSrofrV9(<4Fl#LrWG;2l(9$Z%fOp@Bd4$2iz^aINrN=UCQ)~<0pvIOR#c(}~)yU2< z1m~lWTg!M@EJOA}IvZg44)FY5MGC_K#I}&sk?sO#g|duhJm+j7**Llu(xy9r6&cMY zQlWM9(ER+VlsVLAc^7PGh%7X8&sP)1$Tj=X-e)m4=NS~}y4*Ly_+sQEjH39ATyu1I z&11uQ%~A2xnI^@s&R-j=XV8@%>j<7x_P}f5e=#PhVSWAK${`iLhAm`g(OfEI##0b`SOvBOt_|=*LyYRlybGf**@It|hq0Y!@yp z`|9hP9yo`ghv}ub8n`Ia3Cuk*bMNHS`zMdg9Gp6gX}j!Y%zx3ndvxDpd-feVG_}9? z?oG^}E)}gevS+Vis4#yEu3NiH{iwHe^X5ZS2M+GvGdVpC6!~um??`_et{(kIF-Ja+ p%xu2r(B$C*Gf&)DKoK9rdx$vmnIn^X4jz7dO5pzs#NKR0{y%2tn~wkh 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!");