SHX-файлы: чтение
Главная »Статьи »AutoCAD и Delphi »SHX-файлы: чтение
SHX-файлы: чтение

В данной статье будет рассмотрен формат файла SHX-файлов, его чтение и экспорт в текстовый формат SHP. Исключение составляет формат больших шрифтов (BigFont) — такие шрифты не рассматриваются.

Формат файла SHX

Описание формата файла SHX найти не удалось, поэтому были скомпилированы несколько тестовых файлов, которые затем проанализированы HEX-редактором.

Вначале расположен заголовок, который оканчивается последовательностью их 3-х байт: 0x0D, 0x0A, 0x1A. Затем:

  • для SHAPE-файла:
    1. следуют пары байт, определяющие: номер первой формы, номер последней формы, количество форм.
    2. далее следует список пар, определяющих: номер формы, количество байт (всего, включая имя).
    3. затем следуют байты описаний форм: сначала имя формы, оканчивающееся байтом 0, далее — непосредственно байты описания, также оканчивающиеся нулевым байтом.
    4. в конце файла расположены 3 байта — символы EOF.
  • для UNICODE-файла:
    1. пара байт — количество форм;
    2. далее описание форм: первые 2 байта — номер, следующие 2 байта — количество байт формы, затем следуют байты описаний форм: сначала имя формы, оканчивающееся байтом 0, далее — непосредственно байты описания, также оканчивающиеся нулевым байтом.

Чтение

Объявим тип файла:

type
  TShxFontKind = (fkShpFont, fkUniFont, fkBigFont, fkUnknown);

Объявим тип записи номера формы и количества байт:

type
  TShxShapeInfo = packed record
    ShapeNumber: Word;
    GlyphBytes: Word;
  end;

Создаем определение класса формы:

  TShxShape = class(TObject)
  private
    FFont: TShxFont;
    FNumber: Word;
    FBytesCount: Word;
    FName: String;
    FGlyphHighIndex: Word;
  protected
    FGlyph: array of Byte; // массив байт описания формы
    function GetGlyph(Index: Integer): Byte;
    procedure SetGlyph(Index: Integer; const Value: Byte);
    procedure SetGlyphLength(const Value: Word);
    function GetGlyphLength: Word;
  public
    constructor Create(AFont: TShxFont; ANumber, ABytesCount: Word);
    destructor Destroy; override;
    property Number: Word read FNumber write FNumber;
    property BytesCount: Word read FBytesCount write FBytesCount;
    property Name: String read FName write FName;
    property Glyph[Index: Integer]: Byte read GetGlyph write SetGlyph;
    property GlyphLength: Word read GetGlyphLength write SetGlyphLength;
  end;

Создаем определение класса шрифта:

  TShxFont = class(TObjectList)
  private
    FFontFileName: String;   // полный путь к файлу (устан-ся при успешном LoadFromFile)
    FFontName: String;       // = имя формы 0 (имя шрифта вместе с копирайтами)
    FFontKind: TShxFontKind; // тип шрифта
    FIsFont: Boolean;        // флаг "это шрифт": True если шрифт, False если файл форм
    FAbove: Byte;            // высота строчной буквы над базовой линией
    FBelow: Byte;            // размер подстрочного элемента под базовой линией
    FModes: Byte;            // режимы ориентации шрифта
    FUniEncoding: Byte;      // кодировка Unicode
    FUniType: Byte;          // тип лицензии
    FMaxStrLen: Byte;        // макс. длина строки определения формы (для экспорта в SHP)
  protected
    function GetShape(Index: Integer): TShxShape;
    procedure SetShape(Index: Integer; ASample: TShxShape);
    procedure SetMaxStrLen(const Value: Byte);
    procedure SetDefault;
    procedure LoadShape(const FileHandle: THandle; const ShapeNum: Word);
  public
    constructor Create;
    function Add(AShape: TShxShape): Integer;
    procedure Insert(Index: Integer; ASample: TShxShape);
    function LoadFromFile(const AFileName: String): Boolean;
    function GetShapeIndex(ShapeNumber: Word): Integer;
    property Shapes[Index: Integer]: TShxShape read GetShape write SetShape; default;
    property FontFileName: String read FFontFileName;
    property FontName: String read FFontName;
    property FontKind: TShxFontKind read FFontKind;
    property IsFont: Boolean read FIsFont;
    property Above: Byte read FAbove;
    property Below: Byte read FBelow;
    property Modes: Byte read FModes;
    property UniEncoding: Byte read FUniEncoding;
    property UniType: Byte read FUniType;
    property MaxStrLen: Byte read FMaxStrLen write SetMaxStrLen;
  end;

