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 +4 -4
- data/.rspec +1 -0
- data/README.md +1 -2
- data/Rakefile +7 -7
- data/krill.gemspec +3 -1
- data/lib/krill.rb +21 -1
- data/lib/krill/afm.rb +174 -0
- data/lib/krill/arranger.rb +182 -0
- data/lib/krill/formatter.rb +108 -0
- data/lib/krill/fragment.rb +146 -0
- data/lib/krill/line_wrap.rb +287 -0
- data/lib/krill/text_box.rb +244 -0
- data/lib/krill/ttf.rb +78 -0
- data/lib/krill/version.rb +1 -1
- data/lib/krill/wrapped_text.rb +17 -0
- metadata +42 -6
- data/LICENSE.txt +0 -21
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 263d05f0b3e34007af5a65efed3386497244294e
|
4
|
+
data.tar.gz: 5607a0d4c668dd9cda6d736e78c50986eb108c23
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 [
|
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
t.test_files = FileList['test/**/*_test.rb']
|
8
|
-
end
|
3
|
+
require "rspec/core/rake_task"
|
4
|
+
|
5
|
+
task default: [:spec]
|
9
6
|
|
10
|
-
|
7
|
+
desc "Run all rspec files"
|
8
|
+
RSpec::Core::RakeTask.new("spec") do |c|
|
9
|
+
c.rspec_opts = "-t ~unresolved"
|
10
|
+
end
|
data/krill.gemspec
CHANGED
@@ -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 "
|
25
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
26
|
+
spec.add_development_dependency "ttfunk"
|
27
|
+
spec.add_development_dependency "pry"
|
26
28
|
end
|
data/lib/krill.rb
CHANGED
@@ -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
|
-
|
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
|
data/lib/krill/afm.rb
ADDED
@@ -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
|