prawn-svg 0.9.1
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/LICENSE +21 -0
- data/README +17 -0
- data/lib/prawn-svg.rb +3 -0
- data/lib/prawn/svg/svg.rb +320 -0
- data/lib/prawn/svg/svg_path.rb +167 -0
- data/lib/prawn/svg_document.rb +7 -0
- metadata +67 -0
    
        data/LICENSE
    ADDED
    
    | @@ -0,0 +1,21 @@ | |
| 1 | 
            +
            The MIT License
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            Copyright 2010 Roger Nesbitt
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            Permission is hereby granted, free of charge, to any person obtaining a copy
         | 
| 6 | 
            +
            of this software and associated documentation files (the "Software"), to deal
         | 
| 7 | 
            +
            in the Software without restriction, including without limitation the rights
         | 
| 8 | 
            +
            to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         | 
| 9 | 
            +
            copies of the Software, and to permit persons to whom the Software is
         | 
| 10 | 
            +
            furnished to do so, subject to the following conditions:
         | 
| 11 | 
            +
             | 
| 12 | 
            +
            The above copyright notice and this permission notice shall be included in
         | 
| 13 | 
            +
            all copies or substantial portions of the Software.
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         | 
| 16 | 
            +
            IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         | 
| 17 | 
            +
            FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         | 
| 18 | 
            +
            AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         | 
| 19 | 
            +
            LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         | 
| 20 | 
            +
            OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
         | 
| 21 | 
            +
            THE SOFTWARE.
         | 
    
        data/README
    ADDED
    
    | @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            The very start of an SVG renderer for Prawn.
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            This will take an SVG file as input and render it into your PDF.  Find out more about the Prawn PDF library at:
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              http://wiki.github.com/sandal/prawn/
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            Using prawn-svg:
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              Prawn::Document.generate("svg.pdf") do
         | 
| 10 | 
            +
                svg svg_data, :at => [x, y], :width => w
         | 
| 11 | 
            +
              end
         | 
| 12 | 
            +
              
         | 
| 13 | 
            +
            :at must be specified.  :width, :height, or neither may be specified; if neither is present,
         | 
| 14 | 
            +
            the resolution specified in the SVG will be used.
         | 
| 15 | 
            +
             | 
| 16 | 
            +
            Note that only a very small subset of SVG is currently supported.  It's just enough so that
         | 
| 17 | 
            +
            it renders a simple graph made by Scruffy.
         | 
    
        data/lib/prawn-svg.rb
    ADDED
    
    
| @@ -0,0 +1,320 @@ | |
| 1 | 
            +
            require 'rexml/document'
         | 
| 2 | 
            +
            require 'prawn'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            class Prawn::Svg
         | 
| 5 | 
            +
              include Prawn::Measurements
         | 
| 6 | 
            +
              
         | 
| 7 | 
            +
              attr_reader :data, :prawn, :options
         | 
| 8 | 
            +
              attr_accessor :scale
         | 
| 9 | 
            +
             | 
| 10 | 
            +
              def initialize(data, prawn, options)
         | 
| 11 | 
            +
                @data = data
         | 
| 12 | 
            +
                @prawn = prawn
         | 
| 13 | 
            +
                @options = options
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              def draw
         | 
| 17 | 
            +
                root = parse_document
         | 
| 18 | 
            +
                calculate_dimensions
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                prawn.bounding_box(@options[:at], :width => @width, :height => @height) do
         | 
| 21 | 
            +
                  prawn.save_graphics_state do
         | 
| 22 | 
            +
                    call_tree = generate_call_tree(root)
         | 
| 23 | 
            +
                    proc_creator(prawn, call_tree).call
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
              end
         | 
| 27 | 
            +
              
         | 
| 28 | 
            +
              def generate_call_tree(element)
         | 
| 29 | 
            +
                [].tap {|calls| parse_element(element, calls, {})}
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
              
         | 
| 32 | 
            +
              
         | 
| 33 | 
            +
              protected  
         | 
| 34 | 
            +
              def proc_creator(prawn, calls)
         | 
| 35 | 
            +
                Proc.new {issue_prawn_command(prawn, calls)}
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
              
         | 
| 38 | 
            +
              def issue_prawn_command(prawn, calls)
         | 
| 39 | 
            +
                calls.each do |call, arguments, children|
         | 
| 40 | 
            +
                  if children.empty?
         | 
