prawn-git 2.0.1

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 (252) hide show
  1. checksums.yaml +7 -0
  2. data/.yardopts +10 -0
  3. data/COPYING +2 -0
  4. data/GPLv2 +340 -0
  5. data/GPLv3 +674 -0
  6. data/Gemfile +11 -0
  7. data/LICENSE +56 -0
  8. data/Rakefile +55 -0
  9. data/data/fonts/Courier-Bold.afm +342 -0
  10. data/data/fonts/Courier-BoldOblique.afm +342 -0
  11. data/data/fonts/Courier-Oblique.afm +342 -0
  12. data/data/fonts/Courier.afm +342 -0
  13. data/data/fonts/Helvetica-Bold.afm +2827 -0
  14. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  15. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  16. data/data/fonts/Helvetica.afm +3051 -0
  17. data/data/fonts/MustRead.html +19 -0
  18. data/data/fonts/Symbol.afm +213 -0
  19. data/data/fonts/Times-Bold.afm +2588 -0
  20. data/data/fonts/Times-BoldItalic.afm +2384 -0
  21. data/data/fonts/Times-Italic.afm +2667 -0
  22. data/data/fonts/Times-Roman.afm +2419 -0
  23. data/data/fonts/ZapfDingbats.afm +225 -0
  24. data/data/images/16bit.alpha +0 -0
  25. data/data/images/16bit.color +0 -0
  26. data/data/images/16bit.png +0 -0
  27. data/data/images/arrow.png +0 -0
  28. data/data/images/arrow2.png +0 -0
  29. data/data/images/dice.alpha +0 -0
  30. data/data/images/dice.color +0 -0
  31. data/data/images/dice.png +0 -0
  32. data/data/images/dice_interlaced.png +0 -0
  33. data/data/images/fractal.jpg +0 -0
  34. data/data/images/indexed_color.dat +0 -0
  35. data/data/images/indexed_color.png +0 -0
  36. data/data/images/letterhead.jpg +0 -0
  37. data/data/images/license.md +8 -0
  38. data/data/images/page_white_text.alpha +0 -0
  39. data/data/images/page_white_text.color +0 -0
  40. data/data/images/page_white_text.png +0 -0
  41. data/data/images/pal_bk.png +0 -0
  42. data/data/images/pigs.jpg +0 -0
  43. data/data/images/prawn.png +0 -0
  44. data/data/images/ruport.png +0 -0
  45. data/data/images/ruport_data.dat +0 -0
  46. data/data/images/ruport_transparent.png +0 -0
  47. data/data/images/ruport_type0.png +0 -0
  48. data/data/images/stef.jpg +0 -0
  49. data/data/images/tru256.bmp +0 -0
  50. data/data/images/web-links.dat +1 -0
  51. data/data/images/web-links.png +0 -0
  52. data/data/pdfs/complex_template.pdf +0 -0
  53. data/data/pdfs/contains_ttf_font.pdf +0 -0
  54. data/data/pdfs/encrypted.pdf +0 -0
  55. data/data/pdfs/form.pdf +820 -0
  56. data/data/pdfs/hexagon.pdf +61 -0
  57. data/data/pdfs/indirect_reference.pdf +86 -0
  58. data/data/pdfs/multipage_template.pdf +127 -0
  59. data/data/pdfs/nested_pages.pdf +118 -0
  60. data/data/pdfs/page_without_mediabox.pdf +193 -0
  61. data/data/pdfs/resources_as_indirect_object.pdf +83 -0
  62. data/data/pdfs/two_hexagons.pdf +90 -0
  63. data/data/pdfs/version_1_6.pdf +61 -0
  64. data/data/shift_jis_text.txt +1 -0
  65. data/lib/prawn.rb +89 -0
  66. data/lib/prawn/document.rb +706 -0
  67. data/lib/prawn/document/bounding_box.rb +539 -0
  68. data/lib/prawn/document/column_box.rb +144 -0
  69. data/lib/prawn/document/internals.rb +58 -0
  70. data/lib/prawn/document/span.rb +57 -0
  71. data/lib/prawn/encoding.rb +87 -0
  72. data/lib/prawn/errors.rb +80 -0
  73. data/lib/prawn/font.rb +413 -0
  74. data/lib/prawn/font/afm.rb +256 -0
  75. data/lib/prawn/font/dfont.rb +43 -0
  76. data/lib/prawn/font/ttf.rb +355 -0
  77. data/lib/prawn/font_metric_cache.rb +46 -0
  78. data/lib/prawn/graphics.rb +646 -0
  79. data/lib/prawn/graphics/cap_style.rb +47 -0
  80. data/lib/prawn/graphics/color.rb +232 -0
  81. data/lib/prawn/graphics/dash.rb +109 -0
  82. data/lib/prawn/graphics/join_style.rb +49 -0
  83. data/lib/prawn/graphics/patterns.rb +126 -0
  84. data/lib/prawn/graphics/transformation.rb +157 -0
  85. data/lib/prawn/graphics/transparency.rb +101 -0
  86. data/lib/prawn/grid.rb +279 -0
  87. data/lib/prawn/image_handler.rb +44 -0
  88. data/lib/prawn/images.rb +199 -0
  89. data/lib/prawn/images/image.rb +49 -0
  90. data/lib/prawn/images/jpg.rb +91 -0
  91. data/lib/prawn/images/png.rb +290 -0
  92. data/lib/prawn/measurement_extensions.rb +50 -0
  93. data/lib/prawn/measurements.rb +77 -0
  94. data/lib/prawn/outline.rb +289 -0
  95. data/lib/prawn/repeater.rb +124 -0
  96. data/lib/prawn/security.rb +288 -0
  97. data/lib/prawn/security/arcfour.rb +54 -0
  98. data/lib/prawn/soft_mask.rb +94 -0
  99. data/lib/prawn/stamp.rb +136 -0
  100. data/lib/prawn/text.rb +437 -0
  101. data/lib/prawn/text/box.rb +141 -0
  102. data/lib/prawn/text/formatted.rb +7 -0
  103. data/lib/prawn/text/formatted/arranger.rb +290 -0
  104. data/lib/prawn/text/formatted/box.rb +614 -0
  105. data/lib/prawn/text/formatted/fragment.rb +264 -0
  106. data/lib/prawn/text/formatted/line_wrap.rb +277 -0
  107. data/lib/prawn/text/formatted/parser.rb +224 -0
  108. data/lib/prawn/text/formatted/wrap.rb +160 -0
  109. data/lib/prawn/utilities.rb +46 -0
  110. data/lib/prawn/version.rb +5 -0
  111. data/lib/prawn/view.rb +91 -0
  112. data/manual/absolute_position.pdf +0 -0
  113. data/manual/basic_concepts/adding_pages.rb +27 -0
  114. data/manual/basic_concepts/basic_concepts.rb +36 -0
  115. data/manual/basic_concepts/creation.rb +39 -0
  116. data/manual/basic_concepts/cursor.rb +33 -0
  117. data/manual/basic_concepts/measurement.rb +25 -0
  118. data/manual/basic_concepts/origin.rb +38 -0
  119. data/manual/basic_concepts/other_cursor_helpers.rb +40 -0
  120. data/manual/basic_concepts/view.rb +42 -0
  121. data/manual/bounding_box/bounding_box.rb +39 -0
  122. data/manual/bounding_box/bounds.rb +49 -0
  123. data/manual/bounding_box/canvas.rb +24 -0
  124. data/manual/bounding_box/creation.rb +23 -0
  125. data/manual/bounding_box/indentation.rb +46 -0
  126. data/manual/bounding_box/nesting.rb +45 -0
  127. data/manual/bounding_box/russian_boxes.rb +40 -0
  128. data/manual/bounding_box/stretchy.rb +31 -0
  129. data/manual/contents.rb +29 -0
  130. data/manual/cover.rb +39 -0
  131. data/manual/document_and_page_options/background.rb +27 -0
  132. data/manual/document_and_page_options/document_and_page_options.rb +32 -0
  133. data/manual/document_and_page_options/metadata.rb +23 -0
  134. data/manual/document_and_page_options/page_margins.rb +38 -0
  135. data/manual/document_and_page_options/page_size.rb +34 -0
  136. data/manual/document_and_page_options/print_scaling.rb +20 -0
  137. data/manual/example_helper.rb +7 -0
  138. data/manual/graphics/circle_and_ellipse.rb +22 -0
  139. data/manual/graphics/color.rb +24 -0
  140. data/manual/graphics/common_lines.rb +30 -0
  141. data/manual/graphics/fill_and_stroke.rb +42 -0
  142. data/manual/graphics/fill_rules.rb +37 -0
  143. data/manual/graphics/gradients.rb +37 -0
  144. data/manual/graphics/graphics.rb +58 -0
  145. data/manual/graphics/helper.rb +24 -0
  146. data/manual/graphics/line_width.rb +35 -0
  147. data/manual/graphics/lines_and_curves.rb +41 -0
  148. data/manual/graphics/polygon.rb +29 -0
  149. data/manual/graphics/rectangle.rb +21 -0
  150. data/manual/graphics/rotate.rb +28 -0
  151. data/manual/graphics/scale.rb +41 -0
  152. data/manual/graphics/soft_masks.rb +46 -0
  153. data/manual/graphics/stroke_cap.rb +31 -0
  154. data/manual/graphics/stroke_dash.rb +48 -0
  155. data/manual/graphics/stroke_join.rb +30 -0
  156. data/manual/graphics/translate.rb +29 -0
  157. data/manual/graphics/transparency.rb +35 -0
  158. data/manual/how_to_read_this_manual.rb +40 -0
  159. data/manual/images/absolute_position.rb +23 -0
  160. data/manual/images/fit.rb +21 -0
  161. data/manual/images/horizontal.rb +25 -0
  162. data/manual/images/images.rb +40 -0
  163. data/manual/images/plain_image.rb +18 -0
  164. data/manual/images/scale.rb +22 -0
  165. data/manual/images/vertical.rb +28 -0
  166. data/manual/images/width_and_height.rb +25 -0
  167. data/manual/layout/boxes.rb +27 -0
  168. data/manual/layout/content.rb +25 -0
  169. data/manual/layout/layout.rb +28 -0
  170. data/manual/layout/simple_grid.rb +23 -0
  171. data/manual/outline/add_subsection_to.rb +61 -0
  172. data/manual/outline/insert_section_after.rb +47 -0
  173. data/manual/outline/outline.rb +32 -0
  174. data/manual/outline/sections_and_pages.rb +67 -0
  175. data/manual/repeatable_content/alternate_page_numbering.rb +32 -0
  176. data/manual/repeatable_content/page_numbering.rb +54 -0
  177. data/manual/repeatable_content/repeatable_content.rb +32 -0
  178. data/manual/repeatable_content/repeater.rb +55 -0
  179. data/manual/repeatable_content/stamp.rb +41 -0
  180. data/manual/security/encryption.rb +31 -0
  181. data/manual/security/permissions.rb +38 -0
  182. data/manual/security/security.rb +28 -0
  183. data/manual/table.rb +16 -0
  184. data/manual/text/alignment.rb +44 -0
  185. data/manual/text/color.rb +24 -0
  186. data/manual/text/column_box.rb +32 -0
  187. data/manual/text/fallback_fonts.rb +37 -0
  188. data/manual/text/font.rb +41 -0
  189. data/manual/text/font_size.rb +45 -0
  190. data/manual/text/font_style.rb +23 -0
  191. data/manual/text/formatted_callbacks.rb +60 -0
  192. data/manual/text/formatted_text.rb +50 -0
  193. data/manual/text/free_flowing_text.rb +51 -0
  194. data/manual/text/inline.rb +41 -0
  195. data/manual/text/kerning_and_character_spacing.rb +39 -0
  196. data/manual/text/leading.rb +25 -0
  197. data/manual/text/line_wrapping.rb +41 -0
  198. data/manual/text/paragraph_indentation.rb +34 -0
  199. data/manual/text/positioned_text.rb +38 -0
  200. data/manual/text/registering_families.rb +48 -0
  201. data/manual/text/rendering_and_color.rb +37 -0
  202. data/manual/text/right_to_left_text.rb +47 -0
  203. data/manual/text/rotation.rb +43 -0
  204. data/manual/text/single_usage.rb +37 -0
  205. data/manual/text/text.rb +73 -0
  206. data/manual/text/text_box_excess.rb +32 -0
  207. data/manual/text/text_box_extensions.rb +45 -0
  208. data/manual/text/text_box_overflow.rb +48 -0
  209. data/manual/text/utf8.rb +28 -0
  210. data/manual/text/win_ansi_charset.rb +60 -0
  211. data/prawn.gemspec +45 -0
  212. data/spec/acceptance/png.rb +25 -0
  213. data/spec/annotations_spec.rb +74 -0
  214. data/spec/bounding_box_spec.rb +510 -0
  215. data/spec/column_box_spec.rb +65 -0
  216. data/spec/data/curves.pdf +66 -0
  217. data/spec/destinations_spec.rb +15 -0
  218. data/spec/document_spec.rb +748 -0
  219. data/spec/extensions/encoding_helpers.rb +11 -0
  220. data/spec/extensions/mocha.rb +46 -0
  221. data/spec/font_metric_cache_spec.rb +52 -0
  222. data/spec/font_spec.rb +474 -0
  223. data/spec/formatted_text_arranger_spec.rb +421 -0
  224. data/spec/formatted_text_box_spec.rb +705 -0
  225. data/spec/formatted_text_fragment_spec.rb +298 -0
  226. data/spec/graphics_spec.rb +683 -0
  227. data/spec/grid_spec.rb +96 -0
  228. data/spec/image_handler_spec.rb +54 -0
  229. data/spec/images_spec.rb +153 -0
  230. data/spec/inline_formatted_text_parser_spec.rb +564 -0
  231. data/spec/jpg_spec.rb +25 -0
  232. data/spec/line_wrap_spec.rb +367 -0
  233. data/spec/measurement_units_spec.rb +25 -0
  234. data/spec/outline_spec.rb +430 -0
  235. data/spec/png_spec.rb +245 -0
  236. data/spec/reference_spec.rb +25 -0
  237. data/spec/repeater_spec.rb +160 -0
  238. data/spec/security_spec.rb +158 -0
  239. data/spec/soft_mask_spec.rb +79 -0
  240. data/spec/span_spec.rb +44 -0
  241. data/spec/spec_helper.rb +54 -0
  242. data/spec/stamp_spec.rb +160 -0
  243. data/spec/stroke_styles_spec.rb +211 -0
  244. data/spec/text_at_spec.rb +143 -0
  245. data/spec/text_box_spec.rb +1043 -0
  246. data/spec/text_rendering_mode_spec.rb +45 -0
  247. data/spec/text_spacing_spec.rb +93 -0
  248. data/spec/text_spec.rb +557 -0
  249. data/spec/text_with_inline_formatting_spec.rb +35 -0
  250. data/spec/transparency_spec.rb +91 -0
  251. data/spec/view_spec.rb +43 -0
  252. metadata +509 -0
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require_relative "formatted/wrap"
4
+
5
+ require_relative "formatted/box"
6
+ require_relative "formatted/parser"
7
+ require_relative "formatted/fragment"
@@ -0,0 +1,290 @@
1
+ # encoding: utf-8
2
+
3
+ # core/text/formatted/arranger.rb : Implements a data structure for 2-stage
4
+ # processing of lines of formatted text
5
+ #
6
+ # Copyright February 2010, Daniel Nelson. All Rights Reserved.
7
+ #
8
+ # This is free software. Please see the LICENSE and COPYING files for details.
9
+
10
+ module Prawn
11
+ module Text
12
+ module Formatted #:nodoc:
13
+
14
+ # @private
15
+
16
+ class Arranger #:nodoc:
17
+ attr_reader :max_line_height
18
+ attr_reader :max_descender
19
+ attr_reader :max_ascender
20
+ attr_accessor :consumed
21
+
22
+ # The following present only for testing purposes
23
+ attr_reader :unconsumed
24
+ attr_reader :fragments
25
+ attr_reader :current_format_state
26
+
27
+ def initialize(document, options={})
28
+ @document = document
29
+ @fragments = []
30
+ @unconsumed = []
31
+ @kerning = options[:kerning]
32
+ end
33
+
34
+ def space_count
35
+ if @unfinalized_line
36
+ raise "Lines must be finalized before calling #space_count"
37
+ end
38
+ @fragments.inject(0) do |sum, fragment|
39
+ sum + fragment.space_count
40
+ end
41
+ end
42
+
43
+ def line_width
44
+ if @unfinalized_line
45
+ raise "Lines must be finalized before calling #line_width"
46
+ end
47
+ @fragments.inject(0) do |sum, fragment|
48
+ sum + fragment.width
49
+ end
50
+ end
51
+
52
+ def line
53
+ if @unfinalized_line
54
+ raise "Lines must be finalized before calling #line"
55
+ end
56
+ @fragments.collect do |fragment|
57
+ fragment.text.dup.force_encoding(::Encoding::UTF_8)
58
+ end.join
59
+ end
60
+
61
+ def finalize_line
62
+ @unfinalized_line = false
63
+ omit_trailing_whitespace_from_line_width
64
+ @fragments = []
65
+ @consumed.each do |hash|
66
+ text = hash[:text]
67
+ format_state = hash.dup
68
+ format_state.delete(:text)
69
+ fragment = Prawn::Text::Formatted::Fragment.new(text,
70
+ format_state,
71
+ @document)
72
+ @fragments << fragment
73
+ set_fragment_measurements(fragment)
74
+ set_line_measurement_maximums(fragment)
75
+ end
76
+ end
77
+
78
+ def format_array=(array)
79
+ initialize_line
80
+ @unconsumed = []
81
+ array.each do |hash|
82
+ hash[:text].scan(/[^\n]+|\n/) do |line|
83
+ @unconsumed << hash.merge(:text => line)
84
+ end
85
+ end
86
+ end
87
+
88
+ def initialize_line
89
+ @unfinalized_line = true
90
+ @max_line_height = 0
91
+ @max_descender = 0
92
+ @max_ascender = 0
93
+
94
+ @consumed = []
95
+ @fragments = []
96
+ end
97
+
98
+ def finished?
99
+ @unconsumed.length == 0
100
+ end
101
+
102
+ def next_string
103
+ unless @unfinalized_line
104
+ raise "Lines must not be finalized when calling #next_string"
105
+ end
106
+ hash = @unconsumed.shift
107
+ if hash.nil?
108
+ nil
109
+ else
110
+ @consumed << hash.dup
111
+ @current_format_state = hash.dup
112
+ @current_format_state.delete(:text)
113
+ hash[:text]
114
+ end
115
+ end
116
+
117
+ def preview_next_string
118
+ hash = @unconsumed.first
119
+ if hash.nil? then nil
120
+ else hash[:text]
121
+ end
122
+ end
123
+
124
+ def apply_color_and_font_settings(fragment, &block)
125
+ if fragment.color
126
+ original_fill_color = @document.fill_color
127
+ original_stroke_color = @document.stroke_color
128
+ @document.fill_color(*fragment.color)
129
+ @document.stroke_color(*fragment.color)
130
+ apply_font_settings(fragment, &block)
131
+ @document.stroke_color = original_stroke_color
132
+ @document.fill_color = original_fill_color
133
+ else
134
+ apply_font_settings(fragment, &block)
135
+ end
136
+ end
137
+
138
+ def apply_font_settings(fragment=nil, &block)
139
+ if fragment.nil?
140
+ font = current_format_state[:font]
141
+ size = current_format_state[:size]
142
+ character_spacing = current_format_state[:character_spacing] ||
143
+ @document.character_spacing
144
+ styles = current_format_state[:styles]
145
+ font_style = font_style(styles)
146
+ else
147
+ font = fragment.font
148
+ size = fragment.size
149
+ character_spacing = fragment.character_spacing
150
+ styles = fragment.styles
151
+ font_style = font_style(styles)
152
+ end
153
+
154
+ @document.character_spacing(character_spacing) do
155
+ if font || font_style != :normal
156
+ raise "Bad font family" unless @document.font.family
157
+ @document.font(font || @document.font.family, :style => font_style) do
158
+ apply_font_size(size, styles, &block)
159
+ end
160
+ else
161
+ apply_font_size(size, styles, &block)
162
+ end
163
+ end
164
+ end
165
+
166
+ def update_last_string(printed, unprinted, normalized_soft_hyphen=nil)
167
+ return if printed.nil?
168
+ if printed.empty?
169
+ @consumed.pop
170
+ else
171
+ @consumed.last[:text] = printed
172
+ if normalized_soft_hyphen
173
+ @consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen
174
+ end
175
+ end
176
+
177
+ unless unprinted.empty?
178
+ @unconsumed.unshift(@current_format_state.merge(:text => unprinted))
179
+ end
180
+
181
+ load_previous_format_state if printed.empty?
182
+ end
183
+
184
+ def retrieve_fragment
185
+ if @unfinalized_line
186
+ raise "Lines must be finalized before fragments can be retrieved"
187
+ end
188
+ @fragments.shift
189
+ end
190
+
191
+ def repack_unretrieved
192
+ new_unconsumed = []
193
+ while fragment = retrieve_fragment
194
+ fragment.include_trailing_white_space!
195
+ new_unconsumed << fragment.format_state.merge(:text => fragment.text)
196
+ end
197
+ @unconsumed = new_unconsumed.concat(@unconsumed)
198
+ end
199
+
200
+ def font_style(styles)
201
+ if styles.nil?
202
+ :normal
203
+ elsif styles.include?(:bold) && styles.include?(:italic)
204
+ :bold_italic
205
+ elsif styles.include?(:bold)
206
+ :bold
207
+ elsif styles.include?(:italic)
208
+ :italic
209
+ else
210
+ :normal
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ def load_previous_format_state
217
+ if @consumed.empty?
218
+ @current_format_state = {}
219
+ else
220
+ hash = @consumed.last
221
+ @current_format_state = hash.dup
222
+ @current_format_state.delete(:text)
223
+ end
224
+ end
225
+
226
+ def apply_font_size(size, styles)
227
+ if subscript?(styles) || superscript?(styles)
228
+ relative_size = 0.583
229
+ if size.nil?
230
+ size = @document.font_size * relative_size
231
+ else
232
+ size = size * relative_size
233
+ end
234
+ end
235
+ if size.nil?
236
+ yield
237
+ else
238
+ @document.font_size(size) { yield }
239
+ end
240
+ end
241
+
242
+ def subscript?(styles)
243
+ if styles.nil? then false
244
+ else styles.include?(:subscript)
245
+ end
246
+ end
247
+
248
+ def superscript?(styles)
249
+ if styles.nil? then false
250
+ else styles.include?(:superscript)
251
+ end
252
+ end
253
+
254
+ def omit_trailing_whitespace_from_line_width
255
+ @consumed.reverse_each do |hash|
256
+ if hash[:text] == "\n"
257
+ break
258
+ elsif hash[:text].strip.empty? && @consumed.length > 1
259
+ # this entire fragment is trailing white space
260
+ hash[:exclude_trailing_white_space] = true
261
+ else
262
+ # this fragment contains the first non-white space we have
263
+ # encountered since the end of the line
264
+ hash[:exclude_trailing_white_space] = true
265
+ break
266
+ end
267
+ end
268
+ end
269
+
270
+ def set_fragment_measurements(fragment)
271
+ apply_font_settings(fragment) do
272
+ fragment.width = @document.width_of(fragment.text,
273
+ :kerning => @kerning)
274
+ fragment.line_height = @document.font.height
275
+ fragment.descender = @document.font.descender
276
+ fragment.ascender = @document.font.ascender
277
+ end
278
+ end
279
+
280
+ def set_line_measurement_maximums(fragment)
281
+ @max_line_height = [defined?(@max_line_height) && @max_line_height, fragment.line_height].compact.max
282
+ @max_descender = [defined?(@max_descender) && @max_descender, fragment.descender].compact.max
283
+ @max_ascender = [defined?(@max_ascender) && @max_ascender, fragment.ascender].compact.max
284
+ end
285
+
286
+ end
287
+
288
+ end
289
+ end
290
+ end
@@ -0,0 +1,614 @@
1
+ # encoding: utf-8
2
+
3
+ # text/formatted/rectangle.rb : Implements text boxes with formatted text
4
+ #
5
+ # Copyright February 2010, Daniel Nelson. All Rights Reserved.
6
+ #
7
+ # This is free software. Please see the LICENSE and COPYING files for details.
8
+ #
9
+
10
+ module Prawn
11
+ module Text
12
+ module Formatted
13
+ # @group Stable API
14
+
15
+ # Draws the requested formatted text into a box. When the text overflows
16
+ # the rectangle shrink to fit or truncate the text. Text boxes are
17
+ # independent of the document y position.
18
+ #
19
+ # == Formatted Text Array
20
+ #
21
+ # Formatted text is comprised of an array of hashes, where each hash
22
+ # defines text and format information. As of the time of writing, the
23
+ # following hash options are supported:
24
+ #
25
+ # <tt>:text</tt>::
26
+ # the text to format according to the other hash options
27
+ # <tt>:styles</tt>::
28
+ # an array of styles to apply to this text. Available styles include
29
+ # :bold, :italic, :underline, :strikethrough, :subscript, and
30
+ # :superscript
31
+ # <tt>:size</tt>::
32
+ # a number denoting the font size to apply to this text
33
+ # <tt>:character_spacing</tt>::
34
+ # a number denoting how much to increase or decrease the default
35
+ # spacing between characters
36
+ # <tt>:font</tt>::
37
+ # the name of a font. The name must be an AFM font with the desired
38
+ # faces or must be a font that is already registered using
39
+ # Prawn::Document#font_families
40
+ # <tt>:color</tt>::
41
+ # anything compatible with Prawn::Graphics::Color#fill_color and
42
+ # Prawn::Graphics::Color#stroke_color
43
+ # <tt>:link</tt>::
44
+ # a URL to which to create a link. A clickable link will be created
45
+ # to that URL. Note that you must explicitly underline and color using
46
+ # the appropriate tags if you which to draw attention to the link
47
+ # <tt>:anchor</tt>::
48
+ # a destination that has already been or will be registered using
49
+ # PDF::Core::Destinations#add_dest. A clickable link will be
50
+ # created to that destination. Note that you must explicitly underline
51
+ # and color using the appropriate tags if you which to draw attention
52
+ # to the link
53
+ # <tt>:local</tt>::
54
+ # a file or application to be opened locally. A clickable link will be
55
+ # created to the provided local file or application. If the file is
56
+ # another PDF, it will be opened in a new window. Note that you must
57
+ # explicitly underline and color using the appropriate tags if you which
58
+ # to draw attention to the link
59
+ # <tt>:draw_text_callback</tt>:
60
+ # if provided, this Proc will be called instead of #draw_text! once
61
+ # per fragment for every low-level addition of text to the page.
62
+ # <tt>:callback</tt>::
63
+ # an object (or array of such objects) with two methods:
64
+ # #render_behind and #render_in_front, which are called immediately
65
+ # prior to and immediately after rendring the text fragment and which
66
+ # are passed the fragment as an argument
67
+ #
68
+ # == Example
69
+ #
70
+ # formatted_text_box([{ :text => "hello" },
71
+ # { :text => "world",
72
+ # :size => 24,
73
+ # :styles => [:bold, :italic] }])
74
+ #
75
+ # == Options
76
+ #
77
+ # Accepts the same options as Text::Box with the below exceptions
78
+ #
79
+ # == Returns
80
+ #
81
+ # Returns a formatted text array representing any text that did not print
82
+ # under the current settings.
83
+ #
84
+ # == Exceptions
85
+ #
86
+ # Raises "Bad font family" if no font family is defined for the current font
87
+ #
88
+ # Raises <tt>Prawn::Errors::CannotFit</tt> if not wide enough to print
89
+ # any text
90
+ #
91
+ def formatted_text_box(array, options={})
92
+ Text::Formatted::Box.new(array, options.merge(:document => self)).render
93
+ end
94
+
95
+ # Generally, one would use the Prawn::Text::Formatted#formatted_text_box
96
+ # convenience method. However, using Text::Formatted::Box.new in
97
+ # conjunction with #render(:dry_run => true) enables one to do look-ahead
98
+ # calculations prior to placing text on the page, or to determine how much
99
+ # vertical space was consumed by the printed text
100
+ #
101
+ class Box
102
+ include Prawn::Text::Formatted::Wrap
103
+
104
+ # @group Experimental API
105
+
106
+ # The text that was successfully printed (or, if <tt>dry_run</tt> was
107
+ # used, the text that would have been successfully printed)
108
+ attr_reader :text
109
+
110
+ # True if nothing printed (or, if <tt>dry_run</tt> was
111
+ # used, nothing would have been successfully printed)
112
+ def nothing_printed?
113
+ @nothing_printed
114
+ end
115
+
116
+ # True if everything printed (or, if <tt>dry_run</tt> was
117
+ # used, everything would have been successfully printed)
118
+ def everything_printed?
119
+ @everything_printed
120
+ end
121
+
122
+ # The upper left corner of the text box
123
+ attr_reader :at
124
+ # The line height of the last line printed
125
+ attr_reader :line_height
126
+ # The height of the ascender of the last line printed
127
+ attr_reader :ascender
128
+ # The height of the descender of the last line printed
129
+ attr_reader :descender
130
+ # The leading used during printing
131
+ attr_reader :leading
132
+
133
+ def line_gap
134
+ line_height - (ascender + descender)
135
+ end
136
+
137
+ # See Prawn::Text#text_box for valid options
138
+ #
139
+ def initialize(formatted_text, options={})
140
+ @inked = false
141
+ Prawn.verify_options(valid_options, options)
142
+ options = options.dup
143
+
144
+ self.class.extensions.reverse_each { |e| extend e }
145
+
146
+ @overflow = options[:overflow] || :truncate
147
+ @disable_wrap_by_char = options[:disable_wrap_by_char]
148
+
149
+ self.original_text = formatted_text
150
+ @text = nil
151
+
152
+ @document = options[:document]
153
+ @direction = options[:direction] || @document.text_direction
154
+ @fallback_fonts = options[:fallback_fonts] ||
155
+ @document.fallback_fonts
156
+ @at = (options[:at] ||
157
+ [@document.bounds.left, @document.bounds.top]).dup
158
+ @width = options[:width] ||
159
+ @document.bounds.right - @at[0]
160
+ @height = options[:height] || default_height
161
+ @align = options[:align] ||
162
+ (@direction == :rtl ? :right : :left)
163
+ @vertical_align = options[:valign] || :top
164
+ @leading = options[:leading] || @document.default_leading
165
+ @character_spacing = options[:character_spacing] ||
166
+ @document.character_spacing
167
+ @mode = options[:mode] || @document.text_rendering_mode
168
+ @rotate = options[:rotate] || 0
169
+ @rotate_around = options[:rotate_around] || :upper_left
170
+ @single_line = options[:single_line]
171
+ @draw_text_callback = options[:draw_text_callback]
172
+
173
+ # if the text rendering mode is :unknown, force it back to :fill
174
+ if @mode == :unknown
175
+ @mode = :fill
176
+ end
177
+
178
+ if @overflow == :expand
179
+ # if set to expand, then we simply set the bottom
180
+ # as the bottom of the document bounds, since that
181
+ # is the maximum we should expand to
182
+ @height = default_height
183
+ @overflow = :truncate
184
+ end
185
+ @min_font_size = options[:min_font_size] || 5
186
+ if options[:kerning].nil? then
187
+ options[:kerning] = @document.default_kerning?
188
+ end
189
+ @options = { :kerning => options[:kerning],
190
+ :size => options[:size],
191
+ :style => options[:style] }
192
+
193
+ super(formatted_text, options)
194
+ end
195
+
196
+ # Render text to the document based on the settings defined in initialize.
197
+ #
198
+ # In order to facilitate look-ahead calculations, <tt>render</tt> accepts
199
+ # a <tt>:dry_run => true</tt> option. If provided, then everything is
200
+ # executed as if rendering, with the exception that nothing is drawn on
201
+ # the page. Useful for look-ahead computations of height, unprinted text,
202
+ # etc.
203
+ #
204
+ # Returns any text that did not print under the current settings
205
+ #
206
+ def render(flags={})
207
+ unprinted_text = []
208
+
209
+ @document.save_font do
210
+ @document.character_spacing(@character_spacing) do
211
+ @document.text_rendering_mode(@mode) do
212
+ process_options
213
+
214
+ text = normalized_text(flags)
215
+
216
+ @document.font_size(@font_size) do
217
+ shrink_to_fit(text) if @overflow == :shrink_to_fit
218
+ process_vertical_alignment(text)
219
+ @inked = true unless flags[:dry_run]
220
+ if @rotate != 0 && @inked
221
+ unprinted_text = render_rotated(text)
222
+ else
223
+ unprinted_text = wrap(text)
224
+ end
225
+ @inked = false
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ unprinted_text.map do |e|
232
+ e.merge(:text => @document.font.to_utf8(e[:text]))
233
+ end
234
+ end
235
+
236
+ # The width available at this point in the box
237
+ #
238
+ def available_width
239
+ @width
240
+ end
241
+
242
+ # The height actually used during the previous <tt>render</tt>
243
+ #
244
+ def height
245
+ return 0 if @baseline_y.nil? || @descender.nil?
246
+ (@baseline_y - @descender).abs
247
+ end
248
+
249
+ # <tt>fragment</tt> is a Prawn::Text::Formatted::Fragment object
250
+ #
251
+ def draw_fragment(fragment, accumulated_width=0, line_width=0, word_spacing=0) #:nodoc:
252
+ case(@align)
253
+ when :left
254
+ x = @at[0]
255
+ when :center
256
+ x = @at[0] + @width * 0.5 - line_width * 0.5
257
+ when :right
258
+ x = @at[0] + @width - line_width
259
+ when :justify
260
+ if @direction == :ltr
261
+ x = @at[0]
262
+ else
263
+ x = @at[0] + @width - line_width
264
+ end
265
+ end
266
+
267
+ x += accumulated_width
268
+
269
+ y = @at[1] + @baseline_y
270
+
271
+ y += fragment.y_offset
272
+
273
+ fragment.left = x
274
+ fragment.baseline = y
275
+
276
+ if @inked
277
+ draw_fragment_underlays(fragment)
278
+
279
+ @document.word_spacing(word_spacing) {
280
+ if @draw_text_callback
281
+ @draw_text_callback.call(fragment.text, :at => [x, y],
282
+ :kerning => @kerning)
283
+ else
284
+ @document.draw_text!(fragment.text, :at => [x, y],
285
+ :kerning => @kerning)
286
+ end
287
+ }
288
+
289
+ draw_fragment_overlays(fragment)
290
+ end
291
+ end
292
+
293
+ # @group Extension API
294
+
295
+ # Example (see Prawn::Text::Core::Formatted::Wrap for what is required
296
+ # of the wrap method if you want to override the default wrapping
297
+ # algorithm):
298
+ #
299
+ #
300
+ # module MyWrap
301
+ #
302
+ # def wrap(array)
303
+ # initialize_wrap([{ :text => 'all your base are belong to us' }])
304
+ # @line_wrap.wrap_line(:document => @document,
305
+ # :kerning => @kerning,
306
+ # :width => 10000,
307
+ # :arranger => @arranger)
308
+ # fragment = @arranger.retrieve_fragment
309
+ # format_and_draw_fragment(fragment, 0, @line_wrap.width, 0)
310
+ # []
311
+ # end
312
+ #
313
+ # end
314
+ #
315
+ # Prawn::Text::Formatted::Box.extensions << MyWrap
316
+ #
317
+ # box = Prawn::Text::Formatted::Box.new('hello world')
318
+ # box.render('why can't I print anything other than' +
319
+ # '"all your base are belong to us"?')
320
+ #
321
+ #
322
+ def self.extensions
323
+ @extensions ||= []
324
+ end
325
+
326
+ # @private
327
+ def self.inherited(base)
328
+ extensions.each { |e| base.extensions << e }
329
+ end
330
+
331
+ def valid_options
332
+ PDF::Core::Text::VALID_OPTIONS + [:at, :height, :width,
333
+ :align, :valign,
334
+ :rotate, :rotate_around,
335
+ :overflow, :min_font_size,
336
+ :disable_wrap_by_char,
337
+ :leading, :character_spacing,
338
+ :mode, :single_line,
339
+ :document,
340
+ :direction,
341
+ :fallback_fonts,
342
+ :draw_text_callback]
343
+ end
344
+
345
+ private
346
+
347
+ def normalized_text(flags)
348
+ text = normalize_encoding
349
+
350
+ text.each { |t| t.delete(:color) } if flags[:dry_run]
351
+
352
+ text
353
+ end
354
+
355
+ def original_text
356
+ @original_array.collect { |hash| hash.dup }
357
+ end
358
+
359
+ def original_text=(formatted_text)
360
+ @original_array = formatted_text
361
+ end
362
+
363
+ def normalize_encoding
364
+ formatted_text = original_text
365
+
366
+ unless @fallback_fonts.empty?
367
+ formatted_text = process_fallback_fonts(formatted_text)
368
+ end
369
+
370
+ formatted_text.each do |hash|
371
+ if hash[:font]
372
+ @document.font(hash[:font]) do
373
+ hash[:text] = @document.font.normalize_encoding(hash[:text])
374
+ end
375
+ else
376
+ hash[:text] = @document.font.normalize_encoding(hash[:text])
377
+ end
378
+ end
379
+
380
+ formatted_text
381
+ end
382
+
383
+ def process_fallback_fonts(formatted_text)
384
+ modified_formatted_text = []
385
+
386
+ formatted_text.each do |hash|
387
+ fragments = analyze_glyphs_for_fallback_font_support(hash)
388
+ modified_formatted_text.concat(fragments)
389
+ end
390
+
391
+ modified_formatted_text
392
+ end
393
+
394
+ def analyze_glyphs_for_fallback_font_support(hash)
395
+ font_glyph_pairs = []
396
+
397
+ original_font = @document.font.family
398
+ fragment_font = hash[:font] || original_font
399
+
400
+ fallback_fonts = @fallback_fonts.dup
401
+ # always default back to the current font if the glyph is missing from
402
+ # all fonts
403
+ fallback_fonts << fragment_font
404
+
405
+ @document.save_font do
406
+ hash[:text].each_char do |char|
407
+ font_glyph_pairs << [find_font_for_this_glyph(char,
408
+ fragment_font,
409
+ fallback_fonts.dup),
410
+ char]
411
+ end
412
+ end
413
+
414
+ # Don't add a :font to fragments if it wasn't there originally
415
+ if hash[:font].nil?
416
+ font_glyph_pairs.each do |pair|
417
+ pair[0] = nil if pair[0] == original_font
418
+ end
419
+ end
420
+
421
+ form_fragments_from_like_font_glyph_pairs(font_glyph_pairs, hash)
422
+ end
423
+
424
+ def find_font_for_this_glyph(char, current_font, fallback_fonts)
425
+ @document.font(current_font)
426
+ if fallback_fonts.length == 0 || @document.font.glyph_present?(char)
427
+ current_font
428
+ else
429
+ find_font_for_this_glyph(char, fallback_fonts.shift, fallback_fonts)
430
+ end
431
+ end
432
+
433
+ def form_fragments_from_like_font_glyph_pairs(font_glyph_pairs, hash)
434
+ fragments = []
435
+ fragment = nil
436
+ current_font = nil
437
+
438
+ font_glyph_pairs.each do |font, char|
439
+ if font != current_font || fragments.count == 0
440
+ current_font = font
441
+ fragment = hash.dup
442
+ fragment[:text] = char
443
+ fragment[:font] = font unless font.nil?
444
+ fragments << fragment
445
+ else
446
+ fragment[:text] += char
447
+ end
448
+ end
449
+
450
+ fragments
451
+ end
452
+
453
+ def move_baseline_down
454
+ if @baseline_y == 0
455
+ @baseline_y = -@ascender
456
+ else
457
+ @baseline_y -= (@line_height + @leading)
458
+ end
459
+ end
460
+
461
+ # Returns the default height to be used if none is provided or if the
462
+ # overflow option is set to :expand. If we are in a stretchy bounding
463
+ # box, assume we can stretch to the bottom of the innermost non-stretchy
464
+ # box.
465
+ #
466
+ def default_height
467
+ # Find the "frame", the innermost non-stretchy bbox.
468
+ frame = @document.bounds
469
+ frame = frame.parent while frame.stretchy? && frame.parent
470
+
471
+ @at[1] + @document.bounds.absolute_bottom - frame.absolute_bottom
472
+ end
473
+
474
+ def process_vertical_alignment(text)
475
+ # The vertical alignment must only be done once per text box, but
476
+ # we need to wait until render() is called so that the fonts are set
477
+ # up properly for wrapping. So guard with a boolean to ensure this is
478
+ # only run once.
479
+ return if defined?(@vertical_alignment_processed) && @vertical_alignment_processed
480
+ @vertical_alignment_processed = true
481
+
482
+ return if @vertical_align == :top
483
+
484
+ wrap(text)
485
+
486
+ case @vertical_align
487
+ when :center
488
+ @at[1] -= (@height - height + @descender) * 0.5
489
+ when :bottom
490
+ @at[1] -= (@height - height)
491
+ end
492
+
493
+ @height = height
494
+ end
495
+
496
+ # Decrease the font size until the text fits or the min font
497
+ # size is reached
498
+ def shrink_to_fit(text)
499
+ loop do
500
+ if @disable_wrap_by_char && @font_size > @min_font_size
501
+ begin
502
+ wrap(text)
503
+ rescue Errors::CannotFit
504
+ # Ignore errors while we can still attempt smaller
505
+ # font sizes.
506
+ end
507
+ else
508
+ wrap(text)
509
+ end
510
+
511
+ break if @everything_printed || @font_size <= @min_font_size
512
+
513
+ @font_size = [@font_size - 0.5, @min_font_size].max
514
+ @document.font_size = @font_size
515
+ end
516
+ end
517
+
518
+ def process_options
519
+ # must be performed within a save_font block because
520
+ # document.process_text_options sets the font
521
+ @document.process_text_options(@options)
522
+ @font_size = @options[:size]
523
+ @kerning = @options[:kerning]
524
+ end
525
+
526
+ def render_rotated(text)
527
+ unprinted_text = ''
528
+
529
+ case @rotate_around
530
+ when :center
531
+ x = @at[0] + @width * 0.5
532
+ y = @at[1] - @height * 0.5
533
+ when :upper_right
534
+ x = @at[0] + @width
535
+ y = @at[1]
536
+ when :lower_right
537
+ x = @at[0] + @width
538
+ y = @at[1] - @height
539
+ when :lower_left
540
+ x = @at[0]
541
+ y = @at[1] - @height
542
+ else
543
+ x = @at[0]
544
+ y = @at[1]
545
+ end
546
+
547
+ @document.rotate(@rotate, :origin => [x, y]) do
548
+ unprinted_text = wrap(text)
549
+ end
550
+ unprinted_text
551
+ end
552
+
553
+ def draw_fragment_underlays(fragment)
554
+ fragment.callback_objects.each do |obj|
555
+ obj.render_behind(fragment) if obj.respond_to?(:render_behind)
556
+ end
557
+ end
558
+
559
+ def draw_fragment_overlays(fragment)
560
+ draw_fragment_overlay_styles(fragment)
561
+ draw_fragment_overlay_link(fragment)
562
+ draw_fragment_overlay_anchor(fragment)
563
+ draw_fragment_overlay_local(fragment)
564
+ fragment.callback_objects.each do |obj|
565
+ obj.render_in_front(fragment) if obj.respond_to?(:render_in_front)
566
+ end
567
+ end
568
+
569
+ def draw_fragment_overlay_link(fragment)
570
+ return unless fragment.link
571
+ box = fragment.absolute_bounding_box
572
+ @document.link_annotation(box,
573
+ :Border => [0, 0, 0],
574
+ :A => { :Type => :Action,
575
+ :S => :URI,
576
+ :URI => PDF::Core::LiteralString.new(fragment.link) })
577
+ end
578
+
579
+ def draw_fragment_overlay_anchor(fragment)
580
+ return unless fragment.anchor
581
+ box = fragment.absolute_bounding_box
582
+ @document.link_annotation(box,
583
+ :Border => [0, 0, 0],
584
+ :Dest => fragment.anchor)
585
+ end
586
+
587
+ def draw_fragment_overlay_local(fragment)
588
+ return unless fragment.local
589
+ box = fragment.absolute_bounding_box
590
+ @document.link_annotation(box,
591
+ :Border => [0, 0, 0],
592
+ :A => { :Type => :Action,
593
+ :S => :Launch,
594
+ :F => PDF::Core::LiteralString.new(fragment.local),
595
+ :NewWindow => true })
596
+ end
597
+
598
+ def draw_fragment_overlay_styles(fragment)
599
+ underline = fragment.styles.include?(:underline)
600
+ if underline
601
+ @document.stroke_line(fragment.underline_points)
602
+ end
603
+
604
+ strikethrough = fragment.styles.include?(:strikethrough)
605
+ if strikethrough
606
+ @document.stroke_line(fragment.strikethrough_points)
607
+ end
608
+ end
609
+
610
+ end
611
+
612
+ end
613
+ end
614
+ end