pdf-wrapper 0.0.1 → 0.0.2

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/CHANGELOG CHANGED
@@ -1,2 +1,7 @@
1
+ v0.0.2 (11th January 2008)
2
+ - Added support for a range of extra image formats (jpg, pdf, gif, etc)
3
+ - Various documentation improvements
4
+ - improved the text functions a little, but still lots more to go
5
+
1
6
  v0.0.1 (9th January 2008)
2
7
  - Initial, pre-alpha, eats small children for breakfast release
data/DESIGN CHANGED
@@ -13,6 +13,7 @@
13
13
  - image boxes
14
14
  - lists
15
15
  - tables (port simple table?)
16
+ - add pages from existing PDFs
16
17
 
17
18
  *************************************
18
19
  * Thoughts on the table API
data/README CHANGED
@@ -47,14 +47,16 @@ James Healy <jimmy@deefa.com>
47
47
 
48
48
  = Dependencies
49
49
 
50
- * ruby/cairo
51
- * ruby/pango (optional, required to add text)
52
- * ruby/rsvg2 (optional, required for SVG support)
50
+ * ruby/cairo[http://cairographics.org/rcairo/]
51
+ * ruby/pango[http://ruby-gnome2.sourceforge.jp/] (optional, required to add text)
52
+ * ruby/rsvg2[http://ruby-gnome2.sourceforge.jp/] (optional, required for SVG support)
53
+ * ruby/gdkpixbuf[http://ruby-gnome2.sourceforge.jp/] (optional, required for GIF/JPG support)
54
+ * ruby/poppler[http://ruby-gnome2.sourceforge.jp/] (optional, required embedding PDF images)
53
55
 
54
56
  These are all ruby bindings to C libraries. On Debian/Ubuntu based systems
55
57
  (which I develop on) you can get them by running:
56
58
 
57
- aptitude install libcairo-ruby libpango1-ruby librsvg2-ruby
59
+ aptitude install libcairo-ruby libpango1-ruby librsvg2-ruby libpoppler-glib-ruby
58
60
 
59
61
  For users of other systems, I'd love to receive info on how you set these bindings up.
60
62
 
data/Rakefile CHANGED
@@ -6,7 +6,7 @@ require 'rake/testtask'
6
6
  require "rake/gempackagetask"
7
7
  require 'spec/rake/spectask'
8
8
 
9
- PKG_VERSION = "0.0.1"
9
+ PKG_VERSION = "0.0.2"
10
10
  PKG_NAME = "pdf-wrapper"
11
11
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
12
12
 
@@ -19,7 +19,7 @@ Spec::Rake::SpecTask.new("spec") do |t|
19
19
  t.spec_files = FileList['specs/**/*.rb']
20
20
  t.rcov = true
21
21
  t.rcov_dir = (ENV['CC_BUILD_ARTIFACTS'] || 'doc') + "/rcov"
22
- t.rcov_opts = ["--exclude","spec.*\.rb","--exclude",".*cairo.*","--exclude",".*rcov.*","--exclude",".*rspec.*","--exclude",".*df-reader.*"]
22
+ t.rcov_opts = ["--exclude","spec.*\.rb","--exclude",".*cairo.*","--exclude",".*rcov.*","--exclude",".*rspec.*","--exclude",".*pdf-reader.*"]
23
23
  end
24
24
 
25
25
  # generate specdocs
@@ -51,26 +51,33 @@ Rake::RDocTask.new("doc") do |rdoc|
51
51
  rdoc.options << "--inline-source"
52
52
  end
53
53
 
54
+ desc "Publish rdocs to rubyforge"
55
+ task :publish_rdoc => [ :doc ] do
56
+ rdoc_dir = File.dirname(__FILE__) + "/doc/rdoc/"
57
+ `scp -r #{rdoc_dir} yob@rubyforge.org:/var/www/gforge-projects/pdf-wrapper/`
58
+ end
59
+
54
60
  # a gemspec for packaging this library
55
61
  spec = Gem::Specification.new do |spec|
56
- spec.name = PKG_NAME
57
- spec.version = PKG_VERSION
58
- spec.platform = Gem::Platform::RUBY
59
- spec.summary = "A PDF generating library built on top of cairo"
60
- spec.files = Dir.glob("{examples,lib}/**/**/*") + ["Rakefile"]
62
+ spec.name = PKG_NAME
63
+ spec.version = PKG_VERSION
64
+ spec.platform = Gem::Platform::RUBY
65
+ spec.summary = "A PDF generating library built on top of cairo"
66
+ spec.files = Dir.glob("{examples,lib,specs}/**/**/*") + ["Rakefile"]
61
67
  spec.require_path = "lib"
62
- spec.has_rdoc = true
63
- spec.extra_rdoc_files = %w{README DESIGN CHANGELOG}
64
- spec.rdoc_options << '--title' << 'PDF::Wrapper Documentation' << '--main' << 'README' << '-q'
68
+ spec.has_rdoc = true
69
+ spec.extra_rdoc_files = %w{README DESIGN CHANGELOG}
70
+ spec.rdoc_options << '--title' << 'PDF::Wrapper Documentation' << '--main' << 'README' << '-q'
65
71
  spec.author = "James Healy"
66
- spec.email = "jimmy@deefa.com"
67
- spec.rubyforge_project = "pdf-wrapper"
68
- spec.description = "A PDF writing library that uses the cairo and pango libraries to do the heavy lifting."
72
+ spec.homepage = "http://pdf-wrapper.rubyforge.org/"
73
+ spec.email = "jimmy@deefa.com"
74
+ spec.rubyforge_project = "pdf-wrapper"
75
+ spec.description = "A unicode aware PDF writing library that uses the ruby bindings to various c libraries ( like, cairo, pango, poppler and rsvg ) to do the heavy lifting."
69
76
  end
70
77
 
71
78
  # package the library into a gem
72
79
  desc "Generate a gem for pdf-wrapper"
73
80
  Rake::GemPackageTask.new(spec) do |pkg|
74
- pkg.need_zip = true
75
- pkg.need_tar = true
81
+ pkg.need_zip = true
82
+ pkg.need_tar = true
76
83
  end
data/examples/image.rb CHANGED
@@ -8,5 +8,13 @@ pdf = PDF::Wrapper.new(:paper => :A4)
8
8
  pdf.default_font("Sans Serif")
9
9
  pdf.default_color(:black)
10
10
  pdf.text("PDF::Wrapper Supports Images", :alignment => :center)
11
- pdf.image(File.dirname(__FILE__) + "/google.png", :left => 100, :top => 250)
11
+ pdf.image(File.dirname(__FILE__) + "/../specs/data/zits.gif")
12
+ pdf.image(File.dirname(__FILE__) + "/../specs/data/google.png", :left => 100, :top => 350)
13
+ pdf.image(File.dirname(__FILE__) + "/../specs/data/stef.jpg", :left => 200, :top => 500)
14
+ pdf.start_new_page
15
+ pdf.image(File.dirname(__FILE__) + "/../specs/data/orc.svg", :left => pdf.margin_left, :top => pdf.margin_top, :width => pdf.body_width, :height => pdf.body_height)
16
+ pdf.start_new_page
17
+ pdf.image(File.dirname(__FILE__) + "/../specs/data/utf8-long.pdf", :left => pdf.margin_left, :top => pdf.margin_top, :width => pdf.body_width/2, :height => pdf.body_height/2)
18
+ pdf.default_color(:red)
19
+
12
20
  pdf.render_to_file("image.pdf")
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $:.unshift(File.dirname(__FILE__) + "/../lib")
4
+
5
+ require 'pdf/wrapper'
6
+
7
+ pdf = PDF::Wrapper.new(:paper => :A4)
8
+ pdf.default_font("Sans Serif")
9
+ pdf.default_color(:black)
10
+ pdf.text File.read(File.dirname(__FILE__) + "/../specs/data/utf8-long.txt"), :font => "Monospace", :font_size => 8
11
+ pdf.render_to_file("utf8-long.pdf")
data/lib/pdf/wrapper.rb CHANGED
@@ -24,21 +24,21 @@ module PDF
24
24
  #
25
25
  # Rendering to a file:
26
26
  #
27
- # require 'pdfwrapper'
27
+ # require 'pdf/wrapper'
28
28
  # pdf = PDF::Wrapper.new(:paper => :A4)
29
29
  # pdf.text "Hello World"
30
30
  # pdf.render_to_file("wrapper.pdf")
31
31
  #
32
32
  # Rendering to a string:
33
33
  #
34
- # require 'pdfwrapper'
34
+ # require 'pdf/wrapper'
35
35
  # pdf = PDF::Wrapper.new(:paper => :A4)
36
36
  # pdf.text "Hello World", :font_size => 16
37
37
  # puts pdf.render
38
38
  #
39
39
  # Changing the default font:
40
40
  #
41
- # require 'pdfwrapper'
41
+ # require 'pdf/wrapper'
42
42
  # pdf = PDF::Wrapper.new(:paper => :A4)
43
43
  # pdf.default_font("Monospace")
44
44
  # pdf.text "Hello World", :font => "Sans Serif", :font_size => 18
@@ -234,6 +234,7 @@ module PDF
234
234
  # between 0 and 1. See the API docs at http://cairo.rubyforge.org/ for a list
235
235
  # of predefined colours
236
236
  def default_color(c)
237
+ c = translate_color(c)
237
238
  validate_color(c)
238
239
  @default_color = c
239
240
  end
@@ -506,8 +507,11 @@ module PDF
506
507
  # Functions relating to working with images
507
508
  #####################################################
508
509
 
509
- # add an image to the page
510
- # at this stage the file must be a PNG or SVG
510
+ # add an image to the page - a wide range of image formats are supported,
511
+ # including svg, jpg, png and gif. PDF images are also supported - an attempt
512
+ # to add a multipage PDF will result in only the first page appearing in the
513
+ # new document.
514
+ #
511
515
  # supported options:
512
516
  # <tt>:left</tt>:: The x co-ordinate of the left-hand side of the image.
513
517
  # <tt>:top</tt>:: The y co-ordinate of the top of the image.
@@ -516,42 +520,24 @@ module PDF
516
520
  #
517
521
  # left and top default to the current cursor location
518
522
  # width and height default to the size of the imported image
523
+ #
524
+ # if width or height are specified, the image will *not* be scaled proportionally
519
525
  def image(filename, opts = {})
520
- # TODO: maybe split this up into separate functions for each image type
521
526
  # TODO: add some options for things like justification, scaling and padding
522
- # TODO: png images currently can't be resized
523
527
  # TODO: raise an error if any unrecognised options were supplied
528
+ # TODO: add support for pdf/eps/ps images
524
529
  raise ArgumentError, "file #{filename} not found" unless File.file?(filename)
525
530
 
526
- filetype = detect_image_type(filename)
527
-
528
- if filetype.eql?(:png)
529
- img_surface = Cairo::ImageSurface.from_png(filename)
530
- x, y = current_point
531
- @context.set_source(img_surface, opts[:left] || x, opts[:top] || y)
532
- @context.paint
533
- elsif filetype.eql?(:svg)
534
- # thanks to Nathan Stitt for help with this section
535
- load_librsvg
536
- @context.save
537
-
538
- # import it
539
- handle = RSVG::Handle.new_from_file(filename)
540
-
541
- # size the SVG
542
- if opts[:height] && opts[:width]
543
- handle.set_size_callback do |h,w|
544
- [ opts[:width], opts[:height] ]
545
- end
546
- end
547
-
548
- # place the image on our main context
549
- x, y = current_point
550
- @context.translate( opts[:left] || x, opts[:top] || y )
551
- @context.render_rsvg_handle(handle)
552
- @context.restore
531
+ case detect_image_type(filename)
532
+ when :pdf then draw_pdf filename, opts
533
+ when :png then draw_png filename, opts
534
+ when :svg then draw_svg filename, opts
553
535
  else
554
- raise ArgumentError, "Unrecognised image format"
536
+ begin
537
+ draw_pixbuf filename, opts
538
+ rescue Gdk::PixbufError
539
+ raise ArgumentError, "Unrecognised image format (#{filename})"
540
+ end
555
541
  end
556
542
  end
557
543
 
@@ -676,7 +662,6 @@ module PDF
676
662
  end
677
663
 
678
664
  def detect_image_type(filename)
679
-
680
665
  # read the first Kb from the file to attempt file type detection
681
666
  f = File.new(filename)
682
667
  bytes = f.read(1024)
@@ -684,13 +669,78 @@ module PDF
684
669
  # if the file is a PNG
685
670
  if bytes[1,3].eql?("PNG")
686
671
  return :png
672
+ elsif bytes[0,3].eql?("GIF")
673
+ return :gif
674
+ elsif bytes[0,4].eql?("%PDF")
675
+ return :pdf
687
676
  elsif bytes.include?("<svg")
688
677
  return :svg
678
+ elsif bytes.include?("Exif")
679
+ return :jpg
689
680
  else
690
681
  return nil
691
682
  end
692
683
  end
693
684
 
685
+ def draw_pdf(filename, opts = {})
686
+ # based on a similar function in rabbit. Thanks Kou.
687
+ load_libpoppler
688
+ x, y = current_point
689
+ page = Poppler::Document.new(filename).get_page(1)
690
+ w, h = page.size
691
+ width = (opts[:width] || w).to_f
692
+ height = (opts[:height] || h).to_f
693
+ @context.save do
694
+ @context.translate(opts[:left] || x, opts[:top] || y)
695
+ @context.scale(width / w, height / h)
696
+ @context.render_poppler_page(page)
697
+ end
698
+ end
699
+
700
+ def draw_pixbuf(filename, opts = {})
701
+ # based on a similar function in rabbit. Thanks Kou.
702
+ load_libpixbuf
703
+ x, y = current_point
704
+ pixbuf = Gdk::Pixbuf.new(filename)
705
+ width = (opts[:width] || pixbuf.width).to_f
706
+ height = (opts[:height] || pixbuf.height).to_f
707
+ @context.save do
708
+ @context.translate(opts[:left] || x, opts[:top] || y)
709
+ @context.scale(width / pixbuf.width, height / pixbuf.height)
710
+ @context.set_source_pixbuf(pixbuf, 0, 0)
711
+ @context.paint
712
+ end
713
+ end
714
+
715
+ def draw_png(filename, opts = {})
716
+ # based on a similar function in rabbit. Thanks Kou.
717
+ x, y = current_point
718
+ img_surface = Cairo::ImageSurface.from_png(filename)
719
+ width = (opts[:width] || img_surface.width).to_f
720
+ height = (opts[:height] || img_surface.height).to_f
721
+ @context.save do
722
+ @context.translate(opts[:left] || x, opts[:top] || y)
723
+ @context.scale(width / img_surface.width, height / img_surface.height)
724
+ @context.set_source(img_surface, 0, 0)
725
+ @context.paint
726
+ end
727
+ end
728
+
729
+ def draw_svg(filename, opts = {})
730
+ # based on a similar function in rabbit. Thanks Kou.
731
+ load_librsvg
732
+ x, y = current_point
733
+ handle = RSVG::Handle.new_from_file(filename)
734
+ width = (opts[:width] || handle.width).to_f
735
+ height = (opts[:height] || handle.height).to_f
736
+ @context.save do
737
+ @context.translate(opts[:left] || x, opts[:top] || y)
738
+ @context.scale(width / handle.width, height / handle.height)
739
+ @context.render_rsvg_handle(handle)
740
+ #@context.paint
741
+ end
742
+ end
743
+
694
744
  # adds a single table row to the canvas. Top left of the row will be at the current x,y
695
745
  # co-ordinates, so make sure they're set correctly before calling this function
696
746
  #
@@ -747,12 +797,34 @@ module PDF
747
797
  # http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#Pango+related+APIs
748
798
  def load_libpango
749
799
  begin
750
- require 'pango' unless ::Object.const_defined?(:Pango)
800
+ require 'pango' unless @context.respond_to? :create_pango_layout
751
801
  rescue LoadError
752
802
  raise LoadError, 'Ruby/Pango library not found. Visit http://ruby-gnome2.sourceforge.jp/'
753
803
  end
754
804
  end
755
805
 
806
+ # load lib gdkpixbuf if it isn't already loaded.
807
+ # This will add some methods to the cairo Context class in addition to providing
808
+ # its own classes and constants.
809
+ def load_libpixbuf
810
+ begin
811
+ require 'gdk_pixbuf2' unless @context.respond_to? :set_source_pixbuf
812
+ rescue LoadError
813
+ raise LoadError, 'Ruby/GdkPixbuf library not found. Visit http://ruby-gnome2.sourceforge.jp/'
814
+ end
815
+ end
816
+
817
+ # load lib poppler if it isn't already loaded.
818
+ # This will add some methods to the cairo Context class in addition to providing
819
+ # its own classes and constants.
820
+ def load_libpoppler
821
+ begin
822
+ require 'poppler' unless @context.respond_to? :render_poppler_page
823
+ rescue LoadError
824
+ raise LoadError, 'Ruby/Poppler library not found. Visit http://ruby-gnome2.sourceforge.jp/'
825
+ end
826
+ end
827
+
756
828
  # load librsvg if it isn't already loaded
757
829
  # This will add an additional method to the Cairo::Context class
758
830
  # that allows an existing SVG to be drawn directly onto it
@@ -760,7 +832,7 @@ module PDF
760
832
  # http://ruby-gnome2.sourceforge.jp/fr/hiki.cgi?Cairo%3A%3AContext#render_rsvg_handle
761
833
  def load_librsvg
762
834
  begin
763
- require 'rsvg2' unless ::Object.const_defined?(:RSVG)
835
+ require 'rsvg2' unless @context.respond_to? :render_svg_handle
764
836
  rescue LoadError
765
837
  raise LoadError, 'Ruby/RSVG library not found. Visit http://ruby-gnome2.sourceforge.jp/'
766
838
  end
@@ -771,62 +843,76 @@ module PDF
771
843
  # distributed with rcairo - it's still black magic to me and has a few edge
772
844
  # cases where it doesn't work too well. Needs to be improved.
773
845
  def render_layout(layout, x, y, h, opts = {})
846
+ # we can't use content.show_pango_layout, as that won't start
847
+ # a new page if the layout hits the bottom margin. Instead,
848
+ # we iterate over each line of text in the layout and add it to
849
+ # the canvas, page breaking as necessary
774
850
  options = {:auto_new_page => true }
775
851
  options.merge!(opts)
776
852
 
777
- limit_y = y + h
853
+ # store the starting x and y co-ords. If we start a new page, we'll continue
854
+ # adding text at the same co-ords
855
+ orig_x = x
856
+ orig_y = y
778
857
 
779
- iter = layout.iter
780
- prev_baseline = iter.baseline / Pango::SCALE
781
- begin
782
- line = iter.line
783
- ink_rect, logical_rect = iter.line_extents
784
- y_begin, y_end = iter.line_yrange
785
- if limit_y < (y + y_end / Pango::SCALE)
858
+ # for each line in the layout
859
+ layout.lines.each do |line|
860
+
861
+ # draw the line on the canvas
862
+ @context.show_pango_layout_line(line)
863
+
864
+ #calculate where the next line starts
865
+ ink_rect, logical_rect = line.extents
866
+ y = y + (logical_rect.height / Pango::SCALE * (3.0/4.0)) + 1
867
+
868
+ if y >= (orig_y + h)
869
+ # our text is using the maximum amount of vertical space we want it to
786
870
  if options[:auto_new_page]
871
+ # create a new page and we can continue adding text
787
872
  start_new_page
788
- y = margin_top - prev_baseline
873
+ x = orig_x
874
+ y = orig_y
789
875
  else
876
+ # the user doesn't want us to continue on the next page, so
877
+ # stop adding lines to the canvas
790
878
  break
791
879
  end
792
880
  end
793
- width, height = layout.size
794
- baseline = iter.baseline / Pango::SCALE
795
- move_to(x + logical_rect.x / Pango::SCALE, y + baseline)
796
- @context.show_pango_layout_line(line)
797
- prev_baseline = baseline
798
- end while iter.next_line!
799
- return y + baseline
881
+
882
+ # move to the start of the next line
883
+ move_to(x, y)
884
+ end
885
+
886
+ # return the y co-ord we finished on
887
+ return y
800
888
  end
801
889
 
890
+ def translate_color(c)
891
+ # the follow line converts a color definition from various formats (hex, symbol, etc)
892
+ # into a 4 item array. This is normally handled within cairo itself, however when
893
+ # Cairo and Poppler are both loaded, it breaks.
894
+ Cairo::Color.parse(c).to_rgb.to_a
895
+ end
802
896
  # set the current drawing colour
803
897
  #
804
898
  # for info on what is valid, see the comments for default_color
805
899
  def set_color(c)
806
- # catch and reraise an exception to keep stack traces readable and clear
900
+ c = translate_color(c)
807
901
  validate_color(c)
808
-
809
- if c.kind_of?(Array)
810
- @context.set_source_color(*c)
811
- else
812
- @context.set_source_color(c)
813
- end
902
+ @context.set_source_rgba(*c)
814
903
  end
815
904
 
816
905
  # test to see if the specified colour is a a valid cairo color
817
906
  #
818
907
  # for info on what is valid, see the comments for default_color
819
908
  def validate_color(c)
909
+ c = translate_color(c)
820
910
  @context.save
911
+ # catch and reraise an exception to keep stack traces readable and clear
821
912
  begin
822
- if c.kind_of?(Array)
823
- # if the colour is being specified manually, there must be 3 or 4 elements
824
- raise ArgumentError if c.size != 3 && c.size != 4
825
- @context.set_source_color(c)
826
- else
827
- @context.set_source_color(c)
828
- end
829
- @default_color = c
913
+ raise ArgumentError unless c.kind_of?(Array)
914
+ raise ArgumentError if c.size != 3 && c.size != 4
915
+ @context.set_source_rgba(c)
830
916
  rescue ArgumentError
831
917
  c.kind_of?(Array) ? str = "[#{c.join(",")}]" : str = c.to_s
832
918
  raise ArgumentError, "#{str} is not a valid color definition"