prawn 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. data/README +16 -2
  2. data/Rakefile +3 -3
  3. data/data/images/arrow.png +0 -0
  4. data/data/images/arrow2.png +0 -0
  5. data/data/images/barcode_issue.png +0 -0
  6. data/data/images/ruport_type0.png +0 -0
  7. data/examples/cell.rb +14 -3
  8. data/examples/chinese_text_wrapping.rb +17 -0
  9. data/examples/family_based_styling.rb +21 -0
  10. data/examples/fancy_table.rb +4 -4
  11. data/examples/flowing_text_with_header_and_footer.rb +72 -0
  12. data/examples/font_size.rb +2 -2
  13. data/examples/lazy_bounding_boxes.rb +19 -0
  14. data/examples/table.rb +13 -11
  15. data/examples/text_flow.rb +1 -1
  16. data/lib/prawn.rb +44 -15
  17. data/lib/prawn/compatibility.rb +20 -7
  18. data/lib/prawn/document.rb +72 -122
  19. data/lib/prawn/document/bounding_box.rb +124 -24
  20. data/lib/prawn/document/internals.rb +107 -0
  21. data/lib/prawn/document/table.rb +99 -70
  22. data/lib/prawn/document/text.rb +92 -314
  23. data/lib/prawn/errors.rb +13 -2
  24. data/lib/prawn/font.rb +312 -1
  25. data/lib/prawn/font/cmap.rb +1 -1
  26. data/lib/prawn/font/metrics.rb +52 -49
  27. data/lib/prawn/font/wrapping.rb +14 -12
  28. data/lib/prawn/graphics.rb +23 -74
  29. data/lib/prawn/graphics/cell.rb +30 -25
  30. data/lib/prawn/graphics/color.rb +132 -0
  31. data/lib/prawn/images.rb +37 -16
  32. data/lib/prawn/images/png.rb +29 -24
  33. data/lib/prawn/pdf_object.rb +3 -1
  34. data/spec/bounding_box_spec.rb +12 -3
  35. data/spec/document_spec.rb +40 -72
  36. data/spec/font_spec.rb +97 -0
  37. data/spec/graphics_spec.rb +46 -99
  38. data/spec/images_spec.rb +4 -21
  39. data/spec/pdf_object_spec.rb +8 -8
  40. data/spec/png_spec.rb +47 -12
  41. data/spec/spec_helper.rb +5 -24
  42. data/spec/table_spec.rb +53 -59
  43. data/spec/text_spec.rb +28 -93
  44. data/vendor/pdf-inspector/README +18 -0
  45. data/vendor/pdf-inspector/lib/pdf/inspector.rb +25 -0
  46. data/vendor/pdf-inspector/lib/pdf/inspector/graphics.rb +80 -0
  47. data/vendor/pdf-inspector/lib/pdf/inspector/page.rb +16 -0
  48. data/vendor/pdf-inspector/lib/pdf/inspector/text.rb +31 -0
  49. data/vendor/pdf-inspector/lib/pdf/inspector/xobject.rb +19 -0
  50. metadata +63 -38
  51. data/examples/on_page_start.rb +0 -17
  52. data/examples/table_bench.rb +0 -92
  53. data/spec/box_calculation_spec.rb +0 -17
data/lib/prawn/images.rb CHANGED
@@ -19,7 +19,7 @@ module Prawn
19
19
  #
20
20
  # Options:
21
21
  # <tt>:at</tt>:: the location of the top left corner of the image.
22
- # <tt>:position/tt>::
22
+ # <tt>:position</tt>:: One of (:left, :center, :right) or an x-offset
23
23
  # <tt>:height</tt>:: the height of the image [actual height of the image]
24
24
  # <tt>:width</tt>:: the width of the image [actual width of the image]
25
25
  # <tt>:scale</tt>:: scale the dimensions of the image proportionally
@@ -36,12 +36,11 @@ module Prawn
36
36
  # proportionally. When both are provided, the image will be stretched to