| 41 | 
            +
                    rewrite_call_arguments(prawn, call, arguments)
         | 
| 42 | 
            +
                    prawn.send(call, *arguments)
         | 
| 43 | 
            +
                  else
         | 
| 44 | 
            +
                    prawn.send(call, *arguments, &proc_creator(prawn, children))
         | 
| 45 | 
            +
                  end
         | 
| 46 | 
            +
                end
         | 
| 47 | 
            +
              end
         | 
| 48 | 
            +
              
         | 
| 49 | 
            +
              def rewrite_call_arguments(prawn, call, arguments)
         | 
| 50 | 
            +
                if call == 'text_box'
         | 
| 51 | 
            +
                  if (anchor = arguments.last.delete(:text_anchor)) && %w(middle end).include?(anchor)
         | 
| 52 | 
            +
                    width = prawn.width_of(*arguments)
         | 
| 53 | 
            +
                    width /= 2 if anchor == 'middle'
         | 
| 54 | 
            +
                    arguments.last[:at][0] -= width
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                  
         | 
| 57 | 
            +
                  arguments.last[:at][1] += prawn.height_of(*arguments) / 3 * 2
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
              def parse_document
         | 
| 62 | 
            +
                REXML::Document.new(@data).root.tap do |root|    
         | 
| 63 | 
            +
                  if vb = root.attributes['viewBox']
         | 
| 64 | 
            +
                    x1, y1, x2, y2 = vb.strip.split(/\s+/)
         | 
| 65 | 
            +
                    @x_offset, @y_offset = [x1.to_f, y1.to_f]
         | 
| 66 | 
            +
                    @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f]
         | 
| 67 | 
            +
                  else
         | 
| 68 | 
            +
                    @x_offset, @y_offset = [0, 0]
         | 
| 69 | 
            +
                    @actual_width = root.attributes['width'].to_f
         | 
| 70 | 
            +
                    @actual_height = root.attributes['height'].to_f
         | 
| 71 | 
            +
                  end
         | 
| 72 | 
            +
                end
         | 
| 73 | 
            +
              end
         | 
| 74 | 
            +
                
         | 
| 75 | 
            +
              def parse_element(element, calls, state)
         | 
| 76 | 
            +
                attrs = element.attributes
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                if transform = attrs['transform']
         | 
| 79 | 
            +
                  parse_css_method_calls(transform).each do |name, arguments|
         | 
| 80 | 
            +
                    case name
         | 
| 81 | 
            +
                    when 'translate'
         | 
| 82 | 
            +
                      x, y = arguments
         | 
| 83 | 
            +
                      x, y = x.split(/\s+/) if y.nil?
         | 
| 84 | 
            +
                      calls << [name, [distance(x), -distance(y)], []]
         | 
| 85 | 
            +
                      calls = calls.last.last
         | 
| 86 | 
            +
                    when 'rotate'          
         | 
| 87 | 
            +
                      calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []]
         | 
| 88 | 
            +
                      calls = calls.last.last
         | 
| 89 | 
            +
                    when 'scale'
         | 
| 90 | 
            +
                      calls << [name, [arguments.first.to_f], []]
         | 
| 91 | 
            +
                      calls = calls.last.last
         | 
| 92 | 
            +
                    else
         | 
| 93 | 
            +
                      #raise "unknown transformation '#{name}'"
         | 
| 94 | 
            +
                    end
         | 
| 95 | 
            +
                  end
         | 
| 96 | 
            +
                end
         | 
| 97 | 
            +
             | 
| 98 | 
            +
                calls, style_attrs, draw_type = apply_styles(attrs, calls, state)    
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                state[:draw_type] = draw_type if draw_type != ""
         | 
| 101 | 
            +
                if state[:draw_type] && !%w(g svg).include?(element.name)
         | 
| 102 | 
            +
                  calls << [state[:draw_type], [], []]
         | 
| 103 | 
            +
                  calls = calls.last.last
         | 
| 104 | 
            +
                end
         | 
| 105 | 
            +
              
         | 
| 106 | 
            +
                case element.name
         | 
| 107 | 
            +
                when 'defs', 'desc'
         | 
| 108 | 
            +
                  # ignore these tags
         | 
| 109 | 
            +
                  
         | 
| 110 | 
            +
                when 'g', 'svg'
         | 
| 111 | 
            +
                  element.elements.each do |child|
         | 
| 112 | 
            +
                    parse_element(child, calls, state.dup)
         | 
