# encoding: utf-8 # graphics.rb : Implements PDF drawing primitives # # Copyright April 2008, Gregory Brown. All Rights Reserved. # # This is free software. Please see the LICENSE and COPYING files for details. require "prawn/graphics/color" require "prawn/graphics/dash" require "prawn/graphics/cap_style" require "prawn/graphics/join_style" require "prawn/graphics/transparency" require "prawn/graphics/transformation" require "prawn/graphics/patterns" module Prawn # Implements the drawing facilities for Prawn::Document. # Use this to draw the most beautiful imaginable things. # # This file lifts and modifies several of PDF::Writer's graphics functions # ruby-pdf.rubyforge.org # module Graphics include Color include Dash include CapStyle include JoinStyle include Transparency include Transformation include Patterns ####################################################################### # Low level drawing operations must map the point to absolute coords! # ####################################################################### # Moves the drawing position to a given point. The point can be # specified as a tuple or a flattened argument list # # pdf.move_to [100,50] # pdf.move_to(100,50) # def move_to(*point) x,y = map_to_absolute(point) add_content("%.3f %.3f m" % [ x, y ]) end # Draws a line from the current drawing position to the specified point. # The destination may be described as a tuple or a flattened list: # # pdf.line_to [50,50] # pdf.line_to(50,50) # def line_to(*point) x,y = map_to_absolute(point) add_content("%.3f %.3f l" % [ x, y ]) end # Draws a Bezier curve from the current drawing position to the # specified point, bounded by two additional points. # # pdf.curve_to [100,100], :bounds => [[90,90],[75,75]] # def curve_to(dest,options={}) options[:bounds] or raise Prawn::Errors::InvalidGraphicsPath, "Bounding points for bezier curve must be specified "+ "as :bounds => [[x1,y1],[x2,y2]]" curve_points = (options[:bounds] << dest).map { |e| map_to_absolute(e) } add_content("%.3f %.3f %.3f %.3f %.3f %.3f c" % curve_points.flatten ) end # Draws a rectangle given point, width and # height. The rectangle is bounded by its upper-left corner. # # pdf.rectangle [300,300], 100, 200 # def rectangle(point,width,height) x,y = map_to_absolute(point) add_content("%.3f %.3f %.3f %.3f re" % [ x, y - height, width, height ]) end # Draws a rounded rectangle given point, width and # height and radius for the rounded corner. The rectangle # is bounded by its upper-left corner. # # pdf.rounded_rectangle [300,300], 100, 200, 10 # def rounded_rectangle(point,width,height,radius) x, y = point rounded_polygon(radius, point, [x + width, y], [x + width, y - height], [x, y - height]) end ########################################################### # Higher level functions: May use relative coords # ########################################################### # Sets line thickness to the width specified. # def line_width=(width) self.current_line_width = width write_line_width end # When called without an argument, returns the current line thickness. # When called with an argument, sets the line thickness to the specified # value (in PDF points) # # pdf.line_width #=> 1 # pdf.line_width(5) # pdf.line_width #=> 5 # def line_width(width=nil) if width self.line_width = width else current_line_width end end # Draws a line from one point to another. Points may be specified as # tuples or flattened argument list: # # pdf.line [100,100], [200,250] # pdf.line(100,100,200,250) # def line(*points) x0,y0,x1,y1 = points.flatten move_to(x0, y0) line_to(x1, y1) end # Draws a horizontal line from x1 to x2 at the # current y position, or the position specified by the :at option. # # # draw a line from [25, 75] to [100, 75] # horizontal_line 25, 100, :at => 75 # def horizontal_line(x1,x2,options={}) if options[:at] y1 = options[:at] else y1 = y - bounds.absolute_bottom end line(x1,y1,x2,y1) end # Draws a horizontal line from the left border to the right border of the # bounding box at the current y position. # def horizontal_rule horizontal_line(bounds.left, bounds.right) end # Draws a vertical line at the x cooordinate given by :at from y1 to y2. # # # draw a line from [25, 100] to [25, 300] # vertical_line 100, 300, :at => 25 # def vertical_line(y1,y2,params) line(params[:at],y1,params[:at],y2) end # Draws a Bezier curve between two points, bounded by two additional # points # # pdf.curve [50,100], [100,100], :bounds => [[90,90],[75,75]] # def curve(origin,dest, options={}) move_to(*origin) curve_to(dest,options) 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) # DEPRECATED: Please use circle instead. def circle_at(point, options) warn "[DEPRECATION] 'circle_at' is deprecated in favor of 'circle'. " + "'circle_at' will be removed in release 1.1" circle(point, options[:radius]) end # Draws a circle of radius radius with the centre-point at point # as a complete subpath. The drawing point will be moved to the # centre-point upon completion of the drawing the circle. # # pdf.circle [100,100], 25 # def circle(center, radius) ellipse(center, radius, radius) end # DEPRECATED: Please use ellipse instead. def ellipse_at(point, r1, r2=r1) warn "[DEPRECATION] 'ellipse_at' is deprecated in favor of 'ellipse'. " + "'ellipse_at' will be removed in release 1.1" ellipse(point, r1, r2) end # Draws an ellipse of +x+ radius r1 and +y+ radius r2 # with the centre-point at point as a complete subpath. The # drawing point will be moved to the centre-point upon completion of the # drawing the ellipse. # # # draws an ellipse with x-radius 25 and y-radius 50 # pdf.ellipse [100,100], 25, 50 # def ellipse(point, r1, r2 = r1) x, y = point l1 = r1 * KAPPA l2 = r2 * KAPPA move_to(x + r1, y) # Upper right hand corner curve_to [x, y + r2], :bounds => [[x + r1, y + l2], [x + l1, y + r2]] # Upper left hand corner curve_to [x - r1, y], :bounds => [[x - l1, y + r2], [x - r1, y + l2]] # Lower left hand corner curve_to [x, y - r2], :bounds => [[x - r1, y - l2], [x - l1, y - r2]] # Lower right hand corner curve_to [x + r1, y], :bounds => [[x + l1, y - r2], [x + r1, y - l2]] move_to(x, y) end # Draws a polygon from the specified points. # # # draws a snazzy triangle # pdf.polygon [100,100], [100,200], [200,200] # def polygon(*points) move_to points[0] (points[1..-1] << points[0]).each do |point| line_to(*point) end # close the path add_content "h" end # Draws a rounded polygon from specified points using the radius to define bezier curves # # # draws a rounded filled in polygon # pdf.fill_and_stroke_rounded_polygon(10, [100, 250], [200, 300], [300, 250], # [300, 150], [200, 100], [100, 150]) def rounded_polygon(radius, *points) move_to point_on_line(radius, points[1], points[0]) sides = points.size points << points[0] << points[1] (sides).times do |i| rounded_vertex(radius, points[i], points[i + 1], points[i + 2]) end # close the path add_content "h" end # Creates a rounded vertex for a line segment used for building a rounded polygon # requires a radius to define bezier curve and three points. The first two points define # the line segment and the third point helps define the curve for the vertex. def rounded_vertex(radius, *points) radial_point_1 = point_on_line(radius, points[0], points[1]) bezier_point_1 = point_on_line((radius - radius*KAPPA), points[0], points[1] ) radial_point_2 = point_on_line(radius, points[2], points[1]) bezier_point_2 = point_on_line((radius - radius*KAPPA), points[2], points[1]) line_to(radial_point_1) curve_to(radial_point_2, :bounds => [bezier_point_1, bezier_point_2]) end # Strokes the current path. If a block is provided, yields to the block # before closing the path. See Graphics::Color for color details. # def stroke yield if block_given? add_content "S" end # Closes and strokes the current path. If a block is provided, yields to # the block before closing the path. See Graphics::Color for color details. # def close_and_stroke yield if block_given? add_content "s" end # Draws and strokes a rectangle represented by the current bounding box # def stroke_bounds stroke_rectangle bounds.top_left, bounds.width, bounds.height end # Draws and strokes X and Y axes rulers beginning at the current bounding # box origin (or at a custom location). # # == Options # # +:at+:: # Origin of the X and Y axes (default: [0, 0] = origin of the bounding # box) # # +:width+:: # Length of the X axis (default: width of the bounding box) # # +:height+:: # Length of the Y axis (default: height of the bounding box) # # +:step_length+:: # Length of the step between markers (default: 100) # # +:negative_axes_length+:: # Length of the negative parts of the axes (default: 20) # # +:color+: # The color of the axes and the text. # def stroke_axis(options = {}) options = { :at => [0,0], :height => bounds.height.to_i - (options[:at] || [0,0])[1], :width => bounds.width.to_i - (options[:at] || [0,0])[0], :step_length => 100, :negative_axes_length => 20, :color => "000000", }.merge(options) Prawn.verify_options([:at, :width, :height, :step_length, :negative_axes_length, :color], options) save_graphics_state do fill_color(options[:color]) stroke_color(options[:color]) dash(1, :space => 4) stroke_horizontal_line(options[:at][0] - options[:negative_axes_length], options[:at][0] + options[:width], :at => options[:at][1]) stroke_vertical_line(options[:at][1] - options[:negative_axes_length], options[:at][1] + options[:height], :at => options[:at][0]) undash fill_circle(options[:at], 1) (options[:step_length]..options[:width]).step(options[:step_length]) do |point| fill_circle([options[:at][0] + point, options[:at][1]], 1) draw_text(point, :at => [options[:at][0] + point - 5, options[:at][1] - 10], :size => 7) end (options[:step_length]..options[:height]).step(options[:step_length]) do |point| fill_circle([options[:at][0], options[:at][1] + point], 1) draw_text(point, :at => [options[:at][0] - 17, options[:at][1] + point - 2], :size => 7) end end end # Closes and fills the current path. See Graphics::Color for color details. # # If the option :fill_rule => :even_odd is specified, Prawn will use the # even-odd rule to fill the path. Otherwise, the nonzero winding number rule # will be used. See the PDF reference, "Graphics -> Path Construction and # Painting -> Clipping Path Operators" for details on the difference. # def fill(options={}) yield if block_given? add_content(options[:fill_rule] == :even_odd ? "f*" : "f") end # Closes, fills, and strokes the current path. If a block is provided, # yields to the block before closing the path. See Graphics::Color for # color details. # # If the option :fill_rule => :even_odd is specified, Prawn will use the # even-odd rule to fill the path. Otherwise, the nonzero winding number rule # will be used. See the PDF reference, "Graphics -> Path Construction and # Painting -> Clipping Path Operators" for details on the difference. # def fill_and_stroke(options={}) yield if block_given? add_content(options[:fill_rule] == :even_odd ? "b*" : "b") end # Closes the current path. # def close_path add_content "h" end ## # :method: stroke_rectangle # # Draws and strokes a rectangle given +point+, +width+ and +height+. The # rectangle is bounded by its upper-left corner. # # :call-seq: # stroke_rectangle(point,width,height) ## # :method: fill_rectangle # # Draws and fills ills a rectangle given +point+, +width+ and +height+. The # rectangle is bounded by its upper-left corner. # # :call-seq: # fill_rectangle(point,width,height) ## # :method: fill_and_stroke_rectangle # # Draws, fills, and strokes a rectangle given +point+, +width+ and +height+. # The rectangle is bounded by its upper-left corner. # # :call-seq: # fill_and_stroke_rectangle(point,width,height) ## # :method: stroke_rounded_rectangle # # Draws and strokes a rounded rectangle given +point+, +width+ and +height+ # and +radius+ for the rounded corner. The rectangle is bounded by its # upper-left corner. # # :call-seq: # stroke_rounded_rectangle(point,width,height,radius) ## # :method: fill_rounded_rectangle # # Draws and fills a rounded rectangle given +point+, +width+ and +height+ # and +radius+ for the rounded corner. The rectangle is bounded by its # upper-left corner. # # :call-seq: # fill_rounded_rectangle(point,width,height,radius) ## # :method: stroke_and_fill_rounded_rectangle # # Draws, fills, and strokes a rounded rectangle given +point+, +width+ and # +height+ and +radius+ for the rounded corner. The rectangle is bounded by # its upper-left corner. # # :call-seq: # stroke_and_fill_rounded_rectangle(point,width,height,radius) ## # :method: stroke_line # # Strokes a line from one point to another. Points may be specified as # tuples or flattened argument list. # # :call-seq: # stroke_line(*points) ## # :method: stroke_horizontal_line # # Strokes a horizontal line from +x1+ to +x2+ at the current y position, or # the position specified by the :at option. # # :call-seq: # stroke_horizontal_line(x1,x2,options={}) ## # :method: stroke_horizontal_rule # # Strokes a horizontal line from the left border to the right border of the # bounding box at the current y position. # # :call-seq: # stroke_horizontal_rule ## # :method: stroke_vertical_line # # Strokes a vertical line at the x coordinate given by :at from y1 to y2. # # :call-seq: # stroke_vertical_line(y1,y2,params) ## # :method: stroke_curve # # Strokes a Bezier curve between two points, bounded by two additional # points. # # :call-seq: # stroke_curve(origin,dest,options={}) ## # :method: stroke_circle # # Draws and strokes a circle of radius +radius+ with the centre-point at # +point+. # # :call-seq: # stroke_circle(center,radius) ## # :method: fill_circle # # Draws and fills a circle of radius +radius+ with the centre-point at # +point+. # # :call-seq: # fill_circle(center,radius) ## # :method: fill_and_stroke_circle # # Draws, strokes, and fills a circle of radius +radius+ with the # centre-point at +point+. # # :call-seq: # fill_and_stroke_circle(center,radius) ## # :method: stroke_ellipse # # Draws and strokes an ellipse of x radius +r1+ and y radius +r2+ with the # centre-point at +point+. # # :call-seq: # stroke_ellipse(point, r1, r2 = r1) ## # :method: fill_ellipse # # Draws and fills an ellipse of x radius +r1+ and y radius +r2+ with the # centre-point at +point+. # # :call-seq: # fill_ellipse(point, r1, r2 = r1) ## # :method: fill_and_stroke_ellipse # # Draws, strokes, and fills an ellipse of x radius +r1+ and y radius +r2+ # with the centre-point at +point+. # # :call-seq: # fill_and_stroke_ellipse(point, r1, r2 = r1) ## # :method: stroke_polygon # # Draws and strokes a polygon from the specified points. # # :call-seq: # stroke_polygon(*points) ## # :method: fill_polygon # # Draws and fills a polygon from the specified points. # # :call-seq: # fill_polygon(*points) ## # :method: fill_and_stroke_polygon # # Draws, strokes, and fills a polygon from the specified points. # # :call-seq: # fill_and_stroke_polygon(*points) ## # :method: stroke_rounded_polygon # # Draws and strokes a rounded polygon from specified points, using +radius+ # to define Bezier curves. # # :call-seq: # stroke_rounded_polygon(radius, *points) ## # :method: fill_rounded_polygon # # Draws and fills a rounded polygon from specified points, using +radius+ to # define Bezier curves. # # :call-seq: # fill_rounded_polygon(radius, *points) ## # :method: fill_and_stroke_rounded_polygon # # Draws, strokes, and fills a rounded polygon from specified points, using # +radius+ to define Bezier curves. # # :call-seq: # fill_and_stroke_rounded_polygon(radius, *points) ops = %w{fill stroke fill_and_stroke} shapes = %w{line_to curve_to rectangle rounded_rectangle line horizontal_line horizontal_rule vertical_line curve circle_at circle ellipse_at ellipse polygon rounded_polygon rounded_vertex} ops.product(shapes).each do |operation,shape| class_eval "def #{operation}_#{shape}(*args); #{shape}(*args); #{operation}; end" end private def current_line_width graphic_state.line_width end def current_line_width=(width) graphic_state.line_width = width end def write_line_width add_content("#{current_line_width} w") end def map_to_absolute(*point) x,y = point.flatten [@bounding_box.absolute_left + x, @bounding_box.absolute_bottom + y] end def map_to_absolute!(point) point.replace(map_to_absolute(point)) end def degree_to_rad(angle) angle * Math::PI / 180 end # Returns the coordinates for a point on a line that is a given distance away from the second # point defining the line segement def point_on_line(distance_from_end, *points) x0,y0,x1,y1 = points.flatten length = Math.sqrt((x1 - x0)**2 + (y1 - y0)**2) p = (length - distance_from_end) / length xr = x0 + p*(x1 - x0) yr = y0 + p*(y1 - y0) [xr, yr] end end end