ttx-wasm
Version:
A dual-runtime FontTools TTX library for browser (WebAssembly) and Node.js (native Python) environments
407 lines (370 loc) • 31.8 kB
JavaScript
var oe=Object.defineProperty;var o=(e,t)=>oe(e,"name",{value:t,configurable:true}),A=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,r)=>(typeof require<"u"?require:t)[r]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var W=(()=>{for(var e=new Uint8Array(128),t=0;t<64;t++)e[t<26?t+65:t<52?t+71:t<62?t-4:t*4-205]=t;return r=>{for(var s=r.length,a=new Uint8Array((s-(r[s-1]=="=")-(r[s-2]=="="))*3/4|0),n=0,i=0;n<s;){var c=e[r.charCodeAt(n++)],l=e[r.charCodeAt(n++)],d=e[r.charCodeAt(n++)],u=e[r.charCodeAt(n++)];a[i++]=c<<2|l>>4,a[i++]=l<<4|d>>2,a[i++]=d<<6|u;}return a}})();function se(e){return !isNaN(parseFloat(e))&&isFinite(e)}o(se,"_isNumber");function S(e){return e.charAt(0).toUpperCase()+e.substring(1)}o(S,"_capitalize");function L(e){return function(){return this[e]}}o(L,"_getter");var E=["isConstructor","isEval","isNative","isToplevel"],P=["columnNumber","lineNumber"],k=["fileName","functionName","source"],ae=["args"],ce=["evalOrigin"],R=E.concat(P,k,ae,ce);function p(e){if(e)for(var t=0;t<R.length;t++)e[R[t]]!==void 0&&this["set"+S(R[t])](e[R[t]]);}o(p,"StackFrame");p.prototype={getArgs:o(function(){return this.args},"getArgs"),setArgs:o(function(e){if(Object.prototype.toString.call(e)!=="[object Array]")throw new TypeError("Args must be an Array");this.args=e;},"setArgs"),getEvalOrigin:o(function(){return this.evalOrigin},"getEvalOrigin"),setEvalOrigin:o(function(e){if(e instanceof p)this.evalOrigin=e;else if(e instanceof Object)this.evalOrigin=new p(e);else throw new TypeError("Eval Origin must be an Object or StackFrame")},"setEvalOrigin"),toString:o(function(){var e=this.getFileName()||"",t=this.getLineNumber()||"",r=this.getColumnNumber()||"",s=this.getFunctionName()||"";return this.getIsEval()?e?"[eval] ("+e+":"+t+":"+r+")":"[eval]:"+t+":"+r:s?s+" ("+e+":"+t+":"+r+")":e+":"+t+":"+r},"toString")};p.fromString=o(function(t){var r=t.indexOf("("),s=t.lastIndexOf(")"),a=t.substring(0,r),n=t.substring(r+1,s).split(","),i=t.substring(s+1);if(i.indexOf("@")===0)var c=/@(.+?)(?::(\d+))?(?::(\d+))?$/.exec(i,""),l=c[1],d=c[2],u=c[3];return new p({functionName:a,args:n||void 0,fileName:l,lineNumber:d||void 0,columnNumber:u||void 0})},"StackFrame$$fromString");for(b=0;b<E.length;b++)p.prototype["get"+S(E[b])]=L(E[b]),p.prototype["set"+S(E[b])]=function(e){return function(t){this[e]=!!t;}}(E[b]);var b;for(v=0;v<P.length;v++)p.prototype["get"+S(P[v])]=L(P[v]),p.prototype["set"+S(P[v])]=function(e){return function(t){if(!se(t))throw new TypeError(e+" must be a Number");this[e]=Number(t);}}(P[v]);var v;for(h=0;h<k.length;h++)p.prototype["get"+S(k[h])]=L(k[h]),p.prototype["set"+S(k[h])]=function(e){return function(t){this[e]=String(t);}}(k[h]);var h,O=p;function le(){var e=/^\s*at .*(\S+:\d+|\(native\))/m,t=/^(eval@)?(\[native code])?$/;return {parse:o(function(s){if(s.stack&&s.stack.match(e))return this.parseV8OrIE(s);if(s.stack)return this.parseFFOrSafari(s);throw new Error("Cannot parse given Error object")},"ErrorStackParser$$parse"),extractLocation:o(function(s){if(s.indexOf(":")===-1)return [s];var a=/(.+?)(?::(\d+))?(?::(\d+))?$/,n=a.exec(s.replace(/[()]/g,""));return [n[1],n[2]||void 0,n[3]||void 0]},"ErrorStackParser$$extractLocation"),parseV8OrIE:o(function(s){var a=s.stack.split(`
`).filter(function(n){return !!n.match(e)},this);return a.map(function(n){n.indexOf("(eval ")>-1&&(n=n.replace(/eval code/g,"eval").replace(/(\(eval at [^()]*)|(,.*$)/g,""));var i=n.replace(/^\s+/,"").replace(/\(eval code/g,"(").replace(/^.*?\s+/,""),c=i.match(/ (\(.+\)$)/);i=c?i.replace(c[0],""):i;var l=this.extractLocation(c?c[1]:i),d=c&&i||void 0,u=["eval","<anonymous>"].indexOf(l[0])>-1?void 0:l[0];return new O({functionName:d,fileName:u,lineNumber:l[1],columnNumber:l[2],source:n})},this)},"ErrorStackParser$$parseV8OrIE"),parseFFOrSafari:o(function(s){var a=s.stack.split(`
`).filter(function(n){return !n.match(t)},this);return a.map(function(n){if(n.indexOf(" > eval")>-1&&(n=n.replace(/ line (\d+)(?: > eval line \d+)* > eval:\d+:\d+/g,":$1")),n.indexOf("@")===-1&&n.indexOf(":")===-1)return new O({functionName:n});var i=/((.*".+"[^@]*)?[^@]*)(?:@)/,c=n.match(i),l=c&&c[1]?c[1]:void 0,d=this.extractLocation(n.replace(i,""));return new O({functionName:l,fileName:d[0],lineNumber:d[1],columnNumber:d[2],source:n})},this)},"ErrorStackParser$$parseFFOrSafari")}}o(le,"ErrorStackParser");var de=new le;var $=de;var g=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&!process.browser,T=g&&typeof module<"u"&&typeof module.exports<"u"&&typeof A<"u"&&typeof __dirname<"u",j=g&&!T,ue=typeof Deno<"u",H=!g&&!ue,V=H&&typeof window=="object"&&typeof document=="object"&&typeof document.createElement=="function"&&"sessionStorage"in window&&typeof importScripts!="function",z=H&&typeof importScripts=="function"&&typeof self=="object";typeof navigator=="object"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Chrome")==-1&&navigator.userAgent.indexOf("Safari")>-1;var x=typeof read=="function"&&typeof load=="function";var J,D,Y,q,U;async function C(){if(!g||(J=(await import('node:url')).default,q=await import('node:fs'),U=await import('node:fs/promises'),Y=(await import('node:vm')).default,D=await import('node:path'),M=D.sep,typeof A<"u"))return;let e=q,t=await import('node:crypto'),r=await Promise.resolve().then(function () { return _nodeResolve_empty$1; }),s=await import('node:child_process'),a={fs:e,crypto:t,ws:r,child_process:s};globalThis.require=function(n){return a[n]};}o(C,"initNodeModules");function fe(e,t){return D.resolve(t||".",e)}o(fe,"node_resolvePath");function me(e,t){return t===void 0&&(t=location),new URL(e,t).toString()}o(me,"browser_resolvePath");var N;g?N=fe:x?N=o(e=>e,"resolvePath"):N=me;var M;g||(M="/");function pe(e,t){return e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?{response:fetch(e)}:{binary:U.readFile(e).then(r=>new Uint8Array(r.buffer,r.byteOffset,r.byteLength))}}o(pe,"node_getBinaryResponse");function ge(e,t){if(e.startsWith("file://")&&(e=e.slice(7)),e.includes("://"))throw new Error("Shell cannot fetch urls");return {binary:Promise.resolve(new Uint8Array(readbuffer(e)))}}o(ge,"shell_getBinaryResponse");function ye(e,t){let r=new URL(e,location);return {response:fetch(r,t?{integrity:t}:{})}}o(ye,"browser_getBinaryResponse");var F;g?F=pe:x?F=ge:F=ye;async function K(e,t){let{response:r,binary:s}=F(e,t);if(s)return s;let a=await r;if(!a.ok)throw new Error(`Failed to load '${e}': request failed.`);return new Uint8Array(await a.arrayBuffer())}o(K,"loadBinaryFile");var I;if(V)I=o(async e=>await import(e),"loadScript");else if(z)I=o(async e=>{try{globalThis.importScripts(e);}catch(t){if(t instanceof TypeError)await import(e);else throw t}},"loadScript");else if(g)I=be;else if(x)I=load;else throw new Error("Cannot determine runtime environment");async function be(e){e.startsWith("file://")&&(e=e.slice(7)),e.includes("://")?Y.runInThisContext(await(await fetch(e)).text()):await import(J.pathToFileURL(e).href);}o(be,"nodeLoadScript");async function G(e){if(g){await C();let t=await U.readFile(e,{encoding:"utf8"});return JSON.parse(t)}else if(x){let t=read(e);return JSON.parse(t)}else return await(await fetch(e)).json()}o(G,"loadLockFile");async function X(){if(T)return __dirname;let e;try{throw new Error}catch(s){e=s;}let t=$.parse(e)[0].fileName;if(g&&!t.startsWith("file://")&&(t=`file://${t}`),j){let s=await import('node:path');return (await import('node:url')).fileURLToPath(s.dirname(t))}let r=t.lastIndexOf(M);if(r===-1)throw new Error("Could not extract indexURL path from pyodide module location");return t.slice(0,r)}o(X,"calculateDirname");function Q(e){return e.substring(0,e.lastIndexOf("/")+1)||globalThis.location?.toString()||"."}o(Q,"calculateInstallBaseUrl");function Z(e){let t=e.FS,r=e.FS.filesystems.MEMFS,s=e.PATH,a={DIR_MODE:16895,FILE_MODE:33279,mount:o(function(n){if(!n.opts.fileSystemHandle)throw new Error("opts.fileSystemHandle is required");return r.mount.apply(null,arguments)},"mount"),syncfs:o(async(n,i,c)=>{try{let l=a.getLocalSet(n),d=await a.getRemoteSet(n),u=i?d:l,f=i?l:d;await a.reconcile(n,u,f),c(null);}catch(l){c(l);}},"syncfs"),getLocalSet:o(n=>{let i=Object.create(null);function c(u){return u!=="."&&u!==".."}o(c,"isRealDir");function l(u){return f=>s.join2(u,f)}o(l,"toAbsolute");let d=t.readdir(n.mountpoint).filter(c).map(l(n.mountpoint));for(;d.length;){let u=d.pop(),f=t.stat(u);t.isDir(f.mode)&&d.push.apply(d,t.readdir(u).filter(c).map(l(u))),i[u]={timestamp:f.mtime,mode:f.mode};}return {type:"local",entries:i}},"getLocalSet"),getRemoteSet:o(async n=>{let i=Object.create(null),c=await ve(n.opts.fileSystemHandle);for(let[l,d]of c)l!=="."&&(i[s.join2(n.mountpoint,l)]={timestamp:d.kind==="file"?new Date((await d.getFile()).lastModified):new Date,mode:d.kind==="file"?a.FILE_MODE:a.DIR_MODE});return {type:"remote",entries:i,handles:c}},"getRemoteSet"),loadLocalEntry:o(n=>{let c=t.lookupPath(n).node,l=t.stat(n);if(t.isDir(l.mode))return {timestamp:l.mtime,mode:l.mode};if(t.isFile(l.mode))return c.contents=r.getFileDataAsTypedArray(c),{timestamp:l.mtime,mode:l.mode,contents:c.contents};throw new Error("node type not supported")},"loadLocalEntry"),storeLocalEntry:o((n,i)=>{if(t.isDir(i.mode))t.mkdirTree(n,i.mode);else if(t.isFile(i.mode))t.writeFile(n,i.contents,{canOwn:true});else throw new Error("node type not supported");t.chmod(n,i.mode),t.utime(n,i.timestamp,i.timestamp);},"storeLocalEntry"),removeLocalEntry:o(n=>{var i=t.stat(n);t.isDir(i.mode)?t.rmdir(n):t.isFile(i.mode)&&t.unlink(n);},"removeLocalEntry"),loadRemoteEntry:o(async n=>{if(n.kind==="file"){let i=await n.getFile();return {contents:new Uint8Array(await i.arrayBuffer()),mode:a.FILE_MODE,timestamp:new Date(i.lastModified)}}else {if(n.kind==="directory")return {mode:a.DIR_MODE,timestamp:new Date};throw new Error("unknown kind: "+n.kind)}},"loadRemoteEntry"),storeRemoteEntry:o(async(n,i,c)=>{let l=n.get(s.dirname(i)),d=t.isFile(c.mode)?await l.getFileHandle(s.basename(i),{create:true}):await l.getDirectoryHandle(s.basename(i),{create:true});if(d.kind==="file"){let u=await d.createWritable();await u.write(c.contents),await u.close();}n.set(i,d);},"storeRemoteEntry"),removeRemoteEntry:o(async(n,i)=>{await n.get(s.dirname(i)).removeEntry(s.basename(i)),n.delete(i);},"removeRemoteEntry"),reconcile:o(async(n,i,c)=>{let l=0,d=[];Object.keys(i.entries).forEach(function(m){let y=i.entries[m],w=c.entries[m];(!w||t.isFile(y.mode)&&y.timestamp.getTime()>w.timestamp.getTime())&&(d.push(m),l++);}),d.sort();let u=[];if(Object.keys(c.entries).forEach(function(m){i.entries[m]||(u.push(m),l++);}),u.sort().reverse(),!l)return;let f=i.type==="remote"?i.handles:c.handles;for(let m of d){let y=s.normalize(m.replace(n.mountpoint,"/")).substring(1);if(c.type==="local"){let w=f.get(y),ie=await a.loadRemoteEntry(w);a.storeLocalEntry(m,ie);}else {let w=a.loadLocalEntry(m);await a.storeRemoteEntry(f,y,w);}}for(let m of u)if(c.type==="local")a.removeLocalEntry(m);else {let y=s.normalize(m.replace(n.mountpoint,"/")).substring(1);await a.removeRemoteEntry(f,y);}},"reconcile")};e.FS.filesystems.NATIVEFS_ASYNC=a;}o(Z,"initializeNativeFS");var ve=o(async e=>{let t=[];async function r(a){for await(let n of a.values())t.push(n),n.kind==="directory"&&await r(n);}o(r,"collect"),await r(e);let s=new Map;s.set(".",e);for(let a of t){let n=(await e.resolve(a)).join("/");s.set(n,a);}return s},"getFsHandles");W("AGFzbQEAAAABDANfAGAAAW9gAW8BfwMDAgECByECD2NyZWF0ZV9zZW50aW5lbAAAC2lzX3NlbnRpbmVsAAEKEwIHAPsBAPsbCwkAIAD7GvsUAAs=");var Se=async function(){if(!(globalThis.navigator&&(/iPad|iPhone|iPod/.test(navigator.userAgent)||navigator.platform==="MacIntel"&&typeof navigator.maxTouchPoints<"u"&&navigator.maxTouchPoints>1))){return;}}();async function te(){let e=await Se;if(e)return e.exports;let t=Symbol("error marker");return {create_sentinel:o(()=>t,"create_sentinel"),is_sentinel:o(r=>r===t,"is_sentinel")}}o(te,"getSentinelImport");function ne(e){let t={noImageDecoding:true,noAudioDecoding:true,noWasmDecoding:false,preRun:Ne(e),print:e.stdout,printErr:e.stderr,onExit(r){t.exitCode=r;},thisProgram:e._sysExecutable,arguments:e.args,API:{config:e},locateFile:o(r=>e.indexURL+r,"locateFile"),instantiateWasm:Fe(e.indexURL)};return t}o(ne,"createSettings");function we(e){return function(t){let r="/";try{t.FS.mkdirTree(e);}catch(s){console.error(`Error occurred while making a home directory '${e}':`),console.error(s),console.error(`Using '${r}' for a home directory instead`),e=r;}t.FS.chdir(e);}}o(we,"createHomeDirectory");function Ee(e){return function(t){Object.assign(t.ENV,e);}}o(Ee,"setEnvironment");function Pe(e){return e?[async t=>{t.addRunDependency("fsInitHook");try{await e(t.FS,{sitePackages:t.API.sitePackages});}finally{t.removeRunDependency("fsInitHook");}}]:[]}o(Pe,"callFsInitHook");function ke(e){let t=e.HEAPU32[e._Py_Version>>>2],r=t>>>24&255,s=t>>>16&255,a=t>>>8&255;return [r,s,a]}o(ke,"computeVersionTuple");function Ie(e){let t=K(e);return async r=>{r.API.pyVersionTuple=ke(r);let[s,a]=r.API.pyVersionTuple;r.FS.mkdirTree("/lib"),r.API.sitePackages=`/lib/python${s}.${a}/site-packages`,r.FS.mkdirTree(r.API.sitePackages),r.addRunDependency("install-stdlib");try{let n=await t;r.FS.writeFile(`/lib/python${s}${a}.zip`,n);}catch(n){console.error("Error occurred while installing the standard library:"),console.error(n);}finally{r.removeRunDependency("install-stdlib");}}}o(Ie,"installStdlib");function Ne(e){let t;return e.stdLibURL!=null?t=e.stdLibURL:t=e.indexURL+"python_stdlib.zip",[Ie(t),we(e.env.HOME),Ee(e.env),Z,...Pe(e.fsInit)]}o(Ne,"getFileSystemInitializationFuncs");function Fe(e){if(typeof WasmOffsetConverter<"u")return;let{binary:t,response:r}=F(e+"pyodide.asm.wasm"),s=te();return function(a,n){return async function(){a.sentinel=await s;try{let i;r?i=await WebAssembly.instantiateStreaming(r,a):i=await WebAssembly.instantiate(await t,a);let{instance:c,module:l}=i;n(c,l);}catch(i){console.warn("wasm instantiation failed!"),console.warn(i);}}(),{}}}o(Fe,"getInstantiateWasmFunc");var re="0.28.2";function _(e){return e===void 0||e.endsWith("/")?e:e+"/"}o(_,"withTrailingSlash");var B=re;async function at(e={}){if(e.lockFileContents&&e.lockFileURL)throw new Error("Can't pass both lockFileContents and lockFileURL");await C();let t=e.indexURL||await X();t=_(N(t));let r=e;if(r.packageBaseUrl=_(r.packageBaseUrl),r.cdnUrl=_(r.packageBaseUrl??`https://cdn.jsdelivr.net/pyodide/v${B}/full/`),!e.lockFileContents){let f=e.lockFileURL??t+"pyodide-lock.json";r.lockFileContents=G(f),r.packageBaseUrl??=Q(f);}r.indexURL=t,r.packageCacheDir&&(r.packageCacheDir=_(N(r.packageCacheDir)));let s={fullStdLib:false,jsglobals:globalThis,stdin:globalThis.prompt?globalThis.prompt:void 0,args:[],env:{},packages:[],packageCacheDir:r.packageBaseUrl,enableRunUntilComplete:true,checkAPIVersion:true,BUILD_ID:"941a75c165c70d9d65a059137df0382b4c6a7747ff5f3de2b264ce4a2fb36cf5"},a=Object.assign(s,r);a.env.HOME??="/home/pyodide",a.env.PYTHONINSPECT??="1";let n=ne(a),i=n.API;if(i.lockFilePromise=Promise.resolve(r.lockFileContents),typeof _createPyodideModule!="function"){let f=`${a.indexURL}pyodide.asm.js`;await I(f);}let c;if(e._loadSnapshot){let f=await e._loadSnapshot;ArrayBuffer.isView(f)?c=f:c=new Uint8Array(f),n.noInitialRun=true,n.INITIAL_MEMORY=c.length;}let l=await _createPyodideModule(n);if(n.exitCode!==void 0)throw new l.ExitStatus(n.exitCode);if(e.pyproxyToStringRepr&&i.setPyProxyToStringMethod(true),e.convertNullToNone&&i.setCompatNullToNone(true),i.version!==B&&a.checkAPIVersion)throw new Error(`Pyodide version does not match: '${B}' <==> '${i.version}'. If you updated the Pyodide version, make sure you also updated the 'indexURL' parameter passed to loadPyodide.`);l.locateFile=f=>{throw f.endsWith(".so")?new Error(`Failed to find dynamic library "${f}"`):new Error(`Unexpected call to locateFile("${f}")`)};let d;c&&(d=i.restoreSnapshot(c));let u=i.finalizeBootstrap(d,e._snapshotDeserializer);return i.sys.path.insert(0,""),i._pyodide.set_excepthook(),await i.packageIndexReady,i.initializeStreams(a.stdin,a.stdout,a.stderr),u}o(at,"loadPyodide");
/**
* export interface TTXOptions {
tables?: string[];
skipTables?: string[];
splitTables?: boolean;
splitGlyphs?: boolean;
disassembleInstructions?: boolean;
fontNumber?: number;
flavor?: string;
recalcBBoxes?: boolean; // Control bounding box recalculation
recalcTimestamp?: boolean; // Control timestamp recalculation
// Note: recalcMasterChecksum is not supported by FontTools TTFont.save()
}d TTX implementation using Python FontTools
* Provides 100% feature parity with Python FontTools TTX
*/
class PyodideTTX {
constructor() {
this.pyodide = null;
this.initialized = false;
}
async initialize() {
if (this.initialized)
return;
try {
console.log('Loading Pyodide...');
this.pyodide = await at({
indexURL: './pyodide/', // Use local pyodide files
});
console.log('Pyodide loaded successfully, version:', this.pyodide.version);
console.log('Available globals methods:', Object.keys(this.pyodide.globals));
console.log('Installing FontTools and dependencies...');
// First load micropip package via JavaScript
await this.pyodide.loadPackage(['micropip']);
// Then install our dependencies
await this.pyodide.runPythonAsync(`
import micropip
await micropip.install(['fonttools', 'brotli'])
`);
// Load our Python TTX reference implementation
console.log('Loading TTX implementation...');
await this.pyodide.runPythonAsync(`
import sys
from fontTools.ttLib import TTFont
from fontTools.misc.timeTools import timestampSinceEpoch
from pathlib import Path
import tempfile
import io
import json
from typing import List, Dict, Any, Optional, Union
import xml.etree.ElementTree as ET
class PyodideTTXProcessor:
"""
Python FontTools TTX processor running in Pyodide
Provides 100% feature parity with native FontTools
"""
def detect_format(self, font_data: bytes) -> str:
"""Detect font format from binary data"""
if len(font_data) < 4:
return "UNKNOWN"
signature = font_data[:4]
if signature == b'\\x00\\x01\\x00\\x00':
return "TTF"
elif signature == b'OTTO':
return "OTF"
elif signature == b'ttcf':
return "TTC"
elif signature == b'wOFF':
return "WOFF"
elif signature == b'wOF2':
return "WOFF2"
elif font_data[:5] == b'<?xml':
return "TTX"
else:
return "UNKNOWN"
def get_font_info(self, font_data: bytes, font_number: int = 0) -> Dict[str, Any]:
"""Get comprehensive font information"""
try:
# Create temporary file for font data
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(font_data)
tmp_path = tmp.name
# Open font with FontTools
font = TTFont(tmp_path, fontNumber=font_number, lazy=True)
# Get basic info
info = {
'format': self.detect_format(font_data),
'tables': sorted(font.keys()),
'metadata': {}
}
# Extract metadata from name table
if 'name' in font:
name_table = font['name']
for record in name_table.names:
if record.nameID == 1: # Font Family
info['metadata']['family'] = str(record)
elif record.nameID == 2: # Font Subfamily
info['metadata']['style'] = str(record)
elif record.nameID == 5: # Version
info['metadata']['version'] = str(record)
# Extract metadata from head table
if 'head' in font:
head_table = font['head']
info['metadata']['unitsPerEm'] = head_table.unitsPerEm
info['metadata']['created'] = str(timestampSinceEpoch(head_table.created))
info['metadata']['modified'] = str(timestampSinceEpoch(head_table.modified))
font.close()
return info
except Exception as e:
raise Exception(f"Failed to get font info: {e}")
def dump_to_ttx(self, font_data: bytes, tables=None, skip_tables=None,
split_tables=False, split_glyphs=False,
disassemble_instructions=True, font_number=0) -> str:
"""
Convert font to TTX XML with full FontTools functionality
Args:
font_data: Binary font data
tables: List of tables to include
skip_tables: List of tables to exclude
split_tables: Split tables into separate files
split_glyphs: Split glyph data
disassemble_instructions: Disassemble TrueType instructions
font_number: Font index for TTC files
"""
try:
# Create temporary file for font data
with tempfile.NamedTemporaryFile(delete=False, suffix='.ttf') as tmp:
tmp.write(font_data)
tmp_path = tmp.name
# Open font with FontTools
font = TTFont(tmp_path, fontNumber=font_number)
# Apply table filtering
tables_to_dump = None
if tables:
tables_to_dump = tables
elif skip_tables:
tables_to_dump = [t for t in font.keys() if t not in skip_tables]
# Create XML output
output = io.StringIO()
# Dump to TTX format
font.saveXML(
output,
tables=tables_to_dump,
splitTables=split_tables,
splitGlyphs=split_glyphs,
disassembleInstructions=disassemble_instructions
)
font.close()
return output.getvalue()
except Exception as e:
raise Exception(f"Failed to dump to TTX: {e}")
def compile_from_ttx(self, ttx_content: str, flavor=None, recalc_bboxes=True, recalc_timestamp=True) -> bytes:
"""
Compile TTX XML back to font binary
Args:
ttx_content: TTX XML content
flavor: Output flavor (woff, woff2, etc.)
recalc_bboxes: Whether to recalculate bounding boxes
recalc_timestamp: Whether to recalculate timestamps
"""
try:
# Create temporary file for TTX content
with tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.ttx') as tmp:
tmp.write(ttx_content)
ttx_path = tmp.name
# Create font from TTX
font = TTFont()
font.importXML(ttx_path)
# Control recalculation options
# Note: These affect how FontTools handles derived metrics
font.recalcBBoxes = recalc_bboxes
font.recalcTimestamp = recalc_timestamp
# Save to binary format
output = io.BytesIO()
if flavor:
font.flavor = flavor
# Save the font (checksums are handled automatically by FontTools)
font.save(output)
font.close()
return output.getvalue()
except Exception as e:
raise Exception(f"Failed to compile from TTX: {e}")
def list_tables(self, font_data: bytes, font_number: int = 0) -> List[str]:
"""List all tables in font"""
try:
with tempfile.NamedTemporaryFile(delete=False) as tmp:
tmp.write(font_data)
tmp_path = tmp.name
font = TTFont(tmp_path, fontNumber=font_number, lazy=True)
tables = sorted(font.keys())
font.close()
return tables
except Exception as e:
raise Exception(f"Failed to list tables: {e}")
# Create global processor instance
ttx_processor = PyodideTTXProcessor()
`);
this.initialized = true;
console.log('Pyodide TTX initialized successfully!');
}
catch (error) {
console.error('Failed to initialize Pyodide TTX:', error);
throw error;
}
}
async detectFormat(fontData) {
await this.initialize();
if (!this.pyodide)
throw new Error('Pyodide not initialized');
try {
this.pyodide.globals.set('font_data', fontData);
return this.pyodide.runPython('ttx_processor.detect_format(font_data.to_py())');
}
catch (error) {
console.error('Error in detectFormat:', error);
throw new Error(`Font format detection failed: ${error}`);
}
}
async getFontInfo(fontData, fontNumber = 0) {
await this.initialize();
if (!this.pyodide)
throw new Error('Pyodide not initialized');
try {
this.pyodide.globals.set('font_data', fontData);
this.pyodide.globals.set('font_number', fontNumber);
const result = this.pyodide.runPython(`
import json
info = ttx_processor.get_font_info(font_data.to_py(), font_number)
json.dumps(info)
`);
return JSON.parse(result);
}
catch (error) {
console.error('Error in getFontInfo:', error);
throw new Error(`Font info extraction failed: ${error}`);
}
}
async dumpToTTX(fontData, options = {}) {
await this.initialize();
if (!this.pyodide)
throw new Error('Pyodide not initialized');
this.pyodide.globals.set('font_data', fontData);
this.pyodide.globals.set('tables', options.tables || null);
this.pyodide.globals.set('skip_tables', options.skipTables || null);
this.pyodide.globals.set('split_tables', options.splitTables || false);
this.pyodide.globals.set('split_glyphs', options.splitGlyphs || false);
this.pyodide.globals.set('disassemble_instructions', options.disassembleInstructions || true);
this.pyodide.globals.set('font_number', options.fontNumber || 0);
return this.pyodide.runPython(`
ttx_processor.dump_to_ttx(
font_data.to_py(),
tables=tables,
skip_tables=skip_tables,
split_tables=split_tables,
split_glyphs=split_glyphs,
disassemble_instructions=disassemble_instructions,
font_number=font_number
)
`);
}
async compileFromTTX(ttxContent, options = {}) {
await this.initialize();
if (!this.pyodide)
throw new Error('Pyodide not initialized');
this.pyodide.globals.set('ttx_content', ttxContent);
this.pyodide.globals.set('flavor', options.flavor || null);
this.pyodide.globals.set('recalc_bboxes', options.recalcBBoxes !== false); // Default true
this.pyodide.globals.set('recalc_timestamp', options.recalcTimestamp !== false); // Default true
const result = this.pyodide.runPython(`
binary_data = ttx_processor.compile_from_ttx(
ttx_content,
flavor=flavor,
recalc_bboxes=recalc_bboxes,
recalc_timestamp=recalc_timestamp
)
binary_data
`);
return new Uint8Array(result.toJs());
}
async listTables(fontData, fontNumber = 0) {
await this.initialize();
if (!this.pyodide)
throw new Error('Pyodide not initialized');
this.pyodide.globals.set('font_data', fontData);
this.pyodide.globals.set('font_number', fontNumber);
const result = this.pyodide.runPython(`
import json
tables = ttx_processor.list_tables(font_data.to_py(), font_number)
json.dumps(tables)
`);
return JSON.parse(result);
}
isInitialized() {
return this.initialized;
}
}
// Export singleton instance
const pyodideTTX = new PyodideTTX();
/**
* export interface TTXOptions {
tables?: string[];
skipTables?: string[];
splitTables?: boolean;
splitGlyphs?: boolean;
disassembleInstructions?: boolean;
fontNumber?: number;
flavor?: string;
recalcBBoxes?: boolean; // Control bounding box recalculation
recalcTimestamp?: boolean; // Control timestamp recalculation
// Note: recalcMasterChecksum is not supported by FontTools TTFont.save()
}ntTools TTX for WebAssembly
* Now powered by Pyodide for 100% Python FontTools compatibility
*/
/**
* Main TTX class providing FontTools functionality via Pyodide
*/
class TTX {
/**
* Initialize the TTX processor (loads Pyodide and FontTools)
*/
static async initialize() {
await pyodideTTX.initialize();
}
/**
* Check if TTX is initialized and ready to use
*/
static isInitialized() {
return pyodideTTX.isInitialized();
}
/**
* Detect the format of a font file
*/
static async detectFormat(fontData) {
return await pyodideTTX.detectFormat(fontData);
}
/**
* Get comprehensive information about a font
*/
static async getFontInfo(fontData, fontNumber = 0) {
return await pyodideTTX.getFontInfo(fontData, fontNumber);
}
/**
* Convert font binary to TTX XML format
* Supports all FontTools options for complete feature parity
*/
static async dumpToTTX(fontData, options = {}) {
return await pyodideTTX.dumpToTTX(fontData, options);
}
/**
* Compile TTX XML back to font binary
*/
static async compileFromTTX(ttxContent, options = {}) {
return await pyodideTTX.compileFromTTX(ttxContent, options);
}
/**
* List all tables present in a font
*/
static async listTables(fontData, fontNumber = 0) {
return await pyodideTTX.listTables(fontData, fontNumber);
}
}
var _nodeResolve_empty = {};
var _nodeResolve_empty$1 = /*#__PURE__*/Object.freeze({
__proto__: null,
default: _nodeResolve_empty
});
export { TTX, TTX as default, pyodideTTX };
//# sourceMappingURL=ttx-wasm.esm.js.map