prawn-git 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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