UNPKG

@mlightcad/shx-parser

Version:

A TypeScript library for parsing AutoCAD SHX font files

1,581 lines 55.6 kB
class w { /** * Converts an unsigned byte to a signed byte as used in SHX format. * Values > 127 are converted to their signed equivalent (-128 to -1). * @param value - The unsigned byte value to convert * @returns The signed byte value */ static byteToSByte(t) { return (t & 127) - (t & 128 ? 128 : 0); } /** * Creates a new ShxFileReader instance. * @param arraybuffer - The ArrayBuffer to read from */ constructor(t) { this.position = 0, this.data = new DataView(t); } /** * Reads a specified number of bytes from the current position. * @param length - Number of bytes to read (optional) * @returns A Uint8Array containing the read bytes * @throws Error if reading beyond buffer bounds */ 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; } /** * Skips a specified number of bytes from the current position. * @param length - Number of bytes to skip * @throws Error if skipping beyond buffer bounds */ skip(t) { this.data.byteLength < this.position + t && this.throwOutOfRangeError(this.position + t), this.position += t; } /** * Reads an unsigned 8-bit integer. * @returns The read uint8 value * @throws Error if reading beyond buffer bounds */ readUint8() { this.data.byteLength < this.position + 1 && this.throwOutOfRangeError(this.position + 1); const t = this.data.getUint8(this.position); return this.position += 1, t; } /** * Reads a signed 8-bit integer. * @returns The read int8 value * @throws Error if reading beyond buffer bounds */ readInt8() { this.data.byteLength < this.position + 1 && this.throwOutOfRangeError(this.position + 1); const t = this.data.getInt8(this.position); return this.position += 1, t; } /** * Reads an unsigned 16-bit integer. * @param littleEndian If false, a big-endian value should be read. * @returns The read uint16 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Reads a signed 16-bit integer. * @returns The read int16 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Reads an unsigned 32-bit integer. * @returns The read uint32 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Reads a signed 32-bit integer. * @returns The read int32 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Reads a 32-bit floating point number. * @returns The read float32 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Reads a 64-bit floating point number. * @returns The read float64 value * @throws Error if reading beyond buffer bounds */ 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; } /** * Sets the current read position in the buffer. * @param position - The new position to set */ setPosition(t) { this.data.byteLength < t && this.throwOutOfRangeError(t), this.position = t; } /** * Checks if the current position is at the end of the buffer. * @returns True if at the end of the buffer, false otherwise */ isEnd() { return this.position === this.data.byteLength - 1; } /** * Gets the current position in the buffer. * @returns The current position */ get currentPosition() { return this.position; } /** * Gets the total length of the buffer. * @returns The buffer length in bytes */ get length() { return this.data.byteLength; } /** * Throws an error when attempting to read beyond buffer bounds. * @param position - The position that caused the error * @throws Error with details about the out of range access */ throwOutOfRangeError(t) { throw new Error( `Position ${t} is out of range for the data length ${this.data.byteLength}!` ); } } var d = /* @__PURE__ */ ((c) => (c.SHAPES = "shapes", c.BIGFONT = "bigfont", c.UNIFONT = "unifont", c))(d || {}); class z { parse(t) { const n = this.parseHeader(t).split(" "), i = n[1].toLocaleLowerCase(); if (!Object.values(d).includes(i)) throw new Error(`Invalid font type: ${i}`); return { fileHeader: n[0], fontType: i, fileVersion: n[2] }; } parseHeader(t) { let e = "", i = 0; for (; t.currentPosition < t.length - 2 && i < 1024; ) { const s = t.readUint8(); if (s === 13) { const o = t.currentPosition, r = t.readUint8(), h = t.readUint8(); if (r === 10 && h === 26) break; t.setPosition(o), e += String.fromCharCode(s); } else e += String.fromCharCode(s); i++; } return e.trim(); } } const O = 10, j = [13, 10, 0]; function U(c, t) { if (t === 0) { c.orientation = "horizontal"; return; } if (t === 2) { c.orientation = "horizontal", c.dualOrientation = !0; return; } c.orientation = "vertical"; } function $(c) { const t = {}; for (const [e, n] of Object.entries(c)) t[n] = e; return t; } function H(c) { const t = c.indexOf(0); return t < 0 ? { name: null, bytecode: c } : { name: t > 0 ? new TextDecoder("ascii").decode(c.subarray(0, t)) : null, bytecode: c.subarray(t + 1) }; } class K { parse(t) { try { t.readBytes(4); const e = t.readInt16(); if (e <= 0) throw new Error("Invalid shape count in font file"); const n = []; for (let h = 0; h < e; h++) { const a = t.readUint16(), l = t.readUint16(); l > 0 && n.push({ code: a, length: l }); } const i = {}; for (const h of n) try { const a = t.readBytes(h.length); a.length === h.length && (i[h.code] = a); } catch { console.warn(`Failed to read shape data for code ${h.code}`); } const s = {}, o = {}; for (const [h, a] of Object.entries(i)) { const l = Number(h); if (l === 0) { s[l] = a; continue; } const { name: u, bytecode: p } = H(a); s[l] = p, u && (o[u] = l); } const r = { data: s, names: Object.keys(o).length > 0 ? o : void 0, codeToName: Object.keys(o).length > 0 ? $(o) : void 0, info: "", baseUp: 8, baseDown: 2, height: O, width: O, orientation: "horizontal", isExtended: !1 }; if (0 in s) { const h = s[0]; try { const a = new TextDecoder().decode(h); let l = h.findIndex((u) => j.includes(u)); l >= 0 && (r.info = a.substring(0, l), l + 3 < h.length && (r.baseUp = h[l + 1], r.baseDown = h[l + 2], r.height = r.baseDown + r.baseUp, r.width = r.height, U(r, h[l + 3]))); } catch { console.warn("Failed to parse font info block"); } } return r; } catch (e) { const n = e instanceof Error ? e.message : String(e); throw new Error(`Failed to parse shape font: ${n}`); } } } class q { parse(t) { try { t.readInt16(); const e = t.readInt16(), n = t.readInt16(); if (e <= 0) throw new Error("Invalid character count in font file"); t.skip(n * 4); const i = []; for (let r = 0; r < e; r++) { const h = t.readUint16(), a = t.readUint16(), l = t.readUint32(); (h !== 0 || a !== 0 || l !== 0) && i.push({ code: h, length: a, offset: l }); } const s = {}; for (const r of i) try { t.setPosition(r.offset); const h = t.readBytes(r.length); h.length === r.length && (s[r.code] = h); } catch { console.warn(`Failed to read bigfont data for code ${r.code}`); } const o = { data: s, info: "", baseUp: 8, baseDown: 2, height: O, width: O, orientation: "horizontal", isExtended: !1 }; if (0 in s) { const r = s[0]; try { const h = this.utf8ArrayToStr(r); if (h.pos >= 0) { let a = h.text; for (; a.length > 0 && a.charCodeAt(a.length - 1) === 0; ) a = a.slice(0, -1); o.info = a; const l = this.parseBigfontMetrics(r, h.pos + 1); l && Object.assign(o, l); } } catch { console.warn("Failed to parse bigfont info block"); } } return o; } catch (e) { const n = e instanceof Error ? e.message : String(e); throw new Error(`Failed to parse big font: ${n}`); } } parseBigfontMetrics(t, e) { let n = e; for (; n < t.length && t[n] === 0; ) n++; const i = t.length - n; if (i <= 0) return null; const s = (o) => o === 0 ? "horizontal" : "vertical"; if (i >= 5) { const o = t[n++]; n++; const r = s(t[n++]), h = t[n++]; return { baseUp: o, baseDown: 0, height: o, width: h, orientation: r, isExtended: !0 }; } if (i === 4 && t[n + 1] === 0 && t[n + 3] > 0 && t[n + 3] !== t[n]) { const o = t[n++]; n++; const r = s(t[n++]), h = t[n]; return { baseUp: o, baseDown: 0, height: o, width: h, orientation: r, isExtended: !0 }; } if (i === 4) { const o = t[n++], r = t[n++], h = s(t[n++]); return { baseUp: o, baseDown: r, height: o + r, width: o + r, orientation: h, isExtended: !1 }; } if (i === 3) { const o = t[n++], r = t[n++], h = s(r), a = r === 2; return { baseUp: o, baseDown: 0, height: o, width: o, orientation: h, // Dual-orientation vertical bigfonts (modes=2) use composite bytecode. isExtended: a, verticalDualMode: a }; } return null; } utf8ArrayToStr(t) { let e = "", n = 0; for (; n < t.length; ) { const i = t[n]; switch (i >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: e += String.fromCharCode(i); break; case 12: case 13: { const s = t[n++]; e += String.fromCharCode((i & 31) << 6 | s & 63); break; } case 14: { const s = t[n++], o = t[n++]; e += String.fromCharCode( (i & 15) << 12 | (s & 63) << 6 | (o & 63) << 0 ); break; } } if (e.charCodeAt(e.length - 1) === 0) break; n++; } return { text: e, pos: n }; } } class Z { parse(t) { try { const e = t.readInt32(); if (e <= 0) throw new Error("Invalid character count in font file"); const n = t.readInt16(), i = t.readBytes(n), s = { data: {}, info: "", baseUp: 8, baseDown: 2, height: O, width: O, orientation: "horizontal", isExtended: !1 }; try { const h = new TextDecoder().decode(i); let a = h.indexOf("\0"); a >= 0 && (s.info = h.substring(0, a), a + 3 < i.length && (s.baseUp = i[a + 1], s.baseDown = i[a + 2], s.height = s.baseUp + s.baseDown, s.width = s.height, U(s, i[a + 3]))); } catch { console.warn("Failed to parse unifont info block"); } const o = {}, r = {}; for (let h = 0; h < e - 1; h++) try { const a = t.readUint16(), l = t.readUint16(); if (l > 0) { const u = t.readBytes(l); if (u.length === l) { const { name: p, bytecode: g } = H(u); g.length > 0 && (o[a] = g, p && (r[p] = a)); } } } catch { console.warn("Failed to read unifont character data"); break; } return s.data = o, s.names = Object.keys(r).length > 0 ? r : void 0, s.codeToName = Object.keys(r).length > 0 ? $(r) : void 0, s; } catch (e) { const n = e instanceof Error ? e.message : String(e); throw new Error(`Failed to parse unifont: ${n}`); } } } class J { static createParser(t) { switch (t) { case d.SHAPES: return new K(); case d.BIGFONT: return new q(); case d.UNIFONT: return new Z(); default: throw new Error(`Unsupported font type: ${t}`); } } } const Q = 1e-6, D = 0.2; class L { /** * Whether the aligned glyph should store the resolved advance as explicit. * When false, {@link ShxShape.hasExplicitAdvance} is preserved from the source glyph. */ markAlignedAdvanceExplicit(t) { return !1; } } class mt extends L { resolve(t, e) { var s; const n = ((s = t.lastPoint) == null ? void 0 : s.x) ?? 0, i = t.polylines.some((o) => o.length >= 2); return t.hasExplicitAdvance || !i && Math.abs(n) > Q ? n : e; } } class M extends L { constructor(t = D) { super(), this.cellWidthFactor = t; } /** * True when ink extends left of the glyph origin (UNIFONT center-cell encoding). */ static isCenterOriginGlyph(t) { return t.bbox.minX < -1e-6; } /** * Resolves ink-based advance for a glyph at a scaled cell width. * * Left-origin glyphs (`minX >= 0`): `maxX + cellWidth * factor`. * Center-origin glyphs (`minX < 0`): advance to the right cell edge * (`max(maxX, cellWidth / 2)`) plus padding, so narrow centered punctuation * keeps trailing whitespace instead of colliding with the next glyph. */ static computeAdvance(t, e, n = D) { if (!t.polylines.some((r) => r.length >= 2)) return e * n; const s = e * n, { maxX: o } = t.bbox; return M.isCenterOriginGlyph(t) ? Math.max(o, e / 2) + s : o + s; } resolve(t, e) { var n; return t.hasExplicitAdvance ? ((n = t.lastPoint) == null ? void 0 : n.x) ?? 0 : M.computeAdvance(t, e, this.cellWidthFactor); } markAlignedAdvanceExplicit(t) { return !0; } } const E = new M(); class f { /** * Creates a new Point instance. * @param x - The x-coordinate (defaults to 0) * @param y - The y-coordinate (defaults to 0) */ constructor(t = 0, e = 0) { this.x = t, this.y = e; } /** * Sets the coordinates of the point. * @param x - The new x-coordinate * @param y - The new y-coordinate * @returns The point instance for method chaining */ set(t, e) { return this.x = t, this.y = e, this; } /** * Calculates the length (magnitude) of the vector from origin to this point. * @returns The length of the vector */ length() { return Math.sqrt(this.x * this.x + this.y * this.y); } /** * Normalizes the point vector to have a length of 1. * @returns The point instance for method chaining */ normalize() { const t = this.length(); return t !== 0 && (this.x /= t, this.y /= t), this; } /** * Creates a new Point instance with the same coordinates. * @returns A new Point instance with the same x and y values */ clone() { return new f(this.x, this.y); } /** * Adds another point's coordinates to this point. * @param point - The point to add * @returns The point instance for method chaining */ add(t) { return this.x += t.x, this.y += t.y, this; } /** * Subtracts another point's coordinates from this point. * @param point - The point to subtract * @returns The point instance for method chaining */ subtract(t) { return this.x -= t.x, this.y -= t.y, this; } /** * Multiplies both coordinates by a scalar value. * @param scalar - The scalar value to multiply by * @returns The point instance for method chaining */ multiply(t) { return this.x *= t, this.y *= t, this; } /** * Divides both coordinates by a scalar value. * @param scalar - The scalar value to divide by * @returns The point instance for method chaining */ divide(t) { return t !== 0 && (this.x /= t, this.y /= t), this; } /** * Multiplies x and y coordinates by different scalar values. * @param xScalar - The scalar value to multiply x-coordinate by * @param yScalar - The scalar value to multiply y-coordinate by * @returns The point instance for method chaining */ multiplyScalars(t, e) { return this.x *= t, this.y *= e, this; } /** * Divides x and y coordinates by different scalar values. * @param xScalar - The scalar value to divide x-coordinate by * @param yScalar - The scalar value to divide y-coordinate by * @returns The point instance for method chaining */ divideScalars(t, e) { return t !== 0 && (this.x /= t), e !== 0 && (this.y /= e), this; } /** * Calculates the Euclidean distance to another point. * @param point - The point to calculate distance to * @returns The distance between the two points */ distanceTo(t) { const e = this.x - t.x, n = this.y - t.y; return Math.sqrt(e * e + n * n); } } function _(c) { const { width: t, capHeight: e, descenderHeight: n, origin: i = "baseline" } = c, s = e + n; return i === "top" ? { minX: -t / 2, maxX: t / 2, minY: -s, maxY: 0 } : { minX: 0, maxX: t, minY: -n, maxY: e }; } function tt(c) { return [c.minX, c.minY, c.maxX, c.maxY].every(Number.isFinite); } function et(c, t, e = 1e-6) { return t.minX >= c.minX - e && t.maxX <= c.maxX + e && t.minY >= c.minY - e && t.maxY <= c.maxY + e; } function nt(c, t) { return { minX: Math.min(c.minX, t.minX), minY: Math.min(c.minY, t.minY), maxX: Math.max(c.maxX, t.maxX), maxY: Math.max(c.maxY, t.maxY) }; } function it(c, t, e) { const n = c.maxX - c.minX, i = c.maxY - c.minY; return `<rect x="${c.minX}" y="${-c.maxY}" width="${n}" height="${i}" fill="none" stroke="${t}" stroke-width="${e}"/>`; } function F(c, t) { const e = c.maxX - c.minX, n = c.maxY - c.minY; return `<rect x="${c.minX}" y="${-c.maxY}" width="${e}" height="${n}" fill="${t}"/>`; } function st(c, t, e, n, i, s, o) { const r = ` stroke-dasharray="${o}"`; return `<line x1="${c}" y1="${-t}" x2="${e}" y2="${-n}" stroke="${i}" stroke-width="${s}"${r}/>`; } function ot(c, t, e) { const { capHeight: n, descenderHeight: i, origin: s = "baseline" } = c, o = n + i, r = _(c); let h, a, l; s === "top" ? (n > 0 && (h = { minX: r.minX, maxX: r.maxX, minY: -n, maxY: 0 }), i > 0 && (a = { minX: r.minX, maxX: r.maxX, minY: -o, maxY: -n }), l = -n) : (n > 0 && (h = { minX: r.minX, maxX: r.maxX, minY: 0, maxY: n }), i > 0 && (a = { minX: r.minX, maxX: r.maxX, minY: -i, maxY: 0 }), l = 0); const g = [ h ? F(h, "rgba(255, 0, 0, 0.06)") : "", a ? F(a, "rgba(255, 0, 0, 0.14)") : "", it(r, t, e) ]; return l !== void 0 && i > 0 && n > 0 && g.push( st( r.minX, l, r.maxX, l, t, e, "4 2" ) ), `<g>${g.join("")}</g>`; } class v { constructor(t, e = [], n = !1) { this.lastPoint = t, this.polylines = e, this.hasExplicitAdvance = n; } /** * Get the bounding box of the shape * @returns Bounding box of the shape */ get bbox() { if (this._bbox) return this._bbox; let t = 1 / 0, e = -1 / 0, n = 1 / 0, i = -1 / 0; return this.polylines.forEach((s) => { s.forEach((o) => { t = Math.min(t, o.x), e = Math.max(e, o.x), n = Math.min(n, o.y), i = Math.max(i, o.y); }); }), this._bbox = { minX: t, minY: n, maxX: e, maxY: i }, this._bbox; } /** * Offset the shape by a point * @param p The point to offset the shape by * @param isNewInstance Whether to return a new instance of the shape or modify the current instance * @returns The offset shape */ offset(t, e = !0) { var n, i; return e ? new v( (n = this.lastPoint) == null ? void 0 : n.clone().add(t), this.polylines.map((s) => s.map((o) => o.clone().add(t))), this.hasExplicitAdvance ) : ((i = this.lastPoint) == null || i.add(t), this.polylines.forEach((s) => s.forEach((o) => o.add(t))), this._bbox && (this._bbox.maxX += t.x, this._bbox.minX += t.x, this._bbox.maxY += t.y, this._bbox.minY += t.y), this); } /** * Normalizes a shape so that its bounding box’s bottom-left corner moves to the origin (0,0). * It doesn’t change the size or orientation, only repositions the shape. * @param isNewInstance Whether to return a new instance of the shape or modify the current instance * @returns The offset shape */ normalizeToOrigin(t = !1) { const e = this.bbox; return this.offset(new f(-e.minX, -e.minY), t); } /** * Converts the shape to an SVG string * @param options SVG rendering options * @returns SVG string */ toSVG(t = {}) { const { strokeWidth: e = "0.5%", strokeColor: n = "black", isAutoFit: i = !1, fontCell: s } = t; let o, r; const h = (a) => this.polylines.map((l) => { let u = ""; return l.forEach((p, g) => { const { x: y, y: b } = a(p); u += g === 0 ? `M ${y} ${b} ` : `L ${y} ${b} `; }), `<path d="${u}" stroke="${n}" stroke-width="${e}" fill="none"/>`; }).join(""); if (s) { const { padding: a = 0.1, expandToFit: l = !1, showFrame: u = !1, frameColor: p = "red", frameStrokeWidth: g = "0.5%" } = s, y = _(s), b = this.bbox; let m = y; l && tt(b) && !et(y, b) && (m = nt(y, b)); const S = m.maxX - m.minX, x = m.maxY - m.minY, A = S * a, P = x * a, k = m.minX - A, R = m.maxX + A, W = m.minY - P, I = m.maxY + P; r = h((C) => ({ x: C.x, y: -C.y })); const V = u ? ot(s, p, g) : ""; return o = `${k} ${-I} ${R - k} ${I - W}`, `<svg width="100%" height="100%" viewBox="${o}" preserveAspectRatio="xMidYMid meet">${V}${r}</svg>`; } else if (i) { const a = this.bbox, l = 0.2, u = a.maxX - a.minX, p = a.maxY - a.minY, g = u === 0 ? p : u, y = p === 0 ? u : p, b = a.minX - g * l, m = a.maxX + g * l, S = a.minY - y * l, x = a.maxY + y * l; r = h((A) => ({ x: A.x, y: -A.y })), o = `${b} ${-x} ${m - b} ${x - S}`; } else o = "0 0 20 20", r = h((a) => ({ x: a.x + 5, y: -a.y + 15 })); return `<svg width="100%" height="100%" viewBox="${o}" preserveAspectRatio="xMidYMid meet">${r}</svg>`; } } const N = 8, X = 48, rt = 0.4, at = [ 52164, 45795, 49829, 50150, 54992, 47610, 54754, 46532, 51906, 53947, 49332, 51706, 46532, 54224, 52946, 52714, 52219, 45755, 51403, 46532, 53947, 50410, 49332, 52219, 52141, 46532, 50150, 51120, 50150, 54992, 51663, 50119, 53186, 46532, 46525, 51365, 52149, 47016, 46532, 51889, 51706, 46025, 47037, 55031, 48122, 50935, 47531, 48122, 51965, 55e3 ]; function T(c, t) { const { height: e, width: n, baseUp: i, baseDown: s } = c, o = e > 0 ? t / e : 1, r = o * i, h = o * s; return { size: t, capHeight: r, descenderHeight: h, cellWidth: o * n, totalHeight: r + h }; } function ct(c, t, e) { if (c.header.fontType !== d.SHAPES || !(0 in c.content.data)) return !1; const n = -(e.descenderHeight + e.capHeight * 0.2); return t.bbox.minY < n; } function G(c, t) { const e = -(t.descenderHeight + t.capHeight * 0.05); return !(c.bbox.minY < e || c.bbox.maxY - c.bbox.minY < t.capHeight * 0.05); } function ht(c, t, e) { if (c.header.fontType !== d.BIGFONT || c.content.baseDown > 0) return 0; const { height: n } = c.content; if (n <= 0) return 0; const i = n * rt, s = [], o = /* @__PURE__ */ new Set(), r = (a) => { if (o.has(a) || a <= 255 || !(a in c.content.data)) return; o.add(a); const l = t(a); if (!l) return; const u = l.bbox.minY; u > 0 && u <= i && s.push(u); }; for (const a of at) if (r(a), s.length >= X) break; if (s.length < N) { for (const a of Object.keys(c.content.data)) if (r(Number(a)), s.length >= X) break; } if (s.length < N) return 0; s.sort((a, l) => a - l); const h = Math.floor(s.length / 2); return s.length % 2 === 0 ? (s[h - 1] + s[h]) / 2 : s[h]; } function lt(c, t, e) { if (c.header.fontType !== d.UNIFONT) return !1; if (c.content.dualOrientation) return !0; const n = T(c.content, e); for (const i of [48, 65, 78, 49]) { if (!(i in c.content.data)) continue; const s = t(i); if (s && G(s, n)) return !0; } return !1; } function ut(c, t, e, n = E, i = !1, s = 0) { var a; let o = c; if (t.header.fontType === d.BIGFONT && s > 0) { const l = t.content.height > 0 ? e.size / t.content.height : 1; o = o.offset(new f(0, -s * l), !0); } if (t.header.fontType === d.UNIFONT || ct(t, c, e)) { const l = t.header.fontType === d.UNIFONT && (i || t.content.dualOrientation || G(c, e)); (t.header.fontType === d.UNIFONT ? !l : !0) && (o = o.offset(new f(0, e.capHeight), !0)); } const r = n.resolve(o, e.cellWidth), h = n.markAlignedAdvanceExplicit(o) ? !0 : o.hasExplicitAdvance; return new v( new f(r, ((a = o.lastPoint) == null ? void 0 : a.y) ?? 0), o.polylines, h ); } function dt(c, t, e, n = E, i = !1, s = 0) { const o = T(t.content, e); return ut( c, t, o, n, i, s ); } const Y = Math.PI / 4; class B { /** * Creates a bulge-defined arc * @param start Start point * @param end End point * @param bulge Bulge factor (-1 to 1, where 1 is a semicircle) */ static fromBulge(t, e, n) { const i = Math.max(-1, Math.min(1, n)); return new B({ start: t, end: e, bulge: i }); } /** * Creates an octant-defined arc * @param center Center point of the arc * @param radius Radius of the arc * @param startOctant Starting octant (0-7) * @param octantCount Number of octants to span (0-8, where 0 means 8 octants) * @param isClockwise Whether the arc goes clockwise */ static fromOctant(t, e, n, i, s) { return new B({ center: t, radius: e, startOctant: n, octantCount: i, 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), n = e.length(); if (Math.abs(this.bulge) * n / 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 = n / (2 * Math.sin(s / 2)); const o = this.start.clone().add(e.clone().divide(2)), r = new f(-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 * Y; const e = (t.octantCount === 0 ? 8 : t.octantCount) * Y; this.endAngle = this.startAngle + (this.isClockwise ? -e : e), this.start = this.center.clone().add( new f( this.radius * Math.cos(this.startAngle), this.radius * Math.sin(this.startAngle) ) ), this.end = this.center.clone().add( new f(this.radius * Math.cos(this.endAngle), this.radius * Math.sin(this.endAngle)) ); } else throw new Error("Invalid arc parameters"); } /** * Tessellates the arc into a series of points that approximate the arc. * @param circleSpan The angle span between tessellated points (default Math.PI / 18) * @returns Array of points representing the tessellated arc */ tessellate(t = Math.PI / 18) { if (this.radius === 0) return [this.start.clone(), this.end.clone()]; const e = [this.start.clone()], n = Math.abs(this.endAngle - this.startAngle), i = Math.max(1, Math.floor(n / t)); for (let s = 1; s < i; s++) { const o = s / i, r = this.isClockwise ? this.startAngle - o * n : this.startAngle + o * n; e.push( this.center.clone().add(new f(this.radius * Math.cos(r), this.radius * Math.sin(r))) ); } return e.push( this.end ? this.end.clone() : this.center.clone().add( new f( this.radius * Math.cos(this.endAngle), this.radius * Math.sin(this.endAngle) ) ) ), e; } } const ft = Math.PI / 18, gt = 1e-6; class pt { constructor(t) { this.shapeCache = /* @__PURE__ */ new Map(), this.subshapeCache = /* @__PURE__ */ new Map(), this.shapeData = /* @__PURE__ */ new Map(), this.fontData = t; } /** * Releases parsed shapes and cached shapes */ release() { this.shapeCache.clear(), this.subshapeCache.clear(), this.shapeData.clear(); } /** * Parses a character's shape with the given font size. * @param code - The character code * @param size - The font size * @returns The parsed shape or undefined if the character is not found */ getCharShape(t, e) { const i = this.fontData.header.fontType === d.SHAPES && !(0 in this.fontData.content.data) ? e : e / this.fontData.content.height; return this.parseAndScale(t, { factor: i }); } /** * Parses a character's shape with scaling options * @param code - The character code * @param options - Scaling options (factor or height/width) * @returns The parsed shape or undefined if the character is not found */ parseAndScale(t, e) { if (t === 0) return; let n; if (this.shapeCache.has(t)) n = this.shapeCache.get(t); else { const i = this.fontData.content.data; if (i[t]) { const s = this.prepareBigfontGlyphBytecode(t, i[t]), o = this.fontData.header.fontType !== d.BIGFONT; n = this.parseShape(s, { flushOnEnd: !0, initialPenDown: o }), this.shapeData.set(t, n), this.shapeCache.set(t, n); } } if (n) { if (e.factor !== void 0) return this.scaleShapeByFactor(n, e.factor); if (e.height !== void 0) { const i = e.width ?? e.height; return this.scaleShapeByHeightAndWidth(n, e.height, i); } else return n; } } /** Strips the embedded character-code prefix from dual-byte BIGFONT parent glyphs. */ prepareBigfontGlyphBytecode(t, e) { if (this.fontData.header.fontType !== d.BIGFONT || t <= 255) return e; const n = t >> 8 & 255, i = t & 255; if (e.length >= 2 && e[0] === n && e[1] === i) { let s = 2; return e[s] === 0 && s++, e.slice(s); } return e; } /** * Dual-orientation vertical BIGFONT files (e.g. gbcbig.shx) use 0x8e/0x8f markers on * code 7 instead of real subshapes. Only the marker byte is skipped; following setup * commands (push / pen up / xy origin) are executed normally. */ isVerticalDualBigfontMarker(t, e, n) { return this.fontData.content.verticalDualMode ? n === "open" ? t[e] === 142 : t[e] === 143 : !1; } /** * Scales a shape according to the given scale factor * @param shape - The shape to scale * @param factor - The scale factor * @returns The scaled shape */ scaleShapeByFactor(t, e) { var n; return new v( (n = t.lastPoint) == null ? void 0 : n.clone().multiply(e), t.polylines.map((i) => i.map((s) => s.clone().multiply(e))), t.hasExplicitAdvance ); } /** * Scales a shape according to the given height and width * @param shape - The shape to scale * @param height - The target height * @param width - The target width * @returns The scaled shape */ scaleShapeByHeightAndWidth(t, e, n) { var u; const i = t.bbox, s = i.maxY - i.minY, o = i.maxX - i.minX, r = s > 0 ? e / s : 1, h = o > 0 ? n / o : 1, a = (u = t.lastPoint) == null ? void 0 : u.clone(); a && (a.x *= h, a.y *= r); const l = t.polylines.map( (p) => p.map((g) => { const y = g.clone(); return y.x *= h, y.y *= r, y; }) ); return new v(a, l, t.hasExplicitAdvance); } /** * Whether code 14 (0x0E) should skip the following command for the current layout. * Defaults to horizontal text layout when no orientation is supplied. */ shouldSkipVerticalFlagCommand(t = !1) { const { content: e, header: n } = this.fontData; return n.fontType === d.BIGFONT ? !e.verticalDualMode : e.dualOrientation ? !t : e.orientation === "horizontal"; } /** Marks that bytecode explicitly defines horizontal advance. */ markAdvanceDefined(t) { t.hasExplicitAdvance = !0; } /** Records a terminal pen-up XY move (codes 8/9). */ notePenUpPositioning(t) { t.pendingTerminalAdvance = !0; } /** Clears a pending advance when later bytecode supersedes the prior XY move. */ clearPendingAdvance(t) { t.pendingTerminalAdvance = !1; } stateHasInk(t) { return t.currentPolyline.length > 1 ? !0 : t.polylines.some((e) => e.length >= 2); } /** * Confirms terminal pen-up XY (codes 8/9) as advance definition. * Ignores closure moves that return to the origin after drawing (e.g. txt `A`). */ finalizeAdvanceFlag(t) { if (!t.pendingTerminalAdvance) return; const e = t.currentPoint.x; if (Math.abs(e) > gt) { t.hasExplicitAdvance = !0; return; } this.stateHasInk(t) || (t.hasExplicitAdvance = !0); } /** * Parses the shape of a character. * @param data - The data of the character * @param options - Optional parse settings * @returns The parsed shape */ parseShape(t, e = {}) { let n = new f(); const i = []; let s = []; const o = []; let r = e.initialPenDown ?? !1; r && s.push(n.clone()); const h = { currentPoint: n, polylines: i, currentPolyline: s, sp: o, isPenDown: r, scale: 1, // Top-level glyphs flush trailing pen-down strokes at 0x00. Subshape // primitives and inherited-pen unifont subshapes (amgdt %%132) opt in // via flushOnEnd; bigfont subshape cache keeps flush off to avoid regressions. flushEndPolyline: e.flushOnEnd ?? !1, hasExplicitAdvance: !1, pendingTerminalAdvance: !1 }; for (let a = 0; a < t.length; a++) { const l = t[a]; l <= 15 ? a = this.handleSpecialCommand(l, t, a, h) : (this.clearPendingAdvance(h), this.handleVectorCommand(l, h)); } return this.finalizeAdvanceFlag(h), this.buildShapeFromState(h); } /** Builds a shape result, including any trailing pen-down polyline. */ buildShapeFromState(t) { const e = t.polylines.map((n) => n.map((i) => i.clone())); return t.currentPolyline.length > 1 && e.push(t.currentPolyline.map((n) => n.clone())), new v(t.currentPoint.clone(), e, t.hasExplicitAdvance); } /** * Please refer to special codes reference in the following link for more information. * https://help.autodesk.com/view/OARX/2023/ENU/?guid=GUID-06832147-16BE-4A66-A6D0-3ADF98DC8228 * @param command - The command byte * @param data - The data of the character * @param index - The index of the command byte * @param state - The state of the parser * @returns The index of the next command byte */ handleSpecialCommand(t, e, n, i) { let s = n; switch (t) { case 0: this.finalizeAdvanceFlag(i), i.flushEndPolyline && i.currentPolyline.length > 1 ? (i.polylines.push(i.currentPolyline.slice()), i.currentPolyline = []) : i.flushEndPolyline && (i.currentPolyline = []), i.isPenDown = !1; break; case 1: this.clearPendingAdvance(i), i.isPenDown || i.currentPolyline.push(i.currentPoint.clone()), i.isPenDown = !0; break; case 2: i.isPenDown = !1, i.currentPolyline.length > 1 && i.polylines.push(i.currentPolyline.slice()), i.currentPolyline = []; break; case 3: this.clearPendingAdvance(i), s++, i.scale /= e[s]; break; case 4: this.clearPendingAdvance(i), s++, i.scale *= e[s]; break; case 5: if (this.clearPendingAdvance(i), i.sp.length === 4) throw new Error("The position stack is only four locations deep"); i.sp.push(i.currentPoint.clone()); break; case 6: this.clearPendingAdvance(i), i.currentPoint = i.sp.pop() ?? i.currentPoint, i.currentPolyline.length > 1 && (i.polylines.push(i.currentPolyline.slice()), i.currentPolyline = []), i.isPenDown && i.currentPolyline.push(i.currentPoint.clone()); break; case 7: this.clearPendingAdvance(i), s = this.handleSubshapeCommand(e, s, i); break; case 8: s = this.handleXYDisplacement(e, s, i); break; case 9: s = this.handleMultipleXYDisplacements(e, s, i); break; case 10: this.clearPendingAdvance(i), s = this.handleOctantArc(e, s, i); break; case 11: this.clearPendingAdvance(i), s = this.handleFractionalArc(e, s, i); break; case 12: this.clearPendingAdvance(i), s = this.handleBulgeArc(e, s, i); break; case 13: this.clearPendingAdvance(i), s = this.handleMultipleBulgeArcs(e, s, i); break; case 14: this.clearPendingAdvance(i), this.shouldSkipVerticalFlagCommand() && (s = this.skipCode(e, ++s)); break; } return s; } handleVectorCommand(t, e) { const n = (t & 240) >> 4, i = t & 15, s = this.getVectorForDirection(i); e.currentPoint.add(s.multiply(n * e.scale)), e.isPenDown && e.currentPolyline.push(e.currentPoint.clone()); } /** * Get the vector for the given direction code. Please refer to the following link for more information. * https://help.autodesk.com/view/OARX/2023/ENU/?guid=GUID-0A8E12A1-F4AB-44AD-8A9B-2140E0D5FD23 * @param dir - The direction code of the vector * @returns Returns the vector for the given direction code */ getVectorForDirection(t) { const e = new f(); switch (t) { case 0: e.x = 1; break; case 1: e.x = 1, e.y = 0.5; break; case 2: e.x = 1, e.y = 1; break; case 3: e.x = 0.5, e.y = 1; break; case 4: e.y = 1; break; case 5: e.x = -0.5, e.y = 1; break; case 6: e.x = -1, e.y = 1; break; case 7: e.x = -1, e.y = 0.5; break; case 8: e.x = -1; break; case 9: e.x = -1, e.y = -0.5; break; case 10: e.x = -1, e.y = -1; break; case 11: e.x = -0.5, e.y = -1; break; case 12: e.y = -1; break; case 13: e.x = 0.5, e.y = -1; break; case 14: e.x = 1, e.y = -1; break; case 15: e.x = 1, e.y = -0.5; break; } return e; } handleSubshapeCommand(t, e, n) { let i = e, s = 0, o, r = n.scale * this.fontData.content.baseUp, h = r; const a = n.currentPoint.clone(); switch (n.currentPolyline.length > 1 && (n.polylines.push(n.currentPolyline.slice()), n.currentPolyline = []), this.fontData.header.fontType) { case d.SHAPES: i++, s = t[i]; break; case d.BIGFONT: if (i++, this.isVerticalDualBigfontMarker(t, i, "open")) return i; if (this.isVerticalDualBigfontMarker(t, i, "close")) return i + 1; s = t[i], s === 0 && (i++, s = t[i++] << 8 | t[i++], a.x = w.byteToSByte(t[i++]) * n.scale, a.y = w.byteToSByte(t[i++]) * n.scale, this.fontData.content.isExtended ? (h = t[i++] * n.scale, r = t[i] * n.scale) : (r = t[i] * n.scale, h = r)); break; case d.UNIFONT: i++, s = t[i++] << 8 | t[i++], i--; break; } if (s !== 0) if (this.fontData.header.fontType === d.UNIFONT) { const l = n.isPenDown; o = this.getScaledSubshapeAtInsertPoint( s, h, r, a, l ), o != null && o.polylines.some((u) => u.length >= 2) && (n.polylines.push(...o.polylines.slice()), o.lastPoint && (n.currentPoint = o.lastPoint.clone()), n.currentPolyline = [], n.isPenDown && n.currentPolyline.push(n.currentPoint.clone())); } else if (this.fontData.header.fontType === d.SHAPES) o = this.getScaledSubshapeAtInsertPoint( s, h, r, a ), o && (n.polylines.push(...o.polylines.slice()), o.lastPoint && (n.currentPoint = o.lastPoint.clone(), o.hasExplicitAdvance && this.markAdvanceDefined(n))), n.currentPolyline = [], n.isPenDown && n.currentPolyline.push(n.currentPoint.clone()); else { if (o = this.getScaledSubshapeAtInsertPoint( s, h, r, a ), o && (n.polylines.push(...o.polylines.slice()), s === 2 && o.lastPoint)) { const l = o.lastPoint.x - a.x; l > n.currentPoint.x && (n.currentPoint.x = l), this.markAdvanceDefined(n); } n.currentPolyline = []; } return i; } handleXYDisplacement(t, e, n) { let i = e; const s = new f(); return s.x = w.byteToSByte(t[++i]), s.y = w.byteToSByte(t[++i]), n.currentPoint.add(s.multiply(n.scale)), n.isPenDown ? n.currentPolyline.push(n.currentPoint.clone()) : this.notePenUpPositioning(n), i; } handleMultipleXYDisplacements(t, e, n) { let i = e; for (; !(i + 1 >= t.length); ) { const s = new f(); if (s.x = w.byteToSByte(t[++i]), s.y = w.byteToSByte(t[++i]), s.x === 0 && s.y === 0) break; n.currentPoint.add(s.multiply(n.scale)), n.isPenDown ? n.currentPolyline.push(n.currentPoint.clone()) : this.notePenUpPositioning(n); } return i; } handleOctantArc(t, e, n) { let i = e; const s = t[++i] * n.scale, o = w.byteToSByte(t[++i]), r = (o & 112) >> 4; let h = o & 7; const a = o < 0, l = Math.PI / 4 * r, u = n.currentPoint.clone().subtract(new f(Math.cos(l) * s, Math.sin(l) * s)), g = B.fromOctant(u, s, r, h, a).tessellate(); return n.isPenDown && (n.currentPolyline.pop(), n.currentPolyline.push(...g.slice())), n.currentPoint = g[g.length - 1].clone(), i; } handleFractionalArc(t, e, n) { let i = e; const s = t[++i], o = t[++i], r = t[++i], h = t[++i], a = (r * 255 + h) * n.scale, l = w.byteToSByte(t[++i]), u = (l & 112) >> 4; let p = l & 7; p === 0 && (p = 8), o !== 0 && p--; const g = Math.PI / 4; let y = g * p, b = ft, m = 1; l < 0 && (b = -b, y = -y, m = -1); let S = g * u, x = S + y; S += g * s / 256 * m, x += g * o / 256 * m; const A = n.currentPoint.clone().subtract(new f(a * Math.cos(S), a * Math.sin(S))); if (n.currentPoint = A.clone().add(new f(a * Math.cos(x), a * Math.sin(x))), n.isPenDown) { let P = S; const k = []; if (k.push( A.clone().add(new f(a * Math.cos(P), a * Math.sin(P))) ), b > 0) for (; P + b < x; ) P += b, k.push( A.clone().add(new f(a * Math.cos(P), a * Math.sin(P))) ); else for (; P + b > x; ) P += b, k.push( A.clone().add(new f(a * Math.cos(P), a * Math.sin(P))) ); k.push(A.clone().add(new f(a * Math.cos(x), a * Math.sin(x)))), n.currentPolyline.push(...k); } return i; } handleBulgeArc(t, e, n) { let i = e; const s = new f(); s.x = w.byteToSByte(t[++i]), s.y = w.byteToSByte(t[++i]); const o = w.byteToSByte(t[++i]); return n.currentPoint = this.handleArcSegment( n.currentPoint, s, o, n.scale, n.isPenDown, n.currentPolyline ), i; } handleMultipleBulgeArcs(t, e, n) { let i = e; for (; !(i + 1 >= t.length); ) { const s = new f(); if (s.x = w.byteToSByte(t[++i]), s.y = w.byteToSByte(t[++i]), s.x === 0 && s.y === 0 || i + 1 >= t.length) break; const o = w.byteToSByte(t[++i]); n.currentPoint = this.handleArcSegment( n.currentPoint, s, o, n.scale, n.isPenDown, n.currentPolyline ); } return i; } 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++, this.isVerticalDualBigfontMarker(t, e, "open") ? e++ : this.isVerticalDualBigfontMarker(t, e, "close") ? e += 2 : t[e] === 0 && (e += this.fontData.content.isExtended ? 6 : 5); break; case d.UNIFONT: e += 2; break; } break; case 8: e += 2; break; case 9: for (; e++, !(e >= t.length); ) { const s = t[e]; if (e++, e >= t.length) break; const 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 (; e++, !(e >= t.length); ) { const s = t[e]; if (e++, e >= t.length) break; const o = t[e]; if (s === 0 && o === 0) break; e++; } break; } return e; } getScaledSubshapeAtInsertPoint(t, e, n, i, s = !1) { let o; if (s) { const l = this.fontData.content.data[t]; if (!l) return; o = this.parseShape(l, { initialPenDown: !0, flushOnEnd: !0 }); } else if (o = this.subshapeCache.get(t), !o) { const l = this.fontData.content.data[t]; if (!l) return; const u = this.fontData.header.fontType !== d.BIGFONT; o = this.parseShape(l, { flushOnEnd: !1, initialPenDown: u }), this.shapeData.set(t, o), this.subshapeCache.set(t, o); } const r = this.fontData.header.fontType === d.BIGFONT ? void 0 : n / this.fontData.content.baseUp, h = o.polylines.some((l) => l.length > 0); return (r !== void 0 ? this.scaleShapeByFactor(o, r) : this.scaleShapeByHeightAndWidth( h ? o.normalizeToOrigin(!0) : o, n, e )).offset(i, !1); } /** * Handles drawing an arc segment with the given vector and bulge * @param currentPoint The starting point of the arc * @param vec The displacement vector * @param bulge The bulge value (will be normalized by 127.0) * @param scale The current scale factor * @param isPenDown Whether the pen is currently down (drawing) * @param currentPolyline The current polyline being built * @returns The new current point after the arc */ handleArcSegment(t, e, n, i, s, o) { e.x *= i, e.y *= i, n < -127 && (n = -127); const r = t.clone(); if (s) if (n === 0) o.push(r.clone().add(e)); else { const h = r.clone().add(e), l = B.fromBulge(r, h, n / 127).tessellate(); o.push(...l.slice(1)); } return r.add(e), r; } } class wt { /** * Creates a new ShxFont instance. * @param data - Either raw binary data of the SHX font file (ArrayBuffer) or pre-parsed font data (ShxFontData) * @throws {Error} If the font data is invalid or cannot be parsed */ constructor(t) { if (t instanceof ArrayBuffer) { const e = new w(t), i = new z().parse(e), o = J.createParser(i.fontType).parse(e); this.fontData = { header: i, content: o }; } else this.fontData = t; this.shapeParser = new pt(this.fontData); } /** * Return true if this font contains glyph of the specified character. Otherwise, return false. * @param char - The character to check * @returns True if this font contains glyph of the specified character. Otherwise, return false. */ hasChar(t) { return this.fontData.content.data[t] !== void 0; } /