| 113 | 
            +
                  end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                when 'text'
         | 
| 116 | 
            +
                  # very primitive support for fonts
         | 
| 117 | 
            +
                  if (font = style_attrs['font-family']) && !font.match(/[\/\\]/)
         | 
| 118 | 
            +
                    font = font.strip
         | 
| 119 | 
            +
                    if font != ""
         | 
| 120 | 
            +
                      calls << ['font', [font], []]
         | 
| 121 | 
            +
                      calls = calls.last.last
         | 
| 122 | 
            +
                    end
         | 
| 123 | 
            +
                  end
         | 
| 124 | 
            +
                  
         | 
| 125 | 
            +
                  opts = {:at => [x(attrs['x']), y(attrs['y'])]}
         | 
| 126 | 
            +
                  if size = style_attrs['font-size']
         | 
| 127 | 
            +
                    opts[:size] = size.to_f * @scale
         | 
| 128 | 
            +
                  end
         | 
| 129 | 
            +
                        
         | 
| 130 | 
            +
                  # This is not a prawn option but we can't work out how to render it here - it's handled by #rewrite_call
         | 
| 131 | 
            +
                  if anchor = style_attrs['text-anchor']
         | 
| 132 | 
            +
                    opts[:text_anchor] = anchor        
         | 
| 133 | 
            +
                  end
         | 
| 134 | 
            +
                  
         | 
| 135 | 
            +
                  calls << ['text_box', [element.text, opts], []]
         | 
| 136 | 
            +
             | 
| 137 | 
            +
                when 'line'
         | 
| 138 | 
            +
                  calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []]
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                when 'polyline'
         | 
| 141 | 
            +
                  points = attrs['points'].split(/\s+/)
         | 
| 142 | 
            +
                  x, y = points.shift.split(",")
         | 
| 143 | 
            +
                  calls << ['move_to', [x(x), y(y)], []]
         | 
| 144 | 
            +
                  calls << ['stroke', [], []]
         | 
| 145 | 
            +
                  calls = calls.last.last
         | 
| 146 | 
            +
                  points.each do |point|
         | 
| 147 | 
            +
                    x, y = point.split(",")
         | 
| 148 | 
            +
                    calls << ["line_to", [x(x), y(y)], []]
         | 
| 149 | 
            +
                  end
         | 
| 150 | 
            +
                
         | 
| 151 | 
            +
                when 'polygon'
         | 
| 152 | 
            +
                  points = attrs['points'].split(/\s+/).collect do |point|
         | 
| 153 | 
            +
                    x, y = point.split(",")
         | 
| 154 | 
            +
                    [x(x), y(y)]
         | 
| 155 | 
            +
                  end
         | 
| 156 | 
            +
                  calls << ["polygon", points, []]      
         | 
| 157 | 
            +
                  
         | 
| 158 | 
            +
                when 'circle'
         | 
| 159 | 
            +
                  calls << ["circle_at", 
         | 
| 160 | 
            +
                    [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}], 
         | 
| 161 | 
            +
                    []]
         | 
| 162 | 
            +
                  
         | 
| 163 | 
            +
                when 'ellipse'
         | 
| 164 | 
            +
                  calls << ["ellipse_at", 
         | 
| 165 | 
            +
                    [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])],
         | 
| 166 | 
            +
                    []]
         | 
| 167 | 
            +
                  
         | 
| 168 | 
            +
                when 'rect'
         | 
| 169 | 
            +
                  radius = distance(attrs['rx'] || attrs['ry'])
         | 
| 170 | 
            +
                  args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])]
         | 
| 171 | 
            +
                  if radius
         | 
| 172 | 
            +
                    # n.b. does not support both rx and ry being specified with different values
         | 
| 173 | 
            +
                    calls << ["rounded_rectangle", args + [radius], []]
         | 
| 174 | 
            +
                  else
         | 
| 175 | 
            +
                    calls << ["rectangle", args, []]
         | 
| 176 | 
            +
                  end
         | 
| 177 | 
            +
                  
         | 
| 178 | 
            +
                when 'path'
         | 
| 179 | 
            +
                  @svg_path ||= Path.new
         | 
| 180 | 
            +
                  @svg_path.parse(attrs['d']).each do |command, args|
         | 
| 181 | 
            +
                    point_to = [x(args[0]), y(args[1])]
         | 
| 182 | 
            +
                    if command == 'curve_to'
         | 
