module PDF class Wrapper # Change the default font size # # If no block is provided, the change is permanent. If a block # is provided, the change will revert at the end of the block # # Permanant change: # # pdf.font_size 10 # # Temporary change: # # pdf.font_size 20 # pdf.text "This text is size 20" # pdf.font_size(10) do # pdf.text "This text is size 20" # end # pdf.text "This text is size 20" # def font_size(size) new_size = size.to_i raise ArgumentError, 'font size must be > 0' if new_size <= 0 if block_given? orig_size = @default_font_size @default_font_size = new_size yield @default_font_size = orig_size else @default_font_size = new_size end end alias font_size= font_size # change the default font to write with def font(fontname, style = nil, weight = nil) @default_font = fontname @default_font_style = style unless style.nil? @default_font_weight = weight unless weight.nil? end # add text to the page, bounded by a box with dimensions HxW, with it's top left corner # at x,y. Any text that doesn't fit it the box will be silently dropped. # # In addition to the standard text style options (see the documentation for text()), cell() supports # the following options: # # :border:: Which sides of the cell should have a border? A string with any combination the letters tblr (top, bottom, left, right). Nil for no border, defaults to all sides. # :border_width:: How wide should the border be? # :border_color:: What color should the border be? # :fill_color:: A background color for the cell. Defaults to none. # :radius:: Give the border around the cell rounded corners. Implies :border => "tblr" def cell(str, x, y, w, h, opts={}) # TODO: add a wrap option so wrapping can be disabled # TODO: add an option for vertical alignment # TODO: allow cell contents to be defined as a block, like link_to in EDGE rails options = default_text_options options.merge!({:border => "tblr", :border_width => @default_line_width, :border_color => :black, :fill_color => nil, :padding => 3, :radius => nil}) options.merge!(opts) options.assert_valid_keys(default_text_options.keys + [:width, :border, :border_width, :border_color, :fill_color, :padding, :radius]) # apply padding textw = w - (options[:padding] * 2) texth = h - (options[:padding] * 2) # if the user wants a rounded rectangle, we'll draw the border with a rectangle instead # of 4 lines options[:border] = nil if options[:radius] # normalise the border options[:border] = "" unless options[:border] options[:border].downcase! save_coords do translate(x, y) do # draw a border around the cell if options[:radius] rectangle(0,0,w,h, :radius => options[:radius], :color => options[:border_color], :fill_color => options[:fill_color], :line_width => options[:border_width]) else rectangle(0,0,w,h, :color => options[:fill_color], :fill_color => options[:fill_color]) if options[:fill_color] line(0,0,w,0, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("t") line(0,h,w,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("b") line(0,0,0,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("l") line(w,0,w,h, :color => options[:border_color], :line_width => options[:border_width]) if options[:border].include?("r") end layout = build_pango_layout(str.to_s, textw, options) color(options[:color]) if options[:color] # draw the context on our cairo layout render_layout(layout, options[:padding], options[:padding], texth) end end end # Write text to the page # # By default the text will be rendered using all the space within the margins and using # the default font styling set by font(), font_size, etc # # There is no way to place a bottom bound (or height) onto the text. Text will wrap as # necessary and take all the room it needs. For finer grained control of text boxes, see the # cell method. # # To override all these defaults, use the options hash # # Positioning Options: # # :left:: The x co-ordinate of the left-hand side of the text. # :top:: The y co-ordinate of the top of the text. # :width:: The width of the text to wrap at # # Text Style Options: # # :font:: The font family to use as a string # :font_size:: The size of the font in points # :alignment:: Align the text along the left, right or centre. Use :left, :right, :center # :wrap:: The wrapping technique to use if required. Use :word, :char or :wordchar. Default is :wordchar # :justify:: Justify the text so it exapnds to fill the entire width of each line. Note that this only works in pango >= 1.17 # :spacing:: Space between lines in PDF points # :markup:: Interpret the text as a markup language. Default is nil (none). # # = Markup # # If the markup option is specified, the text can be modified in various ways. At this stage # the only markup syntax implemented is :pango. # # == Pango Markup # # Full details on the Pango markup language are avaialble at http://ruby-gnome2.sourceforge.jp/hiki.cgi?pango-markup # # The format is vaguely XML-like. # # Bold: "Some of this text is bold." # Italics: "Some of this text is in italics." # Strikethrough: "My name is BobJames." # Monospace Font: "Code:\nputs 1." # # For more advanced control, use span tags # # Big and Bold: Some of this text is bold. # Stretched: Some of this text is funny looking. # def text(str, opts={}) # TODO: add converters from various markup languages to pango markup. (markdown, textile, etc) # TODO: add a wrap option so wrapping can be disabled # # the non pango way to add text to the cairo context, not particularly useful for # PDF generation as it doesn't support wrapping text or other advanced layout features # and I really don't feel like re-implementing all that # @context.show_text(str) # the "pango way" x, y = current_point options = default_text_options.merge!({:left => x, :top => y}) options.merge!(opts) options.assert_valid_keys(default_text_options.keys + default_positioning_options.keys) # if the user hasn't specified a width, make the text wrap on the right margin options[:width] = absolute_right_margin - options[:left] if options[:width].nil? layout = build_pango_layout(str.to_s, options[:width], options) color(options[:color]) if options[:color] # draw the context on our cairo layout y = render_layout(layout, options[:left], options[:top]) move_to(options[:left], y) end # Returns the amount of vertical space needed to display the supplied text at the requested width # opts is an options hash that specifies various attributes of the text. See the text function for more information. def text_height(str, width, opts = {}) options = default_text_options.merge(opts) options.assert_valid_keys(default_text_options.keys) options[:width] = width || body_width layout = build_pango_layout(str.to_s, options[:width], options) width, height = layout.size return height / Pango::SCALE end # Returns the amount of horizontal space needed to display the supplied text with the requested options # opts is an options hash that specifies various attributes of the text. See the text function for more information. # The text is assumed to not wrap. def text_width(str, opts = {}) options = default_text_options.merge(opts) options.assert_valid_keys(default_text_options.keys) layout = build_pango_layout(str.to_s, -1, options) width, height = layout.size return width / Pango::SCALE end def default_text_options { :font => @default_font, :font_size => @default_font_size, :alignment => :left, :wrap => :wordchar, :justify => false, :spacing => 0, :color => nil, :markup => nil } end private # takes a string and a range of options and creates a pango layout for us. Pango # does all the hard work of calculating text layout, wrapping, fonts, sizes, # direction and more. Thank $diety. # # The string should be encoded using utf-8. If you get unexpected characters in the # rendered output, check the string encoding. Under Ruby 1.9 compatible VMs, any # non utf-8 strings will be automatically converted if possible. # # The layout will be constrained to the requested width, but has no maximum height. It # is up to some other part of the code to decide how much of the layout should actually # be rendered to the document, when page breaks should be inserted, etc. To specify no # wrapping, set width to nil. This will result in a single line layout that is as wide # as it needs to be to fit the entire string. # # options: # :markup:: The markup language of the string. See Wrapper#text for more information # :spacing:: The spacing between lines. See Wrapper#text for more information # :alignment:: The alignment of the text. See Wrapper#text for more information # :justify:: Should spacing between words be tweaked so each edge of the line touches # the edge of the layout. See Wrapper#text for more information # :font:: The font to use. See Wrapper#text for more information # :font_size:: The font size to use. See Wrapper#text for more information # :wrap:: The wrap technique to use. See Wrapper#text for more information def build_pango_layout(str, w, opts = {}) options = default_text_options.merge!(opts) # if the user hasn't specified a width, make the layout as wide as the page body w = body_width if w.nil? # even though this is a private function, raise this error to force calling functions # to decide how they want to handle converting non-strings into strings for rendering raise ArgumentError, 'build_pango_layout must be passed a string' unless str.kind_of?(String) # if we're running under a M17n aware VM, ensure the string provided is UTF-8 or can be # converted to UTF-8 if RUBY_VERSION >= "1.9" begin str = str.encode("UTF-8") rescue raise ArgumentError, 'Strings must be supplied with a UTF-8 encoding, or an encoding that can be converted to UTF-8' end end # The pango way: load_libpango # create a new Pango layout that our text will be added to layout = @context.create_pango_layout if options[:markup] == :pango layout.markup = str.to_s else layout.text = str.to_s end if w.nil? || w < 0 layout.width = -1 else # width is specified in user points layout.width = w * Pango::SCALE end # spacing is specified in user points layout.spacing = options[:spacing] * Pango::SCALE # set the alignment of the text in the layout if options[:alignment].eql?(:left) layout.alignment = Pango::Layout::ALIGN_LEFT elsif options[:alignment].eql?(:right) layout.alignment = Pango::Layout::ALIGN_RIGHT elsif options[:alignment].eql?(:center) || options[:alignment].eql?(:centre) layout.alignment = Pango::Layout::ALIGN_CENTER else raise ArgumentError, "Invalid alignment requested" end # set the wrapping technique text of the layout if options[:wrap].eql?(:word) layout.wrap = Pango::Layout::WRAP_WORD elsif options[:wrap].eql?(:char) layout.wrap = Pango::Layout::WRAP_CHAR elsif options[:wrap].eql?(:wordchar) layout.wrap = Pango::Layout::WRAP_WORD_CHAR else raise ArgumentError, "Invalid wrap technique requested" end # justify the text if need be - only works in pango >= 1.17 layout.justify = true if options[:justify] # setup the font that will be used to render the text fdesc = Pango::FontDescription.new(options[:font]) # font size should be specified in device points for simplicity's sake. fdesc.size = options[:font_size] * Pango::SCALE layout.font_description = fdesc @context.update_pango_layout(layout) return layout end # renders a pango layout onto our main context # based on a function of the same name found in the text2.rb sample file # distributed with rcairo - it's still black magic to me and has a few edge # cases where it doesn't work too well. Needs to be improved. # # If h is specified, lines of text will be rendered up to that height, and # the rest will be ignored. # # If h is nil, lines will be rendered until the bottom margin is hit, then # a new page will be started and lines will continue being rendered at the # top of the new page. def render_layout(layout, x, y, h = nil) # we can't use context.show_pango_layout, as that won't start # a new page if the layout hits the bottom margin. Instead, # we iterate over each line of text in the layout and add it to # the canvas, page breaking as necessary offset = 0 baseline = 0 spacing = layout.spacing / Pango::SCALE iter = layout.iter loop do line = iter.line ink_rect, logical_rect = iter.line_extents # calculate the relative starting co-ords of this line baseline = iter.baseline / Pango::SCALE linex = logical_rect.x / Pango::SCALE if h && baseline - offset >= h # the user doesn't want us to continue on the next page, so # stop adding lines to the canvas break elsif h.nil? && (y + baseline - offset + spacing) >= self.absolute_bottom_margin # create a new page and we can continue adding text offset = baseline start_new_page y = self.y end # move to the start of this line @context.move_to(x + linex, y + baseline - offset + spacing) # draw the line on the canvas @context.show_pango_layout_line(line) break unless iter.next_line! end width, height = layout.size # return the y co-ord we finished on return y + (height / Pango::SCALE) - offset + spacing end end end