pdf-wrapper 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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"