stationery 0.6.0 → 0.7.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +72 -15
  4. data/lib/stationery/builder.rb +5 -3
  5. data/lib/stationery/canvas/text.rb +4 -3
  6. data/lib/stationery/canvas.rb +5 -2
  7. data/lib/stationery/document.rb +26 -9
  8. data/lib/stationery/elements.rb +1 -1
  9. data/lib/stationery/fonts/fallback.rb +3 -3
  10. data/lib/stationery/fonts/font.rb +39 -21
  11. data/lib/stationery/fonts/gsub/single_subst.rb +37 -0
  12. data/lib/stationery/fonts/gsub.rb +65 -20
  13. data/lib/stationery/fonts/ligatures.rb +6 -4
  14. data/lib/stationery/hyphenation/patterns/LICENSES.md +60 -0
  15. data/lib/stationery/hyphenation/patterns/de-1996.pat.txt +36709 -0
  16. data/lib/stationery/hyphenation/patterns/en-us.hyp.txt +14 -0
  17. data/lib/stationery/hyphenation/patterns/en-us.pat.txt +4938 -0
  18. data/lib/stationery/hyphenation/patterns/sv.pat.txt +4693 -0
  19. data/lib/stationery/hyphenation.rb +132 -0
  20. data/lib/stationery/images/png.rb +66 -0
  21. data/lib/stationery/images/resampled.rb +80 -0
  22. data/lib/stationery/layout/image.rb +28 -2
  23. data/lib/stationery/layout/paginator.rb +1 -1
  24. data/lib/stationery/layout/text.rb +2 -2
  25. data/lib/stationery/minitest.rb +5 -0
  26. data/lib/stationery/page_templates.rb +2 -1
  27. data/lib/stationery/pdf/assembler.rb +6 -2
  28. data/lib/stationery/pdf/page_labels.rb +71 -0
  29. data/lib/stationery/testing/inspector.rb +13 -0
  30. data/lib/stationery/testing/matchers.rb +10 -0
  31. data/lib/stationery/text/entities.rb +14 -4
  32. data/lib/stationery/text/paragraph.rb +1 -1
  33. data/lib/stationery/text/style.rb +14 -3
  34. data/lib/stationery/text/wrapper.rb +73 -12
  35. data/lib/stationery/version.rb +1 -1
  36. data/lib/stationery/warnings.rb +6 -0
  37. data/lib/stationery.rb +4 -0
  38. metadata +10 -1
@@ -3,11 +3,16 @@
3
3
  module Stationery
4
4
  module Text
5
5
  # Greedy line breaking over styled runs. Breaks at spaces (the space is
6
- # dropped at the break) and after hyphens; a word wider than the line is
7
- # broken between characters. A word that changes style midway ("<b>Tot</b>al")
8
- # is still one word.
6
+ # dropped at the break) and after hyphens; a word that does not fit is
7
+ # hyphenated when it may be (a soft hyphen U+00AD names the break points,
8
+ # else the style's `hyphenate` language), and a word wider than the line
9
+ # is broken between characters. A word that changes style midway
10
+ # ("<b>Tot</b>al") is still one word. Soft hyphens are never measured or
11
+ # drawn: a broken word draws "-" at the break and nothing otherwise.
9
12
  class Wrapper
10
13
  TOKEN = /\n|[ \t]+|[^ \t\n-]*-+|[^ \t\n-]+/
14
+ SOFT_HYPHEN = "­"
15
+ EPSILON = 0.0001
11
16
 
12
17
  Segment = Data.define(:text, :style)
13
18
 
@@ -53,16 +58,70 @@ module Stationery
53
58
  end
54
59
  end
55
60
 
56
- def place_word(word)
61
+ # A word holding soft hyphens breaks only at them, even once the part
62
+ # after a break carries none (TeX's rule: a discretionary hyphen exempts
63
+ # the whole word from the patterns).
64
+ def place_word(word, explicit: word.any? { |segment| segment.text.include?(SOFT_HYPHEN) })
65
+ @explicit = explicit
57
66
  needed = width(@pending_space) + width(word)
58
- if @current.empty? || line_width + needed <= @max + 0.0001
67
+ if @current.empty? || line_width + needed <= @max + EPSILON
59
68
  @current.concat(@pending_space, word)
69
+ elsif (head, tail = hyphenated(word, @max - line_width - width(@pending_space)))
70
+ @current.concat(@pending_space, head)
71
+ finish(wrapped: true)
72
+ return place_word(tail, explicit:)
60
73
  else
61
74
  finish(wrapped: true)
