krill 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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