hexapdf 0.5.0 → 0.6.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 (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -2
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/boxes.rb +68 -0
  7. data/examples/graphics.rb +12 -12
  8. data/examples/{text_box_alignment.rb → text_layouter_alignment.rb} +14 -14
  9. data/examples/text_layouter_inline_boxes.rb +66 -0
  10. data/examples/{text_box_line_wrapping.rb → text_layouter_line_wrapping.rb} +9 -10
  11. data/examples/{text_box_shapes.rb → text_layouter_shapes.rb} +58 -54
  12. data/examples/text_layouter_styling.rb +125 -0
  13. data/examples/truetype.rb +5 -7
  14. data/lib/hexapdf/cli/command.rb +1 -0
  15. data/lib/hexapdf/configuration.rb +170 -106
  16. data/lib/hexapdf/content/canvas.rb +41 -36
  17. data/lib/hexapdf/content/graphics_state.rb +15 -0
  18. data/lib/hexapdf/content/operator.rb +1 -1
  19. data/lib/hexapdf/dictionary.rb +20 -8
  20. data/lib/hexapdf/dictionary_fields.rb +8 -6
  21. data/lib/hexapdf/document.rb +25 -26
  22. data/lib/hexapdf/document/fonts.rb +4 -4
  23. data/lib/hexapdf/document/images.rb +2 -2
  24. data/lib/hexapdf/document/pages.rb +16 -16
  25. data/lib/hexapdf/encryption/security_handler.rb +41 -9
  26. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  27. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  28. data/lib/hexapdf/filter/predictor.rb +7 -1
  29. data/lib/hexapdf/font/true_type/font.rb +20 -0
  30. data/lib/hexapdf/font/type1/font.rb +23 -0
  31. data/lib/hexapdf/font_loader.rb +1 -0
  32. data/lib/hexapdf/font_loader/from_configuration.rb +2 -3
  33. data/lib/hexapdf/font_loader/from_file.rb +65 -0
  34. data/lib/hexapdf/image_loader/png.rb +2 -2
  35. data/lib/hexapdf/layout.rb +3 -2
  36. data/lib/hexapdf/layout/box.rb +146 -0
  37. data/lib/hexapdf/layout/inline_box.rb +40 -31
  38. data/lib/hexapdf/layout/{line_fragment.rb → line.rb} +12 -13
  39. data/lib/hexapdf/layout/style.rb +630 -41
  40. data/lib/hexapdf/layout/text_fragment.rb +80 -12
  41. data/lib/hexapdf/layout/{text_box.rb → text_layouter.rb} +164 -109
  42. data/lib/hexapdf/number_tree_node.rb +1 -1
  43. data/lib/hexapdf/parser.rb +4 -1
  44. data/lib/hexapdf/revisions.rb +11 -4
  45. data/lib/hexapdf/stream.rb +8 -9
  46. data/lib/hexapdf/tokenizer.rb +5 -3
  47. data/lib/hexapdf/type.rb +3 -0
  48. data/lib/hexapdf/type/action.rb +56 -0
  49. data/lib/hexapdf/type/actions.rb +52 -0
  50. data/lib/hexapdf/type/actions/go_to.rb +52 -0
  51. data/lib/hexapdf/type/actions/go_to_r.rb +54 -0
  52. data/lib/hexapdf/type/actions/launch.rb +73 -0
  53. data/lib/hexapdf/type/actions/uri.rb +65 -0
  54. data/lib/hexapdf/type/annotation.rb +85 -0
  55. data/lib/hexapdf/type/annotations.rb +51 -0
  56. data/lib/hexapdf/type/annotations/link.rb +70 -0
  57. data/lib/hexapdf/type/annotations/markup_annotation.rb +70 -0
  58. data/lib/hexapdf/type/annotations/text.rb +81 -0
  59. data/lib/hexapdf/type/catalog.rb +3 -1
  60. data/lib/hexapdf/type/embedded_file.rb +6 -11
  61. data/lib/hexapdf/type/file_specification.rb +4 -6
  62. data/lib/hexapdf/type/font.rb +3 -1
  63. data/lib/hexapdf/type/font_descriptor.rb +18 -16
  64. data/lib/hexapdf/type/form.rb +3 -1
  65. data/lib/hexapdf/type/graphics_state_parameter.rb +3 -1
  66. data/lib/hexapdf/type/image.rb +4 -2
  67. data/lib/hexapdf/type/info.rb +2 -5
  68. data/lib/hexapdf/type/names.rb +2 -5
  69. data/lib/hexapdf/type/object_stream.rb +2 -1
  70. data/lib/hexapdf/type/page.rb +14 -1
  71. data/lib/hexapdf/type/page_tree_node.rb +9 -6
  72. data/lib/hexapdf/type/resources.rb +2 -5
  73. data/lib/hexapdf/type/trailer.rb +2 -5
  74. data/lib/hexapdf/type/viewer_preferences.rb +2 -5
  75. data/lib/hexapdf/type/xref_stream.rb +3 -1
  76. data/lib/hexapdf/version.rb +1 -1
  77. data/test/hexapdf/common_tokenizer_tests.rb +3 -1
  78. data/test/hexapdf/content/test_canvas.rb +29 -3
  79. data/test/hexapdf/content/test_graphics_state.rb +11 -0
  80. data/test/hexapdf/content/test_operator.rb +3 -2
  81. data/test/hexapdf/document/test_fonts.rb +8 -8
  82. data/test/hexapdf/document/test_images.rb +4 -12
  83. data/test/hexapdf/document/test_pages.rb +7 -7
  84. data/test/hexapdf/encryption/test_security_handler.rb +1 -5
  85. data/test/hexapdf/filter/test_predictor.rb +40 -12
  86. data/test/hexapdf/font/true_type/test_font.rb +16 -0
  87. data/test/hexapdf/font/type1/test_font.rb +30 -0
  88. data/test/hexapdf/font_loader/test_from_file.rb +29 -0
  89. data/test/hexapdf/font_loader/test_standard14.rb +4 -3
  90. data/test/hexapdf/layout/test_box.rb +104 -0
  91. data/test/hexapdf/layout/test_inline_box.rb +24 -10
  92. data/test/hexapdf/layout/{test_line_fragment.rb → test_line.rb} +9 -9
  93. data/test/hexapdf/layout/test_style.rb +519 -31
  94. data/test/hexapdf/layout/test_text_fragment.rb +136 -15
  95. data/test/hexapdf/layout/{test_text_box.rb → test_text_layouter.rb} +224 -144
  96. data/test/hexapdf/layout/test_text_shaper.rb +1 -1
  97. data/test/hexapdf/test_configuration.rb +12 -6
  98. data/test/hexapdf/test_dictionary.rb +27 -2
  99. data/test/hexapdf/test_dictionary_fields.rb +10 -1
  100. data/test/hexapdf/test_document.rb +14 -13
  101. data/test/hexapdf/test_parser.rb +12 -0
  102. data/test/hexapdf/test_revisions.rb +34 -0
  103. data/test/hexapdf/test_stream.rb +1 -1
  104. data/test/hexapdf/test_type.rb +18 -0
  105. data/test/hexapdf/test_writer.rb +2 -2
  106. data/test/hexapdf/type/actions/test_launch.rb +24 -0
  107. data/test/hexapdf/type/actions/test_uri.rb +23 -0
  108. data/test/hexapdf/type/annotations/test_link.rb +19 -0
  109. data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
  110. data/test/hexapdf/type/annotations/test_text.rb +38 -0
  111. data/test/hexapdf/type/test_annotation.rb +38 -0
  112. data/test/hexapdf/type/test_file_specification.rb +0 -7
  113. data/test/hexapdf/type/test_info.rb +0 -5
  114. data/test/hexapdf/type/test_page.rb +14 -0
  115. data/test/hexapdf/type/test_page_tree_node.rb +4 -1
  116. data/test/hexapdf/type/test_trailer.rb +0 -4
  117. data/test/test_helper.rb +6 -3
  118. metadata +36 -15
  119. data/examples/text_box_inline_boxes.rb +0 -56
  120. data/examples/text_box_styling.rb +0 -72
  121. data/test/hexapdf/type/test_embedded_file.rb +0 -16
  122. data/test/hexapdf/type/test_names.rb +0 -9
@@ -31,52 +31,61 @@
31
31
  # is created or manipulated using HexaPDF.
32
32
  #++
33
33
 
34
+ require 'hexapdf/layout/box'
35
+
34
36
  module HexaPDF
35
37
  module Layout
36
38
 
37
- # An InlineBox can be used as an item for a LineFragment so that inline graphics are possible.
38
- # The box *must* have a fixed size!
39
+ # An InlineBox wraps a regular Box so that it can be used as an item for a Line. This enables
40
+ # inline graphics.
41
+ #
42
+ # The wrapped box *must* have a fixed size!
39
43
  class InlineBox
40
44
 
41
- # The width of the box.
42
- attr_reader :width
43
-
44
- # The height of the box.
45
- attr_reader :height
45
+ # Creates an InlineBox that wraps a basic Box. All arguments (except +valign+) and the block
46
+ # are passed to Box::new.
47
+ #
48
+ # See ::new for the +valign+ argument.
49
+ def self.create(valign: :baseline, **args, &block)
50
+ new(Box.new(**args, &block), valign: valign)
51
+ end
46
52
 
47
53
  # The vertical alignment of the box.
48
54
  #
49
- # Can be any supported value except :text - see LineFragment for all possible values.
55
+ # Can be any supported value except :text - see Line for all possible values.
50
56
  attr_reader :valign
51
57
 
52
- # :call-seq:
53
- # InlineBox.new(width, height, valign: :baseline) {|box, canvas| block} -> inline_box
54
- #
55
- # Creates a new InlineBox object that uses the provided block when it is asked to draw itself
56
- # on a canvas (see #draw).
57
- #
58
- # Since the final location of the box is not known beforehand, the drawing operations inside
59
- # the block should draw inside the rectangle (0, 0, width, height).
58
+ # The wrapped Box object.
59
+ attr_reader :box
60
+
61
+ # Creates a new InlineBox object wrapping +box+.
60
62
  #
61
63
  # The +valign+ argument can be used to specify the vertical alignment of the box relative to
62
- # other items in the LineFragment - see #valign and LineFragment.
63
- def initialize(width, height, valign: :baseline, &block)
64
- @width = width
65
- @height = height
64
+ # other items in the Line.
65
+ def initialize(box, valign: :baseline)
66
+ @box = box
66
67
  @valign = valign
67
- @draw_block = block
68
68
  end
69
69
 
70
- # :call-seq:
71
- # box.draw(canvas, x, y) -> block_result
72
- #
73
- # Draws the contents of the box onto the canvas at the position (x, y), and returns the result
74
- # of the drawing block (see #initialize).
75
- #
76
- # The coordinate system is translated so that the origin is at (x, y) during the drawing
77
- # operations.
70
+ # Returns +true+ if this inline box is just a placeholder without drawing operations.
71
+ def empty?
72
+ box.empty?
73
+ end
74
+
75
+ # Returns the width of the wrapped box plus its left and right margins.
76
+ def width
77
+ box.width + box.style.margin.left + box.style.margin.right
78
+ end
79
+
80
+ # Returns the height of the wrapped box plus its top and bottom margins.
81
+ def height
82
+ box.height + box.style.margin.top + box.style.margin.bottom
83
+ end
84
+
85
+ # Draws the wrapped box. If the box has margins specified, the x and y offsets are correctly
86
+ # adjusted.
78
87
  def draw(canvas, x, y)
79
- canvas.translate(x, y) { @draw_block.call(self, canvas) }
88
+ box.draw(canvas, x + box.style.margin.left, y + box.style.margin.bottom)
80
89
  end
81
90
 
82
91
  # The minimum x-coordinate which is always 0.
@@ -84,7 +93,7 @@ module HexaPDF
84
93
  0
85
94
  end
86
95
 
87
- # The maximum x-coordinate which is equivalent to the width of the box.
96
+ # The maximum x-coordinate which is equivalent to the width of the inline box.
88
97
  def x_max
89
98
  width
90
99
  end
@@ -37,26 +37,25 @@ require 'hexapdf/layout/text_fragment'
37
37
  module HexaPDF
38
38
  module Layout
39
39
 
40
- # A LineFragment describes a line of text and can contain TextFragment objects or InlineBox
41
- # objects.
40
+ # A Line describes a line of text and can contain TextFragment objects or InlineBox objects.
42
41
  #
43
42
  # The items of a line fragment are aligned along the x-axis which coincides with the text
44
43
  # baseline. The vertical alignment is determined by the value of the #valign method:
45
44
  #
46
45
  # :text_top::
47
- # Align the top of the box with the top of the text of the LineFragment.
46
+ # Align the top of the box with the top of the text of the Line.
48
47
  #
49
48
  # :text_bottom::
50
- # Align the bottom of the box with the bottom of the text of the LineFragment.
49
+ # Align the bottom of the box with the bottom of the text of the Line.
51
50
  #
52
51
  # :baseline::
53
- # Align the bottom of the box with the baseline of the LineFragment.
52
+ # Align the bottom of the box with the baseline of the Line.
54
53
  #
55
54
  # :top::
56
- # Align the top of the box with the top of the LineFragment.
55
+ # Align the top of the box with the top of the Line.
57
56
  #
58
57
  # :bottom::
59
- # Align the bottom of the box with the bottom of the LineFragment.
58
+ # Align the bottom of the box with the bottom of the Line.
60
59
  #
61
60
  # :text::
62
61
  # This is a special alignment value for text fragment objects. The text fragment is aligned
@@ -86,7 +85,7 @@ module HexaPDF
86
85
  # implemented:
87
86
  #
88
87
  # #height:: The height of the item.
89
- class LineFragment
88
+ class Line
90
89
 
91
90
  # Helper class for calculating the needed vertical dimensions of a line.
92
91
  class HeightCalculator
@@ -122,7 +121,7 @@ module HexaPDF
122
121
 
123
122
  # Returns the result of the calculations, the array [y_min, y_max, text_y_min, text_y_max].
124
123
  #
125
- # See LineFragment for their meaning.
124
+ # See Line for their meaning.
126
125
  def result
127
126
  y_min = [@text_y_max - @max_text_top_height, @text_y_min].min
128
127
  y_max = [@text_y_min + @max_text_bottom_height, @max_base_height, @text_y_max].max
@@ -180,7 +179,7 @@ module HexaPDF
180
179
  # the baseline of this line.
181
180
  attr_accessor :y_offset
182
181
 
183
- # Creates a new LineFragment object, adding all given items to it.
182
+ # Creates a new Line object, adding all given items to it.
184
183
  def initialize(items = [])
185
184
  @items = []
186
185
  items.each {|i| add(i)}
@@ -211,7 +210,7 @@ module HexaPDF
211
210
  alias :<< :add
212
211
 
213
212
  # :call-seq:
214
- # line_fragment.each {|item, x, y| block }
213
+ # line.each {|item, x, y| block }
215
214
  #
216
215
  # Yields each item together with its horizontal offset from 0 and vertical offset from the
217
216
  # baseline.
@@ -287,7 +286,7 @@ module HexaPDF
287
286
  end
288
287
 
289
288
  # :call-seq:
290
- # line_fragment.clear_cache -> line_fragment
289
+ # line.clear_cache -> line
291
290
  #
292
291
  # Clears all cached values.
293
292
  #
@@ -300,7 +299,7 @@ module HexaPDF
300
299
  private
301
300
 
302
301
  # :call-seq:
303
- # line_fragment.calculate_y_dimensions -> [y_min, y_max, text_y_min, text_y_max]
302
+ # line.calculate_y_dimensions -> [y_min, y_max, text_y_min, text_y_max]
304
303
  #
305
304
  # Calculates all y-values and returns them as array.
306
305
  #
@@ -32,6 +32,7 @@
32
32
  #++
33
33
 
34
34
  require 'hexapdf/error'
35
+ require 'hexapdf/content/graphics_state'
35
36
 
36
37
  module HexaPDF
37
38
  module Layout
@@ -40,6 +41,13 @@ module HexaPDF
40
41
  #
41
42
  # Each property except #font has a default value, so only the desired properties need to be
42
43
  # changed.
44
+ #
45
+ # Each property has three associated methods:
46
+ #
47
+ # property_name:: Getter method.
48
+ # property_name(*args) and property_name=:: Setter method.
49
+ # property_name?:: Tester method to see if a value has been set or if the default value has
50
+ # already been used.
43
51
  class Style
44
52
 
45
53
  # Defines how the distance between the baselines of two adjacent text lines is determined:
@@ -92,7 +100,7 @@ module HexaPDF
92
100
  end
93
101
  end
94
102
 
95
- # Returns the distance between the baselines of the two given LineFragment objects.
103
+ # Returns the distance between the baselines of the two given Line objects.
96
104
  def baseline_distance(line1, line2)
97
105
  case type
98
106
  when :proportional then (line1.y_min.abs + line2.y_max) * value
@@ -101,8 +109,8 @@ module HexaPDF
101
109
  end
102
110
  end
103
111
 
104
- # Returns the gap between the two given LineFragment objects, i.e. the distance between the
105
- # y_min of the first line and the y_max of the second line.
112
+ # Returns the gap between the two given Line objects, i.e. the distance between the y_min of
113
+ # the first line and the y_max of the second line.
106
114
  def gap(line1, line2)
107
115
  case type
108
116
  when :proportional then (line1.y_min.abs + line2.y_max) * (value - 1)
@@ -113,6 +121,374 @@ module HexaPDF
113
121
 
114
122
  end
115
123
 
124
+ # A Quad holds four values and allows them to be accessed by the names top, right, bottom and
125
+ # left. Quads are normally used for holding values pertaining to boxes, like margins, paddings
126
+ # or borders.
127
+ class Quad
128
+
129
+ # The value for top.
130
+ attr_accessor :top
131
+
132
+ # The value for bottom.
133
+ attr_accessor :bottom
134
+
135
+ # The value for left.
136
+ attr_accessor :left
137
+
138
+ # The value for right.
139
+ attr_accessor :right
140
+
141
+ # :call-seq:
142
+ # Quad.new(value)
143
+ # Quad.new(array)
144
+ # Quad.new(quad)
145
+ #
146
+ # Creates a new Quad object.
147
+ #
148
+ # * If a single value is provided that is neither a Quad nor an array, it is handled as if
149
+ # an array with one value was given.
150
+ #
151
+ # * If a Quad is provided, its values are used.
152
+ #
153
+ # * If an array is provided, it depends on the number of elemens in it:
154
+ #
155
+ # * One value: All attributes are set to the same value.
156
+ # * Two values: Top and bottom are set to the first value, left and right to the second
157
+ # value.
158
+ # * Three values: Top is set to the first, left and right to the second, and bottom to the
159
+ # third value.
160
+ # * Four or more values: Top is set to the first, right to the second, bottom to the third
161
+ # and left to the fourth value.
162
+ def initialize(obj)
163
+ case obj
164
+ when Quad
165
+ @top = obj.top
166
+ @bottom = obj.bottom
167
+ @left = obj.left
168
+ @right = obj.right
169
+ when Array
170
+ @top = obj[0]
171
+ @bottom = obj[2] || obj[0]
172
+ @left = obj[3] || obj[1] || obj[0]
173
+ @right = obj[1] || obj[0]
174
+ else
175
+ @top = @bottom = @left = @right = obj
176
+ end
177
+ end
178
+
179
+ # Returns +true+ if the quad effectively contains only one value.
180
+ def simple?
181
+ @top == @bottom && @top == @left && @top == @right
182
+ end
183
+
184
+ end
185
+
186
+ # Represents the border of a rectangular area.
187
+ class Border
188
+
189
+ # The widths of each edge. See Quad.
190
+ attr_reader :width
191
+
192
+ # The colors of each edge. See Quad.
193
+ attr_reader :color
194
+
195
+ # The styles of each edge. See Quad.
196
+ attr_reader :style
197
+
198
+ # Creates a new border style. All arguments can be set to any value that a Quad can process.
199
+ def initialize(width: 0, color: 0, style: :solid)
200
+ @width = Quad.new(width)
201
+ @color = Quad.new(color)
202
+ @style = Quad.new(style)
203
+ end
204
+
205
+ # Returns +true+ if there is no border.
206
+ def none?
207
+ width.simple? && width.top == 0
208
+ end
209
+
210
+ # Draws the border onto the canvas, inside the rectangle (x, y, w, h).
211
+ def draw(canvas, x, y, w, h)
212
+ canvas.save_graphics_state do
213
+ if width.simple? && color.simple? && style.simple?
214
+ draw_simple_border(canvas, x, y, w, h)
215
+ else
216
+ draw_complex_border(canvas, x, y, w, h)
217
+ end
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ # Draws the border assuming that only one width, style and color are used.
224
+ def draw_simple_border(canvas, x, y, w, h)
225
+ offset = width.top / 2.0
226
+ canvas.stroke_color(color.top).
227
+ line_width(width.top).
228
+ line_join_style(:miter).
229
+ miter_limit(10).
230
+ line_cap_style(line_cap_style(:top))
231
+
232
+ if style.top == :solid
233
+ canvas.line_dash_pattern(0).
234
+ rectangle(x + offset, y + offset, w - 2 * offset, h - 2 * offset).stroke
235
+ else
236
+ canvas.rectangle(x, y, w, h).clip_path.end_path
237
+
238
+ canvas.line_dash_pattern(line_dash_pattern(:top, w)).
239
+ line(x, y + h - offset, x + w, y + h - offset).
240
+ line(x + w, y + offset, x, y + offset).stroke
241
+ canvas.line_dash_pattern(line_dash_pattern(:right, h)).
242
+ line(x + w - offset, y + h, x + w - offset, y).
243
+ line(x + offset, y, x + offset, y + h).stroke
244
+ end
245
+ end
246
+
247
+ # Draws a complex border, i.e. one where every edge is potentially differently styled.
248
+ def draw_complex_border(canvas, x, y, w, h)
249
+ left = x
250
+ bottom = y
251
+ right = left + w
252
+ top = bottom + h
253
+ inner_left = left + width.left
254
+ inner_bottom = bottom + width.bottom
255
+ inner_right = right - width.right
256
+ inner_top = top - width.top
257
+
258
+ if width.top > 0
259
+ canvas.save_graphics_state do
260
+ canvas.polyline(left, top, right, top, inner_right, inner_top,
261
+ inner_left, inner_top).
262
+ clip_path.end_path
263
+ canvas.stroke_color(color.top).
264
+ line_width(width.top).
265
+ line_cap_style(line_cap_style(:top)).
266
+ line_dash_pattern(line_dash_pattern(:top, w)).
267
+ line(left, top - width.top / 2.0, right, top - width.top / 2.0).stroke
268
+ end
269
+ end
270
+
271
+ if width.right > 0
272
+ canvas.save_graphics_state do
273
+ canvas.polyline(right, top, right, bottom, inner_right, inner_bottom,
274
+ inner_right, inner_top).
275
+ clip_path.end_path
276
+ canvas.stroke_color(color.right).
277
+ line_width(width.right).
278
+ line_cap_style(line_cap_style(:right)).
279
+ line_dash_pattern(line_dash_pattern(:right, h)).
280
+ line(right - width.right / 2.0, top, right - width.right / 2.0, bottom).stroke
281
+ end
282
+ end
283
+
284
+ if width.bottom > 0
285
+ canvas.save_graphics_state do
286
+ canvas.polyline(right, bottom, left, bottom, inner_left, inner_bottom,
287
+ inner_right, inner_bottom).
288
+ clip_path.end_path
289
+ canvas.stroke_color(color.bottom).
290
+ line_width(width.bottom).
291
+ line_cap_style(line_cap_style(:bottom)).
292
+ line_dash_pattern(line_dash_pattern(:bottom, w)).
293
+ line(right, bottom + width.bottom / 2.0, left, bottom + width.bottom / 2.0).stroke
294
+ end
295
+ end
296
+
297
+ if width.left > 0
298
+ canvas.save_graphics_state do
299
+ canvas.polyline(left, bottom, left, top, inner_left, inner_top,
300
+ inner_left, inner_bottom).
301
+ clip_path.end_path
302
+ canvas.stroke_color(color.left).
303
+ line_width(width.left).
304
+ line_cap_style(line_cap_style(:left)).
305
+ line_dash_pattern(line_dash_pattern(:left, h)).
306
+ line(left + width.left / 2.0, bottom, left + width.left / 2.0, top).stroke
307
+ end
308
+ end
309
+ end
310
+
311
+ # Returns the line cap style for the given edge name.
312
+ def line_cap_style(edge)
313
+ case style.send(edge)
314
+ when :solid then :butt
315
+ when :dashed then :projecting_square
316
+ when :dashed_round, :dotted then :round
317
+ else
318
+ raise ArgumentError, "Invalid border style specified: #{style.send(edge)}"
319
+ end
320
+ end
321
+
322
+ # Returns the line dash pattern for the given edge name. The argument +length+ needs to
323
+ # contain the length of the edge.
324
+ def line_dash_pattern(edge, length)
325
+ case style.send(edge)
326
+ when :solid
327
+ 0
328
+ when :dashed, :dashed_round
329
+ # Due to the used line cap styles, a dash of length w appears with a length of 2w. The
330
+ # gap between dashes is nominally 3w but adjusted so that full dashes start and end in
331
+ # the corners.
332
+ w = width.send(edge)
333
+ count = [(length.to_f / (w * 3)).floor, 1].max
334
+ gap = [(length - w * (count + 2)).to_f, 0].max / count
335
+ HexaPDF::Content::LineDashPattern.new([w, gap], w * 0.5 + gap)
336
+ when :dotted
337
+ # Adjust the gap so that full dots appear in the corners.
338
+ w = width.send(edge)
339
+ gap = (length - w).to_f / (length.to_f / (w * 2)).ceil
340
+ HexaPDF::Content::LineDashPattern.new([0, gap], [gap - w * 0.5, 0].max)
341
+ end
342
+ end
343
+
344
+ end
345
+
346
+ # Represents layers that can be drawn under or over a box.
347
+ #
348
+ # There are two ways to specify layers via #add:
349
+ #
350
+ # * Directly by providing a callable object.
351
+ #
352
+ # * By reference to a callable object or class in the 'style.layers_map' configuration option.
353
+ # The reference name is looked up in the configuration option using
354
+ # HexaPDF::Configuration#constantize. If the resulting object is a callable object, it is
355
+ # used; otherwise it is assumed that it is a class and an object is instantiated, passing in
356
+ # any options given on #add.
357
+ #
358
+ # The object resolved in this way needs to respond to #call(canvas, box) where +canvas+ is the
359
+ # HexaPDF::Content::Canvas object on which it should be drawn and +box+ is a box-like object
360
+ # (e.g. Box or TextFragment). The coordinate system is translated so that the origin is at the
361
+ # lower right corner of the box during the drawing operations.
362
+ class Layers
363
+
364
+ # Creates a new Layers object popuplated with the given +layers+.
365
+ def initialize(layers = [])
366
+ @layers = layers
367
+ end
368
+
369
+ # :call-seq:
370
+ # layers.add {|canvas, box| block}
371
+ # layers.add(name, **options)
372
+ #
373
+ # Adds a new layer object.
374
+ #
375
+ # The layer object can either be specified as a block or by reference to a configured layer
376
+ # object in 'style.layers_map'. In this case +name+ is used as the reference and the options
377
+ # are passed to layer object if it needs initialization.
378
+ def add(name = nil, **options, &block)
379
+ if block_given?
380
+ @layers << block
381
+ elsif name
382
+ @layers << [name, options]
383
+ else
384
+ raise ArgumentError, "Layer object name or block missing"
385
+ end
386
+ end
387
+
388
+ # Draws all layer objects onto the canvas at the position [x, y] for the given box.
389
+ def draw(canvas, x, y, box)
390
+ return if none?
391
+
392
+ canvas.translate(x, y) do
393
+ each(canvas.context.document.config) do |layer|
394
+ canvas.save_graphics_state { layer.call(canvas, box) }
395
+ end
396
+ end
397
+ end
398
+
399
+ # Yields all layer objects. Objects that have been specified via a reference are first
400
+ # resolved using the provided configuration object.
401
+ def each(config) #:yield: layer
402
+ @layers.each do |obj, options|
403
+ obj = config.constantize('style.layers_map', obj) unless obj.respond_to?(:call)
404
+ obj = obj.new(**options) unless obj.respond_to?(:call)
405
+ yield(obj)
406
+ end
407
+ end
408
+
409
+ # Returns +true+ if there are no layers defined.
410
+ def none?
411
+ @layers.empty?
412
+ end
413
+
414
+ end
415
+
416
+ # The LinkLayer class provides support for linking to in-document or remote destinations for
417
+ # Style objects using link annotations. Typical use cases would be linking to a (named)
418
+ # destination on a different page or executing a URI action.
419
+ #
420
+ # See: PDF1.7 s12.5.6.6, Layers, HexaPDF::Type::Annotations::Link
421
+ class LinkLayer
422
+
423
+ # Creates a new LinkLayer object.
424
+ #
425
+ # The following arguments are allowed (note that only *one* of +dest+, +uri+ or +file+ may
426
+ # be specified):
427
+ #
428
+ # +dest+::
429
+ # The destination array or a name of a named destination for in-document links.
430
+ #
431
+ # +uri+::
432
+ # The URI to link to.
433
+ #
434
+ # +file+::
435
+ # The file that should be opened or, if it refers to an application, the application that
436
+ # should be launched. Can either be a string or a Filespec object. Also see:
437
+ # HexaPDF::Type::FileSpecification.
438
+ #
439
+ # +border+::
440
+ # If set to +true+, a standard border is used. Also accepts an array that adheres to the
441
+ # rules for annotation borders.
442
+ #
443
+ # +border_color+::
444
+ # Defines the border color. Can be an array with 0 (transparent), 1 (grayscale), 3 (RGB)
445
+ # or 4 (CMYK) values.
446
+ #
447
+ # Examples:
448
+ # LinkLayer.new(dest: [page, :XYZ, nil, nil, nil], border: true)
449
+ # LinkLayer.new(uri: "https://my.example.com/path", border: [5 5 2])
450
+ def initialize(dest: nil, uri: nil, file: nil, border: false, border_color: nil)
451
+ if dest && (uri || file) || uri && file
452
+ raise ArgumentError, "Only one of dest, uri and file is allowed"
453
+ end
454
+ @dest = dest
455
+ @action = if uri
456
+ {S: :URI, URI: uri}
457
+ elsif file
458
+ {S: :Launch, F: file, NewWindow: true}
459
+ end
460
+ @border = case border
461
+ when false then [0, 0, 0]
462
+ when true then nil
463
+ when Array then border
464
+ else raise ArgumentError, "Invalid value for border: #{border}"
465
+ end
466
+ @border_color = border_color
467
+ end
468
+
469
+ # Creates the needed link annotation if possible, i.e. if the context of the canvas is a page.
470
+ def call(canvas, box)
471
+ return unless canvas.context.type == :Page
472
+ page = canvas.context
473
+ matrix = canvas.graphics_state.ctm
474
+ quad_points = [*matrix.evaluate(0, 0), *matrix.evaluate(box.width, 0),
475
+ *matrix.evaluate(box.width, box.height), *matrix.evaluate(0, box.height)]
476
+ x_minmax = quad_points.values_at(0, 2, 4, 6).minmax
477
+ y_minmax = quad_points.values_at(1, 3, 5, 7).minmax
478
+ annot = {
479
+ Subtype: :Link,
480
+ Rect: [x_minmax[0], y_minmax[0], x_minmax[1], y_minmax[1]],
481
+ QuadPoints: quad_points,
482
+ Dest: @dest,
483
+ A: @action,
484
+ Border: @border,
485
+ C: @border_color && canvas.color_from_specification(@border_color).components,
486
+ }
487
+ (page[:Annots] ||= []) << page.document.add(annot)
488
+ end
489
+
490
+ end
491
+
116
492
  UNSET = ::Object.new # :nodoc:
117
493
 
118
494
  # Creates a new Style object.
@@ -195,6 +571,109 @@ module HexaPDF
195
571
  #
196
572
  # See: HexaPDF::Layout::TextShaper#shape_text for available features.
197
573
 
574
+ ##
575
+ # :method: text_rendering_mode
576
+ # :call-seq:
577
+ # text_rendering_mode(mode = nil)
578
+ #
579
+ # The text rendering mode, i.e. whether text should be filled, stroked, clipped, invisible or
580
+ # a combination thereof, defaults to :fill.
581
+ #
582
+ # See: HexaPDF::Content::Canvas#text_rendering_mode
583
+
584
+ ##
585
+ # :method: subscript
586
+ # :call-seq:
587
+ # subscript(enable = false)
588
+ #
589
+ # Render the text as subscript, i.e. lower and in a smaller font size; defaults to false.
590
+ #
591
+ # If superscript is set, it will be deactivated.
592
+
593
+ ##
594
+ # :method: superscript
595
+ # :call-seq:
596
+ # superscript(enable = false)
597
+ #
598
+ # Render the text as superscript, i.e. higher and in a smaller font size; defaults to false.
599
+ #
600
+ # If subscript is set, it will be deactivated.
601
+
602
+ ##
603
+ # :method: fill_color
604
+ # :call-seq:
605
+ # fill_color(color = nil)
606
+ #
607
+ # The color used for filling (e.g. text), defaults to black.
608
+ #
609
+ # See: HexaPDF::Content::Canvas#fill_color
610
+
611
+ ##
612
+ # :method: fill_alpha
613
+ # :call-seq:
614
+ # fill_alpha(alpha = nil)
615
+ #
616
+ # The alpha value applied to filling operations (e.g. text), defaults to 1 (i.e. 100%
617
+ # opaque).
618
+ #
619
+ # See: HexaPDF::Content::Canvas#opacity
620
+
621
+ ##
622
+ # :method: stroke_color
623
+ # :call-seq:
624
+ # stroke_color(color = nil)
625
+ #
626
+ # The color used for stroking (e.g. text outlines), defaults to black.
627
+ #
628
+ # See: HexaPDF::Content::Canvas#stroke_color
629
+
630
+ ##
631
+ # :method: stroke_alpha
632
+ # :call-seq:
633
+ # stroke_alpha(alpha = nil)
634
+ #
635
+ # The alpha value applied to stroking operations (e.g. text outlines), defaults to 1 (i.e.
636
+ # 100% opaque).
637
+ #
638
+ # See: HexaPDF::Content::Canvas#opacity
639
+
640
+ ##
641
+ # :method: stroke_width
642
+ # :call-seq:
643
+ # stroke_width(width = nil)
644
+ #
645
+ # The line width used for stroking operations (e.g. text outlines), defaults to 1.
646
+ #
647
+ # See: HexaPDF::Content::Canvas#line_width
648
+
649
+ ##
650
+ # :method: stroke_cap_style
651
+ # :call-seq:
652
+ # stroke_cap_style(style = nil)
653
+ #
654
+ # The line cap style used for stroking operations (e.g. text outlines), defaults to :butt.
655
+ #
656
+ # See: HexaPDF::Content::Canvas#line_cap_style
657
+
658
+ ##
659
+ # :method: stroke_join_style
660
+ # :call-seq:
661
+ # stroke_join_style(style = nil)
662
+ #
663
+ # The line join style used for stroking operations (e.g. text outlines), defaults to :miter.
664
+ #
665
+ # See: HexaPDF::Content::Canvas#line_join_style
666
+
667
+ ##
668
+ # :method: stroke_miter_limit
669
+ # :call-seq:
670
+ # stroke_miter_limit(limit = nil)
671
+ #
672
+ # The miter limit used for stroking operations (e.g. text outlines) when #stroke_join_style is
673
+ # :miter, defaults to 10.0.
674
+ #
675
+ # See: HexaPDF::Content::Canvas#miter_limit
676
+
198
677
  ##
199
678
  # :method: align
200
679
  # :call-seq:
@@ -229,6 +708,50 @@ module HexaPDF
229
708
  #
230
709
  # The indentation to be used for the first line of a sequence of text lines, defaults to 0.
231
710
 
711
+ ##
712
+ # :method: background_color
713
+ # :call-seq:
714
+ # background_color(color = nil)
715
+ #
716
+ # The color used for backgrounds, defaults to +nil+ (i.e. no background).
717
+
718
+ ##
719
+ # :method: padding
720
+ # :call-seq:
721
+ # padding(value = nil)
722
+ #
723
+ # The padding between the border and the contents, defaults to 0 for all four sides.
724
+
725
+ ##
726
+ # :method: margin
727
+ # :call-seq:
728
+ # margin(value = nil)
729
+ #
730
+ # The margin around a box, defaults to 0 for all four sides.
731
+
732
+ ##
733
+ # :method: border
734
+ # :call-seq:
735
+ # border(value = nil)
736
+ #
737
+ # The border around the contents, defaults to no border.
738
+
739
+ ##
740
+ # :method: overlays
741
+ # :call-seq:
742
+ # overlays(layers = nil)
743
+ #
744
+ # A Layers object containing all the layers that should be drawn over the box; defaults to no
745
+ # layers being drawn.
746
+
747
+ ##
748
+ # :method: underlays
749
+ # :call-seq:
750
+ # underlays(layers = nil)
751
+ #
752
+ # A Layers object containing all the layers that should be drawn under the box; defaults to no
753
+ # layers being drawn.
754
+
232
755
  [
233
756
  [:font, "raise HexaPDF::Error, 'No font set'"],
234
757
  [:font_size, 10],
@@ -237,73 +760,132 @@ module HexaPDF
237
760
  [:horizontal_scaling, 100],
238
761
  [:text_rise, 0],
239
762
  [:font_features, {}],
763
+ [:text_rendering_mode, :fill],
764
+ [:subscript, false, "value; superscript(false) if superscript"],
765
+ [:superscript, false, "value; subscript(false) if subscript"],
766
+ [:underline, false],
767
+ [:strikeout, false],
768
+ [:fill_color, "default_color"],
769
+ [:fill_alpha, 1],
770
+ [:stroke_color, "default_color"],
771
+ [:stroke_alpha, 1],
772
+ [:stroke_width, 1],
773
+ [:stroke_cap_style, :butt],
774
+ [:stroke_join_style, :miter],
775
+ [:stroke_miter_limit, 10.0],
776
+ [:stroke_dash_pattern, "Content::LineDashPattern.new",
777
+ "Content::LineDashPattern.normalize(value, phase)", ", phase = 0"],
240
778
  [:align, :left],
241
779
  [:valign, :top],
242
780
  [:text_indent, 0],
243
- ].each do |name, default|
781
+ [:line_spacing, "LineSpacing.new(:single)",
782
+ "LineSpacing.new(value, value: extra_arg)", ", extra_arg = nil"],
783
+ [:background_color, nil],
784
+ [:padding, "Quad.new(0)", "Quad.new(value)"],
785
+ [:margin, "Quad.new(0)", "Quad.new(value)"],
786
+ [:border, "Border.new", "Border.new(value)"],
787
+ [:overlays, "Layers.new", "Layers.new(value)"],
788
+ [:underlays, "Layers.new", "Layers.new(value)"],
789
+ ].each do |name, default, setter = "value", extra_args = ""|
244
790
  default = default.inspect unless default.kind_of?(String)