Реализация большинства методов этих классов тривиальна и пояснений не требует. Рассмотрим непосредственно чтение shx-файла:

function TShxFont.LoadFromFile(const AFileName: String): Boolean;
const
  SHX_FILE_SIGNATURE_SIZE: Word = 32;     // размер, в который влезает сигнатура файла
  SHX_SHAPES_NUM_SHAPES_OFS: Word = $1C;  // смещение слова кол. форм от начала файла
  SHX_UNIFONT_NUM_SHAPES_OFS: Word = $19;
  SHX_SHAPES_SHAPE0_GLYPH_LENGTH: Word = $03; // длина инфо о шрифте в форме 0
  SHX_UNIFNT_SHAPE0_GLYPH_LENGTH: Word = $05;
var
  FileHandle: THandle;
  NumShapesOfs: Integer;
  i, NumShapes, Shape0GlyphLen: Word;
  ShapeInfo: TShxShapeInfo;
  Buffer: PAnsiChar;
  S: AnsiString;
begin
  Result:= False;
  Clear;
  FileHandle:= FileOpen(AFileName, fmOpenRead);
  try
    FileSeek(FileHandle, 0, FILE_BEGIN);
    // Определение типа шрифта - проверка сигнатуры
    Buffer:= AllocMem(SHX_FILE_SIGNATURE_SIZE + 1);
    try
      FileRead(FileHandle, Buffer^, SHX_FILE_SIGNATURE_SIZE);
      S:= AnsiString(Buffer);
    finally
      FreeMem(Buffer);
    end;
    FFontKind:= GetFontKind(S);
    if FFontKind = fkUnknown then
      raise Exception.CreateFmt('Error Font Kind [%d]!', [Integer(FFontKind)]);
    if FFontKind = fkBigFont then
      raise Exception.Create('BigFont is not support!');
    // Количество форм
    if FFontKind = fkShpFont then
      NumShapesOfs:= SHX_SHAPES_NUM_SHAPES_OFS
    else
      NumShapesOfs:= SHX_UNIFONT_NUM_SHAPES_OFS;
    FileSeek(FileHandle, NumShapesOfs, FILE_BEGIN);
    FileRead(FileHandle, NumShapes, SizeOf(NumShapes));
    // Для SHAPE-шрифта читаем пары: [Номер формы][Количество байт (всего, вкл. имя)]
    if FFontKind = fkShpFont then
    begin
      for i:= 0 to NumShapes - 1 do
      begin
        FileRead(FileHandle, ShapeInfo, SizeOf(TShxShapeInfo));
        Add(TShxShape.Create(Self, ShapeInfo.ShapeNumber, ShapeInfo.GlyphBytes));
      end;
    end;
    // Имена и байты форм
    for i:= 0 to NumShapes - 1 do
    begin
      case FFontKind of
        fkShpFont: LoadShape(FileHandle, i);
        fkUniFont: begin
          FileRead(FileHandle, ShapeInfo, SizeOf(TShxShapeInfo));
          Add(TShxShape.Create(Self, ShapeInfo.ShapeNumber, ShapeInfo.GlyphBytes));
          LoadShape(FileHandle, i);
        end;
      end;
    end;
    if Count < 1 then
      raise Exception.CreateFmt('Error Reading Shapes [%d]!', [Count]);
    // Присвоение информации по шрифту
    if Shapes[0].Number = 0 then // это файл шрифта
    begin
      FIsFont:= True;
      // Проверка длины глифа для формы 0
      if FFontKind = fkShpFont then
        Shape0GlyphLen:= SHX_SHAPES_SHAPE0_GLYPH_LENGTH
      else
        Shape0GlyphLen:= SHX_UNIFNT_SHAPE0_GLYPH_LENGTH;
      if Shapes[0].GlyphLength < Shape0GlyphLen then
        raise Exception.CreateFmt('Error Shape 0 Corrupt [%d, %d]!',
          [Shapes[0].GlyphLength, Shape0GlyphLen]);
      // Общая часть для fkShpFont и для fkUniFont
      FAbove:= Shapes[0].Glyph[0];
      FBelow:= Shapes[0].Glyph[1];
      FModes:= Shapes[0].Glyph[2];
      // Только для fkUniFont
      if FFontKind = fkUniFont then
      begin
        FUniEncoding:= Shapes[0].Glyph[3];
        FUniType:= Shapes[0].Glyph[4];
      end;
      if Shapes[0].Name <> '' then
        FFontName:= Shapes[0].Name
      else
        FFontName:= ChangeFileExt(FFontFileName, '');
      // Считали все данные из формы 0, теперь удалим ее
      Delete(0);
    end else // это файл форм
      SetDefault;
    FFontFileName:= AFileName;
    Result:= True;
  finally
    FileClose(FileHandle);
  end;
