Class: Fontisan::Converters::Type1Converter

Inherits:
Object
  • Object
show all
Includes:
CffTableBuilder, ConversionStrategy
Defined in:
lib/fontisan/converters/type1_converter.rb

Overview

Converter for Adobe Type 1 fonts to/from SFNT formats.

Type1Converter handles bidirectional conversion between Type 1 fonts (PFB/PFA) and SFNT-based formats (TTF, OTF, WOFF, WOFF2).

Conversion Strategy

Type 1 fonts use PostScript CharStrings that are similar to CFF CharStrings used in OpenType fonts. The conversion uses CharStringConverter for the CharString translation.

  • Type 1 → OTF: Convert Type 1 CharStrings to CFF format, build CFF table
  • OTF → Type 1: Convert CFF CharStrings to Type 1 format, build PFB/PFA
  • Type 1 → TTF: Type 1 → OTF → TTF (via OutlineConverter)
  • TTF → Type 1: TTF → OTF → Type 1

Conversion Options

The converter accepts ConversionOptions with opening and generating options:

  • Opening options: decompose_composites, generate_unicode, read_all_records
  • Generating options: decompose_on_output, hinting_mode, write_pfm, write_afm

Examples:

Convert Type 1 to OTF with options

font = FontLoader.load("font.pfb")
options = ConversionOptions.recommended(from: :type1, to: :otf)
converter = Type1Converter.new
tables = converter.convert(font, options: options)

Convert with preset

options = ConversionOptions.from_preset(:type1_to_modern)
tables = converter.convert(font, options: options)

See Also:

Instance Method Summary collapse

Methods included from CffTableBuilder

#build_cff_table

Methods included from ConversionStrategy

included, #supports?

Constructor Details

#initialize(options = {}) ⇒ Type1Converter

Initialize a new Type1Converter

Parameters:

  • options (Hash) (defaults to: {})

    Converter options

Options Hash (options):

  • :optimize_cff (Boolean)

    Enable CFF optimization (default: false)

  • :preserve_hints (Boolean)

    Preserve hinting (default: true)

  • :target_format (Symbol)

    Target format for conversion



52
53
54
55
56
# File 'lib/fontisan/converters/type1_converter.rb', line 52

def initialize(options = {})
  @optimize_cff = options.fetch(:optimize_cff, false)
  @preserve_hints = options.fetch(:preserve_hints, true)
  @target_format = options[:target_format]
end

Instance Method Details

#apply_opening_options(font, conv_options) ⇒ Object

Apply opening options to source font

Parameters:



145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
# File 'lib/fontisan/converters/type1_converter.rb', line 145

def apply_opening_options(font, conv_options)
  return unless font.is_a?(Type1Font)
  return unless conv_options

  # Generate Unicode codepoints if requested
  if conv_options.opening_option?(:generate_unicode)
    generate_unicode_mappings(font)
  end

  # Decompose seac composites if requested
  if conv_options.opening_option?(:decompose_composites)
    decompose_seac_glyphs(font)
  end

  # Read all font dictionary records if requested.
  if conv_options.opening_option?(:read_all_records) && font.font_dictionary
    font.font_dictionary.parse(font.font_dictionary.raw_data)
  end
end

#build_cff_font_dict(font) ⇒ Hash

Build CFF font dictionary from Type 1 font

Parameters:

Returns:

  • (Hash)

    CFF font dictionary data



412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
# File 'lib/fontisan/converters/type1_converter.rb', line 412

def build_cff_font_dict(font)
  {
    version: font.font_dictionary.version || "001.000",
    notice: font.font_dictionary.notice || "",
    copyright: font.font_dictionary.copyright || "",
    full_name: font.font_dictionary.full_name || font.font_name,
    family_name: font.font_dictionary.family_name || font.font_name,
    weight: font.font_dictionary.weight || "Medium",
    font_b_box: font.font_dictionary.font_bbox || [0, 0, 0, 0],
    font_matrix: font.font_dictionary.font_matrix || [0.001, 0, 0, 0.001,
                                                      0, 0],
    charset: font.charstrings.encoding.keys,
    encoding: font.charstrings.encoding,
  }
end

#build_cff_private_dict(font) ⇒ Hash

Build CFF private dictionary from Type 1 font

Parameters:

Returns:

  • (Hash)

    CFF private dictionary data



432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
# File 'lib/fontisan/converters/type1_converter.rb', line 432

def build_cff_private_dict(font)
  private_dict = font.private_dict
  {
    blue_values: private_dict.blue_values || [],
    other_blues: private_dict.other_blues || [],
    family_blues: private_dict.family_blues || [],
    family_other_blues: private_dict.family_other_blues || [],
    blue_scale: private_dict.blue_scale || 0.039625,
    blue_shift: private_dict.blue_shift || 7,
    blue_fuzz: private_dict.blue_fuzz || 1,
    std_hw: private_dict.std_hw || 0,
    std_vw: private_dict.std_vw || 0,
    stem_snap_h: private_dict.stem_snap_h || [],
    stem_snap_v: private_dict.stem_snap_v || [],
    force_bold: private_dict.force_bold || false,
    language_group: private_dict.language_group || 0,
    expansion_factor: private_dict.expansion_factor || 0.06,
    initial_random_seed: private_dict.initial_random_seed || 0,
  }
end