62
75
  @current.concat(word)
63
76
  end
64
77
  @pending_space = []
65
- break_long_word while line_width > @max + 0.0001 && characters(@current) > 1
78
+ overflow
79
+ end
80
+
81
+ # A word alone on its line and still too wide: hyphenate what fits,
82
+ # else move the characters that overflow onto following lines.
83
+ def overflow
84
+ while line_width > @max + EPSILON && characters(@current) > 1
85
+ head, tail = hyphenated(@current, @max)
86
+ if head
87
+ @current = head
88
+ finish(wrapped: true)
89
+ @current = tail
90
+ else
91
+ break_long_word
92
+ end
93
+ end
94
+ end
95
+
96
+ # [head, tail] with the longest hyphenation of `word` whose head (and
97
+ # its hyphen) fits `available`; nil when the word offers no break that
98
+ # fits. Soft hyphens name the breaks and suppress the patterns.
99
+ def hyphenated(word, available)
100
+ chars = word.flat_map { |segment| segment.text.chars.map { |char| Segment.new(char, segment.style) } }
101
+ break_points(chars).reverse_each do |index|
102
+ head = chars.first(index).reject { |segment| segment.text == SOFT_HYPHEN }
103
+ head << Segment.new(hyphen_for(chars[index - 1].style), chars[index - 1].style)
104
+ return [head, chars.drop(index)] if width(head) <= available + EPSILON
105
+ end
106
+ nil
107
+ end
108
+
109
+ # Indexes into the word's characters before which it may break: after
110
+ # each soft hyphen, else where the language's patterns allow.
111
+ def break_points(chars)
112
+ return (1...chars.length).select { |index| chars[index - 1].text == SOFT_HYPHEN } if @explicit
113
+
114
+ text = chars.map(&:text).join
115
+ language = chars.first.style.hyphenate or return []
116
+ core = text[/\p{L}+/] or return []
117
+ offset = text.index(core)
118
+ Hyphenation.points(core, language).map { |point| point + offset }
119
+ end
120
+
121
+ # The font's hyphen, or the dedicated U+2010 when it lacks "-".
122
+ def hyphen_for(style)
123
+ font = @book.resolve(style).first
124
+ !font.glyph?("-") && font.glyph?("‐") ? "‐" : "-"
66
125
  end
67
126
 
68
127
  # Moves characters that overflow the line onto following lines.
@@ -71,7 +130,7 @@ module Stationery
71
130
  @current.each do |segment|
72
131
  segment.text.each_char do |char|
73
132
  piece = Segment.new(char, segment.style)
74
- return keep_overflow(fitting, piece) if fitting.any? && width(fitting + [piece]) > @max + 0.0001
133
+ return keep_overflow(fitting, piece) if fitting.any? && width(fitting + [piece]) > @max + EPSILON
75
134
 
76
135
  fitting << piece
77
136
  end
@@ -104,8 +163,10 @@ module Stationery
104
163
 
105
164
  def fragments(segments)
106
165
  x = 0
107
- segments.chunk_while { |a, b| a.style == b.style }.map do |group|
108
- text = group.map(&:text).join
166
+ segments.chunk_while { |a, b| a.style == b.style }.filter_map do |group|
167
+ text = group.map(&:text).join.delete(SOFT_HYPHEN)
168
+ next if text.empty?
169
+
109
170
  font, face = @book.resolve(group.first.style)
110
171
  fragment_width = measure(text, group.first.style)
111
172
  Fragment.new(text, group.first.style, font, face, fragment_width, x).tap { x += fragment_width }
@@ -117,16 +178,16 @@ module Stationery
117
178
  end
118
179
 
119
180
  def line_width = width(@current)
120
- def characters(segments) = segments.sum { |segment| segment.text.length }
181
+ def characters(segments) = segments.sum { |segment| segment.text.delete(SOFT_HYPHEN).length }
121
182
 
122
183
  def width(segments)
123
- segments.sum { |segment| measure(segment.text, segment.style) }
184
+ segments.sum { |segment| measure(segment.text.delete(SOFT_HYPHEN), segment.style) }
124
185
  end
125
186
 
126
187
  def measure(text, style)
127
188
  font = @book.resolve(style).first
128
189
  font.width_of(text, style.render_size, letter_spacing: style.letter_spacing, kerning: style.kerning,
129
- ligatures: style.ligatures)
190
+ ligatures: style.ligatures, features: style.features)
130
191
  end
131
192
  end
132
193
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Stationery
4
- VERSION = "0.6.0"
4
+ VERSION = "0.7.0"
5
5
  end
