- This browser can't talk to USB label printers. Open this page in Chrome, Edge or Opera
- over https:// (or http://localhost).
-
-
-
-
-
-
-
-
Label
- {{ tape.mediaWidthMm }} mm tape
-
-
-
- Connect a printer to preview and print a label.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Printer
-
-
-
- No label printer paired yet.
-
-
-
-
-
{{ d.name }}
- connected
- {{ d.reason }}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/frontend/vendor/libweblabel.mjs b/frontend/vendor/libweblabel.mjs
deleted file mode 100644
index a2b7eee..0000000
--- a/frontend/vendor/libweblabel.mjs
+++ /dev/null
@@ -1,2 +0,0 @@
-async function createPrinterBlob(moduleArg={}){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 programArgs=[];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,"/")}programArgs=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{}{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}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 EXITSTATUS;var isFileURI=filename=>filename.startsWith("file://");class EmscriptenEH{}class EmscriptenSjLj extends EmscriptenEH{}var runtimeInitialized=false;function getMemoryBuffer(){return wasmMemory.buffer}function updateMemoryViews(){if(HEAP8?.buffer?.resizable)return;var b=getMemoryBuffer();HEAP8=new Int8Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);HEAP32=new Int32Array(b);HEAPU32=new Uint32Array(b)}function preRun(){var preRun=Module["preRun"];if(preRun){if(typeof preRun=="function")preRun=[preRun];onPreRuns.push(...preRun)}callRuntimeCallbacks(onPreRuns)}function initRuntime(){runtimeInitialized=true;wasmExports["__wasm_call_ctors"]()}function postRun(){var postRun=Module["postRun"];if(postRun){if(typeof postRun=="function")postRun=[postRun];onPostRuns.push(...postRun)}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);throw e}var wasmBinaryFile;function findWasmBinary(){if(Module["locateFile"]){return locateFile("libweblabel.wasm")}return new URL("libweblabel.wasm",import.meta.url).href}function getBinarySync(file){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={env:wasmImports,wasi_snapshot_preview1:wasmImports};return imports}async function createWasm(){function receiveInstance(instance){wasmExports=instance.exports;wasmExports=Asyncify.instrumentWasmExports(wasmExports);assignWasmExports(wasmExports);updateMemoryViews();return wasmExports}function receiveInstantiationResult(result){return receiveInstance(result["instance"])}var info=getWasmImports();var instantiateWasm=Module["instantiateWasm"];if(instantiateWasm){return new Promise(resolve=>{instantiateWasm(info,inst=>resolve(receiveInstance(inst)))})}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 HEAP8;var callRuntimeCallbacks=callbacks=>{while(callbacks.length>0){callbacks.shift()(Module)}};var onPostRuns=[];var onPreRuns=[];var dynCalls={};var dynCallLegacy=(sig,ptr,args)=>{sig=sig.replace(/p/g,"i");var f=dynCalls[sig];return f(ptr,...args)};var noExitRuntime=true;var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();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 HEAPU8;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 _fd_close=fd=>52;var INT53_MAX=9007199254740992;var INT53_MIN=-9007199254740992;var bigintToI53Checked=num=>numINT53_MAX?NaN:Number(num);function _fd_seek(fd,offset,whence,newOffset){offset=bigintToI53Checked(offset);return 70}var printCharBuffers=[null,[],[]];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 printChar=(stream,curr)=>{var buffer=printCharBuffers[stream];if(!curr||curr===10){(stream===1?out:err)(UTF8ArrayToString(buffer));buffer.length=0}else{buffer.push(curr)}};var UTF8ToString=(ptr,maxBytesToRead,ignoreNul)=>ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead,ignoreNul):"";var HEAPU32;var _fd_write=(fd,iov,iovcnt,pnum)=>{var num=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;for(var j=0;j>2]=num;return 0};var runAndAbortIfError=func=>{try{return func()}catch(e){abort(e)}};var handleException=e=>{if(e instanceof ExitStatus||e=="unwind"){return EXITSTATUS}quit_(1,e)};var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var exitJS=(status,implicit)=>{EXITSTATUS=status;_proc_exit(status)};var _exit=exitJS;var maybeExit=()=>{if(!keepRuntimeAlive()){try{_exit(EXITSTATUS)}catch(e){handleException(e)}}};var callUserCallback=func=>{if(ABORT){return}try{return func()}catch(e){handleException(e)}finally{maybeExit()}};var runtimeKeepalivePush=()=>{runtimeKeepaliveCounter+=1};var runtimeKeepalivePop=()=>{runtimeKeepaliveCounter-=1};var HEAP32;var Asyncify={instrumentWasmImports(imports){var importPattern=/^(invoke_.*|__asyncjs__.*)$/;for(let[x,original]of Object.entries(imports)){if(typeof original=="function"){let isAsyncifyImport=original.isAsync||importPattern.test(x)}}},instrumentFunction(original){var wrapper=(...args)=>{Asyncify.exportCallStack.push(original);try{return original(...args)}finally{if(!ABORT){var top=Asyncify.exportCallStack.pop();Asyncify.maybeStopUnwind()}}};Asyncify.funcWrappers.set(original,wrapper);return wrapper},instrumentWasmExports(exports){var ret={};for(let[x,original]of Object.entries(exports)){if(typeof original=="function"){var wrapper=Asyncify.instrumentFunction(original);ret[x]=wrapper}else{ret[x]=original}}return ret},State:{Normal:0,Unwinding:1,Rewinding:2,Disabled:3},state:0,StackSize:16384,currData:null,handleSleepReturnValue:0,exportCallStack:[],callstackFuncToId:new Map,callStackIdToFunc:new Map,funcWrappers:new Map,callStackId:0,asyncPromiseHandlers:null,sleepCallbacks:[],getCallStackId(func){if(!Asyncify.callstackFuncToId.has(func)){var id=Asyncify.callStackId++;Asyncify.callstackFuncToId.set(func,id);Asyncify.callStackIdToFunc.set(id,func)}return Asyncify.callstackFuncToId.get(func)},maybeStopUnwind(){if(Asyncify.currData&&Asyncify.state===Asyncify.State.Unwinding&&!Asyncify.exportCallStack.length){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_unwind);if(typeof Fibers!="undefined"){Fibers.trampoline()}}},whenDone(){return new Promise((resolve,reject)=>{Asyncify.asyncPromiseHandlers={resolve,reject}})},allocateData(){var ptr=_malloc(12+Asyncify.StackSize);Asyncify.setDataHeader(ptr,ptr+12,Asyncify.StackSize);Asyncify.setDataRewindFunc(ptr);return ptr},setDataHeader(ptr,stack,stackSize){HEAPU32[ptr>>2]=stack;HEAPU32[ptr+4>>2]=stack+stackSize},setDataRewindFunc(ptr){var bottomOfCallStack=Asyncify.exportCallStack[0];var rewindId=Asyncify.getCallStackId(bottomOfCallStack);HEAP32[ptr+8>>2]=rewindId},getDataRewindFunc(ptr){var id=HEAP32[ptr+8>>2];var func=Asyncify.callStackIdToFunc.get(id);return func},doRewind(ptr){var original=Asyncify.getDataRewindFunc(ptr);var func=Asyncify.funcWrappers.get(original);return callUserCallback(func)},handleSleep(startAsync){if(ABORT)return;if(Asyncify.state===Asyncify.State.Normal){var reachedCallback=false;var reachedAfterCallback=false;startAsync((handleSleepReturnValue=0)=>{if(ABORT)return;Asyncify.handleSleepReturnValue=handleSleepReturnValue;reachedCallback=true;if(!reachedAfterCallback){return}Asyncify.state=Asyncify.State.Rewinding;runAndAbortIfError(()=>_asyncify_start_rewind(Asyncify.currData));if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.resume()}var asyncWasmReturnValue,isError=false;try{asyncWasmReturnValue=Asyncify.doRewind(Asyncify.currData)}catch(err){asyncWasmReturnValue=err;isError=true}var handled=false;if(!Asyncify.currData){var asyncPromiseHandlers=Asyncify.asyncPromiseHandlers;if(asyncPromiseHandlers){Asyncify.asyncPromiseHandlers=null;(isError?asyncPromiseHandlers.reject:asyncPromiseHandlers.resolve)(asyncWasmReturnValue);handled=true}}if(isError&&!handled){throw asyncWasmReturnValue}});reachedAfterCallback=true;if(!reachedCallback){Asyncify.state=Asyncify.State.Unwinding;Asyncify.currData=Asyncify.allocateData();if(typeof MainLoop!="undefined"&&MainLoop.func){MainLoop.pause()}runAndAbortIfError(()=>_asyncify_start_unwind(Asyncify.currData))}}else if(Asyncify.state===Asyncify.State.Rewinding){Asyncify.state=Asyncify.State.Normal;runAndAbortIfError(_asyncify_stop_rewind);_free(Asyncify.currData);Asyncify.currData=null;Asyncify.sleepCallbacks.forEach(callUserCallback)}else{abort(`invalid state: ${Asyncify.state}`)}return Asyncify.handleSleepReturnValue},handleAsync:startAsync=>Asyncify.handleSleep(async wakeUp=>{wakeUp(await startAsync())})};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 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["arguments"])programArgs=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];var preInit=Module["preInit"];if(preInit){if(typeof preInit=="function")Module["preInit"]=preInit=[preInit];while(preInit.length>0){preInit.shift()()}}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;function __asyncjs__pwusb_list(out,max){return Asyncify.handleAsync(async()=>{const devices=await Module.usbBackend.list(max);const view=new DataView(HEAPU8.buffer);let n=0;for(const d of devices){if(n>=max)break;const base=out+n*6;view.setUint16(base+0,d.vendorId,true);view.setUint16(base+2,d.productId,true);view.setUint8(base+4,d.busNumber||0);view.setUint8(base+5,d.deviceAddress||0);++n}return n})}function __asyncjs__pwusb_open(index){return Asyncify.handleAsync(async()=>await Module.usbBackend.open(index))}function __asyncjs__pwusb_close(handle){return Asyncify.handleAsync(async()=>{await Module.usbBackend.close(handle)})}function __asyncjs__pwusb_claim(handle,interface_number){return Asyncify.handleAsync(async()=>await Module.usbBackend.claim(handle,interface_number))}function __asyncjs__pwusb_release(handle,interface_number){return Asyncify.handleAsync(async()=>await Module.usbBackend.release(handle,interface_number))}function __asyncjs__pwusb_out(handle,endpoint,data,len){return Asyncify.handleAsync(async()=>{const bytes=HEAPU8.slice(data,data+len);return await Module.usbBackend.transferOut(handle,endpoint,bytes)})}function __asyncjs__pwusb_in(handle,endpoint,data,len){return Asyncify.handleAsync(async()=>{const result=await Module.usbBackend.transferIn(handle,endpoint,len);if(typeof result==="number"){return result}const n=Math.min(result.length,len);HEAPU8.set(result.subarray(0,n),data);return n})}function __asyncjs__pwusb_sleep_ms(ms){return Asyncify.handleAsync(async()=>{await new Promise(resolve=>setTimeout(resolve,ms))})}var _pw_manifest,_pw_ptouch_manifest,_pw_ql570_manifest,_pw_ptouch_devices,_pw_ql570_devices,_pw_ptouch_capabilities,_pw_ql570_capabilities,_pw_devices,_pw_capabilities,_pw_driver_for,_pw_ptouch_supports,_pw_ql570_supports,_pw_driver_count,_pw_supports,_pw_abi_version,_pw_last_error,_pw_open,_pw_close,_pw_status_json,_pw_print,_pw_ptouch_last_error,_pw_ptouch_status_json,_pw_ptouch_abi_version,_pw_ptouch_open,_pw_ptouch_close,_pw_ptouch_print,_pw_ql570_last_error,_pw_ql570_status_json,_pw_ql570_abi_version,_pw_ql570_open,_pw_ql570_close,_pw_ql570_print,_malloc,_free,__emscripten_stack_restore,__emscripten_stack_alloc,_emscripten_stack_get_current,dynCall_i,dynCall_ii,dynCall_iii,dynCall_iiii,dynCall_iiiiiii,dynCall_jiji,dynCall_iidiiiii,dynCall_vii,_asyncify_start_unwind,_asyncify_stop_unwind,_asyncify_start_rewind,_asyncify_stop_rewind,memory,__indirect_function_table,wasmMemory;function assignWasmExports(wasmExports){_pw_manifest=Module["_pw_manifest"]=wasmExports["pw_manifest"];_pw_ptouch_manifest=Module["_pw_ptouch_manifest"]=wasmExports["pw_ptouch_manifest"];_pw_ql570_manifest=Module["_pw_ql570_manifest"]=wasmExports["pw_ql570_manifest"];_pw_ptouch_devices=Module["_pw_ptouch_devices"]=wasmExports["pw_ptouch_devices"];_pw_ql570_devices=Module["_pw_ql570_devices"]=wasmExports["pw_ql570_devices"];_pw_ptouch_capabilities=Module["_pw_ptouch_capabilities"]=wasmExports["pw_ptouch_capabilities"];_pw_ql570_capabilities=Module["_pw_ql570_capabilities"]=wasmExports["pw_ql570_capabilities"];_pw_devices=Module["_pw_devices"]=wasmExports["pw_devices"];_pw_capabilities=Module["_pw_capabilities"]=wasmExports["pw_capabilities"];_pw_driver_for=Module["_pw_driver_for"]=wasmExports["pw_driver_for"];_pw_ptouch_supports=Module["_pw_ptouch_supports"]=wasmExports["pw_ptouch_supports"];_pw_ql570_supports=Module["_pw_ql570_supports"]=wasmExports["pw_ql570_supports"];_pw_driver_count=Module["_pw_driver_count"]=wasmExports["pw_driver_count"];_pw_supports=Module["_pw_supports"]=wasmExports["pw_supports"];_pw_abi_version=Module["_pw_abi_version"]=wasmExports["pw_abi_version"];_pw_last_error=Module["_pw_last_error"]=wasmExports["pw_last_error"];_pw_open=Module["_pw_open"]=wasmExports["pw_open"];_pw_close=Module["_pw_close"]=wasmExports["pw_close"];_pw_status_json=Module["_pw_status_json"]=wasmExports["pw_status_json"];_pw_print=Module["_pw_print"]=wasmExports["pw_print"];_pw_ptouch_last_error=Module["_pw_ptouch_last_error"]=wasmExports["pw_ptouch_last_error"];_pw_ptouch_status_json=Module["_pw_ptouch_status_json"]=wasmExports["pw_ptouch_status_json"];_pw_ptouch_abi_version=Module["_pw_ptouch_abi_version"]=wasmExports["pw_ptouch_abi_version"];_pw_ptouch_open=Module["_pw_ptouch_open"]=wasmExports["pw_ptouch_open"];_pw_ptouch_close=Module["_pw_ptouch_close"]=wasmExports["pw_ptouch_close"];_pw_ptouch_print=Module["_pw_ptouch_print"]=wasmExports["pw_ptouch_print"];_pw_ql570_last_error=Module["_pw_ql570_last_error"]=wasmExports["pw_ql570_last_error"];_pw_ql570_status_json=Module["_pw_ql570_status_json"]=wasmExports["pw_ql570_status_json"];_pw_ql570_abi_version=Module["_pw_ql570_abi_version"]=wasmExports["pw_ql570_abi_version"];_pw_ql570_open=Module["_pw_ql570_open"]=wasmExports["pw_ql570_open"];_pw_ql570_close=Module["_pw_ql570_close"]=wasmExports["pw_ql570_close"];_pw_ql570_print=Module["_pw_ql570_print"]=wasmExports["pw_ql570_print"];_malloc=Module["_malloc"]=wasmExports["malloc"];_free=Module["_free"]=wasmExports["free"];__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"];__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"];_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"];dynCall_i=dynCalls["i"]=wasmExports["dynCall_i"];dynCall_ii=dynCalls["ii"]=wasmExports["dynCall_ii"];dynCall_iii=dynCalls["iii"]=wasmExports["dynCall_iii"];dynCall_iiii=dynCalls["iiii"]=wasmExports["dynCall_iiii"];dynCall_iiiiiii=dynCalls["iiiiiii"]=wasmExports["dynCall_iiiiiii"];dynCall_jiji=dynCalls["jiji"]=wasmExports["dynCall_jiji"];dynCall_iidiiiii=dynCalls["iidiiiii"]=wasmExports["dynCall_iidiiiii"];dynCall_vii=dynCalls["vii"]=wasmExports["dynCall_vii"];_asyncify_start_unwind=wasmExports["asyncify_start_unwind"];_asyncify_stop_unwind=wasmExports["asyncify_stop_unwind"];_asyncify_start_rewind=wasmExports["asyncify_start_rewind"];_asyncify_stop_rewind=wasmExports["asyncify_stop_rewind"];memory=wasmMemory=wasmExports["memory"];__indirect_function_table=wasmExports["__indirect_function_table"]}var wasmImports={__asyncjs__pwusb_claim,__asyncjs__pwusb_close,__asyncjs__pwusb_in,__asyncjs__pwusb_list,__asyncjs__pwusb_open,__asyncjs__pwusb_out,__asyncjs__pwusb_release,__asyncjs__pwusb_sleep_ms,emscripten_resize_heap:_emscripten_resize_heap,fd_close:_fd_close,fd_seek:_fd_seek,fd_write:_fd_write};async function run(){preRun();var setStatus=Module["setStatus"];if(setStatus){setStatus("Running...");await new Promise(resolve=>setTimeout(resolve,1));setTimeout(setStatus,1,"")}if(ABORT)return;initRuntime();Module["onRuntimeInitialized"]?.();postRun()}var wasmExports;wasmExports=await createWasm();await run();
-;return Module}export default createPrinterBlob;
diff --git a/frontend/vendor/libweblabel.wasm b/frontend/vendor/libweblabel.wasm
deleted file mode 100644
index 7ba37e1..0000000
Binary files a/frontend/vendor/libweblabel.wasm and /dev/null differ
diff --git a/frontend/vendor/weblabel.mjs b/frontend/vendor/weblabel.mjs
deleted file mode 100644
index 79c65f1..0000000
--- a/frontend/vendor/weblabel.mjs
+++ /dev/null
@@ -1,958 +0,0 @@
-/*
- libweblabel — generated bundle, do not edit.
-
- Built by tools/bundle.py from src/web/index.js and the modules it re-exports:
- src/web/lib/blob.js
- src/web/lib/bitmap.js
- src/web/lib/text.js
- src/web/lib/pattern.js
- src/web/lib/ruler.js
-
- Edit those and rebuild. The wasm driver blobs are separate files loaded at
- runtime; see dist/blobs/.
-*/
-
-/* ---- src/web/lib/blob.js ------------------------------------------------- */
-
-/*
- Loader for USB printer blobs (ABI v1).
-
- Deliberately knows nothing about P-touch printers, Brother, or libptouch.
- It loads a wasm blob, reads the device table out of the blob's manifest and
- calls the six pw_* entry points. A blob for a different vendor's printers,
- built against the same shim and exporting the same ABI, is driven by this
- file unchanged - and so is a future version of this driver that supports
- more printers, because the device table travels in the manifest rather than
- being duplicated here.
-
- Two classes, and the difference is smaller than it looks:
-
- PrinterBlob any blob, holding one driver or many.
- MultiPrinterBlob the same, seen as the set of drivers inside it: which
- one owns a given printer, what that one alone can do,
- and which of its exports to call. A merged blob
- (dist/blobs/libweblabel.mjs) is what makes those
- questions interesting; PrinterBlob drives it either
- way.
-
- The ABI is described in docs/ABI.md.
-*/
-
-/* ------------------------------------------------------------------------
- WebUSB backend
-
- The JS half of the shim's contract. Also vendor-neutral: it moves bytes and
- has no idea what they mean.
- ------------------------------------------------------------------------ */
-
-export class WebUsbBackend {
- constructor({ transferTimeoutMs = 5000 } = {}) {
- this.transferTimeoutMs = transferTimeoutMs;
- this.devices = []; /* the devices the blob is allowed to see */
- this.handles = new Map();
- this.nextHandle = 1;
- }
-
- /* Scope the blob to specific devices. Drivers typically open the first
- match in the list, so passing exactly the device the user picked in the
- chooser is what makes the selection stick. */
- setDevices(devices) {
- this.devices = devices;
- }
-
- async list() {
- return this.devices.map((d, index) => ({
- vendorId: d.vendorId,
- productId: d.productId,
- busNumber: 0, /* WebUSB does not expose bus topology */
- deviceAddress: index,
- }));
- }
-
- async open(index) {
- const device = this.devices[index];
- if (!device) {
- return -1;
- }
- try {
- if (!device.opened) {
- await device.open();
- }
- if (device.configuration === null) {
- await device.selectConfiguration(1);
- }
- } catch (e) {
- this.lastError = e;
- return -1;
- }
- const handle = this.nextHandle++;
- this.handles.set(handle, device);
- return handle;
- }
-
- async close(handle) {
- const device = this.handles.get(handle);
- this.handles.delete(handle);
- if (device && device.opened) {
- try {
- await device.close();
- } catch { /* the device may already be gone */ }
- }
- }
-
- async claim(handle, interfaceNumber) {
- const device = this.handles.get(handle);
- if (!device) return -1;
- try {
- await device.claimInterface(interfaceNumber);
- return 0;
- } catch (e) {
- /* A driver only learns that the claim failed, not why. WebUSB's
- own message is the useful part, and on Linux the overwhelmingly
- likely cause is a bound kernel driver - usblp binds anything of
- printer class - which the browser cannot detach. */
- let detail = `claimInterface(${interfaceNumber}) failed: ${e.message}`;
- if (typeof navigator !== "undefined" && /Linux/.test(navigator.userAgent || "")) {
- detail += ". On Linux this usually means a kernel driver holds the " +
- "interface (usblp claims printer-class devices) and the browser " +
- "cannot detach it. Unbind it: " +
- "echo -n | sudo tee /sys/bus/usb/drivers/usblp/unbind " +
- "— see drivers/ptouch/README.md for the persistent udev rule";
- }
- this.lastError = new Error(detail);
- return -1;
- }
- }
-
- /* Hand the most recent underlying failure to the caller, once. */
- takeLastError() {
- const e = this.lastError;
- this.lastError = null;
- return e ? e.message : null;
- }
-
- async release(handle, interfaceNumber) {
- const device = this.handles.get(handle);
- if (!device) return -1;
- try {
- await device.releaseInterface(interfaceNumber);
- return 0;
- } catch (e) {
- this.lastError = e;
- return -1;
- }
- }
-
- async transferOut(handle, endpoint, bytes) {
- const device = this.handles.get(handle);
- if (!device) return -1;
- try {
- const result = await device.transferOut(endpoint, bytes);
- if (result.status !== "ok") {
- this.lastError = new Error(`transferOut: ${result.status}`);
- return -1;
- }
- return result.bytesWritten;
- } catch (e) {
- this.lastError = e;
- return -1;
- }
- }
-
- async transferIn(handle, endpoint, length) {
- const device = this.handles.get(handle);
- if (!device) return -1;
- /* WebUSB has no per-transfer timeout and a pending transferIn cannot be
- cancelled, so race it against a timer. -7 is LIBUSB_ERROR_TIMEOUT to
- the shim, which is what the driver's own timeout handling expects. */
- const TIMEOUT = Symbol("timeout");
- let timer;
- try {
- const result = await Promise.race([
- device.transferIn(endpoint, length),
- new Promise((resolve) => {
- timer = setTimeout(() => resolve(TIMEOUT), this.transferTimeoutMs);
- }),
- ]);
- if (result === TIMEOUT) {
- /* The abandoned transfer would otherwise deliver its data into
- the next read. Clearing the endpoint discards it. */
- try { await device.clearHalt("in", endpoint); } catch { /* best effort */ }
- this.lastError = new Error(`transferIn timed out after ${this.transferTimeoutMs} ms`);
- return -7;
- }
- if (result.status !== "ok") {
- this.lastError = new Error(`transferIn: ${result.status}`);
- return -1;
- }
- return new Uint8Array(result.data.buffer, result.data.byteOffset, result.data.byteLength);
- } catch (e) {
- this.lastError = e;
- return -1;
- } finally {
- clearTimeout(timer);
- }
- }
-}
-
-/* ------------------------------------------------------------------------
- Blob
- ------------------------------------------------------------------------ */
-
-const MAX_LOG = 500;
-
-export class PrinterBlob {
- constructor(module, backend) {
- this._module = module;
- this._backend = backend;
- this._queue = Promise.resolve();
- this.log = [];
- this.manifest = null;
- this.abiVersion = 0;
- this.openDevice = null; /* {vendorId, productId} while open */
- }
-
- /*
- url location of the blob's .mjs glue, resolved against baseUrl
- backend anything implementing the backend contract; defaults to WebUSB
- baseUrl base for resolving url (defaults to the document / this module)
- */
- static async load(url, { backend, baseUrl } = {}) {
- const base = baseUrl
- || (typeof document !== "undefined" ? document.baseURI : import.meta.url);
- const href = new URL(url, base).href;
-
- const usbBackend = backend || new WebUsbBackend();
- /* `new this`, so a subclass reaching this through super.load() gets an
- instance of itself. */
- const blob = new this(null, usbBackend);
-
- const factory = (await import(/* @vite-ignore */ href)).default;
- blob._module = await factory({
- usbBackend,
- print: (text) => blob._record("out", text),
- printErr: (text) => blob._record("err", text),
- });
-
- blob.abiVersion = await blob._call("pw_abi_version", "number", [], []);
- if (blob.abiVersion !== 1) {
- throw new Error(`unsupported blob ABI version ${blob.abiVersion}, expected 1`);
- }
- const manifest = await blob._call("pw_manifest", "string", [], []);
- blob.manifest = JSON.parse(manifest);
- return blob;
- }
-
- _record(stream, text) {
- this.log.push({ stream, text });
- if (this.log.length > MAX_LOG) {
- this.log.shift();
- }
- }
-
- /* Asyncify unwinds a single wasm stack at a time, so every export call is
- serialized. Without this, two overlapping calls corrupt each other. */
- _call(name, returnType, argTypes, args) {
- const run = () => this._module.ccall(name, returnType, argTypes, args, { async: true });
- const result = this._queue.then(run, run);
- this._queue = result.then(() => undefined, () => undefined);
- return result;
- }
-
- /*
- Call an export by name, through the same queue everything else uses.
-
- The methods below cover the ABI; this is for the exports they do not,
- which is how a caller uses the per-driver symbols that
- MultiPrinterBlob.symbolsFor() names. Going around it and calling
- Module.ccall() directly is what re-entering a suspended blob looks like.
- */
- callExport(name, returnType = "number", argTypes = [], args = []) {
- return this._call(name, returnType, argTypes, args);
- }
-
- /* The driver's own explanation, plus whatever the backend knows about the
- host-level cause. The driver only sees "the claim failed"; the backend
- is the layer that saw the actual exception. */
- async _lastError() {
- const fromDriver = await this._call("pw_last_error", "string", [], []);
- const fromBackend = typeof this._backend.takeLastError === "function"
- ? this._backend.takeLastError() : null;
- return [fromDriver, fromBackend].filter(Boolean).join(" — ") || "unknown error";
- }
-
- /*
- Device filters for navigator.usb.requestDevice(), straight from the
- blob. Deduplicated by USB id: a merged blob's table is several drivers'
- tables at once, and two of them may know the same printer.
- */
- usbFilters() {
- const seen = new Set();
- const filters = [];
- for (const d of this.manifest.devices) {
- const key = `${d.vendorId}:${d.productId}`;
- if (!seen.has(key)) {
- seen.add(key);
- filters.push({ vendorId: d.vendorId, productId: d.productId });
- }
- }
- return filters;
- }
-
- /** What the blob knows about these USB ids, or null if it does not know them. */
- findDevice(vendorId, productId) {
- return this.manifest.devices.find(
- (d) => d.vendorId === vendorId && d.productId === productId) || null;
- }
-
- /** Restrict the blob's view of the bus to these WebUSB devices. */
- setDevices(devices) {
- this._backend.setDevices(devices);
- }
-
- async open(vendorId, productId, { timeoutSeconds = 1 } = {}) {
- const rc = await this._call("pw_open", "number",
- ["number", "number", "number"], [vendorId, productId, timeoutSeconds]);
- if (rc !== 0) {
- throw new Error(await this._lastError());
- }
- this.openDevice = { vendorId, productId };
- }
-
- async status({ timeoutSeconds = 1 } = {}) {
- const json = await this._call("pw_status_json", "string",
- ["number"], [timeoutSeconds]);
- const parsed = JSON.parse(json);
- if (parsed === null) {
- throw new Error(await this._lastError());
- }
- return parsed;
- }
-
- async close() {
- await this._call("pw_close", "number", [], []);
- this.openDevice = null;
- }
-
- /** Does the blob advertise this capability? */
- can(capability) {
- return (this.manifest.capabilities || []).includes(capability);
- }
-
- /*
- Print a bitmap: one byte per pixel, row-major, non-zero = a printed dot.
- width runs along the tape, height across it. The caller is responsible
- for keeping height within the print area the status reports.
- */
- async printBitmap({ data, width, height },
- { chain = false, precut = false, copies = 1 } = {}) {
- if (!this.can("print")) {
- throw new Error("this blob does not support printing");
- }
- if (data.length !== width * height) {
- throw new Error(`bitmap is ${data.length} bytes, expected ${width * height}`);
- }
- /* Staged into the wasm heap so the blob can read it directly. Freed
- even if the print throws, and after the call has fully finished -
- the pointer is live for the whole suspended-stack duration. */
- const ptr = this._module._malloc(data.length);
- if (!ptr) {
- throw new Error(`could not allocate ${data.length} bytes in the blob`);
- }
- try {
- this._module.HEAPU8.set(data, ptr);
- const rc = await this._call("pw_print", "number",
- ["number", "number", "number", "number", "number", "number"],
- [ptr, width, height, chain ? 1 : 0, precut ? 1 : 0, copies]);
- if (rc !== 0) {
- throw new Error(await this._lastError());
- }
- } finally {
- this._module._free(ptr);
- }
- }
-}
-
-/* ------------------------------------------------------------------------
- Blobs seen as a set of drivers
-
- dist/blobs/libweblabel.mjs is every driver this build produced, in one
- module. It forwards the plain ABI to whichever driver owns the printer that
- was opened, so PrinterBlob drives it without knowing that.
-
- What this class covers is the part only the driver set can answer: which
- driver a given printer belongs to, what that driver alone can do, and which
- of its exports to call for it. A blob holding one driver is a set of one, so
- this works on those too and a page never has to choose between the two
- classes. See "Merged blobs" in docs/ABI.md.
- ------------------------------------------------------------------------ */
-
-/* The ABI, as the JavaScript name for each export and the bare C name it is
- built from. A merged blob exports every one of these twice: once plain, and
- once per driver as pw__. */
-const ABI_EXPORTS = {
- abiVersion: "abi_version",
- manifest: "manifest",
- devices: "devices",
- capabilities: "capabilities",
- supports: "supports",
- open: "open",
- close: "close",
- statusJson: "status_json",
- print: "print",
- lastError: "last_error",
-};
-
-export class MultiPrinterBlob extends PrinterBlob {
- /*
- Does this module hold several drivers?
-
- The merged blob reports them under "drivers" and reaches each one
- through prefixed exports; a single-driver blob has only the plain ones.
- It is the one thing that changes the answers below.
- */
- get merged() {
- return Array.isArray(this.manifest.drivers);
- }
-
- /* The drivers inside this blob, each one's own manifest verbatim. A
- single-driver blob's manifest has that shape already, so it is a set of
- one rather than a special case. */
- get drivers() {
- return this.manifest.drivers || [this.manifest];
- }
-
- /** Their names, in the order the build linked them. */
- driverNames() {
- return this.drivers.map((d) => d.driver.name);
- }
-
- /** The manifest of the driver with this name, or null. */
- driver(name) {
- return this.drivers.find((d) => d.driver.name === name) || null;
- }
-
- /*
- The driver that owns these USB ids, or null if none does.
-
- Answered from the merged device table, where every entry names the
- driver it came from. pw_driver_for() inside the blob answers the same
- question from the same tables; this is the cheap synchronous way to ask
- it.
- */
- driverFor(vendorId, productId) {
- const device = this.findDevice(vendorId, productId);
- return device ? this.driver(device.driver) : null;
- }
-
- /*
- Which exports to call for this printer.
-
- const { driver, exports } = blob.symbolsFor(0x04f9, 0x2074);
- await blob.callExport(exports.open, "number",
- ["number", "number", "number"], [0x04f9, 0x2074, 1]);
-
- Going through the plain ABI - blob.open(), blob.status(), and the rest
- - does the same dispatch inside the blob and is what most callers want.
- This is for a caller that has a reason to address one driver directly:
- reading a second driver's manifest while a printer is open, say, or
- driving two of them without letting either one's dispatch state decide
- which is current.
-
- On a single-driver blob the prefix is empty and these are the plain ABI
- names, which is the truth there: the one driver is the dispatch.
-
- Returns null when no driver in this blob knows the device.
- */
- symbolsFor(vendorId, productId) {
- const driver = this.driverFor(vendorId, productId);
- if (!driver) {
- return null;
- }
- const name = driver.driver.name;
- const prefix = this.merged ? `pw_${name}_` : "pw_";
- const exports = {};
- for (const [key, bare] of Object.entries(ABI_EXPORTS)) {
- exports[key] = prefix + bare;
- }
- return { driver: name, prefix, exports, capabilities: driver.capabilities };
- }
-
- /*
- Does a capability apply?
-
- With no device, the answer is the merged one: some driver in this blob
- can do it. With a device - or with one open - it is that device's
- driver alone, which is the honest answer for a control that is about to
- act on that printer. "chain" is the case that matters: the P-touch
- driver honours it and the QL driver does not.
- */
- can(capability, device = this.openDevice) {
- if (!device) {
- return super.can(capability);
- }
- const driver = this.driverFor(device.vendorId, device.productId);
- return Boolean(driver && (driver.capabilities || []).includes(capability));
- }
-}
-
-/* ---- src/web/lib/bitmap.js ----------------------------------------------- */
-
-/*
- Canvas ⇄ the one-byte-per-pixel bitmap the blob ABI takes.
-
- A printhead has one state per dot: a pin either fires or it does not. Canvas
- drawing is anti-aliased, so everything drawn there is greyscale until it is
- thresholded here. Nothing in this file knows what is being printed.
-*/
-
-/* Canvas RGBA to the bitmap: one byte per pixel, row-major, non-zero = a dot. */
-export function canvasToBitmap(canvas) {
- const { width, height } = canvas;
- const rgba = canvas.getContext("2d", { willReadFrequently: true })
- .getImageData(0, 0, width, height).data;
- const data = new Uint8Array(width * height);
- for (let i = 0, p = 0; p < data.length; i += 4, ++p) {
- /* Rounded, because the coefficients do not sum to exactly 1 in binary
- floating point: without this, a pixel at exactly mid grey flips on
- rounding noise instead of landing consistently on one side. */
- const luminance = Math.round(0.299 * rgba[i] + 0.587 * rgba[i + 1] + 0.114 * rgba[i + 2]);
- /* Transparent counts as blank; anything darker than mid grey prints. */
- data[p] = (rgba[i + 3] > 127 && luminance < 128) ? 1 : 0;
- }
- return { data, width, height };
-}
-
-/*
- Paint a bitmap back onto its canvas, so the preview shows exactly the dots
- that will be printed, jaggies and all. canvasToBitmap() is idempotent over
- this: re-reading the canvas afterwards yields the same bytes.
-*/
-export function bitmapToCanvas(canvas, bitmap) {
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
- const img = ctx.createImageData(bitmap.width, bitmap.height);
- for (let p = 0, i = 0; p < bitmap.data.length; ++p, i += 4) {
- const value = bitmap.data[p] ? 0 : 255;
- img.data[i] = img.data[i + 1] = img.data[i + 2] = value;
- img.data[i + 3] = 255;
- }
- ctx.putImageData(img, 0, 0);
-}
-
-/** How many dots the printhead will fire for this bitmap. */
-export function countDots(bitmap) {
- let dots = 0;
- for (const b of bitmap.data) {
- dots += b;
- }
- return dots;
-}
-
-/* ---- src/web/lib/text.js ------------------------------------------------- */
-
-/*
- Text laid out the way `ptouch-print --text` does it.
-
- A port of render_text() and its helpers from ptouch-print.c, which cannot be
- linked into a blob: that file is libgd and argp all the way down. The CLI
- picks a font size that makes the tallest line fill its share of the tape,
- builds an image exactly as wide as the widest line, and distributes the lines
- down the print area. Everything below mirrors that, including the integer
- arithmetic, so the same input yields the same layout.
-
- Verified against the real tool: for the cases in the test suite this picks
- the same point size and produces the same label width as
- `ptouch-print --force-tape-width N --text ... --write-png`.
-
- One deliberate difference: gd is called through gdImageStringFT_180dpi(),
- which hardcodes 180 dpi, so the CLI renders text at half the intended
- physical size on a 360 dpi model. Here the printer's real dpi is used.
-
- gd's brect maps onto canvas TextMetrics like this:
- brect[1] descent below the baseline actualBoundingBoxDescent
- brect[5] -ascent above the baseline -actualBoundingBoxAscent
- brect[0] left edge of the ink -actualBoundingBoxLeft
- brect[2] right edge of the ink actualBoundingBoxRight
- so height = brect[1]-brect[5], needed_width = brect[2]-brect[0] and
- offset_x = -brect[0] all carry over directly.
-*/
-
-export const MAX_LINES = 4; /* as in ptouch-print.c */
-
-/*
- The threshold is 50%, matching what the CLI does. ptouch-print.c renders with
- a *negative* colour (`-black`), which tells libgd to disable anti-aliasing
- and use FreeType's monochrome rasterizer - verified: the same string rendered
- with `black` produces 9 distinct palette entries, with `-black` exactly 2. So
- the CLI's text is natively 1-bit and its coverage rule is "is the pixel centre
- inside the outline", which 50% approximates.
-
- The one thing that cannot be reproduced is FreeType's dropout control, which
- deliberately keeps hairlines that would otherwise fall between pixel centres.
- Lowering TEXT_COVERAGE thickens stems if that ever bites.
-
- What stops small text turning to mush is not the threshold but the size: the
- text is scaled to the print area rather than to a fixed pixel size, so stems
- are several dots wide. fillText()'s maxWidth argument is never used - it
- condenses glyphs horizontally to fit, which destroys legibility far faster
- than omitting the element does.
-*/
-const TEXT_COVERAGE = 0.5;
-
-/* DejaVu Sans is the CLI's default font, so labels come out looking the same
- either way. */
-const TEXT_FONT_STACK = '"DejaVu Sans", "Liberation Sans", Arial, Helvetica, sans-serif';
-const TEXT_WEIGHT = "normal";
-
-/* Verbatim from find_fontsize(): measuring against a fixed set of ascenders and
- descenders keeps every line the same height whatever it contains. */
-const COMMON_CHARS = "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnoprqstuvwxyz{|}~";
-
-let measureCtx = null;
-
-const textFont = (size) => `${TEXT_WEIGHT} ${size}px ${TEXT_FONT_STACK}`;
-
-/* Ink box rather than advance width, so text can be packed tightly. Already
- rounded to integers, which is what FreeType reports, so the arithmetic below
- stays integer as it is in C. */
-function ftMetrics(text, fontPx) {
- if (!measureCtx) {
- measureCtx = document.createElement("canvas").getContext("2d");
- }
- measureCtx.font = textFont(fontPx);
- const m = measureCtx.measureText(text);
- const left = Math.ceil(m.actualBoundingBoxLeft ?? 0);
- const right = Math.ceil(m.actualBoundingBoxRight ?? m.width);
- const ascent = Math.ceil(m.actualBoundingBoxAscent ?? fontPx * 0.8);
- const descent = Math.ceil(m.actualBoundingBoxDescent ?? fontPx * 0.2);
- return { left, width: left + right, ascent, descent, height: ascent + descent };
-}
-
-const ptToPx = (pt, dpi) => pt * dpi / 72;
-
-/* find_fontsize(): the largest whole point size whose line height still fits. */
-function findFontSizePt(wantPx, text, dpi) {
- const combined = text + COMMON_CHARS;
- let save = 0;
- for (let pt = 4; pt <= 500; ++pt) {
- if (ftMetrics(combined, ptToPx(pt, dpi)).height <= wantPx) {
- save = pt;
- } else {
- break;
- }
- }
- return save === 0 ? -1 : save;
-}
-
-/* get_baselineoffset(): how much further below the baseline this text reaches
- than a letter that sits on it. 'z' is the CLI's reference glyph. */
-function baselineOffset(text, fontPx) {
- return ftMetrics(text, fontPx).descent - ftMetrics("z", fontPx).descent;
-}
-
-/* The CLI's --text splits on a literal \n as well as a real newline. */
-export function parseTextLines(input) {
- return input.split(/\\n|\n/).slice(0, MAX_LINES);
-}
-
-/* render_text(): the placement of every line, or a throw carrying the same
- message the CLI prints. printWidth is the print area of the mounted tape. */
-export function layoutTextLabel(lines, printWidth,
- { align = "l", fontSizePt = 0, fontMargin = 0, dpi }) {
- let fsz = fontSizePt;
- if (fsz <= 0) {
- const wantPx = Math.floor((printWidth - 2 * fontMargin) / lines.length);
- for (const line of lines) {
- const candidate = findFontSizePt(wantPx, line, dpi);
- if (candidate < 0) {
- throw new Error("could not estimate needed font size");
- }
- if (fsz === 0 || candidate < fsz) {
- fsz = candidate;
- }
- }
- }
-
- const fontPx = ptToPx(fsz, dpi);
- const metrics = lines.map((line) => ftMetrics(line, fontPx));
- const width = Math.max(...metrics.map((m) => m.width));
- const maxHeight = Math.max(...metrics.map((m) => m.height));
-
- if (maxHeight * lines.length > printWidth) {
- throw new Error(`Font size ${fsz} too large for ${lines.length} lines`);
- }
-
- const unusedPx = printWidth - maxHeight * lines.length;
- const placements = lines.map((line, i) => {
- const ofs = baselineOffset(line, fontPx);
- let pos = i * Math.floor(printWidth / lines.length) + maxHeight - ofs;
- pos += Math.floor(Math.floor(unusedPx / lines.length) / 2);
- let alignOfs = 0;
- if (align === "c") {
- alignOfs = Math.floor((width - metrics[i].width) / 2);
- } else if (align === "r") {
- alignOfs = width - metrics[i].width;
- }
- return { text: line, x: metrics[i].left + alignOfs, baseline: pos };
- });
-
- return { fontSizePt: fsz, fontPx, width, maxHeight, placements };
-}
-
-/*
- Render the whole text block into one buffer and threshold it in a single
- pass, the way the CLI fills one gd image. The result is already 1-bit, so
- whoever composes the label can blit it 1:1 and keep it that way.
-*/
-export function renderTextLabel(lines, printWidth, opts) {
- const layout = layoutTextLabel(lines, printWidth, opts);
-
- const canvas = document.createElement("canvas");
- canvas.width = Math.max(1, layout.width);
- canvas.height = printWidth;
- const ctx = canvas.getContext("2d", { willReadFrequently: true });
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, canvas.width, canvas.height);
- ctx.font = textFont(layout.fontPx);
- ctx.textBaseline = "alphabetic";
- ctx.fillStyle = "#000";
- for (const p of layout.placements) {
- ctx.fillText(p.text, p.x, p.baseline);
- }
-
- /* Opaque white background, so coverage is luminance rather than alpha. */
- const img = ctx.getImageData(0, 0, canvas.width, canvas.height);
- const cutoff = Math.round((1 - TEXT_COVERAGE) * 255);
- for (let i = 0; i < img.data.length; i += 4) {
- const luminance = Math.round(0.299 * img.data[i] + 0.587 * img.data[i + 1]
- + 0.114 * img.data[i + 2]);
- const value = luminance <= cutoff ? 0 : 255;
- img.data[i] = img.data[i + 1] = img.data[i + 2] = value;
- img.data[i + 3] = 255;
- }
- ctx.putImageData(img, 0, 0);
- return { canvas, layout };
-}
-
-/* ---- src/web/lib/pattern.js ---------------------------------------------- */
-
-/*
- A test pattern for a thermal printhead.
-
- Everything is laid out left to right and simply omitted when the tape runs
- out. Nothing is ever scaled down to fit - a squeezed element is a misleading
- test.
-*/
-
-/*
- ctx the label canvas, one pixel per printhead pin
- w, h the area to fill; h is the print area of the mounted tape
- textBlock an already-thresholded canvas to blit at the left, or null
-*/
-export function drawTestPattern(ctx, w, h, textBlock) {
- ctx.fillStyle = "#fff";
- ctx.fillRect(0, 0, w, h);
- ctx.fillStyle = "#000";
- ctx.strokeStyle = "#000";
- ctx.lineWidth = 1;
- /* Crisp 1px strokes need half-pixel coordinates. */
- const line = (x1, y1, x2, y2) => {
- ctx.beginPath();
- ctx.moveTo(x1 + 0.5, y1 + 0.5);
- ctx.lineTo(x2 + 0.5, y2 + 0.5);
- ctx.stroke();
- };
-
- /* A frame on the outermost pins: if the print comes out clipped or off
- centre, this is the thing that shows it. */
- ctx.strokeRect(0.5, 0.5, w - 1, h - 1);
-
- const gap = 6;
- const margin = 5;
- let x = margin;
- const room = (need) => x + need <= w - margin;
-
- /* The text block comes first, rendered by the same --text layout used in
- text-only mode, so the pattern shows real label text next to the geometry
- rather than a second, differently-produced caption. Already 1-bit, so a
- 1:1 blit is all that is needed. */
- if (textBlock && room(textBlock.width)) {
- ctx.drawImage(textBlock, x, 0);
- x += textBlock.width + gap;
- }
-
- /* Vertical bars 1..4 px wide - horizontal (along-tape) resolution. */
- if (room(4 + 3 * 3 + 4)) {
- for (let bar = 1; bar <= 4; ++bar) {
- ctx.fillRect(x, 4, bar, h - 8);
- x += bar + 3;
- }
- x += gap - 3;
- }
-
- /* Horizontal rules at top, middle and bottom - vertical alignment. */
- const rules = 22;
- if (room(rules)) {
- line(x, 4, x + rules, 4);
- line(x, (h >> 1), x + rules, (h >> 1));
- line(x, h - 5, x + rules, h - 5);
- x += rules + gap;
- }
-
- /* Solid block, then a hollow one. */
- const box = Math.max(6, Math.min(24, h - 16));
- const boxTop = (h - box) >> 1;
- if (room(box)) {
- ctx.fillRect(x, boxTop, box, box);
- x += box + gap;
- }
- if (room(box)) {
- ctx.strokeRect(x + 0.5, boxTop + 0.5, box - 1, box - 1);
- x += box + gap;
- }
-
- /* Circle - diagonal edges and anti-aliasing, which thresholding must cope
- with. */
- const r = Math.max(3, Math.min(box, h - 12) / 2);
- if (room(2 * r)) {
- ctx.beginPath();
- ctx.arc(x + r, h / 2, r, 0, Math.PI * 2);
- ctx.stroke();
- x += 2 * r + gap;
- }
-
- /* An X. */
- const d = Math.min(24, h - 12);
- const top = (h - d) >> 1;
- if (room(d)) {
- line(x, top, x + d, top + d);
- line(x, top + d, x + d, top);
- x += d + gap;
- }
-
- /* 2px checkerboard. */
- const check = Math.min(24, h - 12);
- if (room(check)) {
- for (let cy = 0; cy < check; cy += 2) {
- for (let cx = 0; cx < check; cx += 2) {
- if (((cx + cy) / 2) % 2 === 0) {
- ctx.fillRect(x + cx, top + cy, 2, 2);
- }
- }
- }
- x += check + gap;
- }
-}
-
-/* ---- src/web/lib/ruler.js ------------------------------------------------ */
-
-/*
- Millimetre rulers for the label preview.
-
- They live on their own canvases, deliberately not on the label canvas: that
- one is the print bitmap, so anything drawn there would come out on the tape.
-
- One bitmap pixel is one printhead pin, so millimetres follow from the
- printer's own dpi: 1 mm = dpi/25.4 pixels, which is 7.09 px at 180 dpi and
- 14.17 px at 360 dpi.
-*/
-
-export const RULER_H = 20; /* height of the along-tape ruler, CSS px */
-export const RULER_W = 26; /* width of the across-tape ruler, CSS px */
-
-/* Tick spacing in mm, coarsened as the preview shrinks: minor ticks need room
- to stay distinguishable, numbered ticks need room not to collide. */
-function tickSteps(screenPxPerMm) {
- const candidates = [[1, 5], [2, 10], [5, 25], [10, 50], [20, 100], [50, 250]];
- for (const [minor, major] of candidates) {
- if (minor * screenPxPerMm >= 4 && major * screenPxPerMm >= 26) {
- return { minor, major };
- }
- }
- return { minor: 100, major: 500 };
-}
-
-/*
- canvas the ruler's own canvas
- lengthPx the ruled span in bitmap pixels
- axis "x" along the tape (below the label), "y" across it (beside it)
- zoom screen pixels per bitmap pixel
- dpi the printer's resolution, which is what makes a millimetre a
- millimetre
-*/
-export function drawRuler(canvas, { lengthPx, axis, zoom, dpi }) {
- const dpr = window.devicePixelRatio || 1;
- const pxPerMm = dpi / 25.4;
- const span = lengthPx * zoom; /* CSS px along the ruled axis */
- const horizontal = axis === "x";
- const cssW = horizontal ? span : RULER_W;
- const cssH = horizontal ? RULER_H : span;
-
- /* Backed at device resolution so the tick labels stay sharp; the label
- canvas next to it stays 1:1 because it is pixel data, not a drawing. */
- canvas.width = Math.round(cssW * dpr);
- canvas.height = Math.round(cssH * dpr);
- canvas.style.width = `${cssW}px`;
- canvas.style.height = `${cssH}px`;
-
- const ctx = canvas.getContext("2d");
- ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
- ctx.clearRect(0, 0, cssW, cssH);
-
- const ink = getComputedStyle(document.body).color;
- ctx.strokeStyle = ink;
- ctx.fillStyle = ink;
- ctx.globalAlpha = 0.65;
- ctx.lineWidth = 1;
- ctx.font = "9px system-ui, sans-serif";
-
- /* The rule itself, along the edge shared with the preview. */
- ctx.beginPath();
- if (horizontal) {
- ctx.moveTo(0, 0.5);
- ctx.lineTo(span, 0.5);
- } else {
- ctx.moveTo(0.5, 0);
- ctx.lineTo(0.5, span);
- }
- ctx.stroke();
-
- const totalMm = lengthPx / pxPerMm;
- const step = tickSteps(pxPerMm * zoom);
- for (let mm = 0; mm <= totalMm; mm += step.minor) {
- const at = Math.round(mm * pxPerMm * zoom) + 0.5;
- const major = mm % step.major === 0;
- const tick = major ? 8 : 4;
-
- ctx.globalAlpha = major ? 0.75 : 0.45;
- ctx.beginPath();
- if (horizontal) {
- ctx.moveTo(at, 0);
- ctx.lineTo(at, tick);
- } else {
- ctx.moveTo(0, at);
- ctx.lineTo(tick, at);
- }
- ctx.stroke();
-
- if (!major) {
- continue;
- }
- ctx.globalAlpha = 0.75;
- const text = String(mm);
- if (horizontal) {
- /* Skip a number that would run past the end of the tape. */
- if (at + ctx.measureText(text).width + 2 > span) {
- continue;
- }
- ctx.textBaseline = "top";
- ctx.fillText(text, at + 2, tick + 1);
- } else {
- if (at + 4 > span) {
- continue;
- }
- ctx.textBaseline = "middle";
- ctx.fillText(text, tick + 2, at + 4);
- }
- }
-}