#build_cff_table_data(font, charstrings, _font_dict, _private_dict) ⇒ String

Build CFF table data

Parameters:

  • font (Type1Font)

    Source Type 1 font

  • charstrings (Hash)

    CFF CharStrings (glyph_name => data)

  • font_dict (Hash)

    CFF font dictionary (not used, kept for compatibility)

  • private_dict (Hash)

    CFF private dictionary (not used, kept for compatibility)

Returns:

  • (String)

    CFF table binary data



460
461
462
463
464
465
466
467
# File 'lib/fontisan/converters/type1_converter.rb', line 460

def build_cff_table_data(font, charstrings, _font_dict, _private_dict)
  # Convert charstrings hash to array (build_cff_table expects array)
  charstrings_array = charstrings.values

  # Build CFF table using CffTableBuilder
  # We need to pass the Type1Font as-is for metadata extraction
  build_cff_table(charstrings_array, [], font)
end

#build_cmap_format_4(unicode_to_glyph) ⇒ String

Build cmap format 4 subtable

Parameters:

  • unicode_to_glyph (Hash<Integer, Integer>)

    Unicode to glyph index mapping

Returns:

  • (String)

    Format 4 subtable binary data



1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
# File 'lib/fontisan/converters/type1_converter.rb', line 1084

def build_cmap_format_4(unicode_to_glyph)
  data = (+"").b

  # Get sorted Unicode values
  unicode_values = unicode_to_glyph.keys.sort
  return data if unicode_values.empty?

  # For simplicity, create segments for continuous ranges
  # A more sophisticated implementation would optimize this
  segments = []
  current_segment = nil

  unicode_values.each do |unicode|
    glyph_id = unicode_to_glyph[unicode]

    if current_segment.nil?
      current_segment = {
        start: unicode,
        end: unicode,
        start_glyph: glyph_id,
        glyphs: [glyph_id],
      }
    elsif unicode == current_segment[:end] + 1 && glyph_id == current_segment[:glyphs].last + 1
      # Continue current segment (sequential)
      current_segment[:end] = unicode
      current_segment[:glyphs] << glyph_id
    else
      # Start new segment
      segments << current_segment
      current_segment = {
        start: unicode,
        end: unicode,
        start_glyph: glyph_id,
        glyphs: [glyph_id],
      }
    end
  end

  segments << current_segment if current_segment

  # Add end segment marker (0xFFFF)
  segments << { start: 0xFFFF, end: 0xFFFF, start_glyph: 0, glyphs: [0] }

  # Calculate segment count and related values
  seg_count = segments.length
  seg_count_x2 = seg_count * 2
  search_range = 2**Math.log2(seg_count).to_i * 2
  entry_selector = Math.log2(search_range / 2).to_i
  range_shift = (seg_count - search_range / 2) * 2

  # Build format 4 subtable header (14 bytes)
  data << [4].pack("n") # Format
  data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
  data << [0].pack("n")                    # Language (0 = independent)
  data << [seg_count_x2].pack("n")         # segCountX2
  data << [search_range].pack("n")         # searchRange
  data << [entry_selector].pack("n")       # entrySelector
  data << [range_shift].pack("n")          # rangeShift

  # Build segment arrays
  end_codes = []
  start_codes = []
  id_deltas = []
  id_range_offsets = []
  glyph_id_array = []

  segments.each do |seg|
    end_codes << seg[:end]
    start_codes << seg[:start]

    # For sequential glyphs, use delta
    if seg[:start] == 0xFFFF
      # End segment marker
      id_deltas << 1
      id_range_offsets << 0
    elsif seg[:end] - seg[:start] == seg[:glyphs].length - 1
      # Sequential: use delta
      id_deltas << (seg[:start_glyph] - seg[:start])
      id_range_offsets << 0
    else
      # Non-sequential: use glyph ID array
      id_deltas << 0
      id_range_offsets << (glyph_id_array.length * 2 + 2)
      glyph_id_array.concat(seg[:glyphs])
    end
  end

  # Write arrays (padded to even length)
  end_codes.each { |code| data << [code].pack("n") }
  data << [0].pack("n") # Reserved padding
  start_codes.each { |code| data << [code].pack("n") }
  id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
  id_range_offsets.each { |offset| data << [offset].pack("n") }
  glyph_id_array.each { |gid| data << [gid].pack("n") }

  # Update length in header
  length = data.bytesize
  data[2..3] = [length].pack("n")

  data
end

#build_cmap_table(font) ⇒ String

Build cmap table from Type 1 font

Parameters:

Returns:

  • (String)

    cmap table binary data



1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
# File 'lib/fontisan/converters/type1_converter.rb', line 1020