| 183 | 
            +
                      bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]]
         | 
| 184 | 
            +
                      calls << [command, [point_to, {:bounds => bounds}], []]
         | 
| 185 | 
            +
                    else
         | 
| 186 | 
            +
                      calls << [command, point_to, []]
         | 
| 187 | 
            +
                    end
         | 
| 188 | 
            +
                  end
         | 
| 189 | 
            +
             | 
| 190 | 
            +
                else 
         | 
| 191 | 
            +
                  #raise "unknown tag #{element.name}"
         | 
| 192 | 
            +
                end
         | 
| 193 | 
            +
              end
         | 
| 194 | 
            +
              
         | 
| 195 | 
            +
              def parse_css_declarations(declarations)
         | 
| 196 | 
            +
                # copied from css_parser
         | 
| 197 | 
            +
                declarations.gsub!(/(^[\s]*)|([\s]*$)/, '')
         | 
| 198 | 
            +
             | 
| 199 | 
            +
                {}.tap do |o|
         | 
| 200 | 
            +
                  declarations.split(/[\;$]+/m).each do |decs|
         | 
| 201 | 
            +
                    if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i)
         | 
| 202 | 
            +
                      property, value, end_of_declaration = matches.captures
         | 
| 203 | 
            +
                      o[property] = value
         | 
| 204 | 
            +
                    end
         | 
| 205 | 
            +
                  end
         | 
| 206 | 
            +
                end
         | 
| 207 | 
            +
              end
         | 
| 208 | 
            +
              
         | 
| 209 | 
            +
              def apply_styles(attrs, calls, state)
         | 
| 210 | 
            +
                draw_types = []
         | 
| 211 | 
            +
             | 
| 212 | 
            +
                decs = attrs["style"] ? parse_css_declarations(attrs["style"]) : {}
         | 
| 213 | 
            +
                attrs.each {|n,v| decs[n] = v unless decs[n]}
         | 
| 214 | 
            +
                        
         | 
| 215 | 
            +
                # Opacity:
         | 
| 216 | 
            +
                # We can't do nested opacities quite like the SVG requires, but this is close enough.
         | 
| 217 | 
            +
                fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity']
         | 
| 218 | 
            +
                fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity']
         | 
| 219 | 
            +
                stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity']
         | 
| 220 | 
            +
                
         | 
| 221 | 
            +
                if fill_opacity || stroke_opacity      
         | 
| 222 | 
            +
                  state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1)
         | 
| 223 | 
            +
                  state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1)
         | 
| 224 | 
            +
             | 
| 225 | 
            +
                  calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []] 
         | 
| 226 | 
            +
                  calls = calls.last.last
         | 
| 227 | 
            +
                end
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                if decs['fill'] && decs['fill'] != "none"
         | 
| 230 | 
            +
                  if color = color_to_hex(decs['fill'])
         | 
| 231 | 
            +
                    calls << ['fill_color', [color], []]
         | 
| 232 | 
            +
                  end
         | 
| 233 | 
            +
                  draw_types << 'fill'
         | 
| 234 | 
            +
                end
         | 
| 235 | 
            +
                
         | 
| 236 | 
            +
                if decs['stroke'] && decs['stroke'] != "none"
         | 
| 237 | 
            +
                  if color = color_to_hex(decs['stroke'])
         | 
| 238 | 
            +
                    calls << ['stroke_color', [color], []]
         | 
| 239 | 
            +
                  end
         | 
| 240 | 
            +
                  draw_types << 'stroke'
         | 
| 241 | 
            +
                end
         | 
| 242 | 
            +
                
         | 
| 243 | 
            +
                calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width']          
         | 
| 244 | 
            +
                    
         | 
| 245 | 
            +
                [calls, decs, draw_types.join("_and_")]
         | 
| 246 | 
            +
              end
         | 
| 247 | 
            +
              
         | 
| 248 | 
            +
              def parse_css_method_calls(string)
         | 
| 249 | 
            +
                string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call|
         | 
| 250 | 
            +
                  name, argument_string = call
         | 
| 251 | 
            +
                  arguments = argument_string.split(",").collect(&:strip)
         | 
| 252 | 
            +
                  [name, arguments]
         | 
| 253 | 
            +
                end    
         | 
| 254 | 
            +
              end
         | 
| 255 | 
            +
             | 
| 256 | 
            +
              # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords
         | 
