UNPKG

@mlightcad/shx-parser

Version:

A TypeScript library for parsing AutoCAD SHX font files

3 lines (2 loc) 17.2 kB
(function(b,u){typeof exports=="object"&&typeof module<"u"?u(exports):typeof define=="function"&&define.amd?define(["exports"],u):(b=typeof globalThis<"u"?globalThis:b||self,u(b["shx-parser"]={}))})(this,function(b){"use strict";class u{static byteToSByte(t){return(t&127)-(t&128?128:0)}constructor(t){this.position=0,this.data=new DataView(t)}readBytes(t=1){this.data.byteLength<this.position+t&&this.throwOutOfRangeError(this.position+t);const e=new Uint8Array(this.data.buffer,this.position,t);return this.position+=t,e}skip(t){this.data.byteLength<this.position+t&&this.throwOutOfRangeError(this.position+t),this.position+=t}readUint8(){this.data.byteLength<this.position+1&&this.throwOutOfRangeError(this.position+1);const t=this.data.getUint8(this.position);return this.position+=1,t}readInt8(){this.data.byteLength<this.position+1&&this.throwOutOfRangeError(this.position+1);const t=this.data.getInt8(this.position);return this.position+=1,t}readUint16(t=!0){this.data.byteLength<this.position+2&&this.throwOutOfRangeError(this.position+2);const e=this.data.getUint16(this.position,t);return this.position+=2,e}readInt16(){this.data.byteLength<this.position+2&&this.throwOutOfRangeError(this.position+2);const t=this.data.getInt16(this.position,!0);return this.position+=2,t}readUint32(){this.data.byteLength<this.position+4&&this.throwOutOfRangeError(this.position+4);const t=this.data.getUint32(this.position,!0);return this.position+=4,t}readInt32(){this.data.byteLength<this.position+4&&this.throwOutOfRangeError(this.position+4);const t=this.data.getInt32(this.position,!0);return this.position+=4,t}readFloat32(){this.data.byteLength<this.position+4&&this.throwOutOfRangeError(this.position+4);const t=this.data.getFloat32(this.position,!0);return this.position+=4,t}readFloat64(){this.data.byteLength<this.position+8&&this.throwOutOfRangeError(this.position+8);const t=this.data.getFloat64(this.position,!0);return this.position+=8,t}setPosition(t){this.data.byteLength<t&&this.throwOutOfRangeError(t),this.position=t}isEnd(){return this.position===this.data.byteLength-1}get currentPosition(){return this.position}get length(){return this.data.byteLength}throwOutOfRangeError(t){throw new Error(`Position ${t} is out of range for the data length ${this.data.byteLength}!`)}}var d=(p=>(p.SHAPES="shapes",p.BIGFONT="bigfont",p.UNIFONT="unifont",p))(d||{});class B{parse(t){const i=this.parseHeader(t).split(" "),n=i[1].toLocaleLowerCase();if(!Object.values(d).includes(n))throw new Error(`Invalid font type: ${n}`);return{fileHeader:i[0],fontType:n,fileVersion:i[2]}}parseHeader(t){let e="",n=0;for(;t.currentPosition<t.length-2&&n<1024;){const s=t.readUint8();if(s===13){const o=t.currentPosition,r=t.readUint8(),c=t.readUint8();if(r===10&&c===26)break;t.setPosition(o),e+=String.fromCharCode(s)}else e+=String.fromCharCode(s);n++}return e.trim()}}class T{parse(t){try{t.readBytes(4);const e=t.readInt16();if(e<=0)throw new Error("Invalid shape count in font file");const i=[];for(let o=0;o<e;o++){const r=t.readUint16(),c=t.readUint16();c>0&&i.push({code:r,length:c})}const n={};for(const o of i)try{const r=t.readBytes(o.length);r.length===o.length&&(n[o.code]=r)}catch{console.warn(`Failed to read shape data for code ${o.code}`)}const s={data:n,info:"",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1};if(0 in n){const o=n[0];try{const r=new TextDecoder().decode(o);let c=r.indexOf("\0");c>=0&&(s.info=r.substring(0,c),c+3<o.length&&(s.baseUp=o[c+1],s.baseDown=o[c+2],s.orientation=o[c+3]===0?"horizontal":"vertical"))}catch{console.warn("Failed to parse font info block")}}return s}catch(e){return console.error("Error parsing shape font:",e),{data:{},info:"Failed to parse font file",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1}}}}class U{parse(t){try{t.readInt16();const e=t.readInt16(),i=t.readInt16();if(e<=0)throw new Error("Invalid character count in font file");t.skip(i*4);const n=[];for(let r=0;r<e;r++){const c=t.readUint16(!1),a=t.readUint16(),h=t.readUint32();(c!==0||a!==0||h!==0)&&n.push({code:c,length:a,offset:h})}const s={};for(const r of n)try{t.setPosition(r.offset);const c=t.readBytes(r.length);c.length===r.length&&(s[r.code]=c)}catch{console.warn(`Failed to read bigfont data for code ${r.code}`)}const o={data:s,info:"",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1};if(0 in s){const r=s[0];try{const c=this.utf8ArrayToStr(r);let a=c.indexOf("\0");a>=0&&(o.info=c.substring(0,a),a++,a+3<r.length&&(r.length-a===4?(o.baseUp=r[a++],o.baseDown=r[a++],o.orientation=r[a++]===0?"horizontal":"vertical"):(o.baseUp=r[a++],a++,o.orientation=r[a++]===0?"horizontal":"vertical",o.baseDown=r[a++],o.isExtended=!0)))}catch{console.warn("Failed to parse bigfont info block")}}return o}catch(e){return console.error("Error parsing big font:",e),{data:{},info:"Failed to parse font file",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1}}}utf8ArrayToStr(t){let e="",i=0;for(;i<t.length;){const n=t[i++];switch(n>>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:e+=String.fromCharCode(n);break;case 12:case 13:{const s=t[i++];e+=String.fromCharCode((n&31)<<6|s&63);break}case 14:{const s=t[i++],o=t[i++];e+=String.fromCharCode((n&15)<<12|(s&63)<<6|(o&63)<<0);break}}}return e}}class F{parse(t){try{const e=t.readInt32();if(e<=0)throw new Error("Invalid character count in font file");const i=t.readInt16(),n=t.readBytes(i),s={data:{},info:"",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1};try{const r=new TextDecoder().decode(n);let c=r.indexOf("\0");c>=0&&(s.info=r.substring(0,c),c+3<n.length&&(s.baseUp=n[c+1],s.baseDown=n[c+2],s.orientation=n[c+3]===0?"horizontal":"vertical"))}catch{console.warn("Failed to parse unifont info block")}const o={};for(let r=0;r<e-1;r++)try{const c=t.readUint16(),a=t.readUint16();if(a>0){const h=t.readBytes(a);h.length===a&&(o[c]=h)}}catch{console.warn("Failed to read unifont character data");break}return s.data=o,s}catch(e){return console.error("Error parsing unifont:",e),{data:{},info:"Failed to parse font file",baseUp:8,baseDown:2,orientation:"horizontal",isExtended:!1}}}}class v{static createParser(t){switch(t){case d.SHAPES:return new T;case d.BIGFONT:return new U;case d.UNIFONT:return new F;default:throw new Error(`Unsupported font type: ${t}`)}}}class l{constructor(t=0,e=0){this.x=t,this.y=e}set(t,e){return this.x=t,this.y=e,this}length(){return Math.sqrt(this.x*this.x+this.y*this.y)}normalize(){const t=this.length();return t!==0&&(this.x/=t,this.y/=t),this}clone(){return new l(this.x,this.y)}add(t){return this.x+=t.x,this.y+=t.y,this}subtract(t){return this.x-=t.x,this.y-=t.y,this}multiply(t){return this.x*=t,this.y*=t,this}divide(t){return t!==0&&(this.x/=t,this.y/=t),this}multiplyScalars(t,e){return this.x*=t,this.y*=e,this}divideScalars(t,e){return t!==0&&(this.x/=t),e!==0&&(this.y/=e),this}distanceTo(t){const e=this.x-t.x,i=this.y-t.y;return Math.sqrt(e*e+i*i)}}const A=Math.PI/4;class C{static fromBulge(t,e,i){const n=Math.max(-1,Math.min(1,i));return new C({start:t,end:e,bulge:n})}static fromOctant(t,e,i,n,s){return new C({center:t,radius:e,startOctant:i,octantCount:n,isClockwise:s})}constructor(t){if(t.start&&t.end&&t.bulge!==void 0){this.start=t.start.clone(),this.end=t.end.clone(),this.bulge=t.bulge,this.isClockwise=t.bulge<0;const e=this.end.clone().subtract(this.start),i=e.length();if(Math.abs(this.bulge)*i/2===0){this.radius=0,this.center=this.start.clone(),this.startAngle=Math.atan2(e.y,e.x),this.endAngle=this.startAngle;return}const s=4*Math.atan(Math.abs(this.bulge));this.radius=i/(2*Math.sin(s/2));const o=this.start.clone().add(e.clone().divide(2)),r=new l(-e.y,e.x);r.normalize(),r.multiply(Math.abs(this.radius*Math.cos(s/2))),this.center=o.clone(),this.isClockwise?this.center.subtract(r):this.center.add(r),this.startAngle=Math.atan2(this.start.y-this.center.y,this.start.x-this.center.x),this.endAngle=Math.atan2(this.end.y-this.center.y,this.end.x-this.center.x),this.isClockwise?this.endAngle>=this.startAngle&&(this.endAngle-=2*Math.PI):this.endAngle<=this.startAngle&&(this.endAngle+=2*Math.PI)}else if(t.center&&t.radius!==void 0&&t.startOctant!==void 0&&t.octantCount!==void 0&&t.isClockwise!==void 0){this.center=t.center.clone(),this.radius=t.radius,this.isClockwise=t.isClockwise,this.startAngle=t.startOctant*A;const e=(t.octantCount===0?8:t.octantCount)*A;this.endAngle=this.startAngle+(this.isClockwise?-e:e),this.start=this.center.clone().add(new l(this.radius*Math.cos(this.startAngle),this.radius*Math.sin(this.startAngle))),this.end=this.center.clone().add(new l(this.radius*Math.cos(this.endAngle),this.radius*Math.sin(this.endAngle)))}else throw new Error("Invalid arc parameters")}tessellate(t=Math.PI/18){if(this.radius===0)return[this.start.clone(),this.end.clone()];const e=[this.start.clone()],i=Math.abs(this.endAngle-this.startAngle),n=Math.max(1,Math.floor(i/t));for(let s=1;s<n;s++){const o=s/n,r=this.isClockwise?this.startAngle-o*i:this.startAngle+o*i;e.push(this.center.clone().add(new l(this.radius*Math.cos(r),this.radius*Math.sin(r))))}return e.push(this.end?this.end.clone():this.center.clone().add(new l(this.radius*Math.cos(this.endAngle),this.radius*Math.sin(this.endAngle)))),e}}class M{constructor(t,e=[]){this.lastPoint=t,this.polylines=e}get bbox(){let t=1/0,e=-1/0,i=1/0,n=-1/0;return this.polylines.forEach(s=>{s.forEach(o=>{t=Math.min(t,o.x),e=Math.max(e,o.x),i=Math.min(i,o.y),n=Math.max(n,o.y)})}),{minX:t,minY:i,maxX:e,maxY:n}}offset(t,e=!0){var i,n;return e?new M((i=this.lastPoint)==null?void 0:i.clone().add(t),this.polylines.map(s=>s.map(o=>o.clone().add(t)))):((n=this.lastPoint)==null||n.add(t),this.polylines.forEach(s=>s.forEach(o=>o.add(t))),this)}toSVG(t={}){const{strokeWidth:e="0.5%",strokeColor:i="black",isAutoFit:n=!1}=t;let s,o;if(n){const r=this.bbox,c=.2,a=r.maxX-r.minX,h=r.maxY-r.minY,g=r.minX-a*c,y=r.maxX+a*c,w=r.minY-h*c,x=r.maxY+h*c;o=this.polylines.map(P=>{let D="";return P.forEach((m,k)=>{const S=m.x,f=-m.y;D+=k===0?`M ${S} ${f} `:`L ${S} ${f} `}),`<path d="${D}" stroke="${i}" stroke-width="${e}" fill="none"/>`}).join(""),s=`${g} ${-x} ${y-g} ${x-w}`}else s="0 0 20 20",o=this.polylines.map(r=>{let c="";return r.forEach((a,h)=>{const g=a.x+5,y=-a.y+15;c+=h===0?`M ${g} ${y} `:`L ${g} ${y} `}),`<path d="${c}" stroke="${i}" stroke-width="${e}" fill="none"/>`}).join("");return`<svg width="100%" height="100%" viewBox="${s}" preserveAspectRatio="xMidYMid meet">${o}</svg>`}}const $=Math.PI/18,E=12;class I{constructor(t){this.shapeCache=new Map,this.shapeData=new Map,this.fontData=t}release(){this.shapeCache.clear(),this.shapeData.clear()}parse(t,e){var o;const i=`${t}_${e}`;if(this.shapeCache.has(i))return this.shapeCache.get(i);if(t===0)return;const n=this.fontData.content.data;let s;if(!this.shapeData.has(t)&&n[t]){const r=n[t],c=E/this.fontData.content.baseUp;s=this.parseShape(r,c),this.shapeData.set(t,s)}if(this.shapeData.has(t)){const r=e/E,c=this.shapeData.get(t);s=new M((o=c.lastPoint)==null?void 0:o.clone().multiply(r),c.polylines.map(a=>a.map(h=>h.clone().multiply(r))))}return s}parseShape(t,e){const c={currentPoint:new l,polylines:[],currentPolyline:[],sp:[],isPenDown:!1,scale:e};for(let a=0;a<t.length;a++){const h=t[a];h<=15?a=this.handleSpecialCommand(h,t,a,c):this.handleVectorCommand(h,c)}return new M(c.currentPoint,c.polylines)}handleSpecialCommand(t,e,i,n){let s=i;switch(t){case 0:n.currentPolyline=[],n.isPenDown=!1;break;case 1:n.isPenDown=!0,n.currentPolyline.push(n.currentPoint.clone());break;case 2:n.isPenDown=!1,n.currentPolyline.length>1&&n.polylines.push(n.currentPolyline.slice()),n.currentPolyline=[];break;case 3:s++,n.scale/=e[s];break;case 4:s++,n.scale*=e[s];break;case 5:if(n.sp.length===4)throw new Error("The position stack is only four locations deep");n.sp.push(n.currentPoint.clone());break;case 6:n.currentPoint=n.sp.pop()??n.currentPoint;break;case 7:s=this.handleSubshapeCommand(e,s,n);break;case 8:s=this.handleXYDisplacement(e,s,n);break;case 9:s=this.handleMultipleXYDisplacements(e,s,n);break;case 10:s=this.handleOctantArc(e,s,n);break;case 11:s=this.handleFractionalArc(e,s,n);break;case 12:s=this.handleBulgeArc(e,s,n);break;case 13:s=this.handleMultipleBulgeArcs(e,s,n);break;case 14:s=this.skipCode(e,++s);break}return s}handleVectorCommand(t,e){const i=(t&240)>>4,n=t&15,s=this.getVectorForDirection(n);e.currentPoint.add(s.multiply(i*e.scale)),e.isPenDown&&e.currentPolyline.push(e.currentPoint.clone())}getVectorForDirection(t){const e=new l;switch(t){case 0:e.x=1;break;case 1:e.x=1,e.y=.5;break;case 2:e.x=1,e.y=1;break;case 3:e.x=.5,e.y=1;break;case 4:e.y=1;break;case 5:e.x=-.5,e.y=1;break;case 6:e.x=-1,e.y=1;break;case 7:e.x=-1,e.y=.5;break;case 8:e.x=-1;break;case 9:e.x=-1,e.y=-.5;break;case 10:e.x=-1,e.y=-1;break;case 11:e.x=-.5,e.y=-1;break;case 12:e.y=-1;break;case 13:e.x=.5,e.y=-1;break;case 14:e.x=1,e.y=-1;break;case 15:e.x=1,e.y=-.5;break}return e}handleSubshapeCommand(t,e,i){let n=e,s=0,o,r=i.scale*this.fontData.content.baseUp,c=r;const a=i.currentPoint.clone();switch(i.currentPolyline.length>1&&(i.polylines.push(i.currentPolyline.slice()),i.currentPolyline=[]),this.fontData.header.fontType){case d.SHAPES:n++,s=t[n];break;case d.BIGFONT:n++,s=t[n],s===0&&(n++,s=t[n++]|t[n++]<<8,a.x=t[n++]*i.scale,a.y=t[n++]*i.scale,this.fontData.content.isExtended&&(c=t[n++]*i.scale),r=t[n]*i.scale);break;case d.UNIFONT:n++,s=t[n++]|t[n++]<<8;break}return s!==0&&(o=this.getShapeByCodeWithOffset(s,c,r,a),o&&(i.polylines.push(...o.polylines.slice()),i.currentPoint=o.lastPoint?o.lastPoint.clone():a.clone())),i.currentPolyline=[],n}handleXYDisplacement(t,e,i){let n=e;const s=new l;return s.x=u.byteToSByte(t[++n]),s.y=u.byteToSByte(t[++n]),i.currentPoint.add(s.multiply(i.scale)),i.isPenDown&&i.currentPolyline.push(i.currentPoint.clone()),n}handleMultipleXYDisplacements(t,e,i){let n=e;for(;;){const s=new l;if(s.x=u.byteToSByte(t[++n]),s.y=u.byteToSByte(t[++n]),s.x===0&&s.y===0)break;i.currentPoint.add(s.multiply(i.scale)),i.isPenDown&&i.currentPolyline.push(i.currentPoint.clone())}return n}handleOctantArc(t,e,i){var w;let n=e;const s=t[++n]*i.scale,o=u.byteToSByte(t[++n]),r=(o&112)>>4;let c=o&7;const a=o<0,h=Math.PI/4*r,g=i.currentPoint.clone().subtract(new l(Math.cos(h)*s,Math.sin(h)*s)),y=C.fromOctant(g,s,r,c,a);if(i.isPenDown){const x=y.tessellate();i.currentPolyline.pop(),i.currentPolyline.push(...x.slice())}return i.currentPoint=(w=y.tessellate().pop())==null?void 0:w.clone(),n}handleFractionalArc(t,e,i){let n=e;const s=t[++n],o=t[++n],r=t[++n],c=t[++n],a=(r*255+c)*i.scale,h=u.byteToSByte(t[++n]),g=(h&112)>>4;let y=h&7;y===0&&(y=8),o!==0&&y--;const w=Math.PI/4;let x=w*y,P=$,D=1;h<0&&(P=-P,x=-x,D=-1);let m=w*g,k=m+x;m+=w*s/256*D,k+=w*o/256*D;const S=i.currentPoint.clone().subtract(new l(a*Math.cos(m),a*Math.sin(m)));if(i.currentPoint=S.clone().add(new l(a*Math.cos(k),a*Math.sin(k))),i.isPenDown){let f=m;const O=[];if(O.push(S.clone().add(new l(a*Math.cos(f),a*Math.sin(f)))),P>0)for(;f+P<k;)f+=P,O.push(S.clone().add(new l(a*Math.cos(f),a*Math.sin(f))));else for(;f+P>k;)f+=P,O.push(S.clone().add(new l(a*Math.cos(f),a*Math.sin(f))));O.push(S.clone().add(new l(a*Math.cos(k),a*Math.sin(k)))),i.currentPolyline.push(...O)}return n}handleBulgeArc(t,e,i){let n=e;const s=new l;s.x=u.byteToSByte(t[++n]),s.y=u.byteToSByte(t[++n]);const o=u.byteToSByte(t[++n]);return i.currentPoint=this.handleArcSegment(i.currentPoint,s,o,i.scale,i.isPenDown,i.currentPolyline),n}handleMultipleBulgeArcs(t,e,i){let n=e;for(;;){const s=new l;if(s.x=u.byteToSByte(t[++n]),s.y=u.byteToSByte(t[++n]),s.x===0&&s.y===0)break;const o=u.byteToSByte(t[++n]);i.currentPoint=this.handleArcSegment(i.currentPoint,s,o,i.scale,i.isPenDown,i.currentPolyline)}return n}skipCode(t,e){switch(t[e]){case 0:break;case 1:break;case 2:break;case 3:case 4:e++;break;case 5:break;case 6:break;case 7:switch(this.fontData.header.fontType){case d.SHAPES:e++;break;case d.BIGFONT:e++,t[e]===0&&(e+=5);break;case d.UNIFONT:e+=2;break}break;case 8:e+=2;break;case 9:for(;;){const s=t[++e],o=t[++e];if(s===0&&o===0)break}break;case 10:e+=2;break;case 11:e+=5;break;case 12:e+=3;break;case 13:for(;;){const s=t[++e],o=t[++e];if(s===0&&o===0)break;e++}break}return e}getShapeByCodeWithOffset(t,e,i,n){var o;const s=this.parse(t,i);if(s){if(e===i)return s.offset(n);{const r=(o=s.lastPoint)==null?void 0:o.clone();r&&(r.x*=e/i);const c=s.polylines.map(a=>a.map(h=>h.clone()));return c.forEach(a=>a.forEach(h=>h.x*=e/i)),new M(r==null?void 0:r.add(n),c.map(a=>a.map(h=>h.add(n))))}}}handleArcSegment(t,e,i,n,s,o){e.x*=n,e.y*=n,i<-127&&(i=-127);const r=t.clone();if(s)if(i===0)o.push(r.clone().add(e));else{const c=r.clone().add(e),h=C.fromBulge(r,c,i/127).tessellate();o.push(...h.slice(1))}return r.add(e),r}}class L{constructor(t){if(t instanceof ArrayBuffer){const e=new u(t),n=new B().parse(e),o=v.createParser(n.fontType).parse(e);this.fontData={header:n,content:o}}else this.fontData=t;this.shapeParser=new I(this.fontData)}getCharShape(t,e){return this.shapeParser.parse(t,e)}release(){this.shapeParser.release()}}b.Point=l,b.ShxFont=L,b.ShxFontType=d,b.ShxShape=M,b.ShxShapeParser=I,Object.defineProperty(b,Symbol.toStringTag,{value:"Module"})}); //# sourceMappingURL=index.umd.js.map