37
37
  # fit the dimensions without maintaining the aspect ratio.
38
38
  #
39
- def image(filename, options={})
39
+ def image(filename, options={})
40
+ Prawn.verify_options [:at,:position, :height, :width, :scale], options
40
41
  raise ArgumentError, "#{filename} not found" unless File.file?(filename)
41
42
 
42
-
43
- read_mode = ruby_18 { "rb" } || ruby_19 { "rb:ASCII-8BIT" }
44
- image_content = File.open(filename, read_mode) { |f| f.read }
43
+ image_content = File.read_binary(filename)
45
44
 
46
45
  image_sha1 = Digest::SHA1.hexdigest(image_content)
47
46
 
@@ -90,17 +89,15 @@ module Prawn
90
89
 
91
90
  def image_position(w,h,options)
92
91
  options[:position] ||= :left
93
- case options[:position]
92
+ x = case options[:position]
94
93
  when :left
95
- x,y = bounds.absolute_left, self.y
94
+ bounds.absolute_left
96
95
  when :center
97
- x = bounds.absolute_left + (bounds.width - w) / 2.0
98
- y = self.y
96
+ bounds.absolute_left + (bounds.width - w) / 2.0
99
97
  when :right
100
- x,y = bounds.absolute_right - w, self.y
98
+ bounds.absolute_right - w
101
99
  when Numeric
102
- x = options[:position] + bounds.absolute_left
103
- y = self.y
100
+ options[:position] + bounds.absolute_left
104
101
  end
105
102
 
106
103
  return [x,y]
@@ -185,13 +182,37 @@ module Prawn
185
182
  :DeviceRGB,
186
183
  (png.palette.size / 3) -1,
187
184
  palette_obj]
185
+ end
186
+
187
+ # *************************************
188
+ # add transparency data if necessary
189
+ # *************************************
188
190
 
189
- # add transparency data if necessary
190
- #if png.transparency && png.transparency[:type] == 'indexed'
191
- # obj.data[:Mask] = png.transparency[:data]
192
- #end
191
+ # For PNG color types 0, 2 and 3, the transparency data is stored in
192
+ # a dedicated PNG chunk, and is exposed via the transparency attribute
193
+ # of the PNG class.
194
+ if png.transparency[:grayscale]
195
+ # Use Color Key Masking (spec section 4.8.5)
196
+ # - An array with N elements, where N is two times the number of color
197
+ # components.
198
+ val = png.transparency[:grayscale]
199
+ obj.data[:Mask] = [val, val]
200
+ elsif png.transparency[:rgb]
201
+ # Use Color Key Masking (spec section 4.8.5)
202
+ # - An array with N elements, where N is two times the number of color
203
+ # components.
204
+ rgb = png.transparency[:rgb]
205
+ obj.data[:Mask] = rgb.collect { |val| [val,val] }.flatten
206
+ elsif png.transparency[:indexed]
207
+ # TODO: broken. I was attempting to us Color Key Masking, but I think
208
+ # we need to construct an SMask i think. Maybe do it inside
209
+ # the PNG class, and store it in alpha_channel
210
+ #obj.data[:Mask] = png.transparency[:indexed]
193
211
  end
194
212
 
213
+ # For PNG color types 4 and 6, the transparency data is stored as a alpha
214
+ # channel mixed in with the main image data. The PNG class seperates
215
+ # it out for us and makes it available via the alpha_channel attribute
195
216
  if png.alpha_channel