| 257 | 
            +
              HTML_COLORS = {    
         | 
| 258 | 
            +
              	'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00",
         | 
| 259 | 
            +
              	'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00",
         | 
| 260 | 
            +
              	'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff",
         | 
| 261 | 
            +
              	'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff"
         | 
| 262 | 
            +
              }.freeze
         | 
| 263 | 
            +
              
         | 
| 264 | 
            +
              def color_to_hex(color_string)
         | 
| 265 | 
            +
                color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_|
         | 
| 266 | 
            +
                  if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i)
         | 
| 267 | 
            +
                    break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}"
         | 
| 268 | 
            +
                  elsif color.match(/\A#[0-9a-f]{6}\z/i)
         | 
| 269 | 
            +
                    break color[1..6]
         | 
| 270 | 
            +
                  elsif hex = HTML_COLORS[color.downcase]
         | 
| 271 | 
            +
                    break hex
         | 
| 272 | 
            +
                  elsif m = color.match(/\Argb\(\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*,\s*(-?[0-9.]+%?)\s*\)\z/i)
         | 
| 273 | 
            +
                    break (1..3).collect do |n|
         | 
| 274 | 
            +
                      value = m[n].to_f
         | 
| 275 | 
            +
                      value *= 2.55 if m[n][-1..-1] == '%'
         | 
| 276 | 
            +
                      "%02x" % clamp(value.round, 0, 255)
         | 
| 277 | 
            +
                    end.join        
         | 
| 278 | 
            +
                  end    
         | 
| 279 | 
            +
                end
         | 
| 280 | 
            +
              end
         | 
| 281 | 
            +
              
         | 
| 282 | 
            +
              def x(value)
         | 
| 283 | 
            +
                (pixels(value) - @x_offset) * scale
         | 
| 284 | 
            +
              end
         | 
| 285 | 
            +
              
         | 
| 286 | 
            +
              def y(value)
         | 
| 287 | 
            +
                (@actual_height - (pixels(value) - @y_offset)) * scale
         | 
| 288 | 
            +
              end
         | 
| 289 | 
            +
              
         | 
| 290 | 
            +
              def distance(value)
         | 
| 291 | 
            +
                value && (pixels(value) * scale)
         | 
| 292 | 
            +
              end
         | 
| 293 | 
            +
              
         | 
| 294 | 
            +
              def pixels(value)
         | 
| 295 | 
            +
                if value.is_a?(String) && match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/)
         | 
| 296 | 
            +
                  send("#{match[1]}2pt", value.to_f)
         | 
| 297 | 
            +
                else
         | 
| 298 | 
            +
                  value.to_f
         | 
| 299 | 
            +
                end
         | 
| 300 | 
            +
              end
         | 
| 301 | 
            +
              
         | 
| 302 | 
            +
              def calculate_dimensions    
         | 
| 303 | 
            +
                if @options[:width]
         | 
| 304 | 
            +
                  @width = @options[:width]      
         | 
| 305 | 
            +
                  @scale = @options[:width] / @actual_width.to_f
         | 
| 306 | 
            +
                elsif @options[:height]
         | 
| 307 | 
            +
                  @height = @options[:height]
         | 
| 308 | 
            +
                  @scale = @options[:height] / @actual_height.to_f
         | 
| 309 | 
            +
                else
         | 
| 310 | 
            +
                  @scale = 1
         | 
| 311 | 
            +
                end
         | 
| 312 | 
            +
                
         | 
| 313 | 
            +
                @width ||= @actual_width * @scale
         | 
| 314 | 
            +
                @height ||= @actual_height * @scale
         | 
| 315 | 
            +
              end
         | 
| 316 | 
            +
              
         | 
| 317 | 
            +
              def clamp(value, min_value, max_value)
         | 
| 318 | 
            +
                [[value, min_value].max, max_value].min
         | 
| 319 | 
            +
              end  
         | 
| 320 | 
            +
            end
         | 
| @@ -0,0 +1,167 @@ | |
| 1 | 
            +
            class Prawn::Svg::Path
         | 
| 2 | 
            +
              def parse(data)
         | 
| 3 | 
            +
                cmd = values = value = nil
         | 
| 4 | 
            +
                @subpath_initial_point = @last_point = nil
         | 
| 5 | 
            +
                @previous_control_point = @previous_quadratic_control_point = nil
         | 
| 6 | 
            +
                @calls = []
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                data.each_char do |c|
         | 
