fontisan 0.2.12 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop_todo.yml +185 -106
- data/Gemfile +5 -0
- data/README.adoc +3 -2
- data/docs/CONVERSION_GUIDE.adoc +633 -0
- data/docs/TYPE1_FONTS.adoc +445 -0
- data/lib/fontisan/commands/info_command.rb +83 -2
- data/lib/fontisan/converters/format_converter.rb +15 -5
- data/lib/fontisan/converters/type1_converter.rb +734 -59
- data/lib/fontisan/font_loader.rb +1 -1
- data/lib/fontisan/hints/hint_converter.rb +4 -1
- data/lib/fontisan/type1/cff_to_type1_converter.rb +302 -0
- data/lib/fontisan/type1/font_dictionary.rb +62 -0
- data/lib/fontisan/type1/pfa_generator.rb +31 -5
- data/lib/fontisan/type1/pfa_parser.rb +31 -30
- data/lib/fontisan/type1/pfb_generator.rb +28 -5
- data/lib/fontisan/type1/private_dict.rb +57 -0
- data/lib/fontisan/type1/seac_expander.rb +501 -0
- data/lib/fontisan/type1.rb +2 -0
- data/lib/fontisan/type1_font.rb +21 -34
- data/lib/fontisan/version.rb +1 -1
- metadata +6 -3
- data/docs/DOCUMENTATION_SUMMARY.md +0 -141
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "../conversion_options"
|
|
4
4
|
require_relative "../type1/charstring_converter"
|
|
5
|
+
require_relative "../type1/cff_to_type1_converter"
|
|
5
6
|
require_relative "../type1/font_dictionary"
|
|
6
7
|
require_relative "../type1/charstrings"
|
|
8
|
+
require_relative "../type1/seac_expander"
|
|
7
9
|
require_relative "../type1_font"
|
|
8
10
|
require_relative "cff_table_builder"
|
|
9
11
|
|
|
@@ -187,16 +189,29 @@ module Fontisan
|
|
|
187
189
|
# Decompose seac composite glyphs to base glyphs
|
|
188
190
|
#
|
|
189
191
|
# @param font [Type1Font] Source Type 1 font
|
|
190
|
-
def decompose_seac_glyphs(
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
#
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
192
|
+
def decompose_seac_glyphs(font)
|
|
193
|
+
return unless font.charstrings
|
|
194
|
+
|
|
195
|
+
# Create SeacExpander to decompose composite glyphs
|
|
196
|
+
expander = Type1::SeacExpander.new(font.charstrings, font.private_dict)
|
|
197
|
+
|
|
198
|
+
# Get all composite glyphs
|
|
199
|
+
composites = expander.composite_glyphs
|
|
200
|
+
return if composites.empty?
|
|
201
|
+
|
|
202
|
+
# Decompose each composite glyph
|
|
203
|
+
composites.each do |glyph_name|
|
|
204
|
+
decomposed = expander.decompose(glyph_name)
|
|
205
|
+
next if decomposed.nil? || decomposed.empty?
|
|
206
|
+
|
|
207
|
+
# Update the CharString with decomposed version
|
|
208
|
+
# Access the charstrings hash directly and update
|
|
209
|
+
charstrings_hash = font.charstrings.charstrings
|
|
210
|
+
charstrings_hash[glyph_name] = decomposed
|
|
211
|
+
|
|
212
|
+
# Mark as decomposed (no longer a seac composite)
|
|
213
|
+
# The decomposed CharString no longer contains the seac operator
|
|
214
|
+
end
|
|
200
215
|
end
|
|
201
216
|
|
|
202
217
|
# Detect font format
|
|
@@ -303,31 +318,48 @@ module Fontisan
|
|
|
303
318
|
# Convert OpenType/CFF font to Type 1
|
|
304
319
|
#
|
|
305
320
|
# @param font [OpenTypeFont] Source OpenType font
|
|
321
|
+
# @param options [Hash] Conversion options
|
|
306
322
|
# @return [Hash<String, String>] Type 1 font data as PFB
|
|
307
|
-
def convert_otf_to_type1(font)
|
|
323
|
+
def convert_otf_to_type1(font, _options = {})
|
|
308
324
|
# Extract CFF table
|
|
309
325
|
cff_table = font.table("CFF ")
|
|
310
326
|
raise Fontisan::Error, "CFF table not found" unless cff_table
|
|
311
327
|
|
|
312
|
-
#
|
|
313
|
-
|
|
314
|
-
|
|
328
|
+
# Get CharStrings INDEX from CFF
|
|
329
|
+
charstrings_index = cff_table.charstrings_index(0)
|
|
330
|
+
raise Fontisan::Error, "CharStrings INDEX not found" unless charstrings_index
|
|
315
331
|
|
|
316
|
-
#
|
|
332
|
+
# Get Private DICT for context
|
|
333
|
+
private_dict = cff_table.private_dict(0)
|
|
334
|
+
|
|
335
|
+
# Create CFF to Type 1 converter
|
|
336
|
+
converter = Type1::CffToType1Converter.new(
|
|
337
|
+
nominal_width: private_dict&.nominal_width || 0,
|
|
338
|
+
default_width: private_dict&.default_width || 0
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
# Convert each CFF CharString to Type 1 format
|
|
317
342
|
type1_charstrings = {}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
#
|
|
326
|
-
|
|
343
|
+
glyph_count = charstrings_index.count
|
|
344
|
+
|
|
345
|
+
glyph_count.times do |glyph_index|
|
|
346
|
+
# Get raw CFF CharString data
|
|
347
|
+
cff_charstring = charstrings_index[glyph_index]
|
|
348
|
+
next unless cff_charstring
|
|
349
|
+
|
|
350
|
+
# Get glyph name
|
|
351
|
+
glyph_name = font.glyph_name(glyph_index) || "glyph#{glyph_index}"
|
|
352
|
+
|
|
353
|
+
# Convert CFF CharString to Type 1 format
|
|
354
|
+
private_dict_hash = build_private_dict_hash(private_dict)
|
|
355
|
+
type1_charstrings[glyph_name] = converter.convert(
|
|
356
|
+
cff_charstring,
|
|
357
|
+
private_dict: private_dict_hash
|
|
358
|
+
)
|
|
327
359
|
end
|
|
328
360
|
|
|
329
361
|
# Build Type 1 font data
|
|
330
|
-
build_type1_data(font, type1_charstrings)
|
|
362
|
+
build_type1_data(font, type1_charstrings, cff_table)
|
|
331
363
|
end
|
|
332
364
|
|
|
333
365
|
# Convert Type 1 font to TrueType (via OTF)
|
|
@@ -456,28 +488,41 @@ module Fontisan
|
|
|
456
488
|
super
|
|
457
489
|
end
|
|
458
490
|
|
|
459
|
-
#
|
|
491
|
+
# Build Type 1 Private dictionary hash from CFF Private dict
|
|
460
492
|
#
|
|
461
|
-
# @param
|
|
462
|
-
# @return [
|
|
463
|
-
def
|
|
464
|
-
|
|
465
|
-
# This is a placeholder implementation
|
|
466
|
-
# Full implementation requires:
|
|
467
|
-
# 1. Parse CFF CharString to commands
|
|
468
|
-
# 2. Map CFF operators to Type 1 operators
|
|
469
|
-
# 3. Encode numbers in Type 1 format
|
|
470
|
-
# 4. Handle hints and subroutines
|
|
493
|
+
# @param private_dict [Tables::Cff::PrivateDict] CFF Private dict
|
|
494
|
+
# @return [Hash] Private dictionary as hash for Type 1
|
|
495
|
+
def build_private_dict_hash(private_dict)
|
|
496
|
+
return {} unless private_dict
|
|
471
497
|
|
|
472
|
-
|
|
498
|
+
{
|
|
499
|
+
nominal_width: private_dict.nominal_width,
|
|
500
|
+
default_width: private_dict.default_width,
|
|
501
|
+
blue_values: private_dict.blue_values || [],
|
|
502
|
+
other_blues: private_dict.other_blues || [],
|
|
503
|
+
family_blues: private_dict.family_blues || [],
|
|
504
|
+
family_other_blues: private_dict.family_other_blues || [],
|
|
505
|
+
blue_scale: private_dict.blue_scale || 0.039625,
|
|
506
|
+
blue_shift: private_dict.blue_shift || 7,
|
|
507
|
+
blue_fuzz: private_dict.blue_fuzz || 1,
|
|
508
|
+
std_hw: private_dict.std_hw || 0,
|
|
509
|
+
std_vw: private_dict.std_vw || 0,
|
|
510
|
+
stem_snap_h: private_dict.stem_snap_h || [],
|
|
511
|
+
stem_snap_v: private_dict.stem_snap_v || [],
|
|
512
|
+
force_bold: private_dict.force_bold || false,
|
|
513
|
+
language_group: private_dict.language_group || 0,
|
|
514
|
+
expansion_factor: private_dict.expansion_factor || 0.06,
|
|
515
|
+
initial_random_seed: private_dict.initial_random_seed || 0,
|
|
516
|
+
}
|
|
473
517
|
end
|
|
474
518
|
|
|
475
519
|
# Build Type 1 font data
|
|
476
520
|
#
|
|
477
521
|
# @param font [OpenTypeFont] Source OpenType font
|
|
478
522
|
# @param charstrings [Hash] Type 1 CharStrings
|
|
523
|
+
# @param cff_table [Tables::Cff] CFF table for metadata
|
|
479
524
|
# @return [Hash] Type 1 font data with :pfb key
|
|
480
|
-
def build_type1_data(_font, _charstrings)
|
|
525
|
+
def build_type1_data(_font, _charstrings, _cff_table)
|
|
481
526
|
# Build PFB format
|
|
482
527
|
# This is a placeholder implementation
|
|
483
528
|
# Full implementation requires:
|
|
@@ -496,63 +541,693 @@ module Fontisan
|
|
|
496
541
|
#
|
|
497
542
|
# @param font [Type1Font] Source Type 1 font
|
|
498
543
|
# @return [String] head table binary data
|
|
499
|
-
def build_head_table(
|
|
500
|
-
|
|
501
|
-
|
|
544
|
+
def build_head_table(font)
|
|
545
|
+
data = (+"").b
|
|
546
|
+
|
|
547
|
+
# Get font metadata from Type1Font
|
|
548
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
549
|
+
version_str = font.version || "001.000"
|
|
550
|
+
|
|
551
|
+
# Parse version (e.g., "001.000" => 1.0)
|
|
552
|
+
version_parts = version_str.split(".")
|
|
553
|
+
major = version_parts[0].to_i
|
|
554
|
+
minor = version_parts[1]&.to_i || 0
|
|
555
|
+
version = major + (minor / 1000.0)
|
|
556
|
+
|
|
557
|
+
# Version (Fixed 16.16) - stored as int32
|
|
558
|
+
integer_part = version.to_i
|
|
559
|
+
fractional_part = ((version - integer_part) * 65_536).to_i
|
|
560
|
+
version_raw = (integer_part << 16) | fractional_part
|
|
561
|
+
data << [version_raw].pack("N")
|
|
562
|
+
|
|
563
|
+
# Font Revision (Fixed 16.16) - default to 1.0
|
|
564
|
+
font_revision_raw = 0x00010000
|
|
565
|
+
data << [font_revision_raw].pack("N")
|
|
566
|
+
|
|
567
|
+
# Checksum Adjustment (uint32) - will be calculated later
|
|
568
|
+
data << [0].pack("N")
|
|
569
|
+
|
|
570
|
+
# Magic Number (uint32)
|
|
571
|
+
data << [0x5F0F3CF5].pack("N")
|
|
572
|
+
|
|
573
|
+
# Flags (uint16) - bit 0 indicates y direction (0 = mixed)
|
|
574
|
+
data << [0].pack("n")
|
|
575
|
+
|
|
576
|
+
# Units Per Em (uint16) - Type 1 standard is 1000
|
|
577
|
+
data << [1000].pack("n")
|
|
578
|
+
|
|
579
|
+
# Created (LONGDATETIME) - use current time
|
|
580
|
+
created_seconds = Time.now.to_i + 2_082_844_800
|
|
581
|
+
data << [created_seconds].pack("Q>")
|
|
582
|
+
|
|
583
|
+
# Modified (LONGDATETIME) - use current time
|
|
584
|
+
modified_seconds = Time.now.to_i + 2_082_844_800
|
|
585
|
+
data << [modified_seconds].pack("Q>")
|
|
586
|
+
|
|
587
|
+
# Bounding box (int16 each)
|
|
588
|
+
data << [font_bbox[0]].pack("s>") # x_min
|
|
589
|
+
data << [font_bbox[1]].pack("s>") # y_min
|
|
590
|
+
data << [font_bbox[2]].pack("s>") # x_max
|
|
591
|
+
data << [font_bbox[3]].pack("s>") # y_max
|
|
592
|
+
|
|
593
|
+
# Mac Style (uint16) - no style bits set
|
|
594
|
+
data << [0].pack("n")
|
|
595
|
+
|
|
596
|
+
# Lowest Rec PPEM (uint16) - readable size
|
|
597
|
+
data << [8].pack("n")
|
|
598
|
+
|
|
599
|
+
# Font Direction Hint (int16)
|
|
600
|
+
# 2 = Left to right, mixed glyphs
|
|
601
|
+
data << [2].pack("s>")
|
|
602
|
+
|
|
603
|
+
# Index To Loc Format (int16)
|
|
604
|
+
# 0 = short offsets (for CFF fonts we use this)
|
|
605
|
+
data << [0].pack("s>")
|
|
606
|
+
|
|
607
|
+
# Glyph Data Format (int16)
|
|
608
|
+
data << [0].pack("s>")
|
|
609
|
+
|
|
610
|
+
data
|
|
502
611
|
end
|
|
503
612
|
|
|
504
613
|
# Build hhea table from Type 1 font
|
|
505
614
|
#
|
|
506
615
|
# @param font [Type1Font] Source Type 1 font
|
|
507
616
|
# @return [String] hhea table binary data
|
|
508
|
-
def build_hhea_table(
|
|
509
|
-
|
|
510
|
-
|
|
617
|
+
def build_hhea_table(font)
|
|
618
|
+
data = (+"").b
|
|
619
|
+
|
|
620
|
+
# Get font metrics from Type1Font
|
|
621
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
622
|
+
blue_values = font.private_dict&.blue_values || []
|
|
623
|
+
|
|
624
|
+
# Version (Fixed 16.16) - 0x00010000 (1.0)
|
|
625
|
+
data << [0x00010000].pack("N")
|
|
626
|
+
|
|
627
|
+
# Ascent (int16) - Distance from baseline to highest ascender
|
|
628
|
+
# Use BlueValues[2] or [3] if available, otherwise font_bbox[3]
|
|
629
|
+
if blue_values.length >= 4
|
|
630
|
+
ascent = blue_values[3] # Top zone top
|
|
631
|
+
elsif blue_values.length >= 3
|
|
632
|
+
ascent = blue_values[2] # Top zone bottom
|
|
633
|
+
else
|
|
634
|
+
ascent = font_bbox[3] # y_max
|
|
635
|
+
end
|
|
636
|
+
data << [ascent].pack("s>")
|
|
637
|
+
|
|
638
|
+
# Descent (int16) - Distance from baseline to lowest descender (negative)
|
|
639
|
+
# Use BlueValues[0] or [1] if available, otherwise font_bbox[1]
|
|
640
|
+
if blue_values.length >= 2
|
|
641
|
+
descent = blue_values[0] # Bottom zone bottom (negative)
|
|
642
|
+
elsif blue_values.length >= 1
|
|
643
|
+
descent = blue_values[0]
|
|
644
|
+
else
|
|
645
|
+
descent = font_bbox[1] # y_min (should be negative)
|
|
646
|
+
end
|
|
647
|
+
data << [descent].pack("s>")
|
|
648
|
+
|
|
649
|
+
# Line Gap (int16) - Additional space between lines
|
|
650
|
+
# Use typical value of 0 for Type 1 fonts
|
|
651
|
+
data << [0].pack("s>")
|
|
652
|
+
|
|
653
|
+
# Advance Width Max (uint16)
|
|
654
|
+
# Type 1 standard is typically 1000, use font_bbox width + padding
|
|
655
|
+
advance_max = (font_bbox[2] - font_bbox[0]) + 100
|
|
656
|
+
data << [advance_max].pack("n")
|
|
657
|
+
|
|
658
|
+
# Min Left Side Bearing (int16)
|
|
659
|
+
# Use font_bbox[0] (x_min) as reasonable default
|
|
660
|
+
data << [font_bbox[0]].pack("s>")
|
|
661
|
+
|
|
662
|
+
# Min Right Side Bearing (int16)
|
|
663
|
+
# Estimate as 0 (will be updated if actual metrics available)
|
|
664
|
+
data << [0].pack("s>")
|
|
665
|
+
|
|
666
|
+
# x Max Extent (int16) - Max(lsb + xMax)
|
|
667
|
+
# Use font_bbox[2] (x_max) as reasonable default
|
|
668
|
+
data << [font_bbox[2]].pack("s>")
|
|
669
|
+
|
|
670
|
+
# Caret Slope Rise (int16)
|
|
671
|
+
# 1 for upright fonts (not italic)
|
|
672
|
+
data << [1].pack("s>")
|
|
673
|
+
|
|
674
|
+
# Caret Slope Run (int16)
|
|
675
|
+
# 0 for upright fonts
|
|
676
|
+
data << [0].pack("s>")
|
|
677
|
+
|
|
678
|
+
# Caret Offset (int16)
|
|
679
|
+
# Set to 0 for standard fonts
|
|
680
|
+
data << [0].pack("s>")
|
|
681
|
+
|
|
682
|
+
# Reserved (int64) - 8 bytes of zeros
|
|
683
|
+
data << [0, 0].pack("Q>")
|
|
684
|
+
|
|
685
|
+
# Metric Data Format (int16)
|
|
686
|
+
# 0 for current format
|
|
687
|
+
data << [0].pack("s>")
|
|
688
|
+
|
|
689
|
+
# Number of HMetrics (uint16)
|
|
690
|
+
# Number of glyphs with explicit metrics (typically all glyphs)
|
|
691
|
+
num_glyphs = font.charstrings&.count || 1
|
|
692
|
+
data << [[num_glyphs, 1].max].pack("n")
|
|
693
|
+
|
|
694
|
+
data
|
|
511
695
|
end
|
|
512
696
|
|
|
513
697
|
# Build maxp table from Type 1 font
|
|
514
698
|
#
|
|
515
699
|
# @param font [Type1Font] Source Type 1 font
|
|
516
700
|
# @return [String] maxp table binary data
|
|
517
|
-
def build_maxp_table(
|
|
518
|
-
|
|
519
|
-
|
|
701
|
+
def build_maxp_table(font)
|
|
702
|
+
data = (+"").b
|
|
703
|
+
|
|
704
|
+
# Get number of glyphs from Type1Font
|
|
705
|
+
num_glyphs = font.charstrings&.count || 1
|
|
706
|
+
|
|
707
|
+
# Version (Fixed 16.16)
|
|
708
|
+
# For CFF fonts (OTF output), use version 0.5 (0x00005000)
|
|
709
|
+
# For TrueType fonts (TTF output), would use version 1.0 (0x00010000)
|
|
710
|
+
# Type 1 fonts convert to CFF-based OTF, so use version 0.5
|
|
711
|
+
data << [0x00005000].pack("N")
|
|
712
|
+
|
|
713
|
+
# Number of Glyphs (uint16)
|
|
714
|
+
# Must be >= 1 (at minimum, .notdef must be present)
|
|
715
|
+
data << [[num_glyphs, 1].max].pack("n")
|
|
716
|
+
|
|
717
|
+
data
|
|
520
718
|
end
|
|
521
719
|
|
|
522
720
|
# Build name table from Type 1 font
|
|
523
721
|
#
|
|
524
722
|
# @param font [Type1Font] Source Type 1 font
|
|
525
723
|
# @return [String] name table binary data
|
|
526
|
-
def build_name_table(
|
|
527
|
-
#
|
|
528
|
-
|
|
724
|
+
def build_name_table(font)
|
|
725
|
+
# Get font metadata from Type1Font
|
|
726
|
+
font_dict = font.font_dictionary
|
|
727
|
+
font_info = font_dict&.font_info
|
|
728
|
+
|
|
729
|
+
# Extract font names with fallbacks
|
|
730
|
+
font_name = font.font_name || font_dict&.font_name || "Unnamed"
|
|
731
|
+
family_name = if font_info&.respond_to?(:family_name)
|
|
732
|
+
font_info.family_name || font_dict&.family_name || font_name
|
|
733
|
+
else
|
|
734
|
+
font_dict&.family_name || font_name
|
|
735
|
+
end
|
|
736
|
+
full_name = if font_info&.respond_to?(:full_name)
|
|
737
|
+
font_info.full_name || font_dict&.full_name || family_name
|
|
738
|
+
else
|
|
739
|
+
font_dict&.full_name || family_name
|
|
740
|
+
end
|
|
741
|
+
version = if font_info&.respond_to?(:version)
|
|
742
|
+
font_info.version || font.version || "001.000"
|
|
743
|
+
else
|
|
744
|
+
font.version || "001.000"
|
|
745
|
+
end
|
|
746
|
+
copyright = if font_info&.respond_to?(:copyright)
|
|
747
|
+
font_info.copyright || font_dict&.raw_data&.dig(:copyright) || ""
|
|
748
|
+
else
|
|
749
|
+
font_dict&.raw_data&.dig(:copyright) || ""
|
|
750
|
+
end
|
|
751
|
+
postscript_name = font_name
|
|
752
|
+
weight = if font_info&.respond_to?(:weight)
|
|
753
|
+
font_info.weight
|
|
754
|
+
else
|
|
755
|
+
"Regular"
|
|
756
|
+
end
|
|
757
|
+
notice = if font_info&.respond_to?(:notice)
|
|
758
|
+
font_info.notice
|
|
759
|
+
else
|
|
760
|
+
""
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Build name records (Windows Unicode, English US)
|
|
764
|
+
# Platform ID 3 (Windows), Encoding ID 1 (Unicode BMP), Language ID 0x0409 (US English)
|
|
765
|
+
name_records = [
|
|
766
|
+
# Copyright (name ID 0)
|
|
767
|
+
{ name_id: 0, string: copyright },
|
|
768
|
+
# Family Name (name ID 1)
|
|
769
|
+
{ name_id: 1, string: family_name },
|
|
770
|
+
# Subfamily Name (name ID 2) - derive from weight or default to Regular
|
|
771
|
+
{ name_id: 2, string: weight || "Regular" },
|
|
772
|
+
# Unique ID (name ID 3) - format: version;copyright;postscript_name
|
|
773
|
+
{ name_id: 3, string: "#{version};#{copyright};#{postscript_name}" },
|
|
774
|
+
# Full Name (name ID 4)
|
|
775
|
+
{ name_id: 4, string: full_name },
|
|
776
|
+
# Version (name ID 5)
|
|
777
|
+
{ name_id: 5, string: version },
|
|
778
|
+
# PostScript Name (name ID 6)
|
|
779
|
+
{ name_id: 6, string: postscript_name },
|
|
780
|
+
# Trademark (name ID 7) - use notice if available
|
|
781
|
+
{ name_id: 7, string: notice || "" },
|
|
782
|
+
]
|
|
783
|
+
|
|
784
|
+
# Filter out empty strings and build string storage
|
|
785
|
+
name_records = name_records.select { |r| !r[:string].nil? && !r[:string].empty? }
|
|
786
|
+
|
|
787
|
+
# Build string storage (UTF-16BE encoded for Windows platform)
|
|
788
|
+
string_storage = (+"").b
|
|
789
|
+
name_records.each do |record|
|
|
790
|
+
encoded_string = record[:string].encode("UTF-16BE").force_encoding("ASCII-8BIT")
|
|
791
|
+
record[:encoded] = encoded_string
|
|
792
|
+
record[:offset] = string_storage.bytesize
|
|
793
|
+
string_storage << encoded_string
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
# Build name table
|
|
797
|
+
data = (+"").b
|
|
798
|
+
|
|
799
|
+
# Format selector (uint16) - 0 for basic
|
|
800
|
+
data << [0].pack("n")
|
|
801
|
+
|
|
802
|
+
# Count (uint16) - number of name records
|
|
803
|
+
data << [name_records.size].pack("n")
|
|
804
|
+
|
|
805
|
+
# String offset (uint16) - offset to string storage from start of table
|
|
806
|
+
# Header is 6 bytes, each name record is 12 bytes
|
|
807
|
+
string_data_offset = 6 + (name_records.size * 12)
|
|
808
|
+
data << [string_data_offset].pack("n")
|
|
809
|
+
|
|
810
|
+
# Write name records
|
|
811
|
+
platform_id = 3 # Windows
|
|
812
|
+
encoding_id = 1 # Unicode BMP
|
|
813
|
+
language_id = 0x0409 # US English
|
|
814
|
+
|
|
815
|
+
name_records.each do |record|
|
|
816
|
+
data << [platform_id].pack("n") # platform ID
|
|
817
|
+
data << [encoding_id].pack("n") # encoding ID
|
|
818
|
+
data << [language_id].pack("n") # language ID
|
|
819
|
+
data << [record[:name_id]].pack("n") # name ID
|
|
820
|
+
data << [record[:encoded].bytesize].pack("n") # string length
|
|
821
|
+
data << [record[:offset]].pack("n") # string offset
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
# Write string storage
|
|
825
|
+
data << string_storage
|
|
826
|
+
|
|
827
|
+
data
|
|
529
828
|
end
|
|
530
829
|
|
|
531
830
|
# Build OS/2 table from Type 1 font
|
|
532
831
|
#
|
|
533
832
|
# @param font [Type1Font] Source Type 1 font
|
|
534
833
|
# @return [String] OS/2 table binary data
|
|
535
|
-
def build_os2_table(
|
|
536
|
-
|
|
537
|
-
|
|
834
|
+
def build_os2_table(font)
|
|
835
|
+
data = (+"").b
|
|
836
|
+
|
|
837
|
+
# Get font metadata from Type1Font
|
|
838
|
+
font_bbox = font.font_dictionary&.font_bbox || [0, 0, 1000, 1000]
|
|
839
|
+
blue_values = font.private_dict&.blue_values || []
|
|
840
|
+
font_info = font.font_dictionary&.font_info || {}
|
|
841
|
+
weight = font_info.weight || "Medium"
|
|
842
|
+
|
|
843
|
+
# Determine weight class (100-900)
|
|
844
|
+
# Order matters - more specific patterns must come first
|
|
845
|
+
weight_class = case weight.to_s.downcase
|
|
846
|
+
when /thin/ then 100
|
|
847
|
+
when /extralight/ then 200
|
|
848
|
+
when /light/ then 300
|
|
849
|
+
when /regular|normal/ then 400
|
|
850
|
+
when /medium/ then 400
|
|
851
|
+
when /semibold|semib/ then 600
|
|
852
|
+
when /extrabold/ then 800
|
|
853
|
+
when /bold/ then 700
|
|
854
|
+
when /black|heavy/ then 900
|
|
855
|
+
else 400
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
# Version (uint16) - Use version 4 for modern fonts
|
|
859
|
+
data << [4].pack("n")
|
|
860
|
+
|
|
861
|
+
# xAvgCharWidth (int16) - Average character width
|
|
862
|
+
# Use font width estimate
|
|
863
|
+
avg_width = ((font_bbox[2] - font_bbox[0]) * 0.5).to_i
|
|
864
|
+
data << [avg_width].pack("s>")
|
|
865
|
+
|
|
866
|
+
# usWeightClass (uint16)
|
|
867
|
+
data << [weight_class].pack("n")
|
|
868
|
+
|
|
869
|
+
# usWidthClass (uint16) - 1 = Ultra-condensed to 9 = Ultra-expanded
|
|
870
|
+
# Default to 5 (Medium)
|
|
871
|
+
data << [5].pack("n")
|
|
872
|
+
|
|
873
|
+
# fsType (uint16) - Embedding permissions
|
|
874
|
+
# 0 = Installable embedding, 8 = Restricted (use 0 as default)
|
|
875
|
+
data << [0].pack("n")
|
|
876
|
+
|
|
877
|
+
# ySubscriptXSize (int16)
|
|
878
|
+
data << [650].pack("s>")
|
|
879
|
+
|
|
880
|
+
# ySubscriptYSize (int16)
|
|
881
|
+
data << [600].pack("s>")
|
|
882
|
+
|
|
883
|
+
# ySubscriptXOffset (int16)
|
|
884
|
+
data << [0].pack("s>")
|
|
885
|
+
|
|
886
|
+
# ySubscriptYOffset (int16)
|
|
887
|
+
data << [75].pack("s>")
|
|
888
|
+
|
|
889
|
+
# ySuperscriptXSize (int16)
|
|
890
|
+
data << [650].pack("s>")
|
|
891
|
+
|
|
892
|
+
# ySuperscriptYSize (int16)
|
|
893
|
+
data << [600].pack("s>")
|
|
894
|
+
|
|
895
|
+
# ySuperscriptXOffset (int16)
|
|
896
|
+
data << [0].pack("s>")
|
|
897
|
+
|
|
898
|
+
# ySuperscriptYOffset (int16)
|
|
899
|
+
data << [350].pack("s>")
|
|
900
|
+
|
|
901
|
+
# yStrikeoutSize (int16)
|
|
902
|
+
data << [50].pack("s>")
|
|
903
|
+
|
|
904
|
+
# yStrikeoutPosition (int16)
|
|
905
|
+
data << [300].pack("s>")
|
|
906
|
+
|
|
907
|
+
# sFamilyClass (int16) - Family class and subclass
|
|
908
|
+
# 0 = No classification
|
|
909
|
+
data << [0].pack("s>")
|
|
910
|
+
|
|
911
|
+
# PANOSE (10 bytes) - Use default Latin Text family
|
|
912
|
+
# Family: 2 (Text and Display), Serif Style: 11 (Normal Sans)
|
|
913
|
+
panose = [
|
|
914
|
+
2, # Family kind: Latin Text
|
|
915
|
+
11, # Serif style: Normal Sans
|
|
916
|
+
5, # Weight: Medium
|
|
917
|
+
5, # Proportion: Modern
|
|
918
|
+
2, # Contrast: Medium Low
|
|
919
|
+
5, # Stroke variation: Medium
|
|
920
|
+
5, # Arm style: Straight arms/serifs
|
|
921
|
+
5, # Letter form: Normal
|
|
922
|
+
4, # Midline: Standard
|
|
923
|
+
3, # X-height: Medium
|
|
924
|
+
]
|
|
925
|
+
data << panose.pack("C*")
|
|
926
|
+
|
|
927
|
+
# Unicode ranges (4 x uint32) - Basic Latin + Latin-1
|
|
928
|
+
# Bits 0-31: Basic Latin, Latin-1, Latin Extended-A/B, etc.
|
|
929
|
+
data << [0x00000001].pack("N") # Basic Latin (0-7F)
|
|
930
|
+
data << [0x00000000].pack("N")
|
|
931
|
+
data << [0x00000000].pack("N")
|
|
932
|
+
data << [0x00000000].pack("N")
|
|
933
|
+
|
|
934
|
+
# achVendID (4 bytes) - Vendor ID
|
|
935
|
+
data << "UKWN" # Unknown
|
|
936
|
+
|
|
937
|
+
# fsSelection (uint16) - Font selection flags
|
|
938
|
+
# Bit 6 (0x40) = Regular weight if 400-500
|
|
939
|
+
fs_selection = if weight_class >= 400 && weight_class <= 500
|
|
940
|
+
0x40 # REGULAR
|
|
941
|
+
elsif weight_class >= 700
|
|
942
|
+
0x20 # BOLD
|
|
943
|
+
else
|
|
944
|
+
0
|
|
945
|
+
end
|
|
946
|
+
data << [fs_selection].pack("n")
|
|
947
|
+
|
|
948
|
+
# usFirstCharIndex (uint16) - First Unicode character
|
|
949
|
+
data << [32].pack("n") # Space
|
|
950
|
+
|
|
951
|
+
# usLastCharIndex (uint16) - Last Unicode character
|
|
952
|
+
data << [0xFFFD].pack("n") # Replacement character
|
|
953
|
+
|
|
954
|
+
# sTypoAscender (int16) - Use BlueValues or font bbox
|
|
955
|
+
if blue_values.length >= 4
|
|
956
|
+
typo_ascender = blue_values[3]
|
|
957
|
+
else
|
|
958
|
+
typo_ascender = font_bbox[3]
|
|
959
|
+
end
|
|
960
|
+
data << [typo_ascender].pack("s>")
|
|
961
|
+
|
|
962
|
+
# sTypoDescender (int16) - Use BlueValues or font bbox (negative)
|
|
963
|
+
if blue_values.length >= 2
|
|
964
|
+
typo_descender = blue_values[0]
|
|
965
|
+
else
|
|
966
|
+
typo_descender = font_bbox[1]
|
|
967
|
+
end
|
|
968
|
+
data << [typo_descender].pack("s>")
|
|
969
|
+
|
|
970
|
+
# sTypoLineGap (int16)
|
|
971
|
+
data << [0].pack("s>")
|
|
972
|
+
|
|
973
|
+
# usWinAscent (uint16)
|
|
974
|
+
data << [[font_bbox[3], 1000].max].pack("n")
|
|
975
|
+
|
|
976
|
+
# usWinDescent (uint16)
|
|
977
|
+
data << [[-font_bbox[1], 200].max].pack("n")
|
|
978
|
+
|
|
979
|
+
# ulCodePageRange1 (uint32) - Latin 1
|
|
980
|
+
data << [0x00000001].pack("N")
|
|
981
|
+
|
|
982
|
+
# ulCodePageRange2 (uint32)
|
|
983
|
+
data << [0x00000000].pack("N")
|
|
984
|
+
|
|
985
|
+
# sxHeight (int16) - x-height, approximate as 500 for 1000 UPM
|
|
986
|
+
data << [500].pack("s>")
|
|
987
|
+
|
|
988
|
+
# sCapHeight (int16) - Cap height, approximate as 700 for 1000 UPM
|
|
989
|
+
data << [700].pack("s>")
|
|
990
|
+
|
|
991
|
+
# usDefaultChar (uint16)
|
|
992
|
+
data << [0].pack("n")
|
|
993
|
+
|
|
994
|
+
# usBreakChar (uint16) - Space
|
|
995
|
+
data << [32].pack("n")
|
|
996
|
+
|
|
997
|
+
# usMaxContext (uint16)
|
|
998
|
+
data << [0].pack("n")
|
|
999
|
+
|
|
1000
|
+
data
|
|
538
1001
|
end
|
|
539
1002
|
|
|
540
1003
|
# Build post table from Type 1 font
|
|
541
1004
|
#
|
|
542
1005
|
# @param font [Type1Font] Source Type 1 font
|
|
543
1006
|
# @return [String] post table binary data
|
|
544
|
-
def build_post_table(
|
|
545
|
-
|
|
546
|
-
|
|
1007
|
+
def build_post_table(font)
|
|
1008
|
+
data = (+"").b
|
|
1009
|
+
|
|
1010
|
+
# Get font metadata from Type1Font
|
|
1011
|
+
font_info = font.font_dictionary&.font_info || {}
|
|
1012
|
+
|
|
1013
|
+
# Version (Fixed 16.16) - Use version 3.0 for CFF fonts (no glyph names)
|
|
1014
|
+
# Version 2.0 would include glyph names, but for OTF output version 3.0 is fine
|
|
1015
|
+
# since CFF table contains the glyph names
|
|
1016
|
+
data << [0x00030000].pack("N") # Version 3.0
|
|
1017
|
+
|
|
1018
|
+
# Italic Angle (Fixed 16.16)
|
|
1019
|
+
# Get from FontInfo if available, otherwise default to 0
|
|
1020
|
+
italic_angle = font_info.italic_angle || 0
|
|
1021
|
+
angle_raw = (italic_angle * 65_536).to_i
|
|
1022
|
+
data << [angle_raw].pack("N")
|
|
1023
|
+
|
|
1024
|
+
# Underline Position (int16)
|
|
1025
|
+
underline_position = font_info.underline_position || -100
|
|
1026
|
+
data << [underline_position].pack("s>")
|
|
1027
|
+
|
|
1028
|
+
# Underline Thickness (int16)
|
|
1029
|
+
underline_thickness = font_info.underline_thickness || 50
|
|
1030
|
+
data << [underline_thickness].pack("s>")
|
|
1031
|
+
|
|
1032
|
+
# Fixed Pitch (uint32) - Boolean for monospace
|
|
1033
|
+
is_fixed_pitch = (font_info.is_fixed_pitch || false) ? 1 : 0
|
|
1034
|
+
data << [is_fixed_pitch].pack("N")
|
|
1035
|
+
|
|
1036
|
+
# Min/Max Memory for Type 42 (uint32 each) - Not used for CFF, set to 0
|
|
1037
|
+
data << [0].pack("N") # min_mem_type42
|
|
1038
|
+
data << [0].pack("N") # max_mem_type42
|
|
1039
|
+
|
|
1040
|
+
# Min/Max Memory for Type 1 (uint32 each) - Not used for CFF, set to 0
|
|
1041
|
+
data << [0].pack("N") # min_mem_type1
|
|
1042
|
+
data << [0].pack("N") # max_mem_type1
|
|
1043
|
+
|
|
1044
|
+
data
|
|
547
1045
|
end
|
|
548
1046
|
|
|
549
1047
|
# Build cmap table from Type 1 font
|
|
550
1048
|
#
|
|
551
1049
|
# @param font [Type1Font] Source Type 1 font
|
|
552
1050
|
# @return [String] cmap table binary data
|
|
553
|
-
def build_cmap_table(
|
|
554
|
-
|
|
555
|
-
|
|
1051
|
+
def build_cmap_table(font)
|
|
1052
|
+
require_relative "../type1/agl"
|
|
1053
|
+
|
|
1054
|
+
data = (+"").b
|
|
1055
|
+
|
|
1056
|
+
# Get encoding from Type1Font
|
|
1057
|
+
encoding = font.charstrings&.encoding || {}
|
|
1058
|
+
glyph_names = font.charstrings&.glyph_names || encoding.keys
|
|
1059
|
+
|
|
1060
|
+
# Build Unicode mapping from glyph names using AGL
|
|
1061
|
+
unicode_to_glyph = {}
|
|
1062
|
+
glyph_index = 0
|
|
1063
|
+
|
|
1064
|
+
glyph_names.each do |glyph_name|
|
|
1065
|
+
# Get Unicode code point from AGL
|
|
1066
|
+
unicode = Type1::AGL.unicode_for_glyph_name(glyph_name)
|
|
1067
|
+
|
|
1068
|
+
# If no Unicode mapping, try to derive from encoding position
|
|
1069
|
+
if unicode.nil?
|
|
1070
|
+
# For standard encoding, try to map from position
|
|
1071
|
+
# This is a simplified approach - real implementation would be more robust
|
|
1072
|
+
unicode = glyph_index if glyph_index < 128
|
|
1073
|
+
end
|
|
1074
|
+
|
|
1075
|
+
# Map Unicode to glyph index
|
|
1076
|
+
if unicode && unicode <= 0xFFFF
|
|
1077
|
+
unicode_to_glyph[unicode] ||= glyph_index
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
glyph_index += 1
|
|
1081
|
+
end
|
|
1082
|
+
|
|
1083
|
+
# Ensure at least .notdef (glyph 0) maps to something
|
|
1084
|
+
unicode_to_glyph[0x0000] ||= 0
|
|
1085
|
+
|
|
1086
|
+
# Build Format 4 subtable (Segment mapping to delta values)
|
|
1087
|
+
# This is the most common format for BMP Unicode fonts
|
|
1088
|
+
subtable_data = build_cmap_format_4(unicode_to_glyph)
|
|
1089
|
+
|
|
1090
|
+
# Calculate offsets
|
|
1091
|
+
encoding_records_offset = 4 # After version (2) + num_tables (2)
|
|
1092
|
+
subtable_offset = encoding_records_offset + 8 # After one encoding record (8 bytes)
|
|
1093
|
+
|
|
1094
|
+
# Build cmap table header
|
|
1095
|
+
# Version (uint16)
|
|
1096
|
+
data << [0].pack("n")
|
|
1097
|
+
|
|
1098
|
+
# Number of encoding records (uint16)
|
|
1099
|
+
data << [1].pack("n") # One encoding record
|
|
1100
|
+
|
|
1101
|
+
# Encoding record: Platform ID (uint16), Encoding ID (uint16), Subtable offset (uint32)
|
|
1102
|
+
# Platform 3 (Windows), Encoding 1 (Unicode BMP)
|
|
1103
|
+
data << [3].pack("n") # Platform ID: Windows
|
|
1104
|
+
data << [1].pack("n") # Encoding ID: Unicode BMP
|
|
1105
|
+
data << [subtable_offset].pack("N") # Subtable offset
|
|
1106
|
+
|
|
1107
|
+
# Append subtable data
|
|
1108
|
+
data << subtable_data
|
|
1109
|
+
|
|
1110
|
+
data
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Build cmap format 4 subtable
|
|
1114
|
+
#
|
|
1115
|
+
# @param unicode_to_glyph [Hash<Integer, Integer>] Unicode to glyph index mapping
|
|
1116
|
+
# @return [String] Format 4 subtable binary data
|
|
1117
|
+
def build_cmap_format_4(unicode_to_glyph)
|
|
1118
|
+
data = (+"").b
|
|
1119
|
+
|
|
1120
|
+
# Get sorted Unicode values
|
|
1121
|
+
unicode_values = unicode_to_glyph.keys.sort
|
|
1122
|
+
return data if unicode_values.empty?
|
|
1123
|
+
|
|
1124
|
+
# For simplicity, create segments for continuous ranges
|
|
1125
|
+
# A more sophisticated implementation would optimize this
|
|
1126
|
+
segments = []
|
|
1127
|
+
current_segment = nil
|
|
1128
|
+
|
|
1129
|
+
unicode_values.each do |unicode|
|
|
1130
|
+
glyph_id = unicode_to_glyph[unicode]
|
|
1131
|
+
|
|
1132
|
+
if current_segment.nil?
|
|
1133
|
+
current_segment = {
|
|
1134
|
+
start: unicode,
|
|
1135
|
+
end: unicode,
|
|
1136
|
+
start_glyph: glyph_id,
|
|
1137
|
+
glyphs: [glyph_id],
|
|
1138
|
+
}
|
|
1139
|
+
elsif unicode == current_segment[:end] + 1 && glyph_id == current_segment[:glyphs].last + 1
|
|
1140
|
+
# Continue current segment (sequential)
|
|
1141
|
+
current_segment[:end] = unicode
|
|
1142
|
+
current_segment[:glyphs] << glyph_id
|
|
1143
|
+
else
|
|
1144
|
+
# Start new segment
|
|
1145
|
+
segments << current_segment
|
|
1146
|
+
current_segment = {
|
|
1147
|
+
start: unicode,
|
|
1148
|
+
end: unicode,
|
|
1149
|
+
start_glyph: glyph_id,
|
|
1150
|
+
glyphs: [glyph_id],
|
|
1151
|
+
}
|
|
1152
|
+
end
|
|
1153
|
+
end
|
|
1154
|
+
|
|
1155
|
+
segments << current_segment if current_segment
|
|
1156
|
+
|
|
1157
|
+
# Add end segment marker (0xFFFF)
|
|
1158
|
+
segments << { start: 0xFFFF, end: 0xFFFF, start_glyph: 0, glyphs: [0] }
|
|
1159
|
+
|
|
1160
|
+
# Calculate segment count and related values
|
|
1161
|
+
seg_count = segments.length
|
|
1162
|
+
seg_count_x2 = seg_count * 2
|
|
1163
|
+
search_range = 2 ** (Math.log2(seg_count).to_i) * 2
|
|
1164
|
+
entry_selector = Math.log2(search_range / 2).to_i
|
|
1165
|
+
range_shift = (seg_count - search_range / 2) * 2
|
|
1166
|
+
|
|
1167
|
+
# Build format 4 subtable header (14 bytes)
|
|
1168
|
+
data << [4].pack("n") # Format
|
|
1169
|
+
data << [calculate_cmap4_length(segments)].pack("n") # Length (placeholder)
|
|
1170
|
+
data << [0].pack("n") # Language (0 = independent)
|
|
1171
|
+
data << [seg_count_x2].pack("n") # segCountX2
|
|
1172
|
+
data << [search_range].pack("n") # searchRange
|
|
1173
|
+
data << [entry_selector].pack("n") # entrySelector
|
|
1174
|
+
data << [range_shift].pack("n") # rangeShift
|
|
1175
|
+
|
|
1176
|
+
# Build segment arrays
|
|
1177
|
+
end_codes = []
|
|
1178
|
+
start_codes = []
|
|
1179
|
+
id_deltas = []
|
|
1180
|
+
id_range_offsets = []
|
|
1181
|
+
glyph_id_array = []
|
|
1182
|
+
|
|
1183
|
+
segments.each do |seg|
|
|
1184
|
+
end_codes << seg[:end]
|
|
1185
|
+
start_codes << seg[:start]
|
|
1186
|
+
|
|
1187
|
+
# For sequential glyphs, use delta
|
|
1188
|
+
if seg[:start] == 0xFFFF
|
|
1189
|
+
# End segment marker
|
|
1190
|
+
id_deltas << 1
|
|
1191
|
+
id_range_offsets << 0
|
|
1192
|
+
elsif seg[:end] - seg[:start] == seg[:glyphs].length - 1
|
|
1193
|
+
# Sequential: use delta
|
|
1194
|
+
id_deltas << (seg[:start_glyph] - seg[:start])
|
|
1195
|
+
id_range_offsets << 0
|
|
1196
|
+
else
|
|
1197
|
+
# Non-sequential: use glyph ID array
|
|
1198
|
+
id_deltas << 0
|
|
1199
|
+
id_range_offsets << (glyph_id_array.length * 2 + 2)
|
|
1200
|
+
glyph_id_array.concat(seg[:glyphs])
|
|
1201
|
+
end
|
|
1202
|
+
end
|
|
1203
|
+
|
|
1204
|
+
# Write arrays (padded to even length)
|
|
1205
|
+
end_codes.each { |code| data << [code].pack("n") }
|
|
1206
|
+
data << [0].pack("n") # Reserved padding
|
|
1207
|
+
start_codes.each { |code| data << [code].pack("n") }
|
|
1208
|
+
id_deltas.each { |delta| data << [delta].pack("s>") } # Signed
|
|
1209
|
+
id_range_offsets.each { |offset| data << [offset].pack("n") }
|
|
1210
|
+
glyph_id_array.each { |gid| data << [gid].pack("n") }
|
|
1211
|
+
|
|
1212
|
+
# Update length in header
|
|
1213
|
+
length = data.bytesize
|
|
1214
|
+
data[2..3] = [length].pack("n")
|
|
1215
|
+
|
|
1216
|
+
data
|
|
1217
|
+
end
|
|
1218
|
+
|
|
1219
|
+
# Calculate length for format 4 subtable
|
|
1220
|
+
#
|
|
1221
|
+
# @param segments [Array<Hash>] Segment definitions
|
|
1222
|
+
# @return [Integer] Estimated length
|
|
1223
|
+
def calculate_cmap4_length(segments)
|
|
1224
|
+
# Header: 14 bytes
|
|
1225
|
+
# Arrays: seg_count * 2 bytes each
|
|
1226
|
+
# Glyph ID array: variable
|
|
1227
|
+
seg_count = segments.length
|
|
1228
|
+
|
|
1229
|
+
# Rough estimate (actual calculation done during construction)
|
|
1230
|
+
14 + (seg_count * 8) + (seg_count * 2) + 100 # 100 for glyph ID array estimate
|
|
556
1231
|
end
|
|
557
1232
|
end
|
|
558
1233
|
end
|