Class: Fontist::FontFile
- Inherits:
-
Object
- Object
- Fontist::FontFile
- Defined in:
- lib/fontist/font_file.rb
Class Method Summary collapse
-
.check_extension_warning(path, is_collection) ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity.
-
.decode_name_record_manually(name_table, record) ⇒ Object
Manually decode a name record since the fontisan method is private.
-
.detect_font_format(path) ⇒ Object
rubocop:disable Metrics/MethodLength.
-
.extract_any_name(name_table, name_id) ⇒ Object
Extract any available name for a given name ID by manually decoding the name record.
-
.extract_font_info_from_path(path) ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/MethodLength.
-
.extract_names_from_font(font) ⇒ Object
rubocop:disable Metrics/MethodLength.
-
.from_content(content) ⇒ Object
rubocop:disable Metrics/MethodLength.
- .from_path(path) ⇒ Object
-
.load_indexability_validator ⇒ Object
rubocop:enable Metrics/MethodLength.
- .raise_font_file_error(exception) ⇒ Object
-
.validate_and_load_single_font(path) ⇒ Object
rubocop:disable Metrics/MethodLength.
Instance Method Summary collapse
- #family ⇒ Object
- #full_name ⇒ Object
-
#initialize(font_info, tempfile = nil) ⇒ FontFile
constructor
A new instance of FontFile.
- #preferred_family_name ⇒ Object
- #preferred_subfamily_name ⇒ Object
- #subfamily ⇒ Object
Constructor Details
#initialize(font_info, tempfile = nil) ⇒ FontFile
Returns a new instance of FontFile.
250 251 252 253 254 |
# File 'lib/fontist/font_file.rb', line 250 def initialize(font_info, tempfile = nil) @info = font_info # Keep tempfile alive to prevent GC issues on Windows @tempfile = tempfile end |
Class Method Details
.check_extension_warning(path, is_collection) ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
102 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 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 |
# File 'lib/fontist/font_file.rb', line 102 def check_extension_warning(path, is_collection) expected_ext = File.extname(path).downcase.sub(/^\./, "") # Determine actual format based on content actual_format = if is_collection # Use ttc as generic collection extension "ttc" else detect_font_format(path) end # Map common collection extensions to "ttc" collection_extensions = %w[ttc otc dfont] is_expected_collection = collection_extensions.include?(expected_ext) # Check for mismatch if is_collection && !is_expected_collection Fontist.ui.warn( # rubocop:disable Layout/LineLength "WARNING: File '#{File.basename(path)}' has extension '.#{expected_ext}' " \ "but appears to be a font collection (.ttc/.otc/.dfont). " \ "The file will be indexed, but consider renaming for clarity.", # rubocop:enable Layout/LineLength ) elsif !is_collection && is_expected_collection # File has collection extension but is actually a single font Fontist.ui.warn( # rubocop:disable Layout/LineLength "WARNING: File '#{File.basename(path)}' has collection extension '.#{expected_ext}' " \ "but appears to be a single font (.#{actual_format}). " \ "The file will be indexed, but consider renaming for clarity.", # rubocop:enable Layout/LineLength ) elsif !is_collection && expected_ext != actual_format # Single font with wrong format extension Fontist.ui.warn( # rubocop:disable Layout/LineLength "WARNING: File '#{File.basename(path)}' has extension '.#{expected_ext}' " \ "but appears to be a #{actual_format.upcase} font. " \ "The file will be indexed, but consider renaming for clarity.", # rubocop:enable Layout/LineLength ) end rescue StandardError => e # Don't fail indexing just because we can't detect the format # rubocop:disable Layout/LineLength Fontist.ui.debug("Could not detect file format for warning: #{e.}") # rubocop:enable Layout/LineLength end |
.decode_name_record_manually(name_table, record) ⇒ Object
Manually decode a name record since the fontisan method is private
218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/fontist/font_file.rb', line 218 def decode_name_record_manually(name_table, record) # Get raw string storage storage_bytes = name_table.string_storage.to_s.b # Extract this record's string offset = record.string_offset length = record.string_length return nil if offset + length > storage_bytes.bytesize return nil if length.zero? string_data = storage_bytes.byteslice(offset, length) # Decode based on platform (same logic as fontisan) case record.platform_id when 0, 3 # PLATFORM_UNICODE, PLATFORM_WINDOWS string_data.dup.force_encoding("UTF-16BE") .encode("UTF-8", invalid: :replace, undef: :replace) when 1 # PLATFORM_MACINTOSH string_data.dup.force_encoding("ASCII-8BIT") .encode("UTF-8", invalid: :replace, undef: :replace) else string_data.dup.force_encoding("UTF-8") end end |
.detect_font_format(path) ⇒ Object
rubocop:disable Metrics/MethodLength
154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 |
# File 'lib/fontist/font_file.rb', line 154 def detect_font_format(path) # Open file and check SFNT version to determine actual format File.open(path, "rb") do |io| signature = io.read(4) io.rewind case signature when "\x00\x01\x00\x00", "true" "ttf" when "OTTO" "otf" when "wOFF" "woff" when "wOF2" "woff2" else "unknown" end end rescue StandardError "unknown" end |
.extract_any_name(name_table, name_id) ⇒ Object
Extract any available name for a given name ID by manually decoding the name record
203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/fontist/font_file.rb', line 203 def extract_any_name(name_table, name_id) # Find any name record with the matching name_id records = name_table.name_records.select { |r| r.name_id == name_id } return nil if records.empty? # Manually decode each record records.each do |record| decoded = decode_name_record_manually(name_table, record) return decoded unless decoded.nil? || decoded.empty? end nil end |
.extract_font_info_from_path(path) ⇒ Object
rubocop:disable Metrics/AbcSize, Metrics/MethodLength
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
# File 'lib/fontist/font_file.rb', line 32 def extract_font_info_from_path(path) # rubocop:disable Layout/LineLength # First, detect if the file is actually a collection (regardless of extension) # rubocop:enable Layout/LineLength # This handles cases where .ttf files are actually .ttc collections is_collection = Fontisan::FontLoader.collection?(path) # Check for extension mismatch and issue warning check_extension_warning(path, is_collection) # For collections, we need different handling if is_collection # Load the first font in the collection font = Fontisan::FontLoader.load(path, font_index: 0, mode: :metadata, lazy: true) # Validate the font using indexability profile (log issues but don't reject) validator = load_indexability_validator validation_report = validator.validate(font) unless validation_report.valid? = validation_report.errors.map do |e| "#{e.category}: #{e.}" end.join("; ") Fontist.ui.debug( "Font validation issues for #{File.basename(path)}: #{}", ) end else # Single font - validate and load font = validate_and_load_single_font(path) end names = extract_names_from_font(font) font.close names rescue StandardError => e raise_font_file_error(e) end |
.extract_names_from_font(font) ⇒ Object
rubocop:disable Metrics/MethodLength
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 |
# File 'lib/fontist/font_file.rb', line 178 def extract_names_from_font(font) # Access name table directly name_table = font.table(Fontisan::Constants::NAME_TAG) return {} unless name_table # Extract all needed name strings using Fontisan's API # Fall back to non-English names if English names are not available { full_name: name_table.english_name(Fontisan::Tables::Name::FULL_NAME) || extract_any_name(name_table, Fontisan::Tables::Name::FULL_NAME), family_name: name_table.english_name(Fontisan::Tables::Name::FAMILY) || extract_any_name(name_table, Fontisan::Tables::Name::FAMILY), subfamily_name: name_table.english_name(Fontisan::Tables::Name::SUBFAMILY) || extract_any_name(name_table, Fontisan::Tables::Name::SUBFAMILY), preferred_family: name_table.english_name(Fontisan::Tables::Name::PREFERRED_FAMILY) || extract_any_name(name_table, Fontisan::Tables::Name::PREFERRED_FAMILY), preferred_subfamily: name_table.english_name(Fontisan::Tables::Name::PREFERRED_SUBFAMILY) || extract_any_name(name_table, Fontisan::Tables::Name::PREFERRED_SUBFAMILY), postscript_name: name_table.english_name(Fontisan::Tables::Name::POSTSCRIPT_NAME) || extract_any_name(name_table, Fontisan::Tables::Name::POSTSCRIPT_NAME), } end |
.from_content(content) ⇒ Object
rubocop:disable Metrics/MethodLength
13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/fontist/font_file.rb', line 13 def from_content(content) tmpfile = Tempfile.new(["font", ".ttf"]) tmpfile.binmode tmpfile.write(content) tmpfile.flush tmpfile.close font_info = extract_font_info_from_path(tmpfile.path) # Keep tempfile alive to prevent GC issues on Windows # rubocop:disable Layout/LineLength # Return both the font object and tempfile so caller can keep it referenced # rubocop:enable Layout/LineLength new(font_info, tmpfile) rescue StandardError => e raise_font_file_error(e) end |
.from_path(path) ⇒ Object
7 8 9 10 |
# File 'lib/fontist/font_file.rb', line 7 def from_path(path) font_info = extract_font_info_from_path(path) new(font_info) end |
.load_indexability_validator ⇒ Object
rubocop:enable Metrics/MethodLength
97 98 99 |
# File 'lib/fontist/font_file.rb', line 97 def load_indexability_validator Fontisan::Validators::ProfileLoader.load(:indexability) end |
.raise_font_file_error(exception) ⇒ Object
244 245 246 247 |
# File 'lib/fontist/font_file.rb', line 244 def raise_font_file_error(exception) raise Errors::FontFileError, "Font file could not be parsed: #{exception.inspect}." end |
.validate_and_load_single_font(path) ⇒ Object
rubocop:disable Metrics/MethodLength
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/fontist/font_file.rb', line 74 def validate_and_load_single_font(path) # Load font first (we need to validate it, not the file path) font = Fontisan::FontLoader.load(path, mode: :metadata, lazy: true) # Validate the font using indexability profile validator = load_indexability_validator validation_report = validator.validate(font) unless validation_report.valid? # Log validation issues but don't reject outright # We'll check if we can still extract metadata = validation_report.errors.map do |e| "#{e.category}: #{e.}" end.join("; ") Fontist.ui.debug( "Font validation issues for #{File.basename(path)}: #{}", ) end font end |
Instance Method Details
#family ⇒ Object
260 261 262 |
# File 'lib/fontist/font_file.rb', line 260 def family @info[:family_name] end |
#full_name ⇒ Object
256 257 258 |
# File 'lib/fontist/font_file.rb', line 256 def full_name @info[:full_name] end |
#preferred_family_name ⇒ Object
268 269 270 |
# File 'lib/fontist/font_file.rb', line 268 def preferred_family_name @info[:preferred_family] end |
#preferred_subfamily_name ⇒ Object
272 273 274 |
# File 'lib/fontist/font_file.rb', line 272 def preferred_subfamily_name @info[:preferred_subfamily] end |
#subfamily ⇒ Object
264 265 266 |
# File 'lib/fontist/font_file.rb', line 264 def subfamily @info[:subfamily_name] end |