| 9 | 
            +
                  if c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z'
         | 
| 10 | 
            +
                    run_path_command(cmd, values) if cmd
         | 
| 11 | 
            +
                    cmd = c
         | 
| 12 | 
            +
                    values = []
         | 
| 13 | 
            +
                    value = ""
         | 
| 14 | 
            +
                  elsif c >= '0' && c <= '9' || c == '.' || c == "-"
         | 
| 15 | 
            +
                    value << c
         | 
| 16 | 
            +
                  elsif c == ' ' || c == "\t" || c == "\r" || c == "\n" || c == ","
         | 
| 17 | 
            +
                    if value != ""
         | 
| 18 | 
            +
                      values << value.to_f
         | 
| 19 | 
            +
                      value = ""
         | 
| 20 | 
            +
                    end
         | 
| 21 | 
            +
                  else
         | 
| 22 | 
            +
                    raise "invalid character '#{c}' in path data"
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
                
         | 
| 26 | 
            +
                values << value.to_f if value != ""
         | 
| 27 | 
            +
                run_path_command(cmd, values) if cmd
         | 
| 28 | 
            +
                
         | 
| 29 | 
            +
                @calls
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
              
         | 
| 32 | 
            +
              def run_path_command(command, values)
         | 
| 33 | 
            +
                upcase_command = command.upcase
         | 
| 34 | 
            +
                relative = command != upcase_command
         | 
| 35 | 
            +
                
         | 
| 36 | 
            +
                case upcase_command
         | 
| 37 | 
            +
                when 'M' # moveto
         | 
| 38 | 
            +
                  x = values.shift
         | 
| 39 | 
            +
                  y = values.shift
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  if relative && @last_point
         | 
| 42 | 
            +
                    x += @last_point.first
         | 
| 43 | 
            +
                    y += @last_point.last
         | 
| 44 | 
            +
                  end
         | 
| 45 | 
            +
                  
         | 
| 46 | 
            +
                  @last_point = @subpath_initial_point = [x, y]
         | 
| 47 | 
            +
                  @calls << ["move_to", @last_point]
         | 
| 48 | 
            +
                  
         | 
| 49 | 
            +
                  return run_path_command('L', values) if values.any?
         | 
| 50 | 
            +
                  
         | 
| 51 | 
            +
                when 'Z' # closepath
         | 
| 52 | 
            +
                  if @subpath_initial_point
         | 
| 53 | 
            +
                    @calls << ["line_to", @subpath_initial_point]
         | 
| 54 | 
            +
                    @last_point = @subpath_initial_point
         | 
| 55 | 
            +
                  end
         | 
| 56 | 
            +
                
         | 
| 57 | 
            +
                when 'L' # lineto
         | 
| 58 | 
            +
                  while values.any?
         | 
| 59 | 
            +
                    x = values.shift
         | 
| 60 | 
            +
                    y = values.shift
         | 
| 61 | 
            +
                    if relative && @last_point
         | 
| 62 | 
            +
                      x += @last_point.first
         | 
| 63 | 
            +
                      y += @last_point.last
         | 
| 64 | 
            +
                    end
         | 
| 65 | 
            +
                    @last_point = [x, y]
         | 
| 66 | 
            +
                    @calls << ["line_to", @last_point]
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
                  
         | 
| 69 | 
            +
                when 'H' # horizontal lineto
         | 
| 70 | 
            +
                  while values.any?
         | 
| 71 | 
            +
                    x = values.shift
         | 
| 72 | 
            +
                    x += @last_point.first if relative && @last_point
         | 
| 73 | 
            +
                    @last_point = [x, @last_point.last]
         | 
| 74 | 
            +
                    @calls << ["line_to", @last_point]
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                when 'V' # vertical lineto
         | 
| 78 | 
            +
                  while values.any?
         | 
| 79 | 
            +
                    y = values.shift
         | 
| 80 | 
            +
                    y += @last_point.last if relative && @last_point
         | 
| 81 | 
            +
                    @last_point = [@last_point.first, y]
         | 
| 82 | 
            +
                    @calls << ["line_to", @last_point]
         | 
| 83 | 
            +
                  end
         | 
| 84 | 
            +
                
         | 
| 85 | 
            +
                when 'C' # curveto
         | 
| 86 | 
            +
                  while values.any?
         | 
| 87 | 
            +
                    x1, y1, x2, y2, x, y = (1..6).collect {values.shift}
         | 