def build_cmap_table(font)
  data = (+"").b

  # Get encoding from Type1Font
  encoding = font.charstrings&.encoding || {}
  glyph_names = font.charstrings&.glyph_names || encoding.keys

  # Build Unicode mapping from glyph names using AGL
  unicode_to_glyph = {}
  glyph_index = 0

  glyph_names.each do |glyph_name|
    # Get Unicode code point from AGL
    unicode = Type1::AGL.unicode_for_glyph_name(glyph_name)

    # If no Unicode mapping, try to derive from encoding position
    if unicode.nil? && (glyph_index < 128)
      # For standard encoding, try to map from position
      # This is a simplified approach - real implementation would be more robust
      unicode = glyph_index
    end

    # Map Unicode to glyph index
    if unicode && unicode <= 0xFFFF
      unicode_to_glyph[unicode] ||= glyph_index
    end

    glyph_index += 1
  end

  # Ensure at least .notdef (glyph 0) maps to something
  unicode_to_glyph[0x0000] ||= 0

  # Build Format 4 subtable (Segment mapping to delta values)
  # This is the most common format for BMP Unicode fonts
  subtable_data = build_cmap_format_4(unicode_to_glyph)

  # Calculate offsets
  encoding_records_offset = 4 # After version (2) + num_tables (2)
  subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)

  # Build cmap table header
  # Version (uint16)
  data << [0].pack("n")

  # Number of encoding records (uint16)
  data << [1].pack("n") # One encoding record

  # Encoding record: Platform ID (uint16), Encoding ID (uint16), Subtable offset (uint32)
  # Platform 3 (Windows), Encoding 1 (Unicode BMP)
  data << [3].pack("n")           # Platform ID: Windows
  data << [1].pack("n")           # Encoding ID: Unicode BMP
  data << [subtable_offset].pack("N") # Subtable offset

  # Append subtable data
  data << subtable_data

  data
end

#build_head_table(font) ⇒ String

Build head table from Type 1 font

Parameters:

Returns:

  • (String)

    head table binary data



537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
# File 'lib/fontisan/converters/type1_converter.rb', line 537

def build_head_table(font)
  data = (+"").b

  # Get font metadata from Type1Font
  font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
  version_str = font.version || "001.000"

  # Parse version (e.g., "001.000" => 1.0)
  version_parts = version_str.split(".")
  major = version_parts[0].to_i
  minor = version_parts[1].to_i
  version = major + (minor / 1000.0)

  # Version (Fixed 16.16) - stored as int32
  integer_part = version.to_i
  fractional_part = ((version - integer_part) * 65_536).to_i
  version_raw = (integer_part << 16) | fractional_part
  data << [version_raw].pack("N")

  # Font Revision (Fixed 16.16) - default to 1.0
  font_revision_raw = 0x00010000
  data << [font_revision_raw].pack("N")

  # Checksum Adjustment (uint32) - will be calculated later
  data << [0].pack("N")

  # Magic Number (uint32)
  data << [0x5F0F3CF5].pack("N")

  # Flags (uint16) - bit 0 indicates y direction (0 = mixed)
  data << [0].pack("n")

  # Units Per Em (uint16) - Type 1 standard is 1000
  data << [1000].pack("n")

  # Created (LONGDATETIME) - use current time
  data << [Tables::Head.now_longdatetime].pack("Q>")

  # Modified (LONGDATETIME) - use current time
  data << [Tables::Head.now_longdatetime].pack("Q>")

  # Bounding box (int16 each)
  data << [font_bbox[0]].pack("s>") # x_min
  data << [font_bbox[1]].pack("s>") # y_min
  data << [font_bbox[2]].pack("s>") # x_max
  data << [font_bbox[3]].pack("s>") # y_max

  # Mac Style (uint16) - no style bits set
  data << [0].pack("n")

  # Lowest Rec PPEM (uint16) - readable size
  data << [8].pack("n")

  # Font Direction Hint (int16)
  # 2 = Left to right, mixed glyphs
  data << [2].pack("s>")

  # Index To Loc Format (int16)
  # 0 = short offsets (for CFF fonts we use this)
  data << [0].pack("s>")

  # Glyph Data Format (int16)
  data << [0].pack("s>")

  data
end

#build_hhea_table(font) ⇒ String

Build hhea table from Type 1 font

Parameters:

Returns:

  • (String)

    hhea table binary data



608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
# File 'lib/fontisan/converters/type1_converter.rb', line 608

def build_hhea_table(font)
  data = (+"").b

  # Get font metrics from Type1Font
  font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
  blue_values = font.private_dict&.blue_values || []

  # Version (Fixed 16.16) - 0x00010000 (1.0)
  data << [0x00010000].pack("N")

  # Ascent (int16) - Distance from baseline to highest ascender
  # Use BlueValues[2] or [3] if available, otherwise font_bbox[3]
  ascent = if blue_values.length >= 4
             blue_values[3] # Top zone top
           elsif blue_values.length >= 3
             blue_values[2] # Top zone bottom
           else
             font_bbox[3] # y_max
           end
  data << [ascent].pack("s>")

  # Descent (int16) - Distance from baseline to lowest descender (negative)
  # Use BlueValues[0] or [1] if available, otherwise font_bbox[1]
  descent = if blue_values.length >= 2
              blue_values[0] # Bottom zone bottom (negative)
            elsif blue_values.length >= 1
              blue_values[0]
            else
              font_bbox[1] # y_min (should be negative)
            end
  data << [descent].pack("s>")

  # Line Gap (int16) - Additional space between lines
  # Use typical value of 0 for Type 1 fonts
  data << [0].pack("s>")

  # Advance Width Max (uint16)
  # Type 1 standard is typically 1000, use font_bbox width + padding
  advance_max = (font_bbox[2] - font_bbox[0]) + 100
  data << [advance_max].pack("n")

  # Min Left Side Bearing (int16)
  # Use font_bbox[0] (x_min) as reasonable default
  data << [font_bbox[0]].pack("s>")

  # Min Right Side Bearing (int16)
  # Estimate as 0 (will be updated if actual metrics available)
  data << [0].pack("s>")

  # x Max Extent (int16) - Max(lsb + xMax)
  # Use font_bbox[2] (x_max) as reasonable default
  data << [font_bbox[2]].pack("s>")

  # Caret Slope Rise (int16)
  # 1 for upright fonts (not italic)
  data << [1].pack("s>")

  # Caret Slope Run (int16)
  # 0 for upright fonts
  data << [0].pack("s>")

  # Caret Offset (int16)
  # Set to 0 for standard fonts
  data << [0].pack("s>")

  # Reserved (int64) - 8 bytes of zeros
  data << [0, 0].pack("Q>")

  # Metric Data Format (int16)
  # 0 for current format
  data << [0].pack("s>")

  # Number of HMetrics (uint16)
  # Number of glyphs with explicit metrics (typically all glyphs)
  num_glyphs = font.charstrings&.count || 1
  data << [[num_glyphs, 1].max].pack("n")

  data