196
217
  smask_obj = ref(:Type => :XObject,
197
218
  :Subtype => :Image,
@@ -30,6 +30,7 @@ module Prawn
30
30
 
31
31
  @palette = ""
32
32
  @img_data = ""
33
+ @transparency = {}
33
34
 
34
35
  loop do
35
36
  chunk_size = data.read(4).unpack("N")[0]
@@ -61,16 +62,17 @@ module Prawn
61
62
  # the palette index in the PLTE ("palette") chunk up until the
62
63
  # last non-opaque entry. Set up an array, stretching over all
63
64
  # palette entries which will be 0 (opaque) or 1 (transparent).
64
- @transparency[:type] = 'indexed'
65
- @transparency[:data] = data.read(chunk_size).unpack("C*")
65
+ @transparency[:indexed] = data.read(chunk_size).unpack("C*")
66
+ short = 255 - @transparency[:indexed].size
67
+ @transparency[:indexed] += ([255] * short) if short > 0
66
68
  when 0
67
69
  # Greyscale. Corresponding to entries in the PLTE chunk.
68
70
  # Grey is two bytes, range 0 .. (2 ^ bit-depth) - 1
69
- @transparency[:grayscale] = data.read(2).unpack("n")
70
- @transparency[:type] = 'indexed'
71
+ grayval = data.read(chunk_size).unpack("n").first
72
+ @transparency[:grayscale] = grayval
71
73
  when 2
72
74
  # True colour with proper alpha channel.
73
- @transparency[:rgb] = data.read(6).unpack("nnn")
75
+ @transparency[:rgb] = data.read(chunk_size).unpack("nnn")
74
76
  end
75
77
  when 'IEND'
76
78
  # we've got everything we need, exit the loop
@@ -88,8 +90,8 @@ module Prawn
88
90
  end
89
91
 
90
92
  def pixel_bytes
91
- case @color_type
92
- when 0, 4 then 1
93
+ @pixel_bytes ||= case @color_type
94
+ when 0, 3, 4 then 1
93
95
  when 1, 2, 6 then 3
94
96
  end
95
97
  end
@@ -100,17 +102,6 @@ module Prawn
100
102
  @color_type == 4 || @color_type == 6
101
103
  end
102
104
 
103
- def paeth(a, b, c) # left, above, upper left
104
- p = a + b - c
105
- pa = (p - a).abs
106
- pb = (p - b).abs
107
- pc = (p - c).abs
108
-
109
- return a if pa <= pb && pa <= pc
110
- return b if pb <= pc
111
- c
112
- end
113
-
114
105
  def unfilter_image_data
115
106
  data = Zlib::Inflate.inflate(@img_data).unpack 'C*'
116
107
  @img_data = ""
@@ -120,7 +111,8 @@ module Prawn
120
111
  pixel_length = pixel_bytes + 1
121
112
  scanline_length = pixel_length * @width + 1 # for filter
122
113
  row = 0
123
- pixels = []
114
+ pixels = []
115
+ paeth, pa, pb, pc = nil
124
116
  until data.empty? do
125
117
  row_data = data.slice! 0, scanline_length
126
118
  filter = row_data.shift
@@ -152,15 +144,27 @@ module Prawn
152
144
  col = index / pixel_length
153
145
 
154
146
  left = index < pixel_length ? 0 : row_data[index - pixel_length]
155
- if row == 0 then
147
+ if row.zero?
156
148
  upper = upper_left = 0
157
149
  else
158
150
  upper = pixels[row-1][col][index % pixel_length]
159
- upper_left = col == 0 ? 0 :
151
+ upper_left = col.zero? ? 0 :
160
152
  pixels[row-1][col-1][index % pixel_length]
161
153
  end
162
154
 
163
- paeth = paeth left, upper, upper_left
155
+ p = left + upper - upper_left
156
+ pa = (p - left).abs
157
+ pb = (p - upper).abs
158
+ pc = (p - upper_left).abs
159
+
160
+ paeth = if pa <= pb && pa <= pc
161
+ left
162
+ elsif pb <= pc
163
+ upper
164
+ else
165
+ upper_left
166
+ end
167
+
164
168
  row_data[index] = (byte + paeth) % 256
165
169
  #p [byte, paeth, row_data[index]]
166
170
  end
@@ -168,10 +172,11 @@ module Prawn
168
172
  raise ArgumentError, "Invalid filter algorithm #{filter}"
169
173
  end
170
174
 
171
- pixels << []
175
+ s = []
172
176
  row_data.each_slice pixel_length do |slice|
173
- pixels.last << slice
177
+ s << slice
174
178
  end
179
+ pixels << s
175
180
  row += 1
176
181
  end
177
182
 
@@ -6,6 +6,8 @@
6
6
  #
7
7
  # This is free software. Please see the LICENSE and COPYING files for details.
8
8
 
9
+ # Top level Module
10
+ #
9
11
  module Prawn
10
12
 
11
13
  module_function
@@ -24,7 +26,7 @@ module Prawn
24
26
  # PdfObject(:Symbol) #=> "/Symbol"
25
27
  # PdfObject(["foo",:bar, [1,2]]) #=> "[foo /bar [1 2]]"
26
28
  #
27
- def PdfObject(obj, in_content_stream = false) #:nodoc:
29
+ def PdfObject(obj, in_content_stream = false)
28
30
  case(obj)
29
31
  when NilClass then "null"
30
32
  when TrueClass then "true"
@@ -84,10 +84,11 @@ describe "A bounding box" do
84
84
 
85
85
  end
86
86
 
87
- describe "drawing bounding boxes" do
87
+ describe "drawing bounding boxes" do
88
+
89
+ before(:each) { create_pdf }
88
90
 
89
91
  it "should restore the margin box when bounding box exits" do
90
- @pdf = Prawn::Document.new
91
92
  margin_box = @pdf.bounds
92
93
 
93
94
  @pdf.bounding_box [100,500] do
@@ -99,7 +100,6 @@ describe "drawing bounding boxes" do
99
100
  end
100
101
 
101
102
  it "should restore the parent bounding box when calls are nested" do
102
- @pdf = Prawn::Document.new
103
103
  @pdf.bounding_box [100,500], :width => 300, :height => 300 do
104
104
 
105
105
  @pdf.bounds.absolute_top.should == 500 + @pdf.margin_box.absolute_bottom
@@ -116,5 +116,14 @@ describe "drawing bounding boxes" do
116
116
  @pdf.bounds.absolute_left.should == 100 + @pdf.margin_box.absolute_left
117
117
 
118
118
  end
119
+ end
120
+
121
+ it "should calculate a height if none is specified" do
122
+ @pdf.bounding_box([100, 500], :width => 100) do
123
+ @pdf.text "The rain in Spain falls mainly on the plains."
124
+ end
125
+
126
+ @pdf.y.should.be.close 458.384, 0.001
119
127
  end
128
+
120
129
  end
@@ -3,74 +3,61 @@
3
3
  require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
4
 
5
5
  describe "When creating multi-page documents" do
6
-
7
- class PageCounter
8
- attr_accessor :pages
9
-
10
- def initialize
11
- @pages = 0
12
- end
13
-
14
- # Called when page parsing ends
15
- def end_page
16
- @pages += 1
17
- end
18
- end
19
-
20
-
6
+
21
7
  before(:each) { create_pdf }
22
8
 
23
9
  it "should initialize with a single page" do
24
- page_counter = count_pages
10
+ page_counter = PDF::Inspector::Page.analyze(@pdf.render)
25
11
 
26
- page_counter.pages.should == 1
12
+ page_counter.pages.size.should == 1
27
13
  @pdf.page_count.should == 1
28
14
  end
29
15
 
30
16
  it "should provide an accurate page_count" do
31
17
  3.times { @pdf.start_new_page }
32
- page_counter = count_pages
18
+ page_counter = PDF::Inspector::Page.analyze(@pdf.render)
33
19
 
34
- page_counter.pages.should == 4
20
+ page_counter.pages.size.should == 4
35
21
  @pdf.page_count.should == 4
36
- end
37
-
38
- def count_pages
39
- output = @pdf.render
40
- obs = PageCounter.new
41
- PDF::Reader.string(output,obs)
42
- return obs
43
- end
22
+ end
44
23
 
45
24
  end
46
25
 
47
26
  describe "When beginning each new page" do
48
27
 
49
- it "should execute the lambda specified by on_page_start" do
50
- on_start = mock("page_start_proc")
51
-
52
- on_start.expects(:[]).times(3)
28
+ it "should execute codeblock given to Document#header" do
29
+ call_count = 0
53
30
 
54
- pdf = Prawn::Document.new(:on_page_start => on_start)
31
+ pdf = Prawn::Document.new
32
+ pdf.header(pdf.margin_box.top_left) do
33
+ call_count += 1
34
+ end
35
+
55
36
  pdf.start_new_page
56
- pdf.start_new_page
57
- end
37
+ pdf.start_new_page
38
+ pdf.render
39
+
40
+ call_count.should == 3
41
+ end
58
42
 
59
43
  end
60
44
 
61
-
62
45
  describe "When ending each page" do
63
46
 
64
- it "should execute the lambda specified by on_page_end" do
65
-
66
- on_end = mock("page_end_proc")
67
-
68
- on_end.expects(:[]).times(3)
69
-
70
- pdf = Prawn::Document.new(:on_page_stop => on_end)
71
- pdf.start_new_page
72
- pdf.start_new_page
47
+ it "should execute codeblock given to Document#footer" do
48
+
49
+ call_count = 0
50
+
51
+ pdf = Prawn::Document.new
52
+ pdf.footer([pdf.margin_box.left, pdf.margin_box.bottom + 50]) do
53
+ call_count += 1
54
+ end
55
+
56
+ pdf.start_new_page
57
+ pdf.start_new_page
73
58
  pdf.render
59
+
60
+ call_count.should == 3
74
61
  end
75
62
 
76
63
  it "should not compress the page content stream if compression is disabled" do
@@ -99,44 +86,25 @@ describe "When ending each page" do
99
86
 
100
87
  end
101
88
 
102
- class PageDetails
103
-
104
- def begin_page(params)
105
- @geometry = params[:MediaBox]
106
- end
107
-
108
- def size
109
- @geometry[-2..-1]
110
- end
111
-
112
- end
113
-
114
- def detect_page_details
115
- output = @pdf.render
116
- obs = PageDetails.new
117
- PDF::Reader.string(output,obs)
118
- return obs
119
- end
120
-
121
89
  describe "When setting page size" do
122
90
  it "should default to LETTER" do
123
91
  @pdf = Prawn::Document.new
124
- page = detect_page_details
125
- page.size.should == Prawn::Document::PageGeometry::SIZES["LETTER"]
92
+ pages = PDF::Inspector::Page.analyze(@pdf.render).pages
93
+ pages.first[:size].should == Prawn::Document::PageGeometry::SIZES["LETTER"]
126
94
  end
127
95
 
128
96
  (Prawn::Document::PageGeometry::SIZES.keys - ["LETTER"]).each do |k|
129
97
  it "should provide #{k} geometry" do
130
98
  @pdf = Prawn::Document.new(:page_size => k)
131
- page = detect_page_details
132
- page.size.should == Prawn::Document::PageGeometry::SIZES[k]
99
+ pages = PDF::Inspector::Page.analyze(@pdf.render).pages
100
+ pages.first[:size].should == Prawn::Document::PageGeometry::SIZES[k]
133
101
  end
134
102
  end
135
103
 
136
104
  it "should allow custom page size" do
137
- @pdf = Prawn::Document.new(:page_size => [1920, 1080] )
138
- page = detect_page_details
139
- page.size.should == [1920, 1080]
105
+ @pdf = Prawn::Document.new(:page_size => [1920, 1080] )
106
+ pages = PDF::Inspector::Page.analyze(@pdf.render).pages
107
+ pages.first[:size].should == [1920, 1080]
140
108
  end
141
109
 
142
110
  end
@@ -144,8 +112,8 @@ end
144
112
  describe "When setting page layout" do
145
113
  it "should reverse coordinates for landscape" do
146
114
  @pdf = Prawn::Document.new(:page_size => "A4", :page_layout => :landscape)
147
- page = detect_page_details
148
- page.size.should == Prawn::Document::PageGeometry::SIZES["A4"].reverse
115
+ pages = PDF::Inspector::Page.analyze(@pdf.render).pages
116
+ pages.first[:size].should == Prawn::Document::PageGeometry::SIZES["A4"].reverse
149
117
  end
150
118
  end
151
119
 
data/spec/font_spec.rb ADDED
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+
3
+ require File.join(File.expand_path(File.dirname(__FILE__)), "spec_helper")
4
+
5
+ describe "Font Metrics" do
6
+
7
+ it "should default to Helvetica if no font is specified" do
8
+ @pdf = Prawn::Document.new
9
+ @pdf.font.metrics.should == Prawn::Font::Metrics["Helvetica"]
10
+ end
11
+
12
+ it "should use the currently set font for font_metrics" do
13
+ @pdf = Prawn::Document.new
14
+ @pdf.font "Courier"
15
+ @pdf.font.metrics.should == Prawn::Font::Metrics["Courier"]
16
+
17
+ comicsans = "#{Prawn::BASEDIR}/data/fonts/comicsans.ttf"
18
+ @pdf.font(comicsans)
19
+ @pdf.font.metrics.should == Prawn::Font::Metrics[comicsans]
20
+ end
21
+
22
+ end
23
+
24
+ describe "font style support" do
25
+ before(:each) { create_pdf }
26
+
27
+ it "should allow specifying font style by style name and font family" do
28
+ @pdf.font "Courier", :style => :bold
29
+ @pdf.text "In Courier bold"
30
+
31
+ @pdf.font "Courier", :style => :bold_italic
32
+ @pdf.text "In Courier bold-italic"
33
+
34
+ @pdf.font "Courier", :style => :italic
35
+ @pdf.text "In Courier italic"
36
+
37
+ @pdf.font "Courier", :style => :normal
38
+ @pdf.text "In Normal Courier"
39
+
40
+ @pdf.font "Helvetica"
41
+ @pdf.text "In Normal Helvetica"
42
+
43
+ text = PDF::Inspector::Text.analyze(@pdf.render)
44
+ text.font_settings.map { |e| e[:name] }.should ==
45
+ [:"Courier-Bold", :"Courier-BoldOblique", :"Courier-Oblique",
46
+ :Courier, :Helvetica]
47
+ end
48
+
49
+ end
50
+
51
+ describe "Document#page_fonts" do
52
+ before(:each) { create_pdf }
53
+
54
+ it "should register the current font when changing pages" do
55
+ @pdf.font "Courier"
56
+ page_should_include_font("Courier")
57
+ @pdf.start_new_page
58
+ page_should_include_font("Courier")
59
+ end
60
+
61
+ it "should register fonts properly by page" do
62
+ @pdf.font "Courier"
63
+ @pdf.font "Helvetica"
64
+ @pdf.font "Times-Roman"
65
+ ["Courier","Helvetica","Times-Roman"].each { |f|
66
+ page_should_include_font(f)
67
+ }
68
+
69
+ @pdf.start_new_page
70
+ @pdf.font "Helvetica"
71
+ ["Times-Roman","Helvetica"].each { |f|
72
+ page_should_include_font(f)
73
+ }
74
+ page_should_not_include_font("Courier")
75
+ end
76
+
77
+ def page_includes_font?(font)
78
+ @pdf.page_fonts.values.map { |e| e.data[:BaseFont] }.include?(font.to_sym)
79
+ end
80
+
81
+ def page_should_include_font(font)
82
+ assert_block("Expected page to include font: #{font}") do
83
+ page_includes_font?(font)
84
+ end
85
+ end
86
+
87
+ def page_should_not_include_font(font)
88
+ assert_block("Did not expect page to include font: #{font}") do
89
+ not page_includes_font?(font)
90
+ end
91
+ end
92
+
93
+ end
94
+
95
+
96
+
97
+