srdperu-prawn-format 0.1.1.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/lib/prawn/format/effects/link.rb +34 -0
- data/lib/prawn/format/effects/underline.rb +32 -0
- data/lib/prawn/format/instructions/base.rb +62 -0
- data/lib/prawn/format/instructions/tag_close.rb +52 -0
- data/lib/prawn/format/instructions/tag_open.rb +95 -0
- data/lib/prawn/format/instructions/text.rb +89 -0
- data/lib/prawn/format/layout_builder.rb +113 -0
- data/lib/prawn/format/lexer.rb +240 -0
- data/lib/prawn/format/line.rb +99 -0
- data/lib/prawn/format/parser.rb +181 -0
- data/lib/prawn/format/state.rb +189 -0
- data/lib/prawn/format/text_object.rb +107 -0
- data/lib/prawn/format/version.rb +5 -0
- data/lib/prawn/format.rb +229 -0
- metadata +90 -0
@@ -0,0 +1,189 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Prawn
|
4
|
+
module Format
|
5
|
+
class State
|
6
|
+
attr_reader :document
|
7
|
+
attr_reader :original_style, :style
|
8
|
+
|
9
|
+
def initialize(document, options={})
|
10
|
+
@document = document
|
11
|
+
@previous = options[:previous]
|
12
|
+
|
13
|
+
@original_style = (@previous && @previous.inheritable_style || {}).
|
14
|
+
merge(options[:style] || {})
|
15
|
+
|
16
|
+
compute_styles!
|
17
|
+
|
18
|
+
@style[:kerning] = font.has_kerning_data? unless @style.key?(:kerning)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inheritable_style
|
22
|
+
@inheritable_style ||= begin
|
23
|
+
subset = original_style.dup
|
24
|
+
subset.delete(:meta)
|
25
|
+
subset.delete(:display)
|
26
|
+
subset.delete(:width)
|
27
|
+
|
28
|
+
# explicitly set font-size so that relative font-sizes don't get
|
29
|
+
# recomputed upon each nesting.
|
30
|
+
subset[:font_size] = font_size
|
31
|
+
|
32
|
+
subset
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def kerning?
|
37
|
+
@style[:kerning]
|
38
|
+
end
|
39
|
+
|
40
|
+
def display
|
41
|
+
@style[:display] || :inline
|
42
|
+
end
|
43
|
+
|
44
|
+
def font_size
|
45
|
+
@style[:font_size] || 12
|
46
|
+
end
|
47
|
+
|
48
|
+
def font_family
|
49
|
+
@style[:font_family] || "Helvetica"
|
50
|
+
end
|
51
|
+
|
52
|
+
def font_style
|
53
|
+
@style[:font_style] || :normal
|
54
|
+
end
|
55
|
+
|
56
|
+
def font_weight
|
57
|
+
@style[:font_weight] || :normal
|
58
|
+
end
|
59
|
+
|
60
|
+
def color
|
61
|
+
@style[:color] || "000000"
|
62
|
+
end
|
63
|
+
|
64
|
+
def vertical_align
|
65
|
+
@style[:vertical_align] || 0
|
66
|
+
end
|
67
|
+
|
68
|
+
def text_decoration
|
69
|
+
@style[:text_decoration] || :none
|
70
|
+
end
|
71
|
+
|
72
|
+
def white_space
|
73
|
+
@style[:white_space] || :normal
|
74
|
+
end
|
75
|
+
|
76
|
+
def width
|
77
|
+
@style[:width] || 0
|
78
|
+
end
|
79
|
+
|
80
|
+
def font
|
81
|
+
@font ||= document.find_font(font_family, :style => pdf_font_style)
|
82
|
+
end
|
83
|
+
|
84
|
+
def pdf_font_style
|
85
|
+
if bold? && italic?
|
86
|
+
:bold_italic
|
87
|
+
elsif bold?
|
88
|
+
:bold
|
89
|
+
elsif italic?
|
90
|
+
:italic
|
91
|
+
else
|
92
|
+
:normal
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def with_style(style)
|
97
|
+
self.class.new(document, :previous => self, :style => style)
|
98
|
+
end
|
99
|
+
|
100
|
+
def apply!(text_object, cookies)
|
101
|
+
if cookies[:color] != color
|
102
|
+
cookies[:color] = color
|
103
|
+
text_object.fill_color(color)
|
104
|
+
end
|
105
|
+
|
106
|
+
if cookies[:vertical_align] != vertical_align
|
107
|
+
cookies[:vertical_align] = vertical_align
|
108
|
+
text_object.rise(vertical_align)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def apply_font!(text_object, cookies, subset)
|
113
|
+
if cookies[:font] != [font_family, pdf_font_style, font_size, subset]
|
114
|
+
cookies[:font] = [font_family, pdf_font_style, font_size, subset]
|
115
|
+
font = document.font(font_family, :style => pdf_font_style)
|
116
|
+
font.add_to_current_page(subset)
|
117
|
+
text_object.font(font.identifier_for(subset), font_size)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def italic?
|
122
|
+
font_style == :italic
|
123
|
+
end
|
124
|
+
|
125
|
+
def bold?
|
126
|
+
font_weight == :bold
|
127
|
+
end
|
128
|
+
|
129
|
+
def previous(attr=nil, default=nil)
|
130
|
+
return @previous unless attr
|
131
|
+
return default unless @previous
|
132
|
+
return @previous.send(attr) || default
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def compute_styles!
|
138
|
+
@style = @original_style.dup
|
139
|
+
|
140
|
+
evaluate_style(:font_size, 12, :current)
|
141
|
+
evaluate_style(:vertical_align, 0, font_size, :super => "+40%", :sub => "-30%")
|
142
|
+
evaluate_style(:width, 0, document.bounds.width)
|
143
|
+
|
144
|
+
@style[:color] = evaluate_color(@style[:color])
|
145
|
+
end
|
146
|
+
|
147
|
+
def evaluate_style(which, default, relative_to, mappings={})
|
148
|
+
current = previous(which, default)
|
149
|
+
relative_to = current if relative_to == :current
|
150
|
+
@style[which] = document.evaluate_measure(@style[which],
|
151
|
+
:em => @previous && @previous.font_size || 12,
|
152
|
+
:current => current, :relative => relative_to, :mappings => mappings) || default
|
153
|
+
end
|
154
|
+
|
155
|
+
HTML_COLORS = {
|
156
|
+
"aqua" => "00FFFF",
|
157
|
+
"black" => "000000",
|
158
|
+
"blue" => "0000FF",
|
159
|
+
"fuchsia" => "FF00FF",
|
160
|
+
"gray" => "808080",
|
161
|
+
"green" => "008000",
|
162
|
+
"lime" => "00FF00",
|
163
|
+
"maroon" => "800000",
|
164
|
+
"navy" => "000080",
|
165
|
+
"olive" => "808000",
|
166
|
+
"purple" => "800080",
|
167
|
+
"red" => "FF0000",
|
168
|
+
"silver" => "C0C0C0",
|
169
|
+
"teal" => "008080",
|
170
|
+
"white" => "FFFFFF",
|
171
|
+
"yellow" => "FFFF00"
|
172
|
+
}
|
173
|
+
|
174
|
+
def evaluate_color(color)
|
175
|
+
case color
|
176
|
+
when nil then nil
|
177
|
+
when /^\s*#?([a-f0-9]{3})\s*$/i then
|
178
|
+
return $1.gsub(/./, '\&0')
|
179
|
+
when /^\s*#?([a-f0-9]+)$\s*/i then
|
180
|
+
return $1
|
181
|
+
when /^\s*rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)\s*$/
|
182
|
+
return "%02x%02x%02x" % [$1.to_i, $2.to_i, $3.to_i]
|
183
|
+
else
|
184
|
+
return HTML_COLORS[color.strip.downcase]
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/graphics/color'
|
4
|
+
|
5
|
+
module Prawn
|
6
|
+
module Format
|
7
|
+
class TextObject
|
8
|
+
include Prawn::Graphics::Color
|
9
|
+
|
10
|
+
RENDER_MODES = {
|
11
|
+
:fill => 0,
|
12
|
+
:stroke => 1,
|
13
|
+
:fill_stroke => 2,
|
14
|
+
:invisible => 3,
|
15
|
+
:fill_clip => 4,
|
16
|
+
:stroke_clip => 5,
|
17
|
+
:fill_stroke_clip => 6,
|
18
|
+
:clip => 7
|
19
|
+
}
|
20
|
+
|
21
|
+
def initialize
|
22
|
+
@content = nil
|
23
|
+
@last_x = @last_y = 0
|
24
|
+
end
|
25
|
+
|
26
|
+
def open
|
27
|
+
@content = "BT\n"
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def close
|
32
|
+
@content << "ET"
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def move_to(x, y)
|
37
|
+
move_by(x - @last_x, y - @last_y)
|
38
|
+
end
|
39
|
+
|
40
|
+
def move_by(dx,dy)
|
41
|
+
@last_x += dx
|
42
|
+
@last_y += dy
|
43
|
+
@content << "#{dx} #{dy} Td\n"
|
44
|
+
self
|
45
|
+
end
|
46
|
+
|
47
|
+
def next_line(dy)
|
48
|
+
end
|
49
|
+
|
50
|
+
def show(argument)
|
51
|
+
instruction = argument.is_a?(Array) ? "TJ" : "Tj"
|
52
|
+
@content << "#{Prawn::PdfObject(argument, true)} #{instruction}\n"
|
53
|
+
self
|
54
|
+
end
|
55
|
+
|
56
|
+
def character_space(dc)
|
57
|
+
@content << "#{dc} Tc\n"
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
def word_space(dw)
|
62
|
+
@content << "#{dw} Tw\n"
|
63
|
+
self
|
64
|
+
end
|
65
|
+
|
66
|
+
def leading(dl)
|
67
|
+
@content << "#{dl} TL\n"
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
def font(identifier, size)
|
72
|
+
@content << "/#{identifier} #{size} Tf\n"
|
73
|
+
self
|
74
|
+
end
|
75
|
+
|
76
|
+
def render(mode)
|
77
|
+
mode_value = RENDER_MODES[mode] || raise(ArgumentError, "unsupported render mode #{mode.inspect}, should be one of #{RENDER_MODES.keys.inspect}")
|
78
|
+
@content << "#{mode_value} Tr\n"
|
79
|
+
self
|
80
|
+
end
|
81
|
+
|
82
|
+
def rise(value)
|
83
|
+
@content << "#{value} Ts\n"
|
84
|
+
self
|
85
|
+
end
|
86
|
+
|
87
|
+
def rotate(x, y, theta)
|
88
|
+
radians = theta * Math::PI / 180
|
89
|
+
cos, sin = Math.cos(radians), Math.sin(radians)
|
90
|
+
arr = [cos, sin, -sin, cos, x, y]
|
91
|
+
add_content "%.3f %.3f %.3f %.3f %.3f %.3f Tm" % arr
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_s
|
95
|
+
@content
|
96
|
+
end
|
97
|
+
|
98
|
+
def to_str
|
99
|
+
@content
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_content(string)
|
103
|
+
@content << string << "\n"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
data/lib/prawn/format.rb
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'prawn/format/layout_builder'
|
4
|
+
require 'prawn/format/text_object'
|
5
|
+
|
6
|
+
module Prawn
|
7
|
+
module Format
|
8
|
+
def self.included(mod)
|
9
|
+
mod.send :alias_method, :text_without_formatting, :text
|
10
|
+
mod.send :alias_method, :text, :text_with_formatting
|
11
|
+
|
12
|
+
mod.send :alias_method, :width_of_without_formatting, :width_of
|
13
|
+
mod.send :alias_method, :width_of, :width_of_with_formatting
|
14
|
+
|
15
|
+
mod.send :alias_method, :height_of_without_formatting, :height_of
|
16
|
+
mod.send :alias_method, :height_of, :height_of_with_formatting
|
17
|
+
end
|
18
|
+
|
19
|
+
# Overloaded version of #text. Call via #text, rather than #text_with_formatting
|
20
|
+
# (see above, where it aliased to #text).
|
21
|
+
def text_with_formatting(text, options={}) #:nodoc:
|
22
|
+
if unformatted?(text, options)
|
23
|
+
text_without_formatting(text, options)
|
24
|
+
else
|
25
|
+
format(text, options)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Overloaded version of #height_of. Call via #height_of, rather than
|
30
|
+
# #height_of_with_formatting (see above, where it aliased to #height_of).
|
31
|
+
def height_of_with_formatting(string, line_width, size=font_size, options={}) #:nodoc:
|
32
|
+
if unformatted?(string, options)
|
33
|
+
height_of_without_formatting(string, line_width, size)
|
34
|
+
else
|
35
|
+
formatted_height(string, line_width, size, options)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Overloaded version of #width_of. Call via #width_of, rather than
|
40
|
+
# #width_of_with_formatting (see above, where it aliased to #width_of).
|
41
|
+
def width_of_with_formatting(string, options={}) #:nodoc:
|
42
|
+
if unformatted?(string, options)
|
43
|
+
width_of_without_formatting(string, options)
|
44
|
+
else
|
45
|
+
formatted_width(string, options)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
DEFAULT_TAGS = {
|
50
|
+
:a => { :meta => { :name => :anchor, :href => :target }, :color => "0000ff", :text_decoration => :underline },
|
51
|
+
:b => { :font_weight => :bold },
|
52
|
+
:br => { :display => :break },
|
53
|
+
:code => { :font_family => "Courier", :font_size => "90%" },
|
54
|
+
:em => { :font_style => :italic },
|
55
|
+
:font => { :meta => { :face => :font_family, :color => :color, :size => :font_size } },
|
56
|
+
:i => { :font_style => :italic },
|
57
|
+
:pre => { :white_space => :pre, :font_family => "Courier", :font_size => "90%" },
|
58
|
+
:span => {},
|
59
|
+
:strong => { :font_weight => :bold },
|
60
|
+
:sub => { :vertical_align => :sub, :font_size => "70%" },
|
61
|
+
:sup => { :vertical_align => :super, :font_size => "70%" },
|
62
|
+
:tt => { :font_family => "Courier" },
|
63
|
+
:u => { :text_decoration => :underline },
|
64
|
+
}.freeze
|
65
|
+
|
66
|
+
def tags(update={})
|
67
|
+
@tags ||= DEFAULT_TAGS.dup
|
68
|
+
@tags.update(update)
|
69
|
+
end
|
70
|
+
|
71
|
+
def styles(update={})
|
72
|
+
@styles ||= {}
|
73
|
+
@styles.update(update)
|
74
|
+
end
|
75
|
+
|
76
|
+
def default_style
|
77
|
+
{ :font_family => font.family || font.name,
|
78
|
+
:font_size => font_size,
|
79
|
+
:color => fill_color }
|
80
|
+
end
|
81
|
+
|
82
|
+
def evaluate_measure(measure, options={})
|
83
|
+
case measure
|
84
|
+
when nil then nil
|
85
|
+
when Numeric then return measure
|
86
|
+
when Symbol then
|
87
|
+
mappings = options[:mappings] || {}
|
88
|
+
raise ArgumentError, "unrecognized value #{measure.inspect}" unless mappings.key?(measure)
|
89
|
+
return evaluate_measure(mappings[measure], options)
|
90
|
+
when String then
|
91
|
+
operator, value, unit = measure.match(/^([-+]?)(\d+(?:\.\d+)?)(.*)$/)[1,3]
|
92
|
+
|
93
|
+
value = case unit
|
94
|
+
when "%" then
|
95
|
+
relative = options[:relative] || 0
|
96
|
+
relative * value.to_f / 100
|
97
|
+
when "em" then
|
98
|
+
# not a true em, but good enough for approximating. patches welcome.
|
99
|
+
value.to_f * (options[:em] || font_size)
|
100
|
+
when "", "pt" then return value.to_f
|
101
|
+
when "pc" then return value.to_f * 12
|
102
|
+
when "in" then return value.to_f * 72
|
103
|
+
else raise ArgumentError, "unsupport units in style value: #{measure.inspect}"
|
104
|
+
end
|
105
|
+
|
106
|
+
current = options[:current] || 0
|
107
|
+
case operator
|
108
|
+
when "+" then return current + value
|
109
|
+
when "-" then return current - value
|
110
|
+
else return value
|
111
|
+
end
|
112
|
+
else return measure.to_f
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def draw_lines(x, y, width, lines, options={})
|
117
|
+
real_x, real_y = translate(x, y)
|
118
|
+
|
119
|
+
state = options[:state] || {}
|
120
|
+
options[:align] ||= :left
|
121
|
+
|
122
|
+
state = state.merge(:width => width,
|
123
|
+
:x => x, :y => y,
|
124
|
+
:real_x => real_x, :real_y => real_y,
|
125
|
+
:dx => 0, :dy => 0)
|
126
|
+
|
127
|
+
state[:cookies] ||= {}
|
128
|
+
state[:pending_effects] ||= []
|
129
|
+
|
130
|
+
return state if lines.empty?
|
131
|
+
|
132
|
+
text_object do |text|
|
133
|
+
text.rotate(real_x, real_y, options[:rotate] || 0)
|
134
|
+
state[:text] = text
|
135
|
+
lines.each { |line| line.draw_on(self, state, options) }
|
136
|
+
end
|
137
|
+
|
138
|
+
state.delete(:text)
|
139
|
+
|
140
|
+
#rectangle [x, y+state[:dy]], width, state[:dy]
|
141
|
+
#stroke
|
142
|
+
|
143
|
+
return state
|
144
|
+
end
|
145
|
+
|
146
|
+
def layout(text, options={})
|
147
|
+
helper = Format::LayoutBuilder.new(self, text, options)
|
148
|
+
yield helper if block_given?
|
149
|
+
return helper
|
150
|
+
end
|
151
|
+
|
152
|
+
def format(text, options={})
|
153
|
+
if options[:at]
|
154
|
+
x, y = options[:at]
|
155
|
+
format_positioned_text(text, x, y, options)
|
156
|
+
else
|
157
|
+
format_wrapped_text(text, options)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def text_object
|
162
|
+
object = TextObject.new
|
163
|
+
|
164
|
+
if block_given?
|
165
|
+
yield object.open
|
166
|
+
add_content(object.close)
|
167
|
+
end
|
168
|
+
|
169
|
+
return object
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
def unformatted?(text, options={})
|
175
|
+
# If they have a preference, use it
|
176
|
+
if options.key?(:plain)
|
177
|
+
return options[:plain]
|
178
|
+
|
179
|
+
# Otherwise, if they're asking for full-justification, we must assume
|
180
|
+
# the text is formatted (since Prawn's text() method has no full justification)
|
181
|
+
elsif options[:align] == :justify
|
182
|
+
return false
|
183
|
+
|
184
|
+
# Otherwise, look for tags or XML entities in the text
|
185
|
+
else
|
186
|
+
return text !~ /<|&(?:#x?)?\w+;/
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def format_positioned_text(text, x, y, options={})
|
191
|
+
helper = layout(text, options)
|
192
|
+
line = helper.next
|
193
|
+
draw_lines(x, y+line.ascent, line.width, [line], options)
|
194
|
+
end
|
195
|
+
|
196
|
+
def format_wrapped_text(text, options={})
|
197
|
+
helper = layout(text, options)
|
198
|
+
|
199
|
+
start_new_page if self.y < bounds.absolute_bottom
|
200
|
+
|
201
|
+
until helper.done?
|
202
|
+
y = self.y - bounds.absolute_bottom
|
203
|
+
height = bounds.stretchy? ? bounds.absolute_top : y
|
204
|
+
|
205
|
+
y = helper.fill(bounds.left, y, bounds.width, options.merge(:height => height))
|
206
|
+
|
207
|
+
if helper.done?
|
208
|
+
self.y = y + bounds.absolute_bottom
|
209
|
+
else
|
210
|
+
start_new_page
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
def formatted_height(string, line_width, size=font_size, options={})
|
216
|
+
helper = layout(string, options.merge(:size => size))
|
217
|
+
lines = helper.word_wrap(line_width)
|
218
|
+
return lines.inject(0) { |s, line| s + line.height }
|
219
|
+
end
|
220
|
+
|
221
|
+
def formatted_width(string, options={})
|
222
|
+
helper = layout(string, options)
|
223
|
+
helper.next.width
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
require 'prawn/document'
|
229
|
+
Prawn::Document.send(:include, Prawn::Format)
|