#-- # PDF::Writer for Ruby. # http://rubyforge.org/projects/ruby-pdf/ # Copyright 2003 - 2005 Austin Ziegler. # # Licensed under a MIT-style licence. See LICENCE in the main distribution # for full licensing information. # # $Id: graphics.rb 166 2007-11-08 18:22:05Z sandal $ #++ # Points for use in the drawing of polygons. class PDF::Writer::PolygonPoint def initialize(x, y, connector = :line) @x, @y, @connector = x, y, connector end attr_reader :x, :y, :connector end # This module contains graphics primitives. Objects that include this # module must respond to #add_content. # # The PDF::Writer coordinate system is in PDF userspace units. The # coordinate system in PDF::Writer is slightly different than might be # expected, in that (0, 0) is at the lower left-hand corner of # the canvas (page), not the normal top left-hand corner of the canvas. # (See the diagram below.) # # Y Y # 0+-----+X # | | # | | # | | # 0+-----+X # 0 0 # # Each primitive provided below indicates the New Point, or the # coordinates new drawing point at the completion of the drawing # operation. Drawing operations themselves do *not* draw or fill the path. # This must be done by one of the stroke or fill operators, #stroke, # #close_stroke, #fill, #close_fill, #fill_stroke, or #close_fill_stroke. # # Drawing operations return +self+ (the canvas) so that operations may be # chained. module PDF::Writer::Graphics # Close the current path by appending a straight line segment from the # drawing point to the starting point of the path. If the path is # closed, this does nothing. This operator terminates the current # subpath. def close add_content(" h") self end # Stroke the path. This operation terminates a path object and draws it. def stroke add_content(" S") self end # Close the current path by appending a straight line segment from the # drawing point to the starting point of the path, and then stroke it. # This does the same as #close followed by #stroke. def close_stroke add_content(" s") self end # Fills the path. Open subpaths are implicitly closed before being # filled. PDF offers two methods for determining the fill region. The # first is called the "nonzero winding number" and is the default fill. # The second is called "even-odd". # # Use the even-odd rule (called with #fill(:even_odd)) with # caution, as this will cause certain portions of the path to be # considered outside of the fill region, resulting in interesting cutout # patterns. def fill(rule = nil) if :even_odd == rule add_content(" f*") else add_content(" f") end self end # Close the current path by appending a straight line segment from the # drawing point to the starting point of the path, and then fill it. # This does the same as #close followed by #fill. # # See #fill for more information on fill rules. def close_fill(rule = nil) close fill(rule) self end # Fills and then strokes the path. Open subpaths are implicitly closed # before being filled. This is the same as constructing two identical # path objects, calling #fill on one and #stroke on the other. Paths # filled and stroked in this manner are treated as if they were one # object for PDF transparency purposes (the PDF transparency model is # not yet supported by PDF::Writer). # # See #fill for more information on fill rules. def fill_stroke(rule = nil) if :even_odd == rule add_content(" B*") else add_content(" B") end self end # Closes, fills and then strokes the path. Open subpaths are explicitly # closed before being filled (as if #close and then #fill_stroke had # been called). This is the same as constructing two identical path # objects, calling #fill on one and #stroke on the other. Paths filled # and stroked in this manner are treated as if they were one object for # PDF transparency purposes (PDF transparency is not yet supported by # PDF::Writer). # # See #fill for more information on fill rules. def close_fill_stroke(rule = nil) if :even_odd == rule add_content(" b*") else add_content(" b") end self end # Move the drawing point to the specified coordinates (x, y). # # New Point:: (x, y) # Subpath:: New def move_to(x, y) add_content("\n%.3f %.3f m" % [ x, y ]) self end # Draw a straight line from the drawing point to (x, y). # # New Point:: (x, y) # Subpath:: Current def line_to(x, y) add_content("\n%.3f %.3f l" % [ x, y ]) self end # Draws a cubic Bezier curve from the drawing point to (x2, y2) # using (x0, y0) and (x1, y1) as the control points # for the curve. # # New Point:: (x2, y2) # Subpath:: Current def curve_to(x0, y0, x1, y1, x2, y2) add_content("\n%.3f %.3f %.3f %.3f %.3f %.3f c" % [ x0, y0, x1, y1, x2, y2 ]) self end # Draws a cubic Bezier curve from the drawing point to (x1, y1) # using the drawing point and (x0, y0) as the control points # for the curve. # # New Point:: (x1, y1) # Subpath:: Current def scurve_to(x0, y0, x1, y1) add_content("\n%.3f %.3f %.3f %.3f v" % [ x0, y0, x1, y1 ]) self end # Draws a cubic Bezier curve from the drawing point to (x1, y1) # using (x0, y0) and (x1, y1) as the control points # for the curve. # # New Point:: (x1, y1) # Subpath:: Current def ecurve_to(x0, y0, x1, y1) add_content("\n%.3f %.3f %.3f %.3f y" % [ x0, y0, x1, y1 ]) self end # Draw a straight line from (x0, y0) to (x1, y1). The # line is a new subpath. # # New Point:: (x1, y1). # Subpath:: New def line(x0, y0, x1, y1) move_to(x0, y0).line_to(x1, y1) end # Draw a cubic Bezier curve from (x0, y0) to (x3, y3) # using (x1, y1) and (x2, y2) as control points. # # New Point:: (x3, y3) # Subpath:: New def curve(x0, y0, x1, y1, x2, y2, x3, y3) move_to(x0, y0).curve_to(x1, y1, x2, y2, x3, y3) end # Draw a cubic Bezier curve from (x0, y0) to (x2, y2) # using (x0, y0) and (x1, y1) as control points. # # New Point:: (x2, y2) # Subpath:: New def scurve(x0, y0, x1, y1, x2, y2) move_to(x0, y0).scurve_to(x1, y1, x2, y2) end # Draw a cubic Bezier curve from (x0, y0) to (x2, y2) # using (x1, y1) and (x2, y2) as control points. # # New Point:: (x2, y2) # Subpath:: New def ecurve(x0, y0, x1, y1, x2, y2) move_to(x0, y0).ecurve_to(x1, y1, x2, y2) end # This constant is used to approximate a symmetrical arc using a cubic # Bezier curve. KAPPA = 4.0 * ((Math.sqrt(2) - 1.0) / 3.0) # Draws a circle of radius +r+ with the centre-point at (x, y) # as a complete subpath. The drawing point will be moved to the # centre-point upon completion of the drawing the circle. def circle_at(x, y, r) ellipse_at(x, y, r, r) end # Draws an ellipse of +x+ radius r1 and +y+ radius r2 # with the centre-point at (x, y) as a complete subpath. The # drawing point will be moved to the centre-point upon completion of the # drawing the ellipse. def ellipse_at(x, y, r1, r2 = r1) l1 = r1 * KAPPA l2 = r2 * KAPPA move_to(x + r1, y) # Upper right hand corner curve_to(x + r1, y + l1, x + l2, y + r2, x, y + r2) # Upper left hand corner curve_to(x - l2, y + r2, x - r1, y + l1, x - r1, y) # Lower left hand corner curve_to(x - r1, y - l1, x - l2, y - r2, x, y - r2) # Lower right hand corner curve_to(x + l2, y - r2, x + r1, y - l1, x + r1, y) move_to(x, y) end # Draw an ellipse centered at (x, y) with +x+ radius # r1 and +y+ radius r2. A partial ellipse can be drawn # by specifying the starting and finishing angles. # # New Point:: (x, y) # Subpath:: New def ellipse2_at(x, y, r1, r2 = r1, start = 0, stop = 359.99, segments = 8) segments = 2 if segments < 2 start = PDF::Math.deg2rad(start) stop = PDF::Math.deg2rad(stop) arc = stop - start segarc = arc / segments.to_f dtm = segarc / 3.0 theta = start a0 = x + r1 * Math.cos(theta) b0 = y + r2 * Math.sin(theta) c0 = -r1 * Math.sin(theta) d0 = r2 * Math.cos(theta) move_to(a0, b0) (1..segments).each do |ii| theta = ii * segarc + start a1 = x + r1 * Math.cos(theta) b1 = y + r2 * Math.sin(theta) c1 = -r1 * Math.sin(theta) d1 = r2 * Math.cos(theta) curve_to(a0 + (c0 * dtm), b0 + (d0 * dtm), a1 - (c1 * dtm), b1 - (d1 * dtm), a1, b1) a0 = a1 b0 = b1 c0 = c1 d0 = d1 end move_to(x, y) self end # Draws an ellipse segment. Draws a closed partial ellipse. # # New Point:: (x, y) # Subpath:: New def segment_at(x, y, r1, r2 = r1, start = 0, stop = 360, segments = 8) ellipse2_at(x, y, r1, r2, start, stop, segments) start = PDF::Math.deg2rad(start) stop = PDF::Math.deg2rad(stop) ax = x + r1 * Math.cos(start) ay = y + r2 * Math.sin(start) bx = x + r1 * Math.cos(stop) by = y + r2 * Math.sin(stop) move_to(ax, ay) line_to(x, y) line_to(bx, by) move_to(x, y) self end # Draw a polygon. +points+ is an array of PolygonPoint objects, or an # array that can be converted to an array of PolygonPoint objects with # PDF::Writer::PolygonPoint.new(*value). # # New Point:: (points[-1].x, points[-1].y) # Subpath:: New def polygon(points) points = points.map { |pp| pp.kind_of?(Array) ? PDF::Writer::PolygonPoint.new(*pp) : pp } point = points.shift move_to(point.x, point.y) while not points.empty? point = points.shift case point.connector when :curve c1 = point c2 = points.shift point = points.shift curve_to(c1.x, c1.y, c2.x, c2.y, point.x, point.y) when :scurve c1 = point point = points.shift scurve_to(c1.x, c1.y, point.x, point.y) when :ecurve c1 = point point = points.shift ecurve_to(c1.x, c1.y, point.x, point.y) else line_to(point.x, point.y) end end self end # Draw a rectangle. The first corner is (x, y) and the second # corner is (x + w, y - h). # # New Point:: (x + w, y - h) # Subpath:: Current def rectangle(x, y, w, h = w) add_content("\n%.3f %.3f %.3f %.3f re" % [ x, y, w, h ]) self end # Draw a rounded rectangle with corners (x, y) and (x + w, # y - h) and corner radius +r+. The radius should be significantly # smaller than +h+ and +w+. # # New Point:: (x + w, y - h) # Subpath:: New def rounded_rectangle(x, y, w, h, r) x1 = x x2 = x1 + w y1 = y y2 = y1 - h r1 = r r2 = r / 2.0 points = [ [ x1 + r1, y1, :line ], [ x2 - r1, y1, :line ], [ x2 - r2, y1, :curve ], # cp1 [ x2, y1 - r2, ], # cp2 [ x2, y1 - r1, ], # ep [ x2, y2 + r1, :line ], [ x2, y2 + r2, :curve ], # cp1 [ x2 - r2, y2, ], # cp2 [ x2 - r1, y2, ], # ep [ x1 + r1, y2, :line ], [ x1 + r2, y2, :curve ], # cp1 [ x1, y2 + r2, ], # cp2 [ x1, y2 + r1, ], # ep [ x1, y1 - r1, :line ], [ x1, y1 - r2, :curve ], # cp1 [ x1 + r2, y1, ], # cp2 [ x1 + r1, y1, ], # ep ] polygon(points) move_to(x2, y2) self end # Draws a star centered on (x, y) with +rays+ portions of # +length+ from the centre. Stars with an odd number of rays should have # the top ray pointing toward the top of the document. This will not # create a "star" with fewer than four points. # # New Point:: (cx, cy) # Subpath:: New def star(cx, cy, length, rays = 5) rays = 4 if rays < 4 points = [] part = Math::PI / rays.to_f 0.step((rays * 4), 2) do |ray| if ((ray / 2) % 2 == 0) dist = length / 2.0 else dist = length end x = cx + Math.cos((1.5 + ray / 2.0) * part) * dist y = cy + Math.sin((1.5 + ray / 2.0) * part) * dist points << [ x, y ] end polygon(points) move_to(cx, cy) self end # This sets the line drawing style. This *must* be a # PDF::Writer::StrokeStyle object. def stroke_style(style) stroke_style!(style) if @current_stroke_style.nil? or style != @current_stroke_style end # Forces the line drawing style to be set, even if it's the same as the # current color. Emits the current stroke style if +nil+ is provided. def stroke_style!(style = nil) @current_stroke_style = style if style add_content "\n#{@current_stroke_style.render}" if @current_stroke_style end # Returns the current stroke style. def stroke_style? @current_stroke_style end # Set the text rendering style. This may be one of the following # options: # # 0:: fill # 1:: stroke # 2:: fill then stroke # 3:: invisible # 4:: fill and add to clipping path # 5:: stroke and add to clipping path # 6:: fill and stroke and add to clipping path # 7:: add to clipping path def text_render_style(style) text_render_style!(style) unless @current_text_render_style and style == @current_text_render_style end # Forces the text rendering style to be set, even if it's the same as # the current style. def text_render_style!(style) @current_text_render_style = style end # Reutnrs the current text rendering style. def text_render_style? @current_text_render_style end # Sets the color for fill operations. def fill_color(color) fill_color!(color) if @current_fill_color.nil? or color != @current_fill_color end # Forces the color for fill operations to be set, even if the color # is the same as the current color. Does nothing if +nil+ is provided. def fill_color!(color = nil) if color @current_fill_color = color add_content "\n#{@current_fill_color.pdf_fill}" end end # Returns the current fill color. def fill_color? @current_fill_color end # Sets the color for stroke operations. def stroke_color(color) stroke_color!(color) if @current_stroke_color.nil? or color != @current_stroke_color end # Forces the color for stroke operations to be set, even if the color # is the same as the current color. Does nothing if +nil+ is provided. def stroke_color!(color = nil) if color @current_stroke_color = color add_content "\n#{@current_stroke_color.pdf_stroke}" end end # Returns the current stroke color. def stroke_color? @current_stroke_color end # Add an image from a file to the current page at position (x, # y) (the lower left-hand corner of the image). The image will be # scaled to +width+ by +height+ units. The image may be a PNG or JPEG # image. # # The +image+ parameter may be a filename or an object that returns the # full image data when #read is called with no parameters (such as an IO # object). If 'open-uri' is loaded, then the image name may be an URI. # # In PDF::Writer 1.1 or later, the new +link+ parameter is a hash with # two keys: # # :type:: The type of link, either :internal or # :external. # :target:: The destination of the link. For an # :internal link, this is an internal # cross-reference destination. For an # :external link, this is an URI. # # This will automatically make the image a clickable link if set. def add_image_from_file(image, x, y, width = nil, height = nil, link = nil) data = nil if image.respond_to?(:read) data = image.read else open(image, 'rb') { |ff| data = ff.read } end add_image(data, x, y, width, height, nil, link) end # Add an image from a loaded image (JPEG or PNG) resource at position # (x, y) (the lower left-hand corner of the image) and scaled # to +width+ by +height+ units. If provided, +image_info+ is a # PDF::Writer::Graphics::ImageInfo object. # # In PDF::Writer 1.1 or later, the new +link+ parameter is a hash with # two keys: # # :type:: The type of link, either :internal or # :external. # :target:: The destination of the link. For an # :internal link, this is an internal # cross-reference destination. For an # :external link, this is an URI. # # This will automatically make the image a clickable link if set. def add_image(image, x, y, width = nil, height = nil, image_info = nil, link = nil) if image.kind_of?(PDF::Writer::External::Image) label = image.label image_obj = image image_info ||= image.image_info else image_info ||= PDF::Writer::Graphics::ImageInfo.new(image) tt = Time.now @images << tt id = @images.index(tt) label = "I#{id}" image_obj = PDF::Writer::External::Image.new(self, image, image_info, label) @images[id] = image_obj end if width.nil? and height.nil? width = image_info.width height = image_info.height end width ||= height / image_info.height.to_f * image_info.width height ||= width * image_info.height / image_info.width.to_f tt = "\nq\n%.3f 0 0 %.3f %.3f %.3f cm\n/%s Do\nQ" add_content(tt % [ width, height, x, y, label ]) if link case link[:type] when :internal add_internal_link(link[:target], x, y, x + width, y + height) when :external add_link(link[:target], x, y, x + width, y + height) end end image_obj end # Add an image easily to a PDF document. +image+ is the name of a JPG or # PNG image. +options+ is a Hash: # # :pad:: The number of PDF userspace units that will # be on all sides of the image. The default is # 5 units. # :width:: The desired width of the image. The image # will be resized to this width with the # aspect ratio kept. If unspecified, the # image's natural width will be used. # :resize:: How to resize the image, either :width # (resizes the image to be as wide as the # margins) or :full (resizes the image to be # as large as possible). May be a numeric # value, used as a multiplier for the image # size (e.g., 0.5 will shrink the image to # half-sized). If this and :width are # unspecified, the image's natural size will be # used. Mutually exclusive with the # :width option. # :justification:: The placement of the image. May be :center, # :right, or :left. Defaults to :left. # :border:: The border options. No default border. If # specified, must be either +true+, which uses # the default border, or a Hash. # :link:: Makes the image a clickable link. # # Image borders are specified as a hash with two options: # # :color:: The colour of the border. Defaults to 50% grey. # :style:: The stroke style of the border. This must be a # StrokeStyle object and defaults to the default line. # # Image links are defined as a hash with two options: # # :type:: The type of link, either :internal or # :external. # :target:: The destination of the link. For an # :internal link, this is an internal # cross-reference destination. For an # :external link, this is an URI. def image(image, options = {}) width = options[:width] pad = options[:pad] || 5 resize = options[:resize] just = options[:justification] || :left border = options[:border] link = options[:link] if image.kind_of?(PDF::Writer::External::Image) info = image.image_info image_data = image else if image.respond_to?(:read) image_data = image.read else image_data = open(image, "rb") { |file| file.read } end info = PDF::Writer::Graphics::ImageInfo.new(image_data) end raise "Unsupported Image Type" unless %w(JPEG PNG).include?(info.format) width = info.width if width.nil? aspect = info.width.to_f / info.height.to_f # Get the maximum width of the image on insertion. if @columns_on max_width = @columns[:width] - (pad * 2) else max_width = @page_width - (pad * 2) - @left_margin - @right_margin end if resize == :full or resize == :width or width > max_width width = max_width end # Keep the height in an appropriate aspect ratio of the width. height = (width / aspect.to_f) # Resize the image. if resize.kind_of?(Numeric) width *= resize height *= resize end # Resize the image *again*, if it is wider than what is available. if width > max_width height = (width / aspect.to_f) end # If the height is greater than the available space: havail = @y - @bottom_margin - (pad * 2) if height > havail # If the image is to be resized to :full (remaining space # available), adjust the image size appropriately. Otherwise, start # a new page and flow to the next page. if resize == :full height = havail width = (height * aspect) else start_new_page end end # Find the x and y positions. y = @y - pad - height x = @left_margin + pad if (width < max_width) case just when :center x += (max_width - width) / 2.0 when :right x += (max_width - width) end end image_obj = add_image(image_data, x, y, width, height, info) if border border = {} if true == border border[:color] ||= Color::RGB::Grey50 border[:style] ||= PDF::Writer::StrokeStyle::DEFAULT save_state stroke_color border[:color] stroke_style border[:style] rectangle(x, y - pad, width, height - pad).stroke restore_state end if link case link[:type] when :internal add_internal_link(link[:target], x, y - pad, x + width, y + height - pad) when :external add_link(link[:target], x, y - pad, x + width, y + height - pad) end end @y = @y - pad - height image_obj end # Translate the coordinate system axis by the specified user space # coordinates. def translate_axis(x, y) add_content("\n1 0 0 1 %.3f %.3f cm" % [ x, y ]) self end # Rotate the axis of the coordinate system by the specified clockwise # angle. def rotate_axis(angle) rad = PDF::Math.deg2rad(angle) tt = "\n%.3f %.3f %.3f %.3f 0 0 cm" tx = [ Math.cos(rad), Math.sin(rad), -Math.sin(rad), Math.cos(rad) ] add_content(tt % tx) self end # Scale the coordinate system axis by the specified factors. def scale_axis(x = 1, y = 1) add_content("\n%.3f 0 0 %.3f 0 0 cm" % [ x, y ]) self end # Skew the coordinate system axis by the specified angles. def skew_axis(xangle = 0, yangle = 0) xr = PDF::Math.deg2rad(xangle) yr = PDF::Math.deg2rad(yangle) xr = Math.tan(xr) if xangle != 0 yr = Math.tan(yr) if yangle != 0 add_content("\n1 %.3f %.3f 1 0 0 cm" % [ xr, yr ]) self end # Transforms the coordinate axis with the appended matrix. All # transformations (including those above) are performed with this # matrix. The transformation matrix is: # # +- -+ # | a c e | # | b d f | # | 0 0 1 | # +- -+ # # The six values are represented as a six-digit vector: [ a b c d e f ] # # * Axis translation uses [ 1 0 0 1 x y ] where x and y are the new # (0,0) coordinates in the old axis system. # * Scaling uses [ sx 0 0 sy 0 0 ] where sx and sy are the scaling # factors. # * Rotation uses [ cos(a) sin(a) -sin(a) cos(a) 0 0 ] where a is the # angle, measured in radians. # * X axis skewing uses [ 1 0 tan(a) 1 0 0 ] where a is the angle, # measured in radians. # * Y axis skewing uses [ 1 tan(a) 0 1 0 0 ] where a is the angle, # measured in radians. def transform_matrix(a, b, c, d, e, f) add_content("\n%.3f %.3f %.3f %.3f %.3f %.3f cm" % [ a, b, c, d, e, f ]) end end