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 +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
|