krill 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 0e1b96731044a7c541b3d51a6367f3d47ee930ae
4
- data.tar.gz: 3e8a31bb07ec43044890fe0402f2cf54d0cf646d
3
+ metadata.gz: 263d05f0b3e34007af5a65efed3386497244294e
4
+ data.tar.gz: 5607a0d4c668dd9cda6d736e78c50986eb108c23
5
5
  SHA512:
6
- metadata.gz: d919a34509bc1a8b5b4707b76376948799337ba0f5c40df47882fa4b2b8de5dfa9a1b87161fd0d2fda7a09777abbbf8837d200e153ad6db548476ed8e7dfeaa5
7
- data.tar.gz: 417e6a38abf3e71ea0f8fd0b6384db5a4f5d55d9dd37cbef4c6a7954777e04a8748d120da5c003d68e095d269578ac045a031cbdc9ef4fca6286155c81400e27
6
+ metadata.gz: 5dff75341afc874c3694154ccee79ff844c1dd27af1143486850bb256d1095a097d67092ad2252d21892c6e9787e7cfd5e54d0519d7a1841130979f7409bc571
7
+ data.tar.gz: 72c902daa932736530e97c5ff755a4345dc93807241da37cc6c41492544cc55c4f927820f8d1a9293a3bf6060344dd28c9deec94464b8b93bbabd3835e89993a
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/README.md CHANGED
@@ -37,5 +37,4 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERN
37
37
 
38
38
  ## License
39
39
 
