prawn-format 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/Manifest +37 -0
  2. data/Rakefile +31 -0
  3. data/examples/basic-formatting.rb +37 -0
  4. data/examples/christmas-carol.txt +717 -0
  5. data/examples/document.rb +61 -0
  6. data/examples/flowing.rb +24 -0
  7. data/examples/style-classes.rb +12 -0
  8. data/examples/syntax-highlighting.rb +31 -0
  9. data/examples/tags.rb +24 -0
  10. data/lib/prawn/format.rb +211 -0
  11. data/lib/prawn/format/effects/link.rb +30 -0
  12. data/lib/prawn/format/effects/underline.rb +32 -0
  13. data/lib/prawn/format/instructions/base.rb +62 -0
  14. data/lib/prawn/format/instructions/tag_close.rb +52 -0
  15. data/lib/prawn/format/instructions/tag_open.rb +95 -0
  16. data/lib/prawn/format/instructions/text.rb +89 -0
  17. data/lib/prawn/format/layout_builder.rb +113 -0
  18. data/lib/prawn/format/lexer.rb +222 -0
  19. data/lib/prawn/format/line.rb +99 -0
  20. data/lib/prawn/format/parser.rb +181 -0
  21. data/lib/prawn/format/state.rb +189 -0
  22. data/lib/prawn/format/text_object.rb +107 -0
  23. data/lib/prawn/format/version.rb +11 -0
  24. data/manual/html.rb +187 -0
  25. data/manual/include/basics.rb +6 -0
  26. data/manual/include/breaks.rb +13 -0
  27. data/manual/include/custom-tags.rb +10 -0
  28. data/manual/include/custom-tags2.rb +2 -0
  29. data/manual/include/indent.rb +4 -0
  30. data/manual/include/options.rb +15 -0
  31. data/manual/include/style-classes.rb +5 -0
  32. data/manual/manual.txt +101 -0
  33. data/manual/pdf.rb +204 -0
  34. data/prawn-format.gemspec +45 -0
  35. data/spec/layout_builder_spec.rb +27 -0
  36. data/spec/lexer_spec.rb +91 -0
  37. data/spec/parser_spec.rb +103 -0
  38. data/spec/spec_helper.rb +24 -0
  39. metadata +157 -0