@@ -29,6 +29,12 @@ module Stationery
29
29
  def message = %(image "#{source}" skipped: #{reason})
30
30
  end
31
31
 
32
+ OversizedImage = Data.define(:source, :pixels, :ppi, :limit) do
33
+ def message
34
+ %(image "#{source}" #{pixels}px wide is drawn at #{ppi} ppi (limit #{limit}); resize it before embedding)
35
+ end
36
+ end
37
+
32
38
  UnresolvedLink = Data.define(:name, :page) do
33
39
  def message = %(link to "#{name}" on page #{page} has no matching anchor)
34
40
  end
data/lib/stationery.rb CHANGED
@@ -27,6 +27,7 @@ require_relative "stationery/fonts/gpos/pair_pos"
27
27
  require_relative "stationery/fonts/kerning"
28
28
  require_relative "stationery/fonts/gsub"
29
29
  require_relative "stationery/fonts/gsub/ligature_subst"
30
+ require_relative "stationery/fonts/gsub/single_subst"
30
31
  require_relative "stationery/fonts/ligatures"
31
32
  require_relative "stationery/fonts/subset"
32
33
  require_relative "stationery/fonts/registry"
@@ -46,6 +47,7 @@ require_relative "stationery/images/cache"
46
47
  require_relative "stationery/images/scanlines"
47
48
  require_relative "stationery/images/jpeg"
48
49
  require_relative "stationery/images/png"
50
+ require_relative "stationery/images/resampled"
49
51
  require_relative "stationery/color"
50
52
  require_relative "stationery/geometry"
51
53
  require_relative "stationery/page"
@@ -59,11 +61,13 @@ require_relative "stationery/canvas/debug"
59
61
  require_relative "stationery/canvas/marking"
60
62
  require_relative "stationery/canvas"
61
63
  require_relative "stationery/pdf/outline_writer"
64
+ require_relative "stationery/pdf/page_labels"
62
65
  require_relative "stationery/forms/metrics"
63
66
  require_relative "stationery/forms/field"
64
67
  require_relative "stationery/forms/appearance"
65
68
  require_relative "stationery/forms/acro_form"
66
69
  require_relative "stationery/pdf/assembler"
70
+ require_relative "stationery/hyphenation"
67
71
  require_relative "stationery/text/style"
68
72
  require_relative "stationery/fonts/fallback"
69
73
  require_relative "stationery/fonts/font_book"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: stationery
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mikael Henriksson
@@ -71,6 +71,7 @@ files:
71
71
  - lib/stationery/fonts/gpos/pair_pos.rb
72
72
  - lib/stationery/fonts/gsub.rb
73
73
  - lib/stationery/fonts/gsub/ligature_subst.rb
74
+ - lib/stationery/fonts/gsub/single_subst.rb
74
75
  - lib/stationery/fonts/installer.rb
75
76
  - lib/stationery/fonts/kern_table.rb
76
77
  - lib/stationery/fonts/kerning.rb
@@ -94,10 +95,17 @@ files:
94
95
  - lib/stationery/html/tree_builder/blocks.rb
95
96
  - lib/stationery/html/tree_builder/collector.rb
96
97
  - lib/stationery/html/whitespace.rb
98
+ - lib/stationery/hyphenation.rb
99
+ - lib/stationery/hyphenation/patterns/LICENSES.md
100
+ - lib/stationery/hyphenation/patterns/de-1996.pat.txt
101
+ - lib/stationery/hyphenation/patterns/en-us.hyp.txt
102
+ - lib/stationery/hyphenation/patterns/en-us.pat.txt
103
+ - lib/stationery/hyphenation/patterns/sv.pat.txt
97
104
  - lib/stationery/images/cache.rb
98
105
  - lib/stationery/images/images.rb
99
106
  - lib/stationery/images/jpeg.rb
100
107
  - lib/stationery/images/png.rb
108
+ - lib/stationery/images/resampled.rb
101
109
  - lib/stationery/images/scanlines.rb
102
110
  - lib/stationery/instrumentation.rb
103
111
  - lib/stationery/layout/box.rb
@@ -143,6 +151,7 @@ files:
143
151
  - lib/stationery/pdf/encryption/revision6.rb
144
152
  - lib/stationery/pdf/encryption/standard_security.rb
145
153
  - lib/stationery/pdf/outline_writer.rb
154
+ - lib/stationery/pdf/page_labels.rb
146
155
  - lib/stationery/pdf/serializer.rb
147
156
  - lib/stationery/pdf/stream.rb
148
157
  - lib/stationery/pdf/types.rb