245
791
  module_eval(<<-EOF, __FILE__, __LINE__)
246
- def #{name}(value = UNSET)
247
- value == UNSET ? (@#{name} ||= #{default}) : (@#{name} = value; self)
792
+ def #{name}(value = UNSET#{extra_args})
793
+ value == UNSET ? (@#{name} ||= #{default}) : (@#{name} = #{setter}; self)
794
+ end
795
+ def #{name}?
796
+ defined?(@#{name})
248
797
  end
249
798
  EOF
250
799
  alias_method("#{name}=", name)
251
800
  end
252
801
 
253
- # :call-seq:
254
- # line_spacing(type = nil, value = nil)
255
- #
256
- # The spacing between consecutive lines, defaults to type = :single.
257
- #
258
- # See: LineSpacing
259
- def line_spacing(type = UNSET, value = nil)
260
- if type == UNSET
261
- @line_spacing ||= LineSpacing.new(:single)
262
- else
263
- @line_spacing = LineSpacing.new(type, value: value)
264
- self
265
- end
266
- end
267
- alias_method(:line_spacing=, :line_spacing)
268
802
 
803
+ ##
804
+ # :method: text_segmentation_algorithm
269
805
  # :call-seq:
270
806
  # text_segmentation_algorithm(algorithm = nil) {|items| block }
271
807
  #
272
808
  # The algorithm to use for text segmentation purposes, defaults to
273
- # TextBox::SimpleTextSegmentation.
809
+ # TextLayouter::SimpleTextSegmentation.
274
810
  #
275
811
  # When setting the algorithm, either an object that responds to #call(items) or a block can be
276
812
  # used.
277
- def text_segmentation_algorithm(algorithm = UNSET, &block)
278
- if algorithm == UNSET && !block
279
- @text_segmentation_algorithm ||= TextBox::SimpleTextSegmentation
280
- else
281
- @text_segmentation_algorithm = (algorithm != UNSET ? algorithm : block)
282
- self
283
- end
284
- end
285
- alias_method(:text_segmentation_algorithm=, :text_segmentation_algorithm)
286
813
 
814
+ ##
815
+ # :method: text_line_wrapping_algorithm
287
816
  # :call-seq:
288
- # text_line_wrapping_algorithm(algorithm = nil) {|items| block }
817
+ # text_line_wrapping_algorithm(algorithm = nil) {|items, width_block| block }
289
818
  #
290
- # The line wrapping algorithm that should be used, defaults to TextBox::SimpleLineWrapping.
819
+ # The line wrapping algorithm that should be used, defaults to
820
+ # TextLayouter::SimpleLineWrapping.
291
821
  #
292
822
  # When setting the algorithm, either an object that responds to #call or a block can be used.
293
- # See TextBox::SimpleLineWrapping#call for the needed method signature.
294
- def text_line_wrapping_algorithm(algorithm = UNSET, &block)
295
- if algorithm == UNSET && !block
296
- @text_line_wrapping_algorithm ||= TextBox::SimpleLineWrapping
823
+ # See TextLayouter::SimpleLineWrapping#call for the needed method signature.
824
+
825
+ [
826
+ [:text_segmentation_algorithm, 'TextLayouter::SimpleTextSegmentation'],
827
+ [:text_line_wrapping_algorithm, 'TextLayouter::SimpleLineWrapping'],
828
+ ].each do |name, default|
829
+ default = default.inspect unless default.kind_of?(String)
830
+ module_eval(<<-EOF, __FILE__, __LINE__)
831
+ def #{name}(value = UNSET, &block)
832
+ if value == UNSET && !block
833
+ @#{name} ||= #{default}
834
+ else
835
+ @#{name} = (value != UNSET ? value : block)
836
+ self
837
+ end
838
+ end
839
+ def #{name}?
840
+ defined?(@#{name})
841
+ end
842
+ EOF
843
+ alias_method("#{name}=", name)
844
+ end
845
+
846
+ # The calculated text rise, taking superscript and subscript into account.
847
+ def calculated_text_rise
848
+ if superscript
849
+ text_rise + font_size * 0.33
850
+ elsif subscript
851
+ text_rise - font_size * 0.20
297
852
  else
298
- @text_line_wrapping_algorithm = (algorithm != UNSET ? algorithm : block)
299
- self
853
+ text_rise
300
854
  end
301
855
  end
302
- alias_method(:text_line_wrapping_algorithm=, :text_line_wrapping_algorithm)
856
+
857
+ # The calculated font size, taking superscript and subscript into account.
858
+ def calculated_font_size
859
+ (superscript || subscript ? 0.583 : 1) * font_size
860
+ end
861
+
862
+ # Returns the correct offset from the baseline for the underline.
863
+ def calculated_underline_position
864
+ calculated_text_rise +
865
+ calculated_font_size / 1000.0 * font.wrapped_font.underline_position *
866
+ font.scaling_factor - calculated_underline_thickness / 2.0
867
+ end
868
+
869
+ # Returns the correct thickness for the underline.
870
+ def calculated_underline_thickness
871
+ calculated_font_size / 1000.0 * font.wrapped_font.underline_thickness * font.scaling_factor
872
+ end
873
+
874
+ # Returns the correct offset from the baseline for the strikeout line.
875
+ def calculated_strikeout_position
876
+ calculated_text_rise +
877
+ calculated_font_size / 1000.0 * font.wrapped_font.strikeout_position *
878
+ font.scaling_factor - calculated_strikeout_thickness / 2.0
879
+ end
880
+
881
+ # Returns the correct thickness for the strikeout line.
882
+ def calculated_strikeout_thickness
883
+ calculated_font_size / 1000.0 * font.wrapped_font.strikeout_thickness * font.scaling_factor
884
+ end
303
885
 
304
886
  # The font size scaled appropriately.
305
887
  def scaled_font_size
306
- @scaled_font_size ||= font_size / 1000.0 * scaled_horizontal_scaling
888
+ @scaled_font_size ||= calculated_font_size / 1000.0 * scaled_horizontal_scaling
307
889
  end
308
890
 
309
891
  # The character spacing scaled appropriately.
@@ -359,6 +941,13 @@ module HexaPDF
359
941
  @scaled_item_widths.clear
360
942
  end
361
943
 
944
+ private
945
+
946
+ # Returns the default color for an empty PDF page, i.e. black.
947
+ def default_color
948
+ GlobalConfiguration.constantize('color_space.map'.freeze, :DeviceGray).new.default_color
949
+ end
950
+
362
951
  end
363
952
 
364
953
  end