@@ -0,0 +1,61 @@
1
+ #coding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require "prawn"
5
+ require "prawn/format"
6
+
7
+ Prawn::Document.generate("document.pdf") do
8
+ tags :h1 => { :font_size => "2em", :font_weight => :bold },
9
+ :h2 => { :font_size => "1.5em", :font_weight => :bold },
10
+ :stave => { :display => :break, :meta => { :name => :anchor }, :font_weight => :bold, :font_size => "2em" },
11
+ :title => { :font_weight => :bold, :font_size => "1.5em" },
12
+ :indent => { :width => "2em" }
13
+
14
+ font "Times-Roman", :size => 14
15
+ File.open("#{File.dirname(__FILE__)}/christmas-carol.txt") do |story|
16
+ line_number = 0
17
+ story.each_line do |line|
18
+ line_number += 1
19
+ next if line.strip == ""
20
+
21
+ type, data = line.match(/^(\w+)\.\s*(.*)/)[1,2]
22
+
23
+ case type
24
+ when "h1"
25
+ move_text_position 144
26
+ text "<h1>#{data}</h1>", :align => :center
27
+ move_text_position 72
28
+
29
+ when "h2"
30
+ text "<h2>#{data}</h2>", :align => :center
31
+ move_text_position 72
32
+
33
+ when "toc"
34
+ bounding_box [bounds.left+bounds.width/4, y-bounds.absolute_bottom], :width => bounds.width do
35
+ text(data)
36
+ end
37
+
38
+ when "stave"
39
+ start_new_page
40
+ text(data, :align => :center)
41
+ move_text_position 72
42
+
43
+ when "p"
44
+ text("<indent/>" + data, :align => :justify)
45
+
46
+ when "song"
47
+ move_text_position font_size/2
48
+ text(data, :align => :center)
49
+ move_text_position font_size/2
50
+
51
+ when "block"
52
+ text(data, :align => :justify)
53
+
54
+ when "stop"
55
+ break
56
+
57
+ else raise "unknown block type #{type.inspect} (line \##{line_number})"
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,24 @@
1
+ #coding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'prawn'
5
+ require 'prawn/format'
6
+
7
+ dice = "#{Prawn::BASEDIR}/data/images/dice.png"
8
+ Prawn::Document.generate "flowing.pdf" do
9
+ # first, position an image that we'll flow around
10
+ width = bounds.width / 2 - 18 # give us some padding on the left of the image
11
+ info = image(dice, :width => width, :position => :right)
12
+
13
+ # create the layout helper that we'll use to flow the text
14
+ layout(DATA.read, :align => :justify) do |helper|
15
+ # fill the first box
16
+ y = helper.fill(bounds.left, bounds.top, width, :height => info.scaled_height + font_size - 1)
17
+
18
+ # fill the rest
19
+ self.y = helper.fill(bounds.left, y, bounds.width)
20
+ end
21
+ end
22
+
23
+ __END__
24
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse ut neque. Maecenas scelerisque euismod purus. Nam venenatis. Sed velit mauris, cursus sit amet, semper vitae, sagittis quis, tortor. Nam at nisi. Sed ac nibh. Curabitur auctor ipsum nec tellus. Suspendisse potenti. Morbi dapibus magna a velit. Ut fringilla enim eu enim. Quisque lacinia, arcu dignissim gravida semper, eros quam tempor nisi, non scelerisque massa odio ut mi. Nullam ac ipsum. Proin volutpat tristique sem. Sed ipsum felis, volutpat vel, porttitor id, volutpat eu, mauris. Cras eget nulla sit amet dolor cursus semper. Curabitur nulla nunc, accumsan sit amet, vestibulum sit amet, adipiscing in, lorem. Pellentesque sodales purus sit amet odio. Cras commodo. Nam accumsan. Suspendisse potenti. Fusce posuere neque in nulla. Ut cursus blandit nisl. Phasellus ut nulla quis nunc tempus consequat. In hac habitasse platea dictumst. Praesent molestie nisl nec erat. Vivamus pretium nibh sit amet est. Integer dolor. Sed dictum blandit purus. Quisque et mauris vel diam dictum dignissim. Maecenas ultrices. Cras leo risus, tristique quis, hendrerit eu, condimentum tempor, mi. Nullam consectetur ante non nisl. Nulla dignissim sem in turpis. Cras purus felis, molestie ac, rhoncus sed, suscipit et, nisl. Donec tempus justo eu turpis. Aenean quis diam at dolor interdum tempor. Mauris sed nulla vitae erat lobortis lacinia. Nunc mauris. Aliquam mattis egestas ligula. Morbi magna dolor, convallis id, interdum lacinia, pellentesque eu, velit. In non arcu. Duis a nibh. Vestibulum volutpat erat et magna. Vivamus mi ante, aliquam et, vulputate sit amet, pretium non, ligula. Curabitur elementum, nisl ut gravida placerat, nisl sapien posuere augue, a placerat urna enim in velit. Cras nisi. Suspendisse volutpat. Ut eu lorem sed eros sollicitudin volutpat. Quisque blandit, libero sed mattis scelerisque, elit turpis ultricies purus, quis faucibus orci erat quis sem. Vestibulum imperdiet facilisis ante. Mauris dignissim, arcu vitae tincidunt congue, ante nulla aliquam purus, a porta tellus nisl at lectus. Phasellus commodo lacus et tortor. Etiam tortor leo, scelerisque sit amet, lobortis non, gravida vitae, mi. Donec eget augue. Sed ultrices ipsum eget risus. Aliquam rhoncus arcu vitae orci pellentesque pulvinar. Praesent ornare, lacus eget elementum ultrices, massa justo dignissim massa, a suscipit elit nibh quis arcu. Aliquam non massa. Etiam pharetra nisi at risus. Donec felis turpis, suscipit a, luctus vulputate, lobortis id, neque. Proin vitae nibh. Pellentesque nibh risus, volutpat sollicitudin, lobortis vitae, malesuada eu, elit. Duis felis nulla, sagittis id, faucibus luctus, viverra id, eros. Phasellus vulputate leo eget libero. Ut ut ipsum ac dui accumsan placerat. Integer porttitor. Quisque gravida dolor eu diam. Phasellus sodales sem sit amet urna. Morbi ornare viverra mauris.
@@ -0,0 +1,12 @@
1
+ #coding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'prawn'
5
+ require 'prawn/format'
6
+
7
+ Prawn::Document.generate "style-classes.pdf" do
8
+ styles :quote => { :font_style => :italic },
9
+ :product => { :font_weight => :bold, :color => "#007" }
10
+
11
+ text "<span class='product'>Prawn</span> is a fantastic utility for formatting PDF's. Jamis Buck may once have said, speaking of <span class='product'>Prawn</span>, <span class='quote'>\"<span class='product'>Prawn</span> is <em>the</em> single most fantastic PDF creation library ever to be written. In Ruby. In 2008. By Greg Brown.\"</span>"
12
+ end
@@ -0,0 +1,31 @@
1
+ #coding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'prawn'
5
+ require 'prawn/format'
6
+
7
+ begin
8
+ require 'coderay'
9
+ rescue LoadError
10
+ abort "This example (#{__FILE__}) requires the 'coderay' gem.\n" +
11
+ "Please make sure it is installed."
12
+ end
13
+
14
+ # Use CodeRay to parse the source and return HTML
15
+ html = CodeRay.scan(File.read(__FILE__), :ruby).html(:line_numbers => :inline)
16
+
17
+ # Use prawn and prawn/format to parse and format the HTML
18
+ Prawn::Document.generate "syntax-highlighting.pdf", :page_layout => :landscape do
19
+ styles :no => { :color => "gray" },
20
+ :c => { :color => "#666" },
21
+ :s => { :color => "#d20" },
22
+ :dl => { :color => "black" },
23
+ :co => { :color => "#036", :font_weight => :bold },
24
+ :pc => { :color => "#038", :font_weight => :bold },
25
+ :sy => { :color => "#A60" },
26
+ :r => { :color => "#080" },
27
+ :i => { :color => "#00D", :font_weight => :bold },
28
+ :idl => { :color => "#888", :font_weight => :bold }
29
+
30
+ text "<pre>#{html}</pre>"
31
+ end
@@ -0,0 +1,24 @@
1
+ #coding: utf-8
2
+
3
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
4
+ require 'prawn'
5
+ require 'prawn/format'
6
+
7
+ CHINESE_FONT = "#{Prawn::BASEDIR}/data/fonts/gkai00mp.ttf"
8
+
9
+ Prawn::Document.generate "tags.pdf" do
10
+ # tweak an existing tag
11
+ tags[:em][:color] = "red"
12
+
13
+ # completely redefine an existing tag
14
+ tags[:strong] = { :text_decoration => :underline }
15
+
16
+ # define altogether new tags
17
+ tags :chinese => { :font_family => CHINESE_FONT },
18
+ :exp => { :vertical_align => :super, :font_size => "70%", :color => "blue", :font_weight => :bold },
19
+ :header => { :font_weight => :bold, :font_size => 24, :text_decoration => :underline }
20
+
21
+ text "<header>Fun with Tags</header>"
22
+
23
+ text "<em>Emphasized text</em>, <strong>strong text</strong>, <chinese>汉语</chinese>, and <code>E = mc<exp>2</exp></code>"
24
+ end
@@ -0,0 +1,211 @@
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, :height_of_without_formatting, :height_of
13
+ mod.send :alias_method, :height_of, :height_of_with_formatting
14
+ end
15
+
16
+ # Overloaded version of #text. Call via #text, rather than #text_with_formatting
17
+ # (see above, where it aliased to #text).
18
+ def text_with_formatting(text, options={}) #:nodoc:
19
+ if unformatted?(text, options)
20
+ text_without_formatting(text, options)
21
+ else
22
+ format(text, options)
23
+ end
24
+ end
25
+
26
+ # Overloaded version of #height_of. Call via #height_of, rather than
27
+ # #height_of_with_formatting (see above, where it aliased to #height_of).
28
+ def height_of_with_formatting(string, line_width, size=font_size, options={}) #:nodoc:
29
+ if unformatted?(string, options)
30
+ height_of_without_formatting(string, line_width, size)
31
+ else
32
+ formatted_height(string, line_width, size, options)
33
+ end
34
+ end
35
+
36
+ DEFAULT_TAGS = {
37
+ :a => { :meta => { :name => :anchor, :href => :target }, :color => "0000ff", :text_decoration => :underline },
38
+ :b => { :font_weight => :bold },
39
+ :br => { :display => :break },
40
+ :code => { :font_family => "Courier", :font_size => "90%" },
41
+ :em => { :font_style => :italic },
42
+ :font => { :meta => { :face => :font_family, :color => :color, :size => :font_size } },
43
+ :i => { :font_style => :italic },
44
+ :pre => { :white_space => :pre, :font_family => "Courier", :font_size => "90%" },
45
+ :span => {},
46
+ :strong => { :font_weight => :bold },
47
+ :sub => { :vertical_align => :sub, :font_size => "70%" },
48
+ :sup => { :vertical_align => :super, :font_size => "70%" },
49
+ :tt => { :font_family => "Courier" },
50
+ :u => { :text_decoration => :underline },
51
+ }.freeze
52
+
53
+ def tags(update={})
54
+ @tags ||= DEFAULT_TAGS.dup
55
+ @tags.update(update)
56
+ end
57
+
58
+ def styles(update={})
59
+ @styles ||= {}
60
+ @styles.update(update)
61
+ end
62
+
63
+ def default_style
64
+ { :font_family => font.family || font.name,
65
+ :font_size => font_size,
66
+ :color => fill_color }
67
+ end
68
+
69
+ def evaluate_measure(measure, options={})
70
+ case measure
71
+ when nil then nil
72
+ when Numeric then return measure
73
+ when Symbol then
74
+ mappings = options[:mappings] || {}
75
+ raise ArgumentError, "unrecognized value #{measure.inspect}" unless mappings.key?(measure)
76
+ return evaluate_measure(mappings[measure], options)
77
+ when String then
78
+ operator, value, unit = measure.match(/^([-+]?)(\d+(?:\.\d+)?)(.*)$/)[1,3]
79
+
80
+ value = case unit
81
+ when "%" then
82
+ relative = options[:relative] || 0
83
+ relative * value.to_f / 100
84
+ when "em" then
85
+ # not a true em, but good enough for approximating. patches welcome.
86
+ value.to_f * (options[:em] || font_size)
87
+ when "", "pt" then return value.to_f
88
+ when "pc" then return value.to_f * 12
89
+ when "in" then return value.to_f * 72
90
+ else raise ArgumentError, "unsupport units in style value: #{measure.inspect}"
91
+ end
92
+
93
+ current = options[:current] || 0
94
+ case operator
95
+ when "+" then return current + value
96
+ when "-" then return current - value
97
+ else return value
98
+ end
99
+ else return measure.to_f
100
+ end
101
+ end
102
+
103
+ def draw_lines(x, y, width, lines, options={})
104
+ real_x, real_y = translate(x, y)
105
+
106
+ state = options[:state] || {}
107
+ options[:align] ||= :left
108
+
109
+ state = state.merge(:width => width,
110
+ :x => x, :y => y,
111
+ :real_x => real_x, :real_y => real_y,
112
+ :dx => 0, :dy => 0)
113
+
114
+ state[:cookies] ||= {}
115
+ state[:pending_effects] ||= []
116
+
117
+ return state if lines.empty?
118
+
119
+ text_object do |text|
120
+ text.rotate(real_x, real_y, options[:rotate] || 0)
121
+ state[:text] = text
122
+ lines.each { |line| line.draw_on(self, state, options) }
123
+ end
124
+
125
+ state.delete(:text)
126
+
127
+ #rectangle [x, y+state[:dy]], width, state[:dy]
128
+ #stroke
129
+
130
+ return state
131
+ end
132
+
133
+ def layout(text, options={})
134
+ helper = Format::LayoutBuilder.new(self, text, options)
135
+ yield helper if block_given?
136
+ return helper
137
+ end
138
+
139
+ def format(text, options={})
140
+ if options[:at]
141
+ x, y = options[:at]
142
+ format_positioned_text(text, x, y, options)
143
+ else
144
+ format_wrapped_text(text, options)
145
+ end
146
+ end
147
+
148
+ def text_object
149
+ object = TextObject.new
150
+
151
+ if block_given?
152
+ yield object.open
153
+ add_content(object.close)
154
+ end
155
+
156
+ return object
157
+ end
158
+
159
+ private
160
+
161
+ def unformatted?(text, options={})
162
+ # If they have a preference, use it
163
+ if options.key?(:plain)
164
+ return options[:plain]
165
+
166
+ # Otherwise, if they're asking for full-justification, we must assume
167
+ # the text is formatted (since Prawn's text() method has no full justification)
168
+ elsif options[:align] == :justify
169
+ return false
170
+
171
+ # Otherwise, look for tags or XML entities in the text
172
+ else
173
+ return text !~ /<|&(?:#x?)?\w+;/
174
+ end
175
+ end
176
+
177
+ def format_positioned_text(text, x, y, options={})
178
+ helper = layout(text, options)
179
+ line = helper.next
180
+ draw_lines(x, y+line.ascent, line.width, [line], options)
181
+ end
182
+
183
+ def format_wrapped_text(text, options={})
184
+ helper = layout(text, options)
185
+
186
+ start_new_page if self.y < bounds.absolute_bottom
187
+
188
+ until helper.done?
189
+ y = self.y - bounds.absolute_bottom
190
+ height = bounds.stretchy? ? bounds.absolute_top : y
191
+
192
+ y = helper.fill(bounds.left, y, bounds.width, options.merge(:height => height))
193
+
194
+ if helper.done?
195
+ self.y = y + bounds.absolute_bottom
196
+ else
197
+ start_new_page
198
+ end
199
+ end
200
+ end
201
+
202
+ def formatted_height(string, line_width, size=font_size, options={})
203
+ helper = layout(string, options.merge(:size => font_size))
204
+ lines = helper.word_wrap(line_width)
205
+ return lines.inject(0) { |s, line| s + line.height }
206
+ end
207
+ end
208
+ end
209
+
210
+ require 'prawn/document'
211
+ Prawn::Document.send(:include, Prawn::Format)
@@ -0,0 +1,30 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ module Format
5
+ module Effects
6
+
7
+ class Link
8
+ def initialize(target, x)
9
+ @target = target.sub(/^#/, "")
10
+ @x = x
11
+ end
12
+
13
+ def finish(document, draw_state)
14
+ x1 = draw_state[:real_x] + @x
15
+ x2 = draw_state[:real_x] + draw_state[:dx]
16
+ y = draw_state[:real_y] + draw_state[:dy]
17
+
18
+ rect = [x1, y + draw_state[:line].descent, x2, y + draw_state[:line].ascent]
19
+ document.link_annotation(rect, :Dest => @target, :Border => [0,0,0])
20
+ end
21
+
22
+ def wrap(document, draw_state)
23
+ finish(document, draw_state)
24
+ @x = 0
25
+ end
26
+ end
27
+
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,32 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ module Format
5
+ module Effects
6
+
7
+ class Underline
8
+ def initialize(from, state)
9
+ @from = from
10
+ @state = state
11
+ end
12
+
13
+ def finish(document, draw_state)
14
+ x1 = draw_state[:x] + @from
15
+ x2 = draw_state[:x] + draw_state[:dx]
16
+ y = draw_state[:y] + draw_state[:dy] - 2
17
+
18
+ document.stroke_color(@state.color)
19
+ document.move_to(x1, y)
20
+ document.line_to(x2, y)
21
+ document.stroke
22
+ end
23
+
24
+ def wrap(document, draw_state)
25
+ finish(document, draw_state)
26
+ @from = 0
27
+ end
28
+ end
29
+
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ # encoding: utf-8
2
+
3
+ module Prawn
4
+ module Format
5
+ module Instructions
6
+
7
+ class Base
8
+ attr_reader :state, :ascent, :descent
9
+
10
+ def initialize(state)
11
+ @state = state
12
+ state.document.font_size(state.font_size) do
13
+ @height = state.font.height
14
+ @ascent = state.font.ascender
15
+ @descent = state.font.descender
16
+ end
17
+ end
18
+
19
+ def spaces
20
+ 0
21
+ end
22
+
23
+ def width(*args)
24
+ 0
25
+ end
26
+
27
+ def height(*args)
28
+ @height
29
+ end
30
+
31
+ def break?
32
+ false
33
+ end
34
+
35
+ def force_break?
36
+ false
37
+ end
38
+
39
+ def discardable?
40
+ false
41
+ end
42
+
43
+ def start_verbatim?
44
+ false
45
+ end
46
+
47
+ def end_verbatim?
48
+ false
49
+ end
50
+
51
+ def style
52
+ {}
53
+ end
54
+
55
+ def accumulate(list)
56
+ list.push(self)
57
+ end
58
+ end
59
+
60
+ end
61
+ end
62
+ end