Class: Fontisan::Converters::Type1Converter
- Inherits:
-
Object
- Object
- Fontisan::Converters::Type1Converter
- 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
Instance Method Summary collapse
-
#apply_opening_options(font, conv_options) ⇒ Object
Apply opening options to source font.
-
#build_cff_font_dict(font) ⇒ Hash
Build CFF font dictionary from Type 1 font.
-
#build_cff_private_dict(font) ⇒ Hash
Build CFF private dictionary from Type 1 font.
-
#build_cff_table_data(font, charstrings, _font_dict, _private_dict) ⇒ String
Build CFF table data.
-
#build_cmap_format_4(unicode_to_glyph) ⇒ String
Build cmap format 4 subtable.
-
#build_cmap_table(font) ⇒ String
Build cmap table from Type 1 font.
-
#build_head_table(font) ⇒ String
Build head table from Type 1 font.
-
#build_hhea_table(font) ⇒ String
Build hhea table from Type 1 font.
-
#build_maxp_table(font) ⇒ String
Build maxp table from Type 1 font.
-
#build_name_table(font) ⇒ String
Build name table from Type 1 font.
-
#build_os2_table(font) ⇒ String
Build OS/2 table from Type 1 font.
-
#build_post_table(font) ⇒ String
Build post table from Type 1 font.
-
#build_private_dict_hash(private_dict) ⇒ Hash
Build Type 1 Private dictionary hash from CFF Private dict.
-
#build_type1_data(_font, _charstrings, _cff_table) ⇒ Hash
Build Type 1 font data.
-
#calculate_cmap4_length(segments) ⇒ Integer
Calculate length for format 4 subtable.
-
#convert(font, options = {}) ⇒ Hash<String, String>
Convert font to target format.
-
#convert_otf_to_type1(font, _options = {}) ⇒ Hash<String, String>
Convert OpenType/CFF font to Type 1.
-
#convert_ttf_to_type1(font) ⇒ Hash<String, String>
Convert TrueType font to Type 1 (via OTF).
-
#convert_type1_to_otf(font, _options = {}) ⇒ Hash<String, String>
Convert Type 1 font to OpenType/CFF.
-
#convert_type1_to_ttf(font, options = {}) ⇒ Hash<String, String>
Convert Type 1 font to TrueType (via OTF).
-
#decompose_seac_glyphs(font) ⇒ Object
Decompose seac composite glyphs to base glyphs.
-
#detect_format(font) ⇒ Symbol
Detect font format.
-
#detect_target_format(font) ⇒ Symbol
Detect target format from font class or options.
-
#extract_conversion_options(options) ⇒ ConversionOptions?
Extract ConversionOptions from options hash.
-
#extract_font_name(font) ⇒ String
Override extract_font_name to handle Type1Font.
-
#generate_unicode_mappings(_font) ⇒ Object
Generate Unicode codepoints from glyph names/encoding.
-
#initialize(options = {}) ⇒ Type1Converter
constructor
Initialize a new Type1Converter.
-
#supported_conversions ⇒ Array<Array<Symbol>>
Get supported conversions.
-
#validate(font, target_format) ⇒ Boolean
Validate font for conversion.
Methods included from CffTableBuilder
Methods included from ConversionStrategy
Constructor Details
#initialize(options = {}) ⇒ Type1Converter
Initialize a new Type1Converter
52 53 54 55 56 |
# File 'lib/fontisan/converters/type1_converter.rb', line 52 def initialize( = {}) @optimize_cff = .fetch(:optimize_cff, false) @preserve_hints = .fetch(:preserve_hints, true) @target_format = [:target_format] end |
Instance Method Details
#apply_opening_options(font, conv_options) ⇒ Object
Apply opening options to source font
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 (font, ) return unless font.is_a?(Type1Font) return unless # Generate Unicode codepoints if requested if .opening_option?(:generate_unicode) generate_unicode_mappings(font) end # Decompose seac composites if requested if .opening_option?(:decompose_composites) decompose_seac_glyphs(font) end # Read all font dictionary records if requested. if .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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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, = {}) # Extract ConversionOptions if provided = () target_format = [:target_format] || &.to || @target_format || detect_target_format(font) validate(font, target_format) # Apply opening options to source font (font, ) if source_format = detect_format(font) case [source_format, target_format] when %i[type1 otf] convert_type1_to_otf(font, ) when %i[otf type1] convert_otf_to_type1(font, ) when %i[type1 ttf] convert_type1_to_ttf(font, ) when %i[ttf type1] convert_ttf_to_type1(font, ) 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
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, = {}) # 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)
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
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, = {}) # 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)
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, = {}) # First convert to OTF otf_tables = convert_type1_to_otf(font, ) # 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
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 = Type1::SeacExpander.new(font.charstrings, font.private_dict) # Get all composite glyphs composites = .composite_glyphs return if composites.empty? # Decompose each composite glyph composites.each do |glyph_name| decomposed = .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
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
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
135 136 137 138 139 |
# File 'lib/fontisan/converters/type1_converter.rb', line 135 def () return if .is_a?(ConversionOptions) [:options] if .is_a?(Hash) end |
#extract_font_name(font) ⇒ String
Override extract_font_name to handle Type1Font
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
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_conversions ⇒ Array<Array<Symbol>>
Get supported conversions
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
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 |