end

#build_maxp_table(font) ⇒ String

Build maxp table from Type 1 font

Parameters:

Returns:

  • (String)

    maxp table binary data



692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
# File 'lib/fontisan/converters/type1_converter.rb', line 692

def build_maxp_table(font)
  data = (+"").b

  # Get number of glyphs from Type1Font
  num_glyphs = font.charstrings&.count || 1

  # Version (Fixed 16.16)
  # For CFF fonts (OTF output), use version 0.5 (0x00005000)
  # For TrueType fonts (TTF output), would use version 1.0 (0x00010000)
  # Type 1 fonts convert to CFF-based OTF, so use version 0.5
  data << [0x00005000].pack("N")

  # Number of Glyphs (uint16)
  # Must be >= 1 (at minimum, .notdef must be present)
  data << [[num_glyphs, 1].max].pack("n")

  data
end

#build_name_table(font) ⇒ String

Build name table from Type 1 font

Parameters:

Returns:

  • (String)

    name table binary data



715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
# File 'lib/fontisan/converters/type1_converter.rb', line 715

def build_name_table(font)
  # Get font metadata from Type1Font
  font_dict = font.font_dictionary
  font_info = font_dict&.font_info

  # Extract font names with fallbacks
  font_name = font.font_name || font_dict&.font_name || "Unnamed"
  family_name = font_info&.family_name || font_dict&.family_name || font_name
  full_name = font_info&.full_name || font_dict&.full_name || family_name
  version = font_info&.version || font.version || "001.000"
  copyright = font_info&.copyright || font_dict&.raw_data&.dig(:copyright) || ""
  postscript_name = font_name
  weight = font_info&.weight || "Regular"
  notice = font_info&.notice || ""

  # Build name records (Windows Unicode, English US)
  # Platform ID 3 (Windows), Encoding ID 1 (Unicode BMP), Language ID 0x0409 (US English)
  name_records = [
    # Copyright (name ID 0)
    { name_id: 0, string: copyright },
    # Family Name (name ID 1)
    { name_id: 1, string: family_name },
    # Subfamily Name (name ID 2) - derive from weight or default to Regular
    { name_id: 2, string: weight || "Regular" },
    # Unique ID (name ID 3) - format: version;copyright;postscript_name
    { name_id: 3, string: "#{version};#{copyright};#{postscript_name}" },
    # Full Name (name ID 4)
    { name_id: 4, string: full_name },
    # Version (name ID 5)
    { name_id: 5, string: version },
    # PostScript Name (name ID 6)
    { name_id: 6, string: postscript_name },
    # Trademark (name ID 7) - use notice if available
    { name_id: 7, string: notice || "" },
  ]

  # Filter out empty strings and build string storage
  name_records = name_records.select do |r|
    !r[:string].nil? && !r[:string].empty?
  end

  # Build string storage (UTF-16BE encoded for Windows platform)
  string_storage = (+"").b
  name_records.each do |record|
    encoded_string = record[:string].encode("UTF-16BE").force_encoding("ASCII-8BIT")
    record[:encoded] = encoded_string
    record[:offset] = string_storage.bytesize
    string_storage << encoded_string
  end

  # Build name table
  data = (+"").b

  # Format selector (uint16) - 0 for basic
  data << [0].pack("n")

  # Count (uint16) - number of name records
  data << [name_records.size].pack("n")

  # String offset (uint16) - offset to string storage from start of table
  # Header is 6 bytes, each name record is 12 bytes
  string_data_offset = 6 + (name_records.size * 12)
  data << [string_data_offset].pack("n")

  # Write name records
  platform_id = 3  # Windows
  encoding_id = 1  # Unicode BMP
  language_id = 0x0409 # US English

  name_records.each do |record|
    data << [platform_id].pack("n")           # platform ID
    data << [encoding_id].pack("n")           # encoding ID
    data << [language_id].pack("n")           # language ID
    data << [record[:name_id]].pack("n")      # name ID
    data << [record[:encoded].bytesize].pack("n") # string length
    data << [record[:offset]].pack("n") # string offset
  end

  # Write string storage
  data << string_storage

  data
end

#build_os2_table(font) ⇒ String

Build OS/2 table from Type 1 font

Parameters:

Returns:

  • (String)

    OS/2 table binary data



803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
# File 'lib/fontisan/converters/type1_converter.rb', line 803

