prawn-core 0.6.3 → 0.7.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.
- data/Rakefile +1 -1
- data/examples/general/context_sensitive_headers.rb +37 -0
- data/examples/general/float.rb +11 -0
- data/examples/general/repeaters.rb +43 -0
- data/examples/m17n/chinese_text_wrapping.rb +1 -3
- data/examples/text/font_calculations.rb +6 -6
- data/examples/text/text_box.rb +80 -17
- data/lib/prawn/core.rb +3 -1
- data/lib/prawn/document/bounding_box.rb +9 -0
- data/lib/prawn/document/column_box.rb +13 -2
- data/lib/prawn/document/internals.rb +21 -3
- data/lib/prawn/document/snapshot.rb +7 -2
- data/lib/prawn/document/span.rb +3 -3
- data/lib/prawn/document.rb +78 -19
- data/lib/prawn/font/afm.rb +10 -7
- data/lib/prawn/font/ttf.rb +6 -4
- data/lib/prawn/font.rb +34 -24
- data/lib/prawn/graphics/cap_style.rb +5 -2
- data/lib/prawn/graphics/color.rb +117 -57
- data/lib/prawn/graphics/dash.rb +4 -2
- data/lib/prawn/graphics/join_style.rb +6 -3
- data/lib/prawn/graphics/transparency.rb +65 -18
- data/lib/prawn/images/jpg.rb +1 -1
- data/lib/prawn/images/png.rb +1 -1
- data/lib/prawn/object_store.rb +30 -1
- data/lib/prawn/reference.rb +25 -3
- data/lib/prawn/repeater.rb +117 -0
- data/lib/prawn/stamp.rb +102 -40
- data/lib/prawn/text/box.rb +344 -0
- data/lib/prawn/text.rb +255 -0
- data/spec/document_spec.rb +125 -4
- data/spec/object_store_spec.rb +33 -0
- data/spec/repeater_spec.rb +79 -0
- data/spec/stamp_spec.rb +8 -0
- data/spec/text_box_spec.rb +282 -69
- data/spec/text_spec.rb +49 -29
- data/spec/transparency_spec.rb +14 -0
- data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +2 -2
- metadata +158 -155
- data/examples/general/measurement_units.pdf +0 -4667
- data/lib/prawn/document/text/box.rb +0 -90
- data/lib/prawn/document/text/wrapping.rb +0 -62
- data/lib/prawn/document/text.rb +0 -184
data/lib/prawn/reference.rb
CHANGED
@@ -11,9 +11,8 @@ require 'zlib'
|
|
11
11
|
module Prawn
|
12
12
|
|
13
13
|
class Reference #:nodoc:
|
14
|
-
|
15
|
-
|
16
|
-
attr_reader :identifier
|
14
|
+
|
15
|
+
attr_accessor :gen, :data, :offset, :stream, :live, :identifier
|
17
16
|
|
18
17
|
def initialize(id, data)
|
19
18
|
@identifier = id
|
@@ -59,6 +58,29 @@ module Prawn
|
|
59
58
|
@stream = other_ref.stream
|
60
59
|
@compressed = other_ref.compressed?
|
61
60
|
end
|
61
|
+
|
62
|
+
# Marks this and all referenced objects live, recursively.
|
63
|
+
def mark_live
|
64
|
+
return if @live
|
65
|
+
@live = true
|
66
|
+
referenced_objects.each { |o| o.mark_live }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
# All objects referenced by this one. Used for GC.
|
72
|
+
def referenced_objects(obj=@data)
|
73
|
+
case obj
|
74
|
+
when Reference
|
75
|
+
[]
|
76
|
+
when Hash
|
77
|
+
obj.values.map{|v| [v] + referenced_objects(v) }
|
78
|
+
when Array
|
79
|
+
obj.map{|v| [v] + referenced_objects(v) }
|
80
|
+
else []
|
81
|
+
end.flatten.grep(Reference)
|
82
|
+
end
|
83
|
+
|
62
84
|
end
|
63
85
|
|
64
86
|
module_function
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
#
|
3
|
+
# repeater.rb : Implements repeated page elements.
|
4
|
+
# Heavy inspired by repeating_element() in PDF::Wrapper
|
5
|
+
# http://pdf-wrapper.rubyforge.org/
|
6
|
+
#
|
7
|
+
# Copyright November 2009, Gregory Brown. All Rights Reserved.
|
8
|
+
#
|
9
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
10
|
+
|
11
|
+
module Prawn
|
12
|
+
|
13
|
+
class Document
|
14
|
+
|
15
|
+
# A list of all repeaters in the document.
|
16
|
+
# See Document#repeat for details
|
17
|
+
#
|
18
|
+
def repeaters
|
19
|
+
@repeaters ||= []
|
20
|
+
end
|
21
|
+
|
22
|
+
# Provides a way to execute a block of code repeatedly based on a
|
23
|
+
# page_filter. Since Stamp is used under the hood, this method is very space
|
24
|
+
# efficient.
|
25
|
+
#
|
26
|
+
# Available page filters are:
|
27
|
+
# :all -- repeats on every page
|
28
|
+
# :odd -- repeats on odd pages
|
29
|
+
# :even -- repeats on even pages
|
30
|
+
# some_array -- repeats on every page listed in the array
|
31
|
+
# some_range -- repeats on every page included in the range
|
32
|
+
# some_lambda -- yields page number and repeats for true return values
|
33
|
+
#
|
34
|
+
# Example:
|
35
|
+
#
|
36
|
+
# Prawn::Document.generate("repeat.pdf", :skip_page_creation => true) do
|
37
|
+
#
|
38
|
+
# repeat :all do
|
39
|
+
# text "ALLLLLL", :at => bounds.top_left
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# repeat :odd do
|
43
|
+
# text "ODD", :at => [0,0]
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
# repeat :even do
|
47
|
+
# text "EVEN", :at => [0,0]
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# repeat [1,2] do
|
51
|
+
# text "[1,2]", :at => [100,0]
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# repeat 2..4 do
|
55
|
+
# text "2..4", :at => [200,0]
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
# repeat(lambda { |pg| pg % 3 == 0 }) do
|
59
|
+
# text "Every third", :at => [250, 20]
|
60
|
+
# end
|
61
|
+
#
|
62
|
+
# 10.times do
|
63
|
+
# start_new_page
|
64
|
+
# text "A wonderful page", :at => [400,400]
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
def repeat(page_filter, &block)
|
70
|
+
repeaters << Prawn::Repeater.new(self, page_filter, &block)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class Repeater #:nodoc:
|
75
|
+
class << self
|
76
|
+
attr_writer :count
|
77
|
+
|
78
|
+
def count
|
79
|
+
@count ||= 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
attr_reader :name
|
84
|
+
|
85
|
+
def initialize(document, page_filter, &block)
|
86
|
+
@document = document
|
87
|
+
@page_filter = page_filter
|
88
|
+
@stamp_name = "prawn_repeater(#{Repeater.count})"
|
89
|
+
|
90
|
+
@document.create_stamp(@stamp_name, &block)
|
91
|
+
|
92
|
+
Repeater.count += 1
|
93
|
+
end
|
94
|
+
|
95
|
+
def match?(page_number)
|
96
|
+
case @page_filter
|
97
|
+
when :all
|
98
|
+
true
|
99
|
+
when :odd
|
100
|
+
page_number % 2 == 1
|
101
|
+
when :even
|
102
|
+
page_number % 2 == 0
|
103
|
+
when Range, Array
|
104
|
+
@page_filter.include?(page_number)
|
105
|
+
when Proc
|
106
|
+
@page_filter.call(page_number)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def run(page_number)
|
111
|
+
@document.stamp(@stamp_name) if match?(page_number)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
|
data/lib/prawn/stamp.rb
CHANGED
@@ -8,25 +8,58 @@
|
|
8
8
|
#
|
9
9
|
|
10
10
|
module Prawn
|
11
|
-
module Stamp
|
12
|
-
|
13
|
-
def stamp(user_defined_name)
|
14
|
-
raise Prawn::Errors::InvalidName if user_defined_name.empty?
|
15
|
-
unless stamp_dictionary_registry[user_defined_name]
|
16
|
-
raise Prawn::Errors::UndefinedObjectName
|
17
|
-
end
|
18
|
-
|
19
|
-
dict = stamp_dictionary_registry[user_defined_name]
|
20
11
|
|
21
|
-
|
22
|
-
|
12
|
+
# The Prawn::Stamp module is used to create content that will be
|
13
|
+
# included multiple times in a document. Using a stamp has three
|
14
|
+
# advantages over creating content anew each time it is placed on
|
15
|
+
# the page:
|
16
|
+
# i. faster document creation
|
17
|
+
# ii. smaller final document
|
18
|
+
# iii. faster display on subsequent displays of the repeated
|
19
|
+
# element because the viewer application can cache the rendered
|
20
|
+
# results
|
21
|
+
#
|
22
|
+
# Example:
|
23
|
+
# pdf.create_stamp("my_stamp") {
|
24
|
+
# pdf.fill_circle_at([10, 15], :radius => 5)
|
25
|
+
# pdf.text("hello world", :at => [20, 10])
|
26
|
+
# }
|
27
|
+
# pdf.stamp("my_stamp")
|
28
|
+
#
|
29
|
+
module Stamp
|
23
30
|
|
24
|
-
|
25
|
-
|
26
|
-
|
31
|
+
# Renders the stamp named <tt>name</tt> to the page
|
32
|
+
# raises <tt>Prawn::Errors::InvalidName</tt> if name.empty?
|
33
|
+
# raises <tt>Prawn::Errors::UndefinedObjectName</tt> if no stamp
|
34
|
+
# has been created with this name
|
35
|
+
#
|
36
|
+
# Example:
|
37
|
+
# pdf.create_stamp("my_stamp") {
|
38
|
+
# pdf.fill_circle_at([10, 15], :radius => 5)
|
39
|
+
# pdf.text("hello world", :at => [20, 10])
|
40
|
+
# }
|
41
|
+
# pdf.stamp("my_stamp")
|
42
|
+
#
|
43
|
+
def stamp(name)
|
44
|
+
dictionary_name, dictionary = stamp_dictionary(name)
|
45
|
+
add_content "/#{dictionary_name} Do"
|
46
|
+
page_xobjects.merge!(dictionary_name => dictionary)
|
27
47
|
end
|
28
|
-
|
29
|
-
|
48
|
+
|
49
|
+
# Renders the stamp named <tt>name</tt> at a position offset from
|
50
|
+
# the initial coords at which the elements of the stamp was
|
51
|
+
# created
|
52
|
+
#
|
53
|
+
# Example:
|
54
|
+
# pdf.create_stamp("circle") do
|
55
|
+
# pdf.fill_circle_at([0, 0], :radius => 25)
|
56
|
+
# end
|
57
|
+
# # draws a circle at 100, 100
|
58
|
+
# pdf.stamp_at("circle", [100, 100])
|
59
|
+
#
|
60
|
+
# See stamp() for exceptions that might be raised
|
61
|
+
#
|
62
|
+
def stamp_at(name, point)
|
30
63
|
# Save the graphics state
|
31
64
|
add_content "q"
|
32
65
|
|
@@ -36,40 +69,36 @@ module Prawn
|
|
36
69
|
add_content translate_position
|
37
70
|
|
38
71
|
# Draw the stamp in the now translated user space
|
39
|
-
stamp(
|
72
|
+
stamp(name)
|
40
73
|
|
41
74
|
# Restore the graphics state to remove the translation
|
42
75
|
add_content "Q"
|
43
76
|
end
|
44
|
-
|
45
|
-
def create_stamp(user_defined_name="", &block)
|
46
|
-
raise Prawn::Errors::InvalidName if user_defined_name.empty?
|
47
|
-
|
48
|
-
if stamp_dictionary_registry[user_defined_name]
|
49
|
-
raise Prawn::Errors::NameTaken
|
50
|
-
end
|
51
77
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
78
|
+
# Creates a re-usable stamp named <tt>name</tt>
|
79
|
+
#
|
80
|
+
# raises <tt>Prawn::Errors::NameTaken</tt> if a stamp already
|
81
|
+
# exists in this document with this name
|
82
|
+
# raises <tt>Prawn::Errors::InvalidName</tt> if name.empty?
|
83
|
+
#
|
84
|
+
# Example:
|
85
|
+
# pdf.create_stamp("my_stamp") {
|
86
|
+
# pdf.fill_circle_at([10, 15], :radius => 5)
|
87
|
+
# pdf.text("hello world", :at => [20, 10])
|
88
|
+
# }
|
89
|
+
#
|
90
|
+
def create_stamp(name, &block)
|
91
|
+
dictionary = create_stamp_dictionary(name)
|
58
92
|
|
59
|
-
stamp_dictionary_name = "Stamp#{next_stamp_dictionary_id}"
|
60
|
-
|
61
|
-
stamp_dictionary_registry[user_defined_name] =
|
62
|
-
{ :stamp_dictionary_name => stamp_dictionary_name,
|
63
|
-
:stamp_dictionary => stamp_dictionary}
|
64
|
-
|
65
|
-
|
66
93
|
@active_stamp_stream = ""
|
67
|
-
@active_stamp_dictionary =
|
94
|
+
@active_stamp_dictionary = dictionary
|
68
95
|
|
96
|
+
update_colors
|
69
97
|
yield if block_given?
|
98
|
+
update_colors
|
70
99
|
|
71
|
-
|
72
|
-
|
100
|
+
dictionary.data[:Length] = @active_stamp_stream.length + 1
|
101
|
+
dictionary << @active_stamp_stream
|
73
102
|
|
74
103
|
@active_stamp_stream = nil
|
75
104
|
@active_stamp_dictionary = nil
|
@@ -85,5 +114,38 @@ module Prawn
|
|
85
114
|
stamp_dictionary_registry.length + 1
|
86
115
|
end
|
87
116
|
|
117
|
+
def stamp_dictionary(name)
|
118
|
+
raise Prawn::Errors::InvalidName if name.empty?
|
119
|
+
if stamp_dictionary_registry[name].nil?
|
120
|
+
raise Prawn::Errors::UndefinedObjectName
|
121
|
+
end
|
122
|
+
|
123
|
+
dict = stamp_dictionary_registry[name]
|
124
|
+
|
125
|
+
dictionary_name = dict[:stamp_dictionary_name]
|
126
|
+
dictionary = dict[:stamp_dictionary]
|
127
|
+
[dictionary_name, dictionary]
|
128
|
+
end
|
129
|
+
|
130
|
+
def create_stamp_dictionary(name)
|
131
|
+
raise Prawn::Errors::InvalidName if name.empty?
|
132
|
+
raise Prawn::Errors::NameTaken unless stamp_dictionary_registry[name].nil?
|
133
|
+
# BBox origin is the lower left margin of the page, so we need
|
134
|
+
# it to be the full dimension of the page, or else things that
|
135
|
+
# should appear near the top or right margin are invisible
|
136
|
+
dictionary = ref!(:Type => :XObject,
|
137
|
+
:Subtype => :Form,
|
138
|
+
:BBox => [0, 0,
|
139
|
+
page_dimensions[2], page_dimensions[3]])
|
140
|
+
|
141
|
+
dictionary_name = "Stamp#{next_stamp_dictionary_id}"
|
142
|
+
|
143
|
+
stamp_dictionary_registry[name] = {
|
144
|
+
:stamp_dictionary_name => dictionary_name,
|
145
|
+
:stamp_dictionary => dictionary
|
146
|
+
}
|
147
|
+
dictionary
|
148
|
+
end
|
149
|
+
|
88
150
|
end
|
89
151
|
end
|
@@ -0,0 +1,344 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# text/rectangle.rb : Implements text boxes
|
4
|
+
#
|
5
|
+
# Copyright November 2009, Daniel Nelson. All Rights Reserved.
|
6
|
+
#
|
7
|
+
# This is free software. Please see the LICENSE and COPYING files for details.
|
8
|
+
|
9
|
+
module Prawn
|
10
|
+
module Text
|
11
|
+
|
12
|
+
# Draws the requested text into a box. When the text overflows
|
13
|
+
# the rectangle, you can display ellipses, shrink to fit, or
|
14
|
+
# truncate the text. Text boxes are independent of the document
|
15
|
+
# y position.
|
16
|
+
#
|
17
|
+
# == Encoding
|
18
|
+
#
|
19
|
+
# Note that strings passed to this function should be encoded as UTF-8.
|
20
|
+
# If you get unexpected characters appearing in your rendered document,
|
21
|
+
# check this.
|
22
|
+
#
|
23
|
+
# If the current font is a built-in one, although the string must be
|
24
|
+
# encoded as UTF-8, only characters that are available in WinAnsi
|
25
|
+
# are allowed.
|
26
|
+
#
|
27
|
+
# If an empty box is rendered to your PDF instead of the character you
|
28
|
+
# wanted it usually means the current font doesn't include that character.
|
29
|
+
#
|
30
|
+
# == Options (default values marked in [])
|
31
|
+
#
|
32
|
+
# <tt>:kerning</tt>:: <tt>boolean</tt>. Whether or not to use kerning (if it
|
33
|
+
# is available with the current font) [true]
|
34
|
+
# <tt>:size</tt>:: <tt>number</tt>. The font size to use. [current font
|
35
|
+
# size]
|
36
|
+
# <tt>:style</tt>:: The style to use. The requested style must be part of
|
37
|
+
# the current font familly. [current style]
|
38
|
+
#
|
39
|
+
# <tt>:at</tt>:: <tt>[x, y]</tt>. The upper left corner of the box
|
40
|
+
# [@document.bounds.left, @document.bounds.top]
|
41
|
+
# <tt>:width</tt>:: <tt>number</tt>. The width of the box
|
42
|
+
# [@document.bounds.right - @at[0]]
|
43
|
+
# <tt>:height</tt>:: <tt>number</tt>. The height of the box [@at[1] -
|
44
|
+
# @document.bounds.bottom]
|
45
|
+
# <tt>:align</tt>:: <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
|
46
|
+
# Alignment within the bounding box [:left]
|
47
|
+
# <tt>:valign</tt>:: <tt>:top</tt>, <tt>:center</tt>, or <tt>:bottom</tt>.
|
48
|
+
# Vertical alignment within the bounding box [:top]
|
49
|
+
# <tt>:leading</tt>:: <tt>number</tt>. Additional space between lines [0]
|
50
|
+
# <tt>:overflow</tt>:: <tt>:truncate</tt>, <tt>:shrink_to_fit</tt>,
|
51
|
+
# <tt>:expand</tt>, or <tt>:ellipses</tt>. This
|
52
|
+
# controls the behavior when
|
53
|
+
# the amount of text exceeds the available space
|
54
|
+
# [:truncate]
|
55
|
+
# <tt>:min_font_size</tt>:: <tt>number</tt>. The minimum font size to use
|
56
|
+
# when :overflow is set to :shrink_to_fit (that is
|
57
|
+
# the font size will not be
|
58
|
+
# reduced to less than this value, even if it
|
59
|
+
# means that some text will be cut off). [5]
|
60
|
+
# <tt>:wrap_block</tt>:: <tt>proc</tt>. A proc used for custom line
|
61
|
+
# wrapping. The proc must accept a single
|
62
|
+
# <tt>line</tt> of text and an <tt>options</tt> hash
|
63
|
+
# and return the string from that single line that
|
64
|
+
# can fit on the line under the conditions defined by
|
65
|
+
# <tt>options</tt>. If omitted, the default wrapping
|
66
|
+
# proc is used. The options hash passed into the
|
67
|
+
# wrap_block proc includes the following options:
|
68
|
+
# <tt>:width</tt>:: the width available for the
|
69
|
+
# current line of text
|
70
|
+
# <tt>:document</tt>:: the pdf object
|
71
|
+
# <tt>:kerning</tt>:: boolean
|
72
|
+
# <tt>:size</tt>:: the font size
|
73
|
+
#
|
74
|
+
# Returns any text that did not print under the current settings
|
75
|
+
#
|
76
|
+
def text_box(text, options)
|
77
|
+
Text::Box.new(text, options.merge(:document => self)).render
|
78
|
+
end
|
79
|
+
|
80
|
+
# Generally, one would use the text_box convenience method. However, using
|
81
|
+
# Text::Box.new in conjunction with render() enables one to do look-ahead
|
82
|
+
# calculations prior to placing text on the page, or to determine how much
|
83
|
+
# vertical space was consumed by the printed text
|
84
|
+
#
|
85
|
+
class Box
|
86
|
+
|
87
|
+
# The text that was successfully printed (or, if <tt>dry_run</tt> was
|
88
|
+
# used, the test that would have been successfully printed)
|
89
|
+
attr_reader :text
|
90
|
+
# The upper left corner of the text box
|
91
|
+
attr_reader :at
|
92
|
+
# The line height of the last line printed
|
93
|
+
attr_reader :line_height
|
94
|
+
# The height of the ascender of the last line printed
|
95
|
+
attr_reader :ascender
|
96
|
+
# The height of the descender of the last line printed
|
97
|
+
attr_reader :descender
|
98
|
+
# The leading used during printing
|
99
|
+
attr_reader :leading
|
100
|
+
|
101
|
+
# See Prawn::Text#text_box for valid options
|
102
|
+
#
|
103
|
+
def initialize(text, options={})
|
104
|
+
@inked = false
|
105
|
+
Prawn.verify_options(valid_options, options)
|
106
|
+
options = options.dup
|
107
|
+
@overflow = options[:overflow] || :truncate
|
108
|
+
# we'll be messing with the strings encoding, don't change the user's
|
109
|
+
# original string
|
110
|
+
@text_to_print = text.dup
|
111
|
+
@text = nil
|
112
|
+
|
113
|
+
@document = options[:document]
|
114
|
+
@at = options[:at] ||
|
115
|
+
[@document.bounds.left, @document.bounds.top]
|
116
|
+
@width = options[:width] ||
|
117
|
+
@document.bounds.right - @at[0]
|
118
|
+
@height = options[:height] ||
|
119
|
+
@at[1] - @document.bounds.bottom
|
120
|
+
@center = [@at[0] + @width * 0.5, @at[1] + @height * 0.5]
|
121
|
+
@align = options[:align] || :left
|
122
|
+
@vertical_align = options[:valign] || :top
|
123
|
+
@leading = options[:leading] || 0
|
124
|
+
|
125
|
+
if @overflow == :expand
|
126
|
+
# if set to expand, then we simply set the bottom
|
127
|
+
# as the bottom of the document bounds, since that
|
128
|
+
# is the maximum we should expand to
|
129
|
+
@height = @at[1] - @document.bounds.bottom
|
130
|
+
@overflow = :truncate
|
131
|
+
end
|
132
|
+
@min_font_size = options[:min_font_size] || 5
|
133
|
+
@wrap_block = options [:wrap_block] || default_wrap_block
|
134
|
+
@options = @document.text_options.merge(:kerning => options[:kerning],
|
135
|
+
:size => options[:size],
|
136
|
+
:style => options[:style])
|
137
|
+
end
|
138
|
+
|
139
|
+
# Render text to the document based on the settings defined in initialize.
|
140
|
+
#
|
141
|
+
# In order to facilitate look-ahead calculations, <tt>render</tt> accepts
|
142
|
+
# a <tt>:dry_run => true</tt> option. If provided then everything is
|
143
|
+
# executed as if rendering, with the exception that nothing is drawn on
|
144
|
+
# the page. Useful for look-ahead computations of height, unprinted text,
|
145
|
+
# etc.
|
146
|
+
#
|
147
|
+
# Returns any text that did not print under the current settings
|
148
|
+
#
|
149
|
+
def render(flags={})
|
150
|
+
unprinted_text = ''
|
151
|
+
@document.save_font do
|
152
|
+
process_options
|
153
|
+
|
154
|
+
unless @document.skip_encoding
|
155
|
+
@document.font.normalize_encoding!(@text_to_print)
|
156
|
+
end
|
157
|
+
|
158
|
+
@document.font_size(@font_size) do
|
159
|
+
shrink_to_fit if @overflow == :shrink_to_fit
|
160
|
+
process_vertical_alignment
|
161
|
+
@inked = true unless flags[:dry_run]
|
162
|
+
unprinted_text = _render(@text_to_print)
|
163
|
+
@inked = false
|
164
|
+
end
|
165
|
+
end
|
166
|
+
unprinted_text
|
167
|
+
end
|
168
|
+
|
169
|
+
# The height actually used during the previous <tt>render</tt>
|
170
|
+
#
|
171
|
+
def height
|
172
|
+
return 0 if @baseline_y.nil? || @descender.nil?
|
173
|
+
# baseline is already pushed down one line below the current
|
174
|
+
# line, so we need to subtract line line_height and leading,
|
175
|
+
# but we need to add in the descender since baseline is
|
176
|
+
# above the descender
|
177
|
+
@baseline_y.abs + @descender - @line_height - @leading
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
def valid_options
|
183
|
+
Text::VALID_TEXT_OPTIONS.dup.concat([:at, :height, :width,
|
184
|
+
:align, :valign,
|
185
|
+
:overflow, :min_font_size,
|
186
|
+
:wrap_block,
|
187
|
+
:leading,
|
188
|
+
:document])
|
189
|
+
end
|
190
|
+
|
191
|
+
def process_vertical_alignment
|
192
|
+
return if @vertical_align == :top
|
193
|
+
_render(@text_to_print)
|
194
|
+
case @vertical_align
|
195
|
+
when :center
|
196
|
+
@at[1] = @at[1] - (@height - height) * 0.5
|
197
|
+
when :bottom
|
198
|
+
@at[1] = @at[1] - (@height - height)
|
199
|
+
end
|
200
|
+
@height = height
|
201
|
+
end
|
202
|
+
|
203
|
+
# Decrease the font size until the text fits or the min font
|
204
|
+
# size is reached
|
205
|
+
def shrink_to_fit
|
206
|
+
while (unprinted_text = _render(@text_to_print)).length > 0 &&
|
207
|
+
@font_size > @min_font_size
|
208
|
+
@font_size -= 0.5
|
209
|
+
@document.font_size = @font_size
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def process_options
|
214
|
+
# must be performed within a save_font bock because
|
215
|
+
# document.process_text_options sets the font
|
216
|
+
@document.process_text_options(@options)
|
217
|
+
@font_size = @options[:size]
|
218
|
+
@kerning = @options[:kerning]
|
219
|
+
end
|
220
|
+
|
221
|
+
def _render(remaining_text)
|
222
|
+
@line_height = @document.font.height
|
223
|
+
@descender = @document.font.descender
|
224
|
+
@ascender = @document.font.ascender
|
225
|
+
@baseline_y = -@ascender
|
226
|
+
|
227
|
+
printed_text = []
|
228
|
+
|
229
|
+
while remaining_text &&
|
230
|
+
remaining_text.length > 0 &&
|
231
|
+
@baseline_y.abs + @descender <= @height
|
232
|
+
line_to_print = @wrap_block.call(remaining_text.first_line,
|
233
|
+
:document => @document,
|
234
|
+
:kerning => @kerning,
|
235
|
+
:size => @font_size,
|
236
|
+
:width => @width)
|
237
|
+
remaining_text = remaining_text.slice(line_to_print.length..
|
238
|
+
remaining_text.length)
|
239
|
+
print_ellipses = (@overflow == :ellipses && last_line? &&
|
240
|
+
remaining_text.length > 0)
|
241
|
+
printed_text << print_line(line_to_print, print_ellipses)
|
242
|
+
@baseline_y -= (@line_height + @leading)
|
243
|
+
end
|
244
|
+
|
245
|
+
@text = printed_text.join("\n") if @inked
|
246
|
+
|
247
|
+
remaining_text
|
248
|
+
end
|
249
|
+
|
250
|
+
def print_line(line_to_print, print_ellipses)
|
251
|
+
# strip so that trailing and preceding white space don't
|
252
|
+
# interfere with alignment
|
253
|
+
line_to_print.strip!
|
254
|
+
|
255
|
+
insert_ellipses(line_to_print) if print_ellipses
|
256
|
+
|
257
|
+
case(@align)
|
258
|
+
when :left
|
259
|
+
x = @center[0] - @width * 0.5
|
260
|
+
when :center
|
261
|
+
line_width = @document.width_of(line_to_print, :kerning => @kerning)
|
262
|
+
x = @center[0] - line_width * 0.5
|
263
|
+
when :right
|
264
|
+
line_width = @document.width_of(line_to_print, :kerning => @kerning)
|
265
|
+
x = @center[0] + @width * 0.5 - line_width
|
266
|
+
end
|
267
|
+
|
268
|
+
y = @at[1] + @baseline_y
|
269
|
+
|
270
|
+
if @inked
|
271
|
+
@document.text_at(line_to_print, :at => [x, y],
|
272
|
+
:size => @font_size, :kerning => @kerning)
|
273
|
+
end
|
274
|
+
|
275
|
+
line_to_print
|
276
|
+
end
|
277
|
+
|
278
|
+
def last_line?
|
279
|
+
@baseline_y.abs + @descender > @height - @line_height
|
280
|
+
end
|
281
|
+
|
282
|
+
def insert_ellipses(line_to_print)
|
283
|
+
if @document.width_of(line_to_print + "...",
|
284
|
+
:kerning => @kerning) < @width
|
285
|
+
line_to_print.insert(-1, "...")
|
286
|
+
else
|
287
|
+
line_to_print[-3..-1] = "..." if line_to_print.length > 3
|
288
|
+
end
|
289
|
+
end
|
290
|
+
|
291
|
+
def default_wrap_block
|
292
|
+
lambda do |line, options|
|
293
|
+
scan_pattern = /\S+|\s+/
|
294
|
+
output = ""
|
295
|
+
accumulated_width = 0
|
296
|
+
line.scan(scan_pattern).each do |segment|
|
297
|
+
segment_width = options[:document].width_of(segment,
|
298
|
+
:size => options[:size],
|
299
|
+
:kerning => options[:kerning])
|
300
|
+
|
301
|
+
if accumulated_width + segment_width <= options[:width]
|
302
|
+
accumulated_width += segment_width
|
303
|
+
output << segment
|
304
|
+
else
|
305
|
+
# if the line contains white space, don't split the
|
306
|
+
# final word that doesn't fit, just return what fits nicely
|
307
|
+
break if output =~ /\s/
|
308
|
+
|
309
|
+
# if there is no white space on the curren tline, then just
|
310
|
+
# print whatever part of the last segment that will fit on the
|
311
|
+
# line
|
312
|
+
begin
|
313
|
+
segment.unpack("U*").each do |char_int|
|
314
|
+
char = [char_int].pack("U")
|
315
|
+
accumulated_width += options[:document].width_of(char,
|
316
|
+
:size => options[:size],
|
317
|
+
:kerning => options[:kerning])
|
318
|
+
break if accumulated_width >= options[:width]
|
319
|
+
output << char
|
320
|
+
end
|
321
|
+
rescue
|
322
|
+
segment.each_char do |char|
|
323
|
+
accumulated_width += options[:document].width_of(char,
|
324
|
+
:size => options[:size],
|
325
|
+
:kerning => options[:kerning])
|
326
|
+
break if accumulated_width >= options[:width]
|
327
|
+
output << char
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
331
|
+
end
|
332
|
+
output
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
|
340
|
+
class String
|
341
|
+
def first_line
|
342
|
+
self.each_line { |line| return line }
|
343
|
+
end
|
344
|
+
end
|