| 88 | 
            +
                    if relative && @last_point
         | 
| 89 | 
            +
                      x += @last_point.first
         | 
| 90 | 
            +
                      x1 += @last_point.first
         | 
| 91 | 
            +
                      x2 += @last_point.first                    
         | 
| 92 | 
            +
                      y += @last_point.last 
         | 
| 93 | 
            +
                      y1 += @last_point.last 
         | 
| 94 | 
            +
                      y2 += @last_point.last 
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
                    
         | 
| 97 | 
            +
                    @last_point = [x, y]
         | 
| 98 | 
            +
                    @previous_control_point = [x2, y2]
         | 
| 99 | 
            +
                    @calls << ["curve_to", [x, y, x1, y1, x2, y2]]
         | 
| 100 | 
            +
                  end      
         | 
| 101 | 
            +
             | 
| 102 | 
            +
                when 'S' # shorthand/smooth curveto
         | 
| 103 | 
            +
                  while values.any?
         | 
| 104 | 
            +
                    x2, y2, x, y = (1..4).collect {values.shift}
         | 
| 105 | 
            +
                    if relative && @last_point
         | 
| 106 | 
            +
                      x += @last_point.first
         | 
| 107 | 
            +
                      x2 += @last_point.first                    
         | 
| 108 | 
            +
                      y += @last_point.last 
         | 
| 109 | 
            +
                      y2 += @last_point.last 
         | 
| 110 | 
            +
                    end
         | 
| 111 | 
            +
                    
         | 
| 112 | 
            +
                    if @previous_control_point
         | 
| 113 | 
            +
                      x1 = 2 * @last_point.first - @previous_control_point.first
         | 
| 114 | 
            +
                      y1 = 2 * @last_point.last - @previous_control_point.last
         | 
| 115 | 
            +
                    else
         | 
| 116 | 
            +
                      x1, y1 = @last_point
         | 
| 117 | 
            +
                    end
         | 
| 118 | 
            +
                    
         | 
| 119 | 
            +
                    @last_point = [x, y]
         | 
| 120 | 
            +
                    @previous_control_point = [x2, y2]        
         | 
| 121 | 
            +
                    @calls << ["curve_to", [x, y, x1, y1, x2, y2]]
         | 
| 122 | 
            +
                  end
         | 
| 123 | 
            +
                  
         | 
| 124 | 
            +
                when 'Q', 'T' # quadratic curveto
         | 
| 125 | 
            +
                  while values.any?
         | 
| 126 | 
            +
                    if shorthand = upcase_command == 'T'
         | 
| 127 | 
            +
                      x, y = (1..2).collect {values.shift}          
         | 
| 128 | 
            +
                    else        
         | 
| 129 | 
            +
                      x1, y1, x, y = (1..4).collect {values.shift}
         | 
| 130 | 
            +
                    end
         | 
| 131 | 
            +
                    
         | 
| 132 | 
            +
                    if relative && @last_point
         | 
| 133 | 
            +
                      x += @last_point.first
         | 
| 134 | 
            +
                      x1 += @last_point.first if x1
         | 
| 135 | 
            +
                      y += @last_point.last 
         | 
| 136 | 
            +
                      y1 += @last_point.last if y1
         | 
| 137 | 
            +
                    end
         | 
| 138 | 
            +
                    
         | 
| 139 | 
            +
                    if shorthand 
         | 
| 140 | 
            +
                      if @previous_quadratic_control_point
         | 
| 141 | 
            +
                        x1 = 2 * @last_point.first - @previous_quadratic_control_point.first
         | 
| 142 | 
            +
                        y1 = 2 * @last_point.last - @previous_quadratic_control_point.last
         | 
| 143 | 
            +
                      else
         | 
| 144 | 
            +
                        x1, y1 = @last_point
         | 
| 145 | 
            +
                      end
         | 
| 146 | 
            +
                    end                
         | 
| 147 | 
            +
                    
         | 
| 148 | 
            +
                    # convert from quadratic to cubic
         | 
| 149 | 
            +
                    cx1 = @last_point.first + (x1 - @last_point.first) * 2 / 3.0
         | 
| 150 | 
            +
                    cy1 = @last_point.last + (y1 - @last_point.last) * 2 / 3.0
         | 
| 151 | 
            +
                    cx2 = cx1 + (x - @last_point.first) / 3.0
         | 
