Class: Fontisan::Type1::CharStringConverter

Inherits:
Object
  • Object
show all
Defined in:
lib/fontisan/type1/charstring_converter.rb

Overview

Converter for Type 1 CharStrings to CFF CharStrings

CharStringConverter converts Type 1 CharString bytecode to CFF (Compact Font Format) CharString bytecode.

Type 1 and CFF use similar stack-based languages but have different operator codes and some structural differences:

  • Operator codes differ between formats
  • Type 1 has seac operator for composites; CFF doesn't support it
  • Hint operators need to be preserved with code translation

Examples:

Convert a Type 1 CharString to CFF

converter = Fontisan::Type1::CharStringConverter.new
cff_charstring = converter.convert(type1_charstring)

See Also:

Constant Summary collapse

TYPE1_TO_CFF =

Type 1 to CFF operator mapping

Maps Type 1 operator codes to CFF operator codes. Some operators have the same code, others differ.

{
  # Path construction operators
  hmoveto: 22,      # Type 1: 22, CFF: 22
  vmoveto: 4,       # Type 1: 4, CFF: 4
  rlineto: 5,       # Type 1: 5, CFF: 5
  hlineto: 6,       # Type 1: 6, CFF: 6
  vlineto: 7,       # Type 1: 7, CFF: 7
  rrcurveto: 8,     # Type 1: 8, CFF: 8
  hhcurveto: 27,    # Type 1: 27, CFF: 27
  hvcurveto: 31,    # Type 1: 31, CFF: 31
  vhcurveto: 30,    # Type 1: 30, CFF: 30
  rcurveline: 24,   # Type 1: 24, CFF: 24
  rlinecurve: 25,   # Type 1: 25, CFF: 25

  # Hint operators
  hstem: 1,         # Type 1: 1, CFF: 1
  vstem: 3,         # Type 1: 3, CFF: 3
  hstemhm: 18,      # Type 1: 18, CFF: 18
  vstemhm: 23,      # Type 1: 23, CFF: 23

  # Hint substitution (not in Type 1, but we preserve for compatibility)
  hintmask: 19,     # Type 1: N/A, CFF: 19
  cntrmask: 20,     # Type 1: N/A, CFF: 20

  # End char
  endchar: 14, # Type 1: 14 (or 11 in some specs), CFF: 14

  # Miscellaneous
  callsubr: 10,     # Type 1: 10, CFF: 10
  return: 11,       # Type 1: 11, CFF: 11

  # Deprecated operators (preserve for compatibility)
  hstem3: 12,       # Type 1: 12 (escape 0), CFF: 12 (escape 0)
  vstem3: 13,       # Type 1: 13 (escape 1), CFF: 13 (escape 1)
  seac: 12,         # Type 1: 12 (escape 6), CFF: Not supported
}.freeze
ESCAPE_BYTE =

Escape code for two-byte operators

12
SEAC_ESCAPE_CODE =

seac operator escape code (second byte)

6

Instance Method Summary collapse

Constructor Details

#initialize(charstrings = nil) ⇒ CharStringConverter

Initialize a new CharStringConverter

Parameters:

  • charstrings (CharStrings, nil) (defaults to: nil)

    CharStrings dictionary for seac expansion



73
74
75
# File 'lib/fontisan/type1/charstring_converter.rb', line 73

def initialize(charstrings = nil)
  @charstrings = charstrings
end

Instance Method Details

#convert(type1_charstring) ⇒ String

Convert Type 1 CharString to CFF CharString

Examples:

Convert a CharString

converter = Fontisan::Type1::CharStringConverter.new
cff_bytes = converter.convert(type1_bytes)

Parameters:

  • type1_charstring (String)

    Type 1 CharString bytecode

Returns:

  • (String)

    CFF CharString bytecode



85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/fontisan/type1/charstring_converter.rb', line 85

def convert(type1_charstring)
  # Parse Type 1 CharString into commands
  parser = Type1::CharStrings::CharStringParser.new
  commands = parser.parse(type1_charstring)

  # Check for seac operator and expand if needed
  if parser.seac_components
    return expand_seac(parser.seac_components)
  end

  # Convert commands to CFF format
  convert_commands(commands)
end

#convert_commands(commands) ⇒ String

Convert parsed commands to CFF CharString

Parameters:

  • commands (Array<Array>)

    Parsed Type 1 commands

Returns:

  • (String)

    CFF CharString bytecode



103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
# File 'lib/fontisan/type1/charstring_converter.rb', line 103