40
- The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
41
-
40
+ The gem is available as open source under the terms of the [GPLv3](https://opensource.org/licenses/GPL-3.0).
data/Rakefile CHANGED
@@ -1,10 +1,10 @@
1
1
  require "bundler/gem_tasks"
2
- require "rake/testtask"
3
2
 
4
- Rake::TestTask.new(:test) do |t|
5
- t.libs << "test"
6
- t.libs << "lib"
7
- t.test_files = FileList['test/**/*_test.rb']
8
- end
3
+ require "rspec/core/rake_task"
4
+
5
+ task default: [:spec]
9
6
 
10
- task :default => :test
7
+ desc "Run all rspec files"
8
+ RSpec::Core::RakeTask.new("spec") do |c|
9
+ c.rspec_opts = "-t ~unresolved"
10
+ end
@@ -22,5 +22,7 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.14"
24
24
  spec.add_development_dependency "rake", "~> 10.0"
25
- spec.add_development_dependency "minitest", "~> 5.0"
25
+ spec.add_development_dependency "rspec", "~> 3.0"
26
+ spec.add_development_dependency "ttfunk"
27
+ spec.add_development_dependency "pry"
26
28
  end
@@ -1,5 +1,25 @@
1
1
  require "krill/version"
2
+ require "krill/text_box"
3
+ require "krill/wrapped_text"
4
+ require "krill/formatter"
2
5
 
3
6
  module Krill
4
- # Your code goes here...
7
+
8
+ def self.wrap_text(runs, width:, leading:)
9
+ box = Krill::TextBox.new(runs, width: width, leading: leading, height: Float::INFINITY)
10
+ box.render
11
+ Krill::WrappedText.new(box)
12
+ end
13
+
14
+ CannotFit = Class.new(StandardError)
15
+
16
+ # No-Break Space
17
+ NBSP = " ".freeze
18
+
19
+ # Zero Width Space (indicate word boundaries without a space)
20
+ ZWSP = [8203].pack("U").freeze
21
+
22
+ # Soft Hyphen (invisible, except when causing a line break)
23
+ SHY = "­".freeze
24
+
5
25
  end
@@ -0,0 +1,174 @@
1
+ module Krill
2
+ AFM = Struct.new(:filename) do
3
+ attr_reader :name, :family, :ascender, :descender, :line_gap,:character_widths, :kernings
4
+
5
+ def self.open(filename)
6
+ new filename
7
+ end
8
+
9
+ def initialize(filename)
10
+ super
11
+
12
+ data = parse(filename)
13
+
14
+ @character_widths = data.fetch(:character_widths)
15
+ @kernings = data.fetch(:kernings)
16
+ attributes = data.fetch(:attributes)
17
+
18
+ bbox = attributes['fontbbox'].split(/\s+/).map { |e| Integer(e) }
19
+ line_height = Float(bbox[3] - bbox[1]) / 1000.0
20
+
21
+ @name = attributes['fontname']
22
+ @family = attributes['familyname']
23
+ @ascender = attributes['ascender'].to_i / 1000.0
24
+ @descender = attributes['descender'].to_i / 1000.0
25
+ @line_gap = line_height - (ascender - descender)
26
+ end
27
+
28
+ def bold?
29
+ name["Bold"].present?
30
+ end
31
+
32
+ def italic?
33
+ name["Italic"].present?
34
+ end
35
+
36
+ def unicode?
37
+ false
38
+ end
39
+
40
+
41
+ private
42
+
43
+ def parse(filename)
44
+ character_widths = {}
45
+ kernings = {}
46
+ attributes = {}
47
+ section = []
48
+
49
+ File.foreach(filename) do |line|
50
+ case line
51
+ when /^Start(\w+)/
52
+ section.push Regexp.last_match(1)
53
+ next
54
+ when /^End(\w+)/
55
+ section.pop
56
+ next
57
+ end
58
+
59
+ case section
60
+ when %w{FontMetrics CharMetrics}
61
+ next unless line =~ /^CH?\s/
62
+
63
+ name = line[/\bN\s+(\.?\w+)\s*;/, 1]
64
+ char = WinAnsi.to_utf8(name)
65
+ character_widths[char] = line[/\bWX\s+(\d+)\s*;/, 1].to_i / 1000.0
66
+
67
+ when %w{FontMetrics KernData KernPairs}
68
+ next unless line =~ /^KPX\s+(\.?\w+)\s+(\.?\w+)\s+(-?\d+)/
69
+
70
+ pair = [WinAnsi.to_utf8(Regexp.last_match(1)), WinAnsi.to_utf8(Regexp.last_match(2))].join
71
+ kerning = Regexp.last_match(3).to_i / 1000.0
72
+ kernings[pair] = kerning unless kerning.zero?
73
+
74
+ when %w{FontMetrics KernData TrackKern}, %w{FontMetrics Composites}
75
+ next
76
+
77
+ else
78
+ line =~ /(^\w+)\s+(.*)/
79
+ key = Regexp.last_match(1).to_s.downcase
80
+ value = Regexp.last_match(2)
81
+ attributes[key] = attributes[key] ? Array(attributes[key]) << value : value
82
+
83
+ end
84
+ end
85
+
86
+ character_widths.freeze
87
+ kernings.freeze
88
+ attributes.freeze
89
+ { character_widths: character_widths, kernings: kernings, attributes: attributes }.freeze
90
+ end
91
+
92
+
93
+ class WinAnsi
94
+ def self.to_utf8(char)
95
+ CHARACTERS.index(char)&.chr(Encoding::UTF_8)
96
+ end
97
+
98
+ CHARACTERS = %w{
99
+ .notdef .notdef .notdef .notdef
100
+ .notdef .notdef .notdef .notdef
101
+ .notdef .notdef .notdef .notdef
102
+ .notdef .notdef .notdef .notdef
103
+ .notdef .notdef .notdef .notdef
104
+ .notdef .notdef .notdef .notdef
105
+ .notdef .notdef .notdef .notdef
106
+ .notdef .notdef .notdef .notdef
107
+
108
+ space exclam quotedbl numbersign
109
+ dollar percent ampersand quotesingle
110
+ parenleft parenright asterisk plus
111
+ comma hyphen period slash
112
+ zero one two three
113
+ four five six seven
114
+ eight nine colon semicolon
115
+ less equal greater question
116
+
117
+ at A B C
118
+ D E F G
119
+ H I J K
120
+ L M N O
121
+ P Q R S
122
+ T U V W
123
+ X Y Z bracketleft
124
+ backslash bracketright asciicircum underscore
125
+
126
+ grave a b c
127
+ d e f g
128
+ h i j k
129
+ l m n o
130
+ p q r s
131
+ t u v w
132
+ x y z braceleft
133
+ bar braceright asciitilde .notdef
134
+
135
+ Euro .notdef quotesinglbase florin
136
+ quotedblbase ellipsis dagger daggerdbl
137
+ circumflex perthousand Scaron guilsinglleft
138
+ OE .notdef Zcaron .notdef
139
+ .notdef quoteleft quoteright quotedblleft
140
+ quotedblright bullet endash emdash
141
+ tilde trademark scaron guilsinglright
142
+ oe .notdef zcaron ydieresis
143
+
144
+ space exclamdown cent sterling
145
+ currency yen brokenbar section
146
+ dieresis copyright ordfeminine guillemotleft
147
+ logicalnot hyphen registered macron
148
+ degree plusminus twosuperior threesuperior
149
+ acute mu paragraph periodcentered
150
+ cedilla onesuperior ordmasculine guillemotright
151
+ onequarter onehalf threequarters questiondown
152
+
153
+ Agrave Aacute Acircumflex Atilde
154
+ Adieresis Aring AE Ccedilla
155
+ Egrave Eacute Ecircumflex Edieresis
156
+ Igrave Iacute Icircumflex Idieresis
157
+ Eth Ntilde Ograve Oacute
158
+ Ocircumflex Otilde Odieresis multiply
159
+ Oslash Ugrave Uacute Ucircumflex
160
+ Udieresis Yacute Thorn germandbls
161
+
162
+ agrave aacute acircumflex atilde
163
+ adieresis aring ae ccedilla
164
+ egrave eacute ecircumflex edieresis
165
+ igrave iacute icircumflex idieresis
166
+ eth ntilde ograve oacute
167
+ ocircumflex otilde odieresis divide
168
+ oslash ugrave uacute ucircumflex
169
+ udieresis yacute thorn ydieresis
170
+ }.freeze
171
+ end
172
+
173
+ end
174
+ end
@@ -0,0 +1,182 @@
1
+ require "krill/fragment"
2
+
3
+ module Krill
4
+ class Arranger
5
+ attr_reader :max_line_height
6
+ attr_reader :max_descender
7
+ attr_reader :max_ascender
8
+ attr_reader :finalized
9
+ attr_accessor :consumed
10
+
11
+ # The following present only for testing purposes
12
+ attr_reader :unconsumed
13
+ attr_reader :fragments
14
+ attr_reader :current_format_state
15
+
16
+ def initialize(options={})
17
+ @fragments = []
18
+ @unconsumed = []
19
+ @kerning = options[:kerning]
20
+ end
21
+
22
+ def current_formatter
23
+ current_format_state.fetch(:font)
24
+ end
25
+
26
+ def space_count
27
+ fail "Lines must be finalized before calling #space_count" unless finalized
28
+
29
+ @fragments.inject(0) do |sum, fragment|
30
+ sum + fragment.space_count
31
+ end
32
+ end
33
+
34
+ def line_width
35
+ fail "Lines must be finalized before calling #line_width" unless finalized
36
+
37
+ @fragments.inject(0) do |sum, fragment|
38
+ sum + fragment.width
39
+ end
40
+ end
41
+
42
+ def line
43
+ fail "Lines must be finalized before calling #line" unless finalized
44
+
45
+ @fragments.collect do |fragment|
46
+ fragment.text.dup.force_encoding(::Encoding::UTF_8)
47
+ end.join
48
+ end
49
+
50
+ def finalize_line
51
+ @finalized = true
52
+
53
+ omit_trailing_whitespace_from_line_width
54
+ @fragments = []
55
+ @consumed.each do |hash|
56
+ text = hash[:text]
57
+ format_state = hash.dup
58
+ format_state.delete(:text)
59
+ fragment = Krill::Fragment.new(text, format_state)
60
+ @fragments << fragment
61
+ set_fragment_measurements(fragment)
62
+ set_line_measurement_maximums(fragment)
63
+ end
64
+ end
65
+
66
+ def format_array=(array)
67
+ initialize_line
68
+ @unconsumed = []
69
+ array.each do |hash|
70
+ binding.pry unless hash.is_a?(Hash)
71
+ hash[:text].scan(/[^\n]+|\n/) do |line|
72
+ @unconsumed << hash.merge(text: line)
73
+ end
74
+ end
75
+ end
76
+
77
+ def initialize_line
78
+ @finalized = false
79
+ @max_line_height = 0
80
+ @max_descender = 0
81
+ @max_ascender = 0
82
+
83
+ @consumed = []
84
+ @fragments = []
85
+ end
86
+
87
+ def finished?
88
+ @unconsumed.none?
89
+ end
90
+
91
+ def next_string
92
+ fail "Lines must not be finalized when calling #next_string" if finalized
93
+
94
+ next_unconsumed_hash = @unconsumed.shift
95
+
96
+ if next_unconsumed_hash
97
+ @consumed << next_unconsumed_hash.dup
98
+ @current_format_state = next_unconsumed_hash.dup
99
+ @current_format_state.delete(:text)
100
+
101
+ next_unconsumed_hash[:text]
102
+ end
103
+ end
104
+
105
+ def preview_next_string
106
+ next_unconsumed_hash = @unconsumed.first
107
+ next_unconsumed_hash[:text] if next_unconsumed_hash
108
+ end
109
+
110
+ def update_last_string(printed, unprinted, normalized_soft_hyphen = nil)
111
+ return if printed.nil?
112
+
113
+ if printed.empty?
114
+ @consumed.pop
115
+ else
116
+ @consumed.last[:text] = printed
117
+ @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen if normalized_soft_hyphen
118
+ end
119
+
120
+ @unconsumed.unshift(@current_format_state.merge(text: unprinted)) unless unprinted.empty?
121
+
122
+ load_previous_format_state if printed.empty?
123
+ end
124
+
125
+ def retrieve_fragment
126
+ fail "Lines must be finalized before fragments can be retrieved" unless finalized
127
+
128
+ @fragments.shift
129
+ end
130
+
131
+ def repack_unretrieved
132
+ new_unconsumed = []
133
+ while fragment = retrieve_fragment
134
+ fragment.include_trailing_white_space!
135
+ new_unconsumed << fragment.format_state.merge(:text => fragment.text)
136
+ end
137
+ @unconsumed = new_unconsumed.concat(@unconsumed)
138
+ end
139
+
140
+ private
141
+
142
+ def load_previous_format_state
143
+ if @consumed.empty?
144
+ @current_format_state = {}
145
+ else
146
+ hash = @consumed.last
147
+ @current_format_state = hash.dup
148
+ @current_format_state.delete(:text)
149
+ end
150
+ end
151
+
152
+ def omit_trailing_whitespace_from_line_width
153
+ @consumed.reverse_each do |hash|
154
+ if hash[:text] == "\n"
155
+ break
156
+ elsif hash[:text].strip.empty? && @consumed.length > 1
157
+ # this entire fragment is trailing white space
158
+ hash[:exclude_trailing_white_space] = true
159
+ else
160
+ # this fragment contains the first non-white space we have
161
+ # encountered since the end of the line
162
+ hash[:exclude_trailing_white_space] = true
163
+ break
164
+ end
165
+ end
166
+ end
167
+
168
+ def set_fragment_measurements(fragment)
169
+ fragment.width = fragment.formatter.width_of(fragment.text, kerning: @kerning)
170
+ fragment.line_height = fragment.formatter.height
171
+ fragment.descender = fragment.formatter.descender
172
+ fragment.ascender = fragment.formatter.ascender
173
+ end
174
+
175
+ def set_line_measurement_maximums(fragment)
176
+ @max_line_height = [defined?(@max_line_height) && @max_line_height, fragment.line_height].compact.max
177
+ @max_descender = [defined?(@max_descender) && @max_descender, fragment.descender].compact.max
178
+ @max_ascender = [defined?(@max_ascender) && @max_ascender, fragment.ascender].compact.max
179
+ end
180
+
181
+ end
182
+ end
@@ -0,0 +1,108 @@
1
+ module Krill
2
+ Formatter = Struct.new(:font, :size, :character_spacing) do
3
+
4
+ def initialize(font, size, character_spacing: 0)
5
+ super font, size, character_spacing
6
+ end
7
+
8
+ def name
9
+ font.name
10
+ end
11
+
12
+ def family
13
+ font.family
14
+ end
15
+
16
+
17
+ def width_of(string, kerning: true)
18
+ length = compute_width_of(string, kerning: kerning)
19
+ length + (character_spacing * character_count(string))
20
+ end
21
+
22
+ # NOTE: +string+ must be UTF8-encoded.
23
+ def compute_width_of(string, kerning: true)
24
+ if kerning
25
+ kern(string).inject(0) do |s, r|
26
+ if r.is_a?(Numeric)
27
+ s - r
28
+ else
29
+ r.inject(s) { |s2, u| s2 + font.character_widths.fetch(u, 0) }
30
+ end
31
+ end * size
32
+ else
33
+ string.chars.inject(0) do |sum, char|
34
+ sum + font.character_widths.fetch(char, 0.0)
35
+ end * size
36
+ end
37
+ end
38
+
39
+
40
+ def ascender
41
+ font.ascender * size
42
+ end
43
+
44
+ def descender
45
+ -font.descender * size
46
+ end
47
+
48
+ def line_gap
49
+ font.line_gap * size
50
+ end
51
+
52
+ def height
53
+ normalized_height * size
54
+ end
55
+
56
+
57
+ def superscript?
58
+ false # <-- TODO
59
+ end
60
+
61
+ def subscript?
62
+ false # <-- TODO
63
+ end
64
+
65
+
66
+ def unicode?
67
+ true
68
+ end
69
+
70
+ def normalize_encoding(text)
71
+ text.encode(::Encoding::UTF_8)
72
+ end
73
+
74
+
75
+ private
76
+
77
+ def normalized_height
78
+ @normalized_height ||= font.ascender - font.descender + font.line_gap
79
+ end
80
+
81
+ # Returns the number of characters in +str+ (a UTF-8-encoded string).
82
+ def character_count(str)
83
+ str.length
84
+ end
85
+
86
+ # +string+ must be UTF8-encoded.
87
+ #
88
+ # Returns an array. If an element is a numeric, it represents the
89
+ # kern amount to inject at that position. Otherwise, the element
90
+ # is an array of UTF-16 characters.
91
+ def kern(string)
92
+ a = []
93
+
94
+ string.each_char do |char|
95
+ if a.empty?
96
+ a << [char]
97
+ elsif (kern = font.kernings["#{a.last.last}#{char}"])
98
+ a << -kern << [char]
99
+ else
100
+ a.last << char
101
+ end
102
+ end
103
+
104
+ a
105
+ end
106
+
107
+ end
108
+ end