| 152 | 
            +
                    cy2 = cy1 + (y - @last_point.last) / 3.0        
         | 
| 153 | 
            +
             | 
| 154 | 
            +
                    @last_point = [x, y]
         | 
| 155 | 
            +
                    @previous_quadratic_control_point = [x1, y1]
         | 
| 156 | 
            +
                            
         | 
| 157 | 
            +
                    @calls << ["curve_to", [x, y, cx1, cy1, cx2, cy2]]
         | 
| 158 | 
            +
                  end      
         | 
| 159 | 
            +
                  
         | 
| 160 | 
            +
                when 'A'
         | 
| 161 | 
            +
                  # unsupported      
         | 
| 162 | 
            +
                end
         | 
| 163 | 
            +
                
         | 
| 164 | 
            +
                @previous_control_point = nil unless %w(C S).include?(upcase_command)
         | 
| 165 | 
            +
                @previous_quadratic_control_point = nil unless %w(Q T).include?(upcase_command)
         | 
| 166 | 
            +
              end  
         | 
| 167 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,67 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification 
         | 
| 2 | 
            +
            name: prawn-svg
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version 
         | 
| 4 | 
            +
              prerelease: false
         | 
| 5 | 
            +
              segments: 
         | 
| 6 | 
            +
              - 0
         | 
| 7 | 
            +
              - 9
         | 
| 8 | 
            +
              - 1
         | 
| 9 | 
            +
              version: 0.9.1
         | 
| 10 | 
            +
            platform: ruby
         | 
| 11 | 
            +
            authors: 
         | 
| 12 | 
            +
            - Roger Nesbitt
         | 
| 13 | 
            +
            autorequire: 
         | 
| 14 | 
            +
            bindir: bin
         | 
| 15 | 
            +
            cert_chain: []
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            date: 2010-03-26 00:00:00 +13:00
         | 
| 18 | 
            +
            default_executable: 
         | 
| 19 | 
            +
            dependencies: []
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            description: SVG renderer for Prawn PDF library
         | 
| 22 | 
            +
            email: roger@seriousorange.com
         | 
| 23 | 
            +
            executables: []
         | 
| 24 | 
            +
             | 
| 25 | 
            +
            extensions: []
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            extra_rdoc_files: []
         | 
| 28 | 
            +
             | 
| 29 | 
            +
            files: 
         | 
| 30 | 
            +
            - README
         | 
| 31 | 
            +
            - LICENSE
         | 
| 32 | 
            +
            - lib/prawn/svg/svg.rb
         | 
| 33 | 
            +
            - lib/prawn/svg/svg_path.rb
         | 
| 34 | 
            +
            - lib/prawn/svg_document.rb
         | 
| 35 | 
            +
            - lib/prawn-svg.rb
         | 
| 36 | 
            +
            has_rdoc: true
         | 
| 37 | 
            +
            homepage: 
         | 
| 38 | 
            +
            licenses: []
         | 
| 39 | 
            +
             | 
| 40 | 
            +
            post_install_message: 
         | 
| 41 | 
            +
            rdoc_options: []
         | 
| 42 | 
            +
             | 
| 43 | 
            +
            require_paths: 
         | 
| 44 | 
            +
            - lib
         | 
| 45 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement 
         | 
| 46 | 
            +
              requirements: 
         | 
| 47 | 
            +
              - - ">="
         | 
| 48 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 49 | 
            +
                  segments: 
         | 
| 50 | 
            +
                  - 0
         | 
| 51 | 
            +
                  version: "0"
         | 
| 52 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement 
         | 
| 53 | 
            +
              requirements: 
         | 
| 54 | 
            +
              - - ">="
         | 
| 55 | 
            +
                - !ruby/object:Gem::Version 
         | 
| 56 | 
            +
                  segments: 
         | 
| 57 | 
            +
                  - 0
         | 
| 58 | 
            +
                  version: "0"
         | 
| 59 | 
            +
            requirements: []
         | 
| 60 | 
            +
             | 
| 61 | 
            +
            rubyforge_project: 
         | 
| 62 | 
            +
            rubygems_version: 1.3.6
         | 
| 63 | 
            +
            signing_key: 
         | 
| 64 | 
            +
            specification_version: 3
         | 
| 65 | 
            +
            summary: SVG renderer for Prawn PDF library
         | 
| 66 | 
            +
            test_files: []
         | 
| 67 | 
            +
             |