def convert_commands(commands)
  result = String.new(encoding: Encoding::ASCII_8BIT)

  commands.each do |command|
    case command[0]
    when :number
      # Encode number in CFF format
      result << encode_cff_number(command[1])
    when :seac
      # seac should be expanded before this point
      raise Fontisan::Error,
            "seac operator not supported in CFF, must be expanded first"
    else
      # Convert operator
      op_code = TYPE1_TO_CFF[command[0]]
      if op_code.nil?
        # Unknown operator, skip or raise error
        next
      end

      result << encode_cff_operator(op_code)
    end
  end

  result
end

#encode_cff_number(value) ⇒ String

Encode number in CFF format

CFF uses a variable-length encoding for numbers:

  • 32-246: 1 byte (value - 139)
  • 247-250: 2 bytes (first byte indicates format)
  • 251-254: 3 bytes (first byte indicates format)
  • 255: 5 bytes (signed 16-bit integer)
  • 28: 2 bytes (signed 16.16 fixed point, not used for CharStrings)

Parameters:

  • value (Integer)

    Number to encode

Returns:

  • (String)

    Encoded number bytes



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
# File 'lib/fontisan/type1/charstring_converter.rb', line 186

def encode_cff_number(value)
  result = String.new(encoding: Encoding::ASCII_8BIT)

  if value >= -107 && value <= 107
    # 1-byte number: value + 139
    result << (value + 139).chr
  elsif value >= 108 && value <= 1131
    # 2-byte positive number
    value -= 108
    result << ((value >> 8) + 247).chr
    result << (value & 0xFF).chr
  elsif value >= -1131 && value <= -108
    # 2-byte negative number
    value = -value - 108
    result << ((value >> 8) + 251).chr
    result << (value & 0xFF).chr
  elsif value >= -32768 && value <= 32767
    # 5-byte number (16-bit integer)
    result << 255.chr
    result << [(value >> 8) & 0xFF, value & 0xFF].pack("CC")
    result << [0, 0].pack("CC") # Pad to 5 bytes
  else
    raise Fontisan::Error,
          "Number out of range for CFF encoding: #{value}"
  end

  result
end

#encode_cff_operator(op_code) ⇒ String

Encode operator in CFF format

Most operators are single-byte. Some use escape byte (12) followed by a second byte.

Parameters:

  • op_code (Integer)

    Operator code

Returns:

  • (String)

    Encoded operator bytes



222
223
224
225
226
227
228
229
230
231
232
233
234
235
# File 'lib/fontisan/type1/charstring_converter.rb', line 222

def encode_cff_operator(op_code)
  result = String.new(encoding: Encoding::ASCII_8BIT)

  if op_code > 31 && op_code != ESCAPE_BYTE
    # Two-byte operator (escape + code)
    result << ESCAPE_BYTE.chr
    result << (op_code - ESCAPE_BYTE).chr
  else
    # Single-byte operator
    result << op_code.chr
  end

  result
end

#expand_seac(seac_data) ⇒ String

Expand seac composite glyph

The seac operator in Type 1 creates composite glyphs (like À = A + `). CFF doesn't support seac, so we need to expand it into the base glyphs with appropriate positioning.

Parameters:

  • seac_data (Hash)

    seac component data

Returns:

  • (String)

    CFF CharString bytecode with expanded seac



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/fontisan/type1/charstring_converter.rb', line 138

def expand_seac(seac_data)
  return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless @charstrings

  base_name = @charstrings.encoding[seac_data[:base]]
  accent_name = @charstrings.encoding[seac_data[:accent]]
  return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_name && accent_name

  base_cs = @charstrings[base_name]
  accent_cs = @charstrings[accent_name]
  return encode_cff_operator(TYPE1_TO_CFF[:endchar]) unless base_cs && accent_cs

  base_cff = convert(base_cs)
  accent_cff = convert(accent_cs)
  return base_cff unless accent_cff

  io = String.new(encoding: Encoding::ASCII_8BIT)
  io << base_cff.delete_suffix("\u000E")
  io << encode_cff_number(seac_data[:adx].to_i) if seac_data[:adx].to_i != 0
  io << encode_cff_number(seac_data[:ady].to_i) if seac_data[:ady].to_i != 0
  io << accent_cff.delete_suffix("\u000E")
  io << encode_cff_operator(TYPE1_TO_CFF[:endchar])
rescue StandardError
  encode_cff_operator(TYPE1_TO_CFF[:endchar])
end

#seac?(type1_charstring) ⇒ Boolean

Check if CharString contains seac operator

Parameters:

  • type1_charstring (String)

    Type 1 CharString bytecode

Returns:

  • (Boolean)

    True if CharString contains seac



167
168
169
170
171
# File 'lib/fontisan/type1/charstring_converter.rb', line 167

def seac?(type1_charstring)
  parser = Type1::CharStrings::CharStringParser.new
  parser.parse(type1_charstring)
  !parser.seac_components.nil?
end