end;

Сначала читаем из файла 32 байта и определяем тип файла (функция GetFontKind). Далее читаем количество форм. Затем:

  • для SHAPE-шрифта читаем пары: [Номер формы][Количество байт] и далее байты описания форм (метод LoadShape).
  • для UNICODE-шрифта читаем: пару [Номер формы][Количество байт] и далее байты описания формы (метод LoadShape).

После чтения данных из файла разбираемся с формой 0. Если ее нет, то это файл форм, а не шрифта — устанавливаем дефолтные значения для полей (метод SetDefault). Если форма 0 есть, то устанавливаем значения полей из нее. В заключение удаляем нулевую форму, т.к. она больше не требуется.

Реализация функции GetFontKind:

function GetFontKind(AFileHeader: AnsiString): TShxFontKind;
const
  SHX_SHAPES10: AnsiString = 'AutoCAD-86 shapes 1.0';
  SHX_SHAPES11: AnsiString = 'AutoCAD-86 shapes 1.1';
  SHX_UNIFONT10: AnsiString = 'AutoCAD-86 unifont 1.0';
  SHX_BIGFONT10: AnsiString = 'AutoCAD-86 bigfont 1.0';
begin
  if (Pos(SHX_SHAPES10, AFileHeader) <> 0) or (Pos(SHX_SHAPES11, AFileHeader) <> 0) then
    Result:= fkShpFont
  else if (Pos(SHX_UNIFONT10, AFileHeader) <> 0) then
    Result:= fkUniFont
  else if (Pos(SHX_BIGFONT10, AFileHeader) <> 0) then
    Result:= fkBigFont
  else
    Result:= fkUnknown;
end;

Реализация метода LoadShape:

type
  THackedShxShape = class(TShxShape);
  
procedure TShxFont.LoadShape(const FileHandle: THandle; const ShapeNum: Word);
var
  GlyphLen, GlyphOfs: Word;
  Buffer: PAnsiChar;
  S: AnsiString;
begin
  Buffer:= System.AllocMem(Shapes[ShapeNum].BytesCount + 1);
  try
    FileRead(FileHandle, Buffer^, Shapes[ShapeNum].BytesCount);
    S:= AnsiString(Buffer);
    Shapes[ShapeNum].Name:= String(S);
    // Байты формы
    GlyphOfs:= Length(Shapes[ShapeNum].Name) + 1;
    GlyphLen:= Shapes[ShapeNum].BytesCount - GlyphOfs;
    Shapes[ShapeNum].GlyphLength:= GlyphLen;
    System.Move(Buffer[GlyphOfs], THackedShxShape(Shapes[ShapeNum]).FGlyph[0], GlyphLen);
  finally
    FreeMem(Buffer);
  end;
end;
В представленных в статье реализациях опущено большинство проверок, чтобы сделать код более простым для понимания.

С чтением закончили, теперь переходим к экспорту в SHP-файл.

Экспорт в SHP-файл

Добавим в класс TShxShape следующий метод, который возвращает описание формы в SHP формате:

function TShxShape.GetGlyphDefinition(AMaxStrLen: Byte): String;
var
  i: Integer;
  n, MaxLen: Byte;
begin
  MaxLen:= EnsureRange(AMaxStrLen, MAX_STR_LEN_MIN, MAX_STR_LEN_MAX);
  Result:= Format('*%d,%d', [FNumber, GlyphLength]);
  if FName = '' then
    Result:= Result + #13#10
  else
    Result:= Format('%s,%s'#13#10, [Result, FName]);
  n:= 0;
  FGlyphHighIndex:= High(FGlyph);
  i:= Low(FGlyph);
  while i <= FGlyphHighIndex do
  begin
    case FGlyph[i] of
      $0: Result:= Format('%s0', [Result]); // конец описания формы
      $1: Result:= Format('%s%d,', [Result, FGlyph[i]]); // опускание пера
      $2: Result:= Format('%s%d,', [Result, FGlyph[i]]); // поднятие пера
      $3: begin // деление длин векторов на следующий байт
            Result:= Format('%s%d,%d,', [Result, FGlyph[i], FGlyph[i + 1]]);
            Inc(i);
            Inc(n);
          end;
      $4: begin // умножение длин векторов на следующий байт
            Result:= Format('%s%d,%d,', [Result, FGlyph[i], FGlyph[i + 1]]);
            Inc(i);
            Inc(n);
          end;
      // занесение текущей позиции в стек
      $5: Result:= Format('%s%d,', [Result, FGlyph[i]]);
      // восстановление текущей позиции из стека
      $6: Result:= Format('%s%d,', [Result, FGlyph[i]]);
      $7: begin // отрисовка субформы, номер которой определяется следующим байтом
            if FFont.FFontKind = fkUniFont then // (для UNIFONT - двумя)
            begin
              Result:= Format('%s%d,0%.4x,', [Result, FGlyph[i],
                  (FGlyph[i + 1] shl 8) + FGlyph[i + 2]]);
              Inc(i, 2);
              Inc(n, 2);
            end else
            begin
              Result:= Format('%s%d,%d,', [Result, FGlyph[i], FGlyph[i + 1]]);
              Inc(i);
              Inc(n);
            end;
          end;
      $8: begin // смещение по осям X-Y, заданное следующими двумя байтами
            Result:= Format('%s%d,(%d,%d),', [Result, FGlyph[i],
                Shortint(FGlyph[i + 1]), Shortint(FGlyph[i + 2])]);
            Inc(i, 2);
            Inc(n, 2);
          end;
      $9: begin // ряд из нескольких смещений по осям X-Y, оканчивающийся на (0,0)
            Result:= Format('%s%d,', [Result, FGlyph[i]]);
            Inc(n);
            Inc(i, 2); // сейчас указывает на dY
            while True do
            begin
              Result:= Format('%s(%d,%d),', [Result,
                Shortint(FGlyph[i - 1]), Shortint(FGlyph[i])]);
              Inc(n, 2);
              if n > MaxLen then
              begin
                Result:= Result + #13#10;
                n:= 0;
              end;
              if (FGlyph[i - 1] = 0) and (FGlyph[i] = 0) then // [(dX,dY) = (0,0)]?
                Break;
              Inc(i, 2);
            end;
          end;
      $A: begin // октантная дуга, заданная следующими двумя байтами
            Result:= Format('%s%d,(%d,%s),', [Result, FGlyph[i], FGlyph[i + 1],
                EncodeOctant(FGlyph[i + 2])]);
            Inc(i, 2);
            Inc(n, 2);
          end;
      $B: begin // дробная дуга, заданная следующими 5 байтами
            Result:= Format('%s%d,(%d,%d,%d,%d,%s),', [Result,
                FGlyph[i], FGlyph[i + 1], FGlyph[i + 2], FGlyph[i + 3], FGlyph[i + 4],
                EncodeOctant(FGlyph[i + 5])]);
            Inc(i, 5);
            Inc(n, 5);
          end;
      $C: begin // дуга, заданная смещением X-Y и прогибом
            Result:= Format('%s%d,(%d,%d,%d),', [Result, FGlyph[i],
              Shortint(FGlyph[i + 1]), Shortint(FGlyph[i + 2]), Shortint(FGlyph[i + 3])]);
            Inc(i, 3);
            Inc(n, 3);
          end;
      $D: begin // несколько дуг, заданных смещением X-Y и прогибом
            Result:= Format('%s%d,', [Result, FGlyph[i]]);
            Inc(i, 3); // сейчас указывает на Bulge
            while True do
            begin
              if (FGlyph[i - 2] = 0) and (FGlyph[i - 1] = 0) then // [(dX,dY) = (0,0)]?
              begin
                Result:= Format('%s(0,0),', [Result]);
                Dec(i);
                Break;
              end else
              begin
                Result:= Format('%s(%d,%d,%d),', [Result,
                  Shortint(FGlyph[i - 2]), Shortint(FGlyph[i - 1]), Shortint(FGlyph[i])]);
                if n > MaxLen then
                begin
                  Result:= Result + #13#10;
                  n:= 0;
                end;
              end;
              Inc(i, 3);
              Inc(n, 3);
            end;
          end;
      // обработка следующей команды только для вертикального текста
      $E: Result:= Format('%s%d,', [Result, FGlyph[i]]);
    else
      // длина вектора и код направления
      Result:= Format('%s0%.2x,', [Result, FGlyph[i]]);
    end;
    Inc(n);
    if n > MaxLen then
    begin
      Result:= Result + #13#10;
      n:= 0;
    end;
    Inc(i);
  end;
end;

Реализация функции EncodeOctant:

function EncodeOctant(DSC: Byte): String;
var
  S, C: Byte;
  CW: Boolean;
  tmp: Shortint;
  b: Byte;
begin
  tmp:= Shortint(DSC);
  CW:= ((tmp and $80) <> 0);
  b:= Byte(tmp);
  S:= (b and $70) shr 4;
  C:= b and $07;
  if CW then
    Result:= Format('-0%.1x%.1x', [S, C])
  else
    Result:= Format('0%.1x%.1x', [S, C]);
end;

Добавим метод экспорта в класс TShxFont:

procedure TShxFont.ExportToShpFile(const AFileName: String);
var
  Lst: TStringList;
  i: Integer;
begin
  if Count < 1 then Exit;
  Lst:= TStringList.Create;
  try
    if FFontKind = fkShpFont then
    begin
      Lst.Add(Format('*0,4,%s', [FFontName]));
      Lst.Add(Format('%d,%d,%d,0', [FAbove, FBelow, FModes]));
    end else // fkUniFont
    begin
      Lst.Add(Format('*UNIFONT,6,%s', [FFontName]));
      Lst.Add(Format('%d,%d,%d,%d,%d,0', [FAbove, FBelow, FModes, FUniEncoding, FUniType]));
    end;
    for i:= 0 to Count - 1 do
      Lst.Add(Shapes[i].GetGlyphDefinition(FMaxStrLen));
    Lst.Add('');
    Lst.SaveToFile(AFileName);
  finally
    Lst.Free;
  end;
end;

В прилагаемом примере построено приложение, реализующее описанные классы.

В следующей части рассмотрим отрисовку символов.

К статье прилагаются файлы:


Внимание! Запрещается воспроизведение данной статьи или ее части без согласования с автором. Если вы желаете разместить эту статью на своем сайте или издать в печатном виде, свяжитесь с автором.
Автор статьи: Вершинин И.В.