def build_os2_table(font)
  data = (+"").b

  # Get font metadata from Type1Font
  font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
  blue_values = font.private_dict&.blue_values || []
  font_info = font.font_dictionary&.font_info || {}
  weight = font_info.weight || "Medium"

  # Determine weight class (100-900)
  # Order matters - more specific patterns must come first
  weight_class = case weight.to_s.downcase
                 when /thin/ then 100
                 when /extralight/ then 200
                 when /light/ then 300
                 when /regular|normal/ then 400
                 when /medium/ then 400
                 when /semibold|semib/ then 600
                 when /extrabold/ then 800
                 when /bold/ then 700
                 when /black|heavy/ then 900
                 else 400
                 end

  # Version (uint16) - Use version 4 for modern fonts
  data << [4].pack("n")

  # xAvgCharWidth (int16) - Average character width
  # Use font width estimate
  avg_width = ((font_bbox[2] - font_bbox[0]) * 0.5).to_i
  data << [avg_width].pack("s>")

  # usWeightClass (uint16)
  data << [weight_class].pack("n")

  # usWidthClass (uint16) - 1 = Ultra-condensed to 9 = Ultra-expanded
  # Default to 5 (Medium)
  data << [5].pack("n")

  # fsType (uint16) - Embedding permissions
  # 0 = Installable embedding, 8 = Restricted (use 0 as default)
  data << [0].pack("n")

  # ySubscriptXSize (int16)
  data << [650].pack("s>")

  # ySubscriptYSize (int16)
  data << [600].pack("s>")

  # ySubscriptXOffset (int16)
  data << [0].pack("s>")

  # ySubscriptYOffset (int16)
  data << [75].pack("s>")

  # ySuperscriptXSize (int16)
  data << [650].pack("s>")

  # ySuperscriptYSize (int16)
  data << [600].pack("s>")

  # ySuperscriptXOffset (int16)
  data << [0].pack("s>")

  # ySuperscriptYOffset (int16)
  data << [350].pack("s>")

  # yStrikeoutSize (int16)
  data << [50].pack("s>")

  # yStrikeoutPosition (int16)
  data << [300].pack("s>")

  # sFamilyClass (int16) - Family class and subclass
  # 0 = No classification
  data << [0].pack("s>")

  # PANOSE (10 bytes) - Use default Latin Text family
  # Family: 2 (Text and Display), Serif Style: 11 (Normal Sans)
  panose = [
    2,   # Family kind: Latin Text
    11,  # Serif style: Normal Sans
    5,   # Weight: Medium
    5,   # Proportion: Modern
    2,   # Contrast: Medium Low
    5,   # Stroke variation: Medium
    5,   # Arm style: Straight arms/serifs
    5,   # Letter form: Normal
    4,   # Midline: Standard
    3,   # X-height: Medium
  ]
  data << panose.pack("C*")

  # Unicode ranges (4 x uint32) - Basic Latin + Latin-1
  # Bits 0-31: Basic Latin, Latin-1, Latin Extended-A/B, etc.
  data << [0x00000001].pack("N") # Basic Latin (0-7F)
  data << [0x00000000].pack("N")
  data << [0x00000000].pack("N")
  data << [0x00000000].pack("N")

  # achVendID (4 bytes) - Vendor ID
  data << "UKWN" # Unknown

  # fsSelection (uint16) - Font selection flags
  # Bit 6 (0x40) = Regular weight if 400-500
  fs_selection = if weight_class >= 400 && weight_class <= 500
                   0x40  # REGULAR
                 elsif weight_class >= 700
                   0x20  # BOLD
                 else
                   0
                 end
  data << [fs_selection].pack("n")

  # usFirstCharIndex (uint16) - First Unicode character
  data << [32].pack("n") # Space

  # usLastCharIndex (uint16) - Last Unicode character
  data << [0xFFFD].pack("n") # Replacement character

  # sTypoAscender (int16) - Use BlueValues or font bbox
  typo_ascender = if blue_values.length >= 4
                    blue_values[3]
                  else
                    font_bbox[3]
                  end
  data << [typo_ascender].pack("s>")

  # sTypoDescender (int16) - Use BlueValues or font bbox (negative)
  typo_descender = if blue_values.length >= 2
                     blue_values[0]
                   else
                     font_bbox[1]
                   end
  data << [typo_descender].pack("s>")

  # sTypoLineGap (int16)
  data << [0].pack("s>")

  # usWinAscent (uint16)
  data << [[font_bbox[3], 1000].max].pack("n")

  # usWinDescent (uint16)
  data << [[-font_bbox[1], 200].max].pack("n")

  # ulCodePageRange1 (uint32) - Latin 1
  data << [0x00000001].pack("N")

  # ulCodePageRange2 (uint32)
  data << [0x00000000].pack("N")

  # sxHeight (int16) - x-height, approximate as 500 for 1000 UPM
  data << [500].pack("s>")

  # sCapHeight (int16) - Cap height, approximate as 700 for 1000 UPM
  data << [700].pack("s>")

  # usDefaultChar (uint16)
  data << [0].pack("n")

  # usBreakChar (uint16) - Space
  data << [32].pack("n")

  # usMaxContext (uint16)
  data << [0].pack("n")

  data
end

#build_post_table(font) ⇒ String

Build post table from Type 1 font

Parameters:

Returns:

  • (String)

    post table binary data



976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
# File 'lib/fontisan/converters/type1_converter.rb', line 976

def build_post_table(font)
  data = (+"").b

  # Get font metadata from Type1Font
  font_info = font.font_dictionary&.font_info || {}

  # Version (Fixed 16.16) - Use version 3.0 for CFF fonts (no glyph names)
  # Version 2.0 would include glyph names, but for OTF output version 3.0 is fine
  # since CFF table contains the glyph names
  data << [0x00030000].pack("N") # Version 3.0

  # Italic Angle (Fixed 16.16)
  # Get from FontInfo if available, otherwise default to 0
  italic_angle = font_info.italic_angle || 0
  angle_raw = (italic_angle * 65_536).to_i
  data << [angle_raw].pack("N")

  # Underline Position (int16)
  underline_position = font_info.underline_position || -100
  data << [underline_position].pack("s>")

  # Underline Thickness (int16)
  underline_thickness = font_info.underline_thickness || 50
  data << [underline_thickness].pack("s>")

  # Fixed Pitch (uint32) - Boolean for monospace
  is_fixed_pitch = font_info.is_fixed_pitch || false ? 1 : 0
  data << [is_fixed_pitch].pack("N")

  # Min/Max Memory for Type 42 (uint32 each) - Not used for CFF, set to 0
  data << [0].pack("N")  # min_mem_type42
  data << [0].pack("N")  # max_mem_type42

  # Min/Max Memory for Type 1 (uint32 each) - Not used for CFF, set to 0
  data << [0].pack("N")  # min_mem_type1
  data << [0].pack("N")  # max_mem_type1

  data
end

#build_private_dict_hash(private_dict) ⇒ Hash

Build Type 1 Private dictionary hash from CFF Private dict

Parameters:

Returns:

  • (Hash)

    Private dictionary as hash for Type 1



488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
# File 'lib/fontisan/converters/type1_converter.rb', line 488

def build_private_dict_hash(private_dict)
  return {} unless private_dict

  {
    nominal_width: private_dict.nominal_width,
    default_width: private_dict.default_width,
    blue_values: private_dict.blue_values || [],
    other_blues: private_dict.other_blues || [],
    family_blues: private_dict.family_blues || [],
    family_other_blues: private_dict.family_other_blues || [],
    blue_scale: private_dict.blue_scale || 0.039625,
    blue_shift: private_dict.blue_shift || 7,
    blue_fuzz: private_dict.blue_fuzz || 1,
    std_hw: private_dict.std_hw || 0,
    std_vw: private_dict.std_vw || 0,
    stem_snap_h: private_dict.stem_snap_h || [],
    stem_snap_v: private_dict.stem_snap_v || [],
    force_bold: private_dict.force_bold || false,
    language_group: private_dict.language_group || 0,
    expansion_factor: private_dict.expansion_factor || 0.06,
    initial_random_seed: private_dict.initial_random_seed || 0,
  }
end

#build_type1_data(_font, _charstrings, _cff_table) ⇒ Hash

Build Type 1 font data

Parameters:

  • font (OpenTypeFont)

    Source OpenType font

  • charstrings (Hash)

    Type 1 CharStrings

  • cff_table (Tables::Cff)

    CFF table for metadata

Returns:

  • (Hash)

    Type 1 font data with :pfb key



518
519
520
521
522
523
524
525
526
527
528
529
530
531
# File 'lib/fontisan/converters/type1_converter.rb', line 518

def build_type1_data(_font, _charstrings, _cff_table)
  # Build PFB format
  # This is a placeholder implementation
  # Full implementation requires:
  # 1. Build Font Dictionary
  # 2. Build Private Dictionary
  # 3. Build CharStrings
  # 4. Encrypt with eexec
  # 5. Format as PFB chunks

  pfb_data = String.new(encoding: Encoding::ASCII_8BIT)

  { pfb: pfb_data }
end

#calculate_cmap4_length(segments) ⇒ Integer

Calculate length for format 4 subtable

Parameters:

  • segments (Array<Hash>)

    Segment definitions

Returns:

  • (Integer)

    Estimated length



1190
1191
1192
1193
1194
1195
1196
1197
1198
# File 'lib/fontisan/converters/type1_converter.rb', line 1190

def calculate_cmap4_length(segments)
  # Header: 14 bytes
  # Arrays: seg_count * 2 bytes each
  # Glyph ID array: variable
  seg_count = segments.length

  # Rough estimate (actual calculation done during construction)
  14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
end

#convert(font, options = {}) ⇒ Hash<String, String>

Convert font to target format

Parameters:

Options Hash (options):

  • :target_format (Symbol)

    Target format override

  • :options (ConversionOptions)

    ConversionOptions object

Returns:

  • (Hash<String, String>)

    Map of table tags to binary data



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/fontisan/converters/type1_converter.rb', line 65

def convert(font, options = {})
  # Extract ConversionOptions if provided
  conv_options = extract_conversion_options(options)

  target_format = options[:target_format] || conv_options&.to || @target_format ||
    detect_target_format(font)
  validate(font, target_format)

  # Apply opening options to source font
  apply_opening_options(font, conv_options) if conv_options

  source_format = detect_format(font)

  case [source_format, target_format]
  when %i[type1 otf]
    convert_type1_to_otf(font, conv_options)
  when %i[otf type1]
    convert_otf_to_type1(font, conv_options)
  when %i[type1 ttf]
    convert_type1_to_ttf(font, conv_options)
  when %i[ttf type1]
    convert_ttf_to_type1(font, conv_options)
  else
    raise Fontisan::Error,
          "Unsupported conversion: #{source_format}#{target_format}"
  end
end

#convert_otf_to_type1(font, _options = {}) ⇒ Hash<String, String>

Convert OpenType/CFF font to Type 1

Parameters:

  • font (OpenTypeFont)

    Source OpenType font

  • options (Hash)

    Conversion options

Returns:

  • (Hash<String, String>)

    Type 1 font data as PFB

Raises:



313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/fontisan/converters/type1_converter.rb', line 313

def convert_otf_to_type1(font, _options = {})
  # Extract CFF table
  cff_table = font.table("CFF ")
  raise Fontisan::Error, "CFF table not found" unless cff_table

  # Get CharStrings INDEX from CFF
  charstrings_index = cff_table.charstrings_index(0)
  unless charstrings_index
    raise Fontisan::Error,
          "CharStrings INDEX not found"
  end

  # Get Private DICT for context
  private_dict = cff_table.private_dict(0)

  # Create CFF to Type 1 converter
  converter = Type1::CffToType1Converter.new(
    nominal_width: private_dict&.nominal_width || 0,
    default_width: private_dict&.default_width || 0,
  )

  # Convert each CFF CharString to Type 1 format
  type1_charstrings = {}
  glyph_count = charstrings_index.count

  glyph_count.times do |glyph_index|
    # Get raw CFF CharString data
    cff_charstring = charstrings_index[glyph_index]
    next unless cff_charstring

    # Get glyph name
    glyph_name = font.glyph_name(glyph_index) || "glyph#{glyph_index}"

    # Convert CFF CharString to Type 1 format
    private_dict_hash = build_private_dict_hash(private_dict)
    type1_charstrings[glyph_name] = converter.convert(
      cff_charstring,
      private_dict: private_dict_hash,
    )
  end

  # Build Type 1 font data
  build_type1_data(font, type1_charstrings, cff_table)
end

#convert_ttf_to_type1(font) ⇒ Hash<String, String>

Convert TrueType font to Type 1 (via OTF)

Parameters:

Returns:

  • (Hash<String, String>)

    Type 1 font data as PFB



388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
# File 'lib/fontisan/converters/type1_converter.rb', line 388

def convert_ttf_to_type1(font)
  # First use OutlineConverter to convert TTF to OTF
  outline_converter = OutlineConverter.new(
    optimize_cff: @optimize_cff,
    preserve_hints: @preserve_hints,
    target_format: :otf,
  )

  otf_tables = outline_converter.convert(font, target_format: :otf)

  # Create a temporary OTF font object
  temp_otf = OpenTypeFont.new
  otf_tables.each do |tag, data|
    temp_otf.tables[tag] = data
  end

  # Then convert OTF to Type 1
  convert_otf_to_type1(temp_otf)
end

#convert_type1_to_otf(font, _options = {}) ⇒ Hash<String, String>

Convert Type 1 font to OpenType/CFF

Parameters:

  • font (Type1Font)

    Source Type 1 font

  • options (Hash)

    Conversion options

Returns:

  • (Hash<String, String>)

    Target tables including CFF table



257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
# File 'lib/fontisan/converters/type1_converter.rb', line 257

def convert_type1_to_otf(font, _options = {})
  # Convert Type 1 CharStrings to CFF format
  converter = Type1::CharStringConverter.new(font.charstrings)
  cff_charstrings = {}

  font.charstrings.each_charstring do |glyph_name, charstring|
    cff_charstrings[glyph_name] = converter.convert(charstring)
  end

  # Build font dictionary for CFF
  font_dict = build_cff_font_dict(font)

  # Build private dictionary for CFF
  private_dict = build_cff_private_dict(font)

  # Build CFF table
  # Note: This is a simplified implementation
  # A full implementation would build proper CFF INDEX structures
  cff_data = build_cff_table_data(font, cff_charstrings, font_dict,
                                  private_dict)

  # Build other required SFNT tables
  tables = {}

  # Build head table
  tables["head"] = build_head_table(font)

  # Build hhea table
  tables["hhea"] = build_hhea_table(font)

  # Build maxp table
  tables["maxp"] = build_maxp_table(font)

  # Build name table
  tables["name"] = build_name_table(font)

  # Build OS/2 table
  tables["OS/2"] = build_os2_table(font)

  # Build post table
  tables["post"] = build_post_table(font)

  # Build cmap table
  tables["cmap"] = build_cmap_table(font)

  # Add CFF table
  tables["CFF "] = cff_data

  tables
end

#convert_type1_to_ttf(font, options = {}) ⇒ Hash<String, String>

Convert Type 1 font to TrueType (via OTF)

Parameters:

  • font (Type1Font)

    Source Type 1 font

  • options (Hash) (defaults to: {})

    Conversion options

Returns:

  • (Hash<String, String>)

    Target tables including glyf table



363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/fontisan/converters/type1_converter.rb', line 363

def convert_type1_to_ttf(font, options = {})
  # First convert to OTF
  otf_tables = convert_type1_to_otf(font, options)

  # Then use OutlineConverter to convert OTF to TTF
  # Create a temporary OTF font object
  temp_otf = OpenTypeFont.new
  otf_tables.each do |tag, data|
    temp_otf.tables[tag] = data
  end

  # Use OutlineConverter for OTF → TTF
  outline_converter = OutlineConverter.new(
    optimize_cff: @optimize_cff,
    preserve_hints: @preserve_hints,
    target_format: :ttf,
  )

  outline_converter.convert(temp_otf, target_format: :ttf)
end

#decompose_seac_glyphs(font) ⇒ Object

Decompose seac composite glyphs to base glyphs

Parameters:



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/fontisan/converters/type1_converter.rb', line 182

def decompose_seac_glyphs(font)
  return unless font.charstrings

  # Create SeacExpander to decompose composite glyphs
  expander = Type1::SeacExpander.new(font.charstrings, font.private_dict)

  # Get all composite glyphs
  composites = expander.composite_glyphs
  return if composites.empty?

  # Decompose each composite glyph
  composites.each do |glyph_name|
    decomposed = expander.decompose(glyph_name)
    next if decomposed.nil? || decomposed.empty?

    # Update the CharString with decomposed version
    # Access the charstrings hash directly and update
    charstrings_hash = font.charstrings.charstrings
    charstrings_hash[glyph_name] = decomposed

    # Mark as decomposed (no longer a seac composite)
    # The decomposed CharString no longer contains the seac operator
  end
end

#detect_format(font) ⇒ Symbol

Detect font format

Parameters:

Returns:

  • (Symbol)

    Font format (:type1, :ttf, :otf)



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
# File 'lib/fontisan/converters/type1_converter.rb', line 211

def detect_format(font)
  case font
  when Type1Font
    :type1
  when TrueTypeFont
    :ttf
  when OpenTypeFont
    :otf
  else
    # Try to detect from tables
    if font.is_a?(SfntSource)
      if font.tables.key?("glyf")
        :ttf
      elsif font.tables.key?("CFF ") || font.tables.key?("CFF2")
        :otf
      else
        raise Fontisan::Error, "Cannot detect font format"
      end
    else
      raise Fontisan::Error, "Unknown font type: #{font.class}"
    end
  end
end

#detect_target_format(font) ⇒ Symbol

Detect target format from font class or options

Parameters:

Returns:

  • (Symbol)

    Target format



239
240
241
242
243
244
245
246
247
248
249
250
# File 'lib/fontisan/converters/type1_converter.rb', line 239

def detect_target_format(font)
  case font
  when Type1Font
    :otf # Default: Type 1 → OTF
  when TrueTypeFont
    :type1 # TTF → Type 1
  when OpenTypeFont
    :type1 # OTF → Type 1
  else
    :otf
  end
end

#extract_conversion_options(options) ⇒ ConversionOptions?

Extract ConversionOptions from options hash

Parameters:

Returns:



135
136
137
138
139
# File 'lib/fontisan/converters/type1_converter.rb', line 135

def extract_conversion_options(options)
  return options if options.is_a?(ConversionOptions)

  options[:options] if options.is_a?(Hash)
end

#extract_font_name(font) ⇒ String

Override extract_font_name to handle Type1Font

Parameters:

Returns:

  • (String)

    Font name



473
474
475
476
477
478
479
480
481
482
# File 'lib/fontisan/converters/type1_converter.rb', line 473

def extract_font_name(font)
  if font.is_a?(Type1Font)
    # Get font name from Type1Font
    name = font.font_name || font.font_dictionary&.font_name
    return name.dup.force_encoding("ASCII-8BIT") if name
  end

  # Fall back to original implementation for TrueTypeFont/OpenTypeFont
  super
end

#generate_unicode_mappings(_font) ⇒ Object

Generate Unicode codepoints from glyph names/encoding

Parameters:



168
169
170
171
172
173
174
175
176
177
# File 'lib/fontisan/converters/type1_converter.rb', line 168

def generate_unicode_mappings(_font)
  # Placeholder: Generate Unicode mappings from glyph names
  # A full implementation would:
  # 1. Parse the Adobe Glyph List
  # 2. Map glyph names to Unicode codepoints
  # 3. Update the charstrings encoding
  #
  # For now, this is a no-op placeholder
  nil
end

#supported_conversionsArray<Array<Symbol>>

Get supported conversions

Returns:

  • (Array<Array<Symbol>>)

    Supported conversion pairs



96
97
98
99
100
101
102
103
# File 'lib/fontisan/converters/type1_converter.rb', line 96

def supported_conversions
  [
    %i[type1 otf],
    %i[otf type1],
    %i[type1 ttf],
    %i[ttf type1],
  ]
end

#validate(font, target_format) ⇒ Boolean

Validate font for conversion

Parameters:

Returns:

  • (Boolean)

    True if valid

Raises:

  • (ArgumentError)

    If font is invalid

  • (Error)

    If conversion is not supported



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/fontisan/converters/type1_converter.rb', line 112

def validate(font, target_format)
  raise ArgumentError, "Font cannot be nil" if font.nil?

  unless font.is_a?(Type1Font) || font.is_a?(SfntSource)
    raise ArgumentError,
          "Font must be a Type1Font or SfntSource instance"
  end

  source_format = detect_format(font)
  unless supports?(source_format, target_format)
    raise Fontisan::Error,
          "Conversion #{source_format}#{target_format} not supported"
  end

  true
end