Skip to content

mlightcad/shx-parser

Repository files navigation

SHX Parser

License: MIT npm version

A TypeScript library for parsing AutoCAD SHX font files. It is ported from this project written by C#. This project fixed many bugs on the original parser. Moreover, support parsing extended big font.

If you are interested in the format of SHX font, please refer to this document.

🌐 Live Demo

Features

  • Parse SHX font files and extract font data
  • Support for various SHX font types:
    • Shapes
    • Bigfont (including Extended Big Font)
    • Unifont
  • Shape parsing with performance optimization:
    • On-demand parsing
    • Shape caching by character code
  • Modern TypeScript implementation
  • Object-oriented design
  • Comprehensive test coverage

Installation

Using npm:

npm install @mlightcad/shx-parser

Using pnpm:

pnpm add @mlightcad/shx-parser

Using yarn:

yarn add @mlightcad/shx-parser

Demo app

The demo app is provided with a web-based interface for viewing and exploring SHX font files with the following features:

  • Dual Loading Modes:

    • Upload local SHX files
    • Select from a remote font library
  • Main Features:

    • View all characters in a responsive grid layout
    • Search characters by code (decimal/hex)
    • Click characters to see them in a larger modal view
    • Toggle between decimal and hexadecimal code display
  • Display Information:

    • Shows font type, version, and character count
    • Renders characters as SVG graphics
    • Responsive grid layout that works on different screen sizes

Quick Start

import { ShxFont } from '@mlightcad/shx-parser';

// Load the font file
const fontFileData = /* ArrayBuffer containing SHX font file data */;
const font = new ShxFont(fontFileData);

// Get shape for a character
const charCode = 65; // ASCII code for 'A'
const fontSize = 12;
const shape = font.getCharShape(charCode, fontSize);

if (shape) {
  console.log(shape.polylines); // Array of polylines representing the character
  console.log(shape.lastPoint); // End point of the character
}

// Clean up resources when done
font.release();

API Documentation

ShxFont

The main class for working with SHX fonts.

class ShxFont {
  fontData: ShxFontData;
  constructor(data: ArrayBuffer);
  getCharShape(charCode: number, size: number): ShxShape | null;
  release(): void;
}

ShxShape

Represents a parsed character shape.

interface ShxShape {
  polylines: Array<{ x: number; y: number }[]>; // Array of polyline points
  lastPoint?: { x: number; y: number };         // End point of the shape (optional)
  bbox: {                                       // Bounding box of the shape
    minX: number;
    minY: number;
    maxX: number;
    maxY: number;
  };
}

Font Data Structure

The library parses SHX font files into the following structure:

interface ShxFontData {
  header: {
    fontType: ShxFontType;     // 'shapes' | 'bigfont' | 'unifont'
    fileHeader: string;        // Font file header information
    fileVersion: string;       // Font file version
  };
  content: {
    data: Record<number, Uint8Array>;  // Character code to bytecode data mapping
    info: string;              // Additional font information
    orientation: string;       // Text orientation ('horizontal' | 'vertical')
    height: number;            // Character height (units used to scale primitives)
    width: number;             // Character width (units used to scale primitives)
    isExtended: boolean;       // Flag to indicate if the font is an extended big font
  };
}

Example

Loading and Displaying Font Information

import { readFile } from 'fs/promises';
import { ShxFont } from '@mlightcad/shx-parser';

async function loadFont(filePath: string) {
  const buffer = await readFile(filePath);
  const font = new ShxFont(buffer.buffer);
  
  // Display font information
  const fontData = font.fontData;
  console.log('Font Information:');
  console.log('----------------');
  console.log('Font Type:', fontData.header.fontType);
  console.log('Header:', fontData.header.fileHeader);
  console.log('Version:', fontData.header.fileVersion);
  console.log('Info:', fontData.content.info);
  console.log('Orientation:', fontData.content.orientation);
  console.log('Height:', fontData.content.height);
  console.log('Width:', fontData.content.width);
  console.log('Number of shapes:', Object.keys(fontData.content.data).length);
  
  return font;
}

Converting Shape to SVG Path

function shapeToSvgPath(shape: ShxShape, x: number = 0, y: number = 0): string {
  if (!shape?.polylines.length) return '';
  
  return shape.polylines.map(polyline => {
    if (!Array.isArray(polyline) || polyline.length === 0) return '';
    
    return polyline.map((point, i) => {
      const scaledX = (Number(point.x) || 0) + x;
      const scaledY = -(Number(point.y) || 0) + y; // Flip Y coordinate for SVG
      const command = i === 0 ? 'M' : 'L';
      return `${command} ${scaledX.toFixed(2)} ${scaledY.toFixed(2)}`;
    }).join(' ');
  }).filter(Boolean).join(' ');
}

Rendering Text to SVG

interface SvgOptions {
  width?: number;
  height?: number;
  strokeWidth?: string;
  strokeColor?: string;
  isAutoFit?: boolean;
}

function renderTextToSvg(
  font: ShxFont,
  text: string,
  fontSize: number,
  options: SvgOptions = {}
): SVGElement {
  const {
    width = 1000,
    height = 1000,
    strokeWidth = '0.1%',
    strokeColor = 'black',
    isAutoFit = false
  } = options;

  // Create SVG element
  const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
  svg.setAttribute('width', width.toString());
  svg.setAttribute('height', height.toString());
  svg.setAttribute('viewBox', `0 0 ${width} ${height}`);

  const padding = fontSize;
  let currentX = padding;
  let maxHeight = 0;

  // Process each character
  for (const char of text) {
    const charCode = char.charCodeAt(0);
    const shape = font.getCharShape(charCode, fontSize);
    
    if (shape) {
      const group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
      
      if (isAutoFit) {
        // Auto-fit positioning
        const bbox = shape.bbox;
        const padding = 0.2; // 20% padding
        const charBBoxWidth = bbox.maxX - bbox.minX;
        const charBBoxHeight = bbox.maxY - bbox.minY;
        const centerX = (bbox.minX + bbox.maxX) / 2;
        const centerY = (bbox.minY + bbox.maxY) / 2;
        group.setAttribute('transform', `translate(${currentX - centerX}, ${-centerY})`);
      } else {
        // Fixed positioning
        group.setAttribute('transform', `translate(${currentX + width / 2}, ${height / 2})`);
      }

      // Create path for the character
      const pathData = shapeToSvgPath(shape);
      const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      path.setAttribute('d', pathData);
      path.setAttribute('fill', 'none');
      path.setAttribute('stroke', strokeColor);
      path.setAttribute('stroke-width', strokeWidth);
      
      group.appendChild(path);
      svg.appendChild(group);

      // Update position for next character
      if (shape.lastPoint) {
        currentX += shape.lastPoint.x + fontSize * 0.5;
      } else {
        currentX += fontSize;
      }
      
      maxHeight = Math.max(maxHeight, fontSize);
    }
  }

  return svg;
}

// Example usage:
async function main() {
  try {
    const font = await loadFont('path/to/your/font.shx');
    
    // Example 1: Basic rendering
    const svgElement1 = renderTextToSvg(font, "Hello", 12);
    document.body.appendChild(svgElement1);
    
    // Example 2: Auto-fit rendering with custom options
    const svgElement2 = renderTextToSvg(font, "Hello", 12, {
      width: 1000,
      height: 1000,
      strokeWidth: '0.1%',
      strokeColor: 'black',
      isAutoFit: true
    });
    document.body.appendChild(svgElement2);
    
    // Clean up resources when done
    font.release();
  } catch (error) {
    console.error('Error:', error instanceof Error ? error.message : 'An unknown error occurred');
  }
}

Contributing

Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

Please make sure to update tests as appropriate.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

If you have any questions or run into issues, please:

  1. Check the GitHub Issues page
  2. Open a new issue if your problem hasn't been reported yet

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •