lib/prawn/svg/parser.rb in prawn-svg-0.9.1.6 vs lib/prawn/svg/parser.rb in prawn-svg-0.9.1.7

- old
+ new

@@ -9,443 +9,449 @@ # # This class is not passed the prawn object, so knows nothing about # prawn specifically - this might be useful if you want to take this code and use it to convert # SVG to another format. # -class Prawn::Svg::Parser - begin - require 'css_parser' - CSS_PARSER_LOADED = true - rescue LoadError - CSS_PARSER_LOADED = false - end +module Prawn + module Svg + class Parser + begin + require 'css_parser' + CSS_PARSER_LOADED = true + rescue LoadError + CSS_PARSER_LOADED = false + end - include Prawn::Measurements + include Prawn::Measurements - attr_reader :width, :height + attr_reader :width, :height - # An +Array+ of warnings that occurred while parsing the SVG data. - attr_reader :warnings + # An +Array+ of warnings that occurred while parsing the SVG data. + attr_reader :warnings - # The scaling factor, as determined by the :width or :height options. - attr_accessor :scale + # The scaling factor, as determined by the :width or :height options. + attr_accessor :scale - # - # Construct a Parser object. - # - # The +data+ argument is SVG data. - # - # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points. - # - # +options+ can optionally contain - # the key :width or :height. If both are specified, only :width will be used. - # - def initialize(data, bounds, options) - @data = data - @bounds = bounds - @options = options - @warnings = [] - @css_parser = CssParser::Parser.new if CSS_PARSER_LOADED + # + # Construct a Parser object. + # + # The +data+ argument is SVG data. + # + # +bounds+ is a tuple [width, height] that specifies the bounds of the drawing space in points. + # + # +options+ can optionally contain + # the key :width or :height. If both are specified, only :width will be used. + # + def initialize(data, bounds, options) + @data = data + @bounds = bounds + @options = options + @warnings = [] + @css_parser = CssParser::Parser.new if CSS_PARSER_LOADED - if data - parse_document - calculate_dimensions - end - end + if data + parse_document + calculate_dimensions + end + end - # - # Parse the SVG data and return a call tree. The returned +Array+ is in the format: - # - # [ - # ['prawn_method_name', ['argument1', 'argument2'], []], - # ['method_that_takes_a_block', ['argument1', 'argument2'], [ - # ['method_called_inside_block', ['argument'], []] - # ] - # ] - # - def parse - @warnings = [] - [].tap {|calls| parse_element(@root, calls, {})} - end + # + # Parse the SVG data and return a call tree. The returned +Array+ is in the format: + # + # [ + # ['prawn_method_name', ['argument1', 'argument2'], []], + # ['method_that_takes_a_block', ['argument1', 'argument2'], [ + # ['method_called_inside_block', ['argument'], []] + # ] + # ] + # + def parse + @warnings = [] + calls = [] + parse_element(@root, calls, {}) + calls + end - private - def parse_document - @root = REXML::Document.new(@data).root - @actual_width, @actual_height = @bounds # set this first so % width/heights can be used + private + def parse_document + @root = REXML::Document.new(@data).root + @actual_width, @actual_height = @bounds # set this first so % width/heights can be used - if vb = @root.attributes['viewBox'] - x1, y1, x2, y2 = vb.strip.split(/\s+/) - @x_offset, @y_offset = [x1.to_f, y1.to_f] - @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f] - else - @x_offset, @y_offset = [0, 0] - @actual_width = points(@root.attributes['width'], :x) - @actual_height = points(@root.attributes['height'], :y) - end - end + if vb = @root.attributes['viewBox'] + x1, y1, x2, y2 = vb.strip.split(/\s+/) + @x_offset, @y_offset = [x1.to_f, y1.to_f] + @actual_width, @actual_height = [x2.to_f - x1.to_f, y2.to_f - y1.to_f] + else + @x_offset, @y_offset = [0, 0] + @actual_width = points(@root.attributes['width'], :x) + @actual_height = points(@root.attributes['height'], :y) + end + end - REQUIRED_ATTRIBUTES = { - "line" => %w(x1 y1 x2 y2), - "polyline" => %w(points), - "polygon" => %w(points), - "circle" => %w(r), - "ellipse" => %w(rx ry), - "rect" => %w(x y width height), - "path" => %w(d) - } + REQUIRED_ATTRIBUTES = { + "line" => %w(x1 y1 x2 y2), + "polyline" => %w(points), + "polygon" => %w(points), + "circle" => %w(r), + "ellipse" => %w(rx ry), + "rect" => %w(x y width height), + "path" => %w(d) + } - def parse_element(element, calls, state) - attrs = element.attributes - calls, style_attrs = apply_styles(element, calls, state) + def parse_element(element, calls, state) + attrs = element.attributes + calls, style_attrs = apply_styles(element, calls, state) - if required_attributes = REQUIRED_ATTRIBUTES[element.name] - return unless check_attrs_present(element, required_attributes) - end + if required_attributes = REQUIRED_ATTRIBUTES[element.name] + return unless check_attrs_present(element, required_attributes) + end - case element.name - when 'title', 'desc' - # ignore + case element.name + when 'title', 'desc' + # ignore - when 'g', 'svg' - element.elements.each do |child| - parse_element(child, calls, state.dup) - end + when 'g', 'svg' + element.elements.each do |child| + parse_element(child, calls, state.dup) + end - when 'defs' - # Pass calls as a blank array so that nothing under this tag can be added to our call tree. - element.elements.each do |child| - parse_element(child, [], state.dup.merge(:display => false)) - end + when 'defs' + # Pass calls as a blank array so that nothing under this tag can be added to our call tree. + element.elements.each do |child| + parse_element(child, [], state.dup.merge(:display => false)) + end - when 'style' - load_css_styles(element) + when 'style' + load_css_styles(element) - when 'text' - # Very primitive support for font-family; it won't work in most cases because - # PDF only has a few built-in fonts, and they're not the same as the names - # used typically with the web fonts. - if font_family = style_attrs["font-family"] - if font_family != "" && pdf_font = map_font_family_to_pdf_font(font_family) - calls << ['font', [pdf_font], []] - calls = calls.last.last - else - @warnings << "#{font_family} is not a known font." - end - end + when 'text' + # Very primitive support for font-family; it won't work in most cases because + # PDF only has a few built-in fonts, and they're not the same as the names + # used typically with the web fonts. + if font_family = style_attrs["font-family"] + if font_family != "" && pdf_font = map_font_family_to_pdf_font(font_family) + calls << ['font', [pdf_font], []] + calls = calls.last.last + else + @warnings << "#{font_family} is not a known font." + end + end - opts = {:at => [x(attrs['x']), y(attrs['y'])]} - if size = style_attrs['font-size'] - opts[:size] = size.to_f * @scale - end + opts = {:at => [x(attrs['x']), y(attrs['y'])]} + if size = style_attrs['font-size'] + opts[:size] = size.to_f * @scale + end - # This is not a prawn option but we can't work out how to render it here - - # it's handled by Svg#rewrite_call_arguments - if anchor = style_attrs['text-anchor'] - opts[:text_anchor] = anchor - end + # This is not a prawn option but we can't work out how to render it here - + # it's handled by Svg#rewrite_call_arguments + if anchor = style_attrs['text-anchor'] + opts[:text_anchor] = anchor + end - calls << ['text_box', [element.text, opts], []] + calls << ['text_box', [element.text, opts], []] - when 'line' - calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []] + when 'line' + calls << ['line', [x(attrs['x1']), y(attrs['y1']), x(attrs['x2']), y(attrs['y2'])], []] - when 'polyline' - points = attrs['points'].split(/\s+/) - return unless base_point = points.shift - x, y = base_point.split(",") - calls << ['move_to', [x(x), y(y)], []] - calls << ['stroke', [], []] - calls = calls.last.last - points.each do |point| - x, y = point.split(",") - calls << ["line_to", [x(x), y(y)], []] - end + when 'polyline' + points = attrs['points'].split(/\s+/) + return unless base_point = points.shift + x, y = base_point.split(",") + calls << ['move_to', [x(x), y(y)], []] + calls << ['stroke', [], []] + calls = calls.last.last + points.each do |point| + x, y = point.split(",") + calls << ["line_to", [x(x), y(y)], []] + end - when 'polygon' - points = attrs['points'].split(/\s+/).collect do |point| - x, y = point.split(",") - [x(x), y(y)] - end - calls << ["polygon", points, []] + when 'polygon' + points = attrs['points'].split(/\s+/).collect do |point| + x, y = point.split(",") + [x(x), y(y)] + end + calls << ["polygon", points, []] - when 'circle' - calls << ["circle_at", - [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}], - []] + when 'circle' + calls << ["circle_at", + [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], {:radius => distance(attrs['r'])}], + []] - when 'ellipse' - calls << ["ellipse_at", - [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])], - []] + when 'ellipse' + calls << ["ellipse_at", + [[x(attrs['cx'] || "0"), y(attrs['cy'] || "0")], distance(attrs['rx']), distance(attrs['ry'])], + []] - when 'rect' - radius = distance(attrs['rx'] || attrs['ry']) - args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])] - if radius - # n.b. does not support both rx and ry being specified with different values - calls << ["rounded_rectangle", args + [radius], []] - else - calls << ["rectangle", args, []] - end + when 'rect' + radius = distance(attrs['rx'] || attrs['ry']) + args = [[x(attrs['x']), y(attrs['y'])], distance(attrs['width']), distance(attrs['height'])] + if radius + # n.b. does not support both rx and ry being specified with different values + calls << ["rounded_rectangle", args + [radius], []] + else + calls << ["rectangle", args, []] + end - when 'path' - @svg_path ||= Path.new + when 'path' + @svg_path ||= Path.new - begin - commands = @svg_path.parse(attrs['d']) - rescue Prawn::Svg::Parser::Path::InvalidError => e - commands = [] - @warnings << e.message - end + begin + commands = @svg_path.parse(attrs['d']) + rescue Prawn::Svg::Parser::Path::InvalidError => e + commands = [] + @warnings << e.message + end - commands.each do |command, args| - point_to = [x(args[0]), y(args[1])] - if command == 'curve_to' - bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]] - calls << [command, [point_to, {:bounds => bounds}], []] - else - calls << [command, point_to, []] + commands.each do |command, args| + point_to = [x(args[0]), y(args[1])] + if command == 'curve_to' + bounds = [[x(args[2]), y(args[3])], [x(args[4]), y(args[5])]] + calls << [command, [point_to, {:bounds => bounds}], []] + else + calls << [command, point_to, []] + end + end + + else + @warnings << "Unknown tag '#{element.name}'; ignoring" end end - - else - @warnings << "Unknown tag '#{element.name}'; ignoring" - end - end - def load_css_styles(element) - if @css_parser - data = if element.cdatas.any? - element.cdatas.collect(&:to_s).join - else - element.text - end + def load_css_styles(element) + if @css_parser + data = if element.cdatas.any? + element.cdatas.collect(&:to_s).join + else + element.text + end - @css_parser.add_block!(data) - end - end + @css_parser.add_block!(data) + end + end - def parse_css_declarations(declarations) - # copied from css_parser - declarations.gsub!(/(^[\s]*)|([\s]*$)/, '') + def parse_css_declarations(declarations) + # copied from css_parser + declarations.gsub!(/(^[\s]*)|([\s]*$)/, '') - {}.tap do |o| - declarations.split(/[\;$]+/m).each do |decs| - if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i) - property, value, end_of_declaration = matches.captures - o[property] = value + output = {} + declarations.split(/[\;$]+/m).each do |decs| + if matches = decs.match(/\s*(.[^:]*)\s*\:\s*(.[^;]*)\s*(;|\Z)/i) + property, value, end_of_declaration = matches.captures + output[property] = value + end end + output end - end - end - def determine_style_for(element) - if @css_parser - tag_style = @css_parser.find_by_selector(element.name) - id_style = @css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"] + def determine_style_for(element) + if @css_parser + tag_style = @css_parser.find_by_selector(element.name) + id_style = @css_parser.find_by_selector("##{element.attributes["id"]}") if element.attributes["id"] - if classes = element.attributes["class"] - class_styles = classes.strip.split(/\s+/).collect do |class_name| - @css_parser.find_by_selector(".#{class_name}") - end - end + if classes = element.attributes["class"] + class_styles = classes.strip.split(/\s+/).collect do |class_name| + @css_parser.find_by_selector(".#{class_name}") + end + end - element_style = element.attributes['style'] + element_style = element.attributes['style'] - style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s| - s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}" - end.join - else - style = element.attributes['style'] || "" - end - - decs = parse_css_declarations(style) - element.attributes.each {|n,v| decs[n] = v unless decs[n]} - decs - end - - def apply_styles(element, calls, state) - decs = determine_style_for(element) - draw_types = [] - - # Transform - if transform = decs['transform'] - parse_css_method_calls(transform).each do |name, arguments| - case name - when 'translate' - x, y = arguments - x, y = x.split(/\s+/) if y.nil? - calls << [name, [distance(x), -distance(y)], []] - calls = calls.last.last - when 'rotate' - calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []] - calls = calls.last.last - when 'scale' - calls << [name, [arguments.first.to_f], []] - calls = calls.last.last + style = [tag_style, class_styles, id_style, element_style].flatten.collect do |s| + s.nil? || s.strip == "" ? "" : "#{s}#{";" unless s.match(/;\s*\z/)}" + end.join else - @warnings << "Unknown transformation '#{name}'; ignoring" + style = element.attributes['style'] || "" end + + decs = parse_css_declarations(style) + element.attributes.each {|n,v| decs[n] = v unless decs[n]} + decs end - end + + def apply_styles(element, calls, state) + decs = determine_style_for(element) + draw_types = [] + + # Transform + if transform = decs['transform'] + parse_css_method_calls(transform).each do |name, arguments| + case name + when 'translate' + x, y = arguments + x, y = x.split(/\s+/) if y.nil? + calls << [name, [distance(x), -distance(y)], []] + calls = calls.last.last + when 'rotate' + calls << [name, [-arguments.first.to_f, {:origin => [0, y('0')]}], []] + calls = calls.last.last + when 'scale' + calls << [name, [arguments.first.to_f], []] + calls = calls.last.last + else + @warnings << "Unknown transformation '#{name}'; ignoring" + end + end + end - # Opacity: - # We can't do nested opacities quite like the SVG requires, but this is close enough. - fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity'] - fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity'] - stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity'] + # Opacity: + # We can't do nested opacities quite like the SVG requires, but this is close enough. + fill_opacity = stroke_opacity = clamp(decs['opacity'].to_f, 0, 1) if decs['opacity'] + fill_opacity = clamp(decs['fill-opacity'].to_f, 0, 1) if decs['fill-opacity'] + stroke_opacity = clamp(decs['stroke-opacity'].to_f, 0, 1) if decs['stroke-opacity'] - if fill_opacity || stroke_opacity - state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1) - state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1) + if fill_opacity || stroke_opacity + state[:fill_opacity] = (state[:fill_opacity] || 1) * (fill_opacity || 1) + state[:stroke_opacity] = (state[:stroke_opacity] || 1) * (stroke_opacity || 1) - calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []] - calls = calls.last.last - end + calls << ['transparent', [state[:fill_opacity], state[:stroke_opacity]], []] + calls = calls.last.last + end - # Fill and stroke - if decs['fill'] && decs['fill'] != "none" - if color = color_to_hex(decs['fill']) - calls << ['fill_color', [color], []] - end - draw_types << 'fill' - end + # Fill and stroke + if decs['fill'] && decs['fill'] != "none" + if color = color_to_hex(decs['fill']) + calls << ['fill_color', [color], []] + end + draw_types << 'fill' + end - if decs['stroke'] && decs['stroke'] != "none" - if color = color_to_hex(decs['stroke']) - calls << ['stroke_color', [color], []] - end - draw_types << 'stroke' - end + if decs['stroke'] && decs['stroke'] != "none" + if color = color_to_hex(decs['stroke']) + calls << ['stroke_color', [color], []] + end + draw_types << 'stroke' + end - calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width'] + calls << ['line_width', [distance(decs['stroke-width'])], []] if decs['stroke-width'] - draw_type = draw_types.join("_and_") - state[:draw_type] = draw_type if draw_type != "" - if state[:draw_type] && !%w(g svg).include?(element.name) - calls << [state[:draw_type], [], []] - calls = calls.last.last - end + draw_type = draw_types.join("_and_") + state[:draw_type] = draw_type if draw_type != "" + if state[:draw_type] && !%w(g svg).include?(element.name) + calls << [state[:draw_type], [], []] + calls = calls.last.last + end - [calls, decs] - end + [calls, decs] + end - def parse_css_method_calls(string) - string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call| - name, argument_string = call - arguments = argument_string.split(",").collect(&:strip) - [name, arguments] - end - end + def parse_css_method_calls(string) + string.scan(/\s*(\w+)\(([^)]+)\)\s*/).collect do |call| + name, argument_string = call + arguments = argument_string.split(",").collect(&:strip) + [name, arguments] + end + end - BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"] - GENERIC_CSS_FONT_MAPPING = { - "serif" => "Times-Roman", - "sans-serif" => "Helvetica", - "cursive" => "Times-Roman", - "fantasy" => "Times-Roman", - "monospace" => "Courier"} + BUILT_IN_FONTS = ["Courier", "Helvetica", "Times-Roman", "Symbol", "ZapfDingbats"] + GENERIC_CSS_FONT_MAPPING = { + "serif" => "Times-Roman", + "sans-serif" => "Helvetica", + "cursive" => "Times-Roman", + "fantasy" => "Times-Roman", + "monospace" => "Courier"} - def installed_fonts - @installed_fonts ||= Prawn::Svg.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten - end + def installed_fonts + @installed_fonts ||= Prawn::Svg::Interface.font_path.uniq.collect {|path| Dir["#{path}/*"]}.flatten + end - def map_font_family_to_pdf_font(font_family) - font_family.split(",").detect do |font| - font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase + def map_font_family_to_pdf_font(font_family) + font_family.split(",").detect do |font| + font = font.gsub(/['"]/, '').gsub(/\s{2,}/, ' ').strip.downcase - built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font} - break built_in_font if built_in_font + built_in_font = BUILT_IN_FONTS.detect {|f| f.downcase == font} + break built_in_font if built_in_font - generic_font = GENERIC_CSS_FONT_MAPPING[font] - break generic_font if generic_font + generic_font = GENERIC_CSS_FONT_MAPPING[font] + break generic_font if generic_font - installed_font = installed_fonts.detect do |file| - (matches = File.basename(file).match(/(.+)\./)) && matches[1].downcase == font + installed_font = installed_fonts.detect do |file| + (matches = File.basename(file).match(/(.+)\./)) && matches[1].downcase == font + end + break installed_font if installed_font + end end - break installed_font if installed_font - end - end - # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords - HTML_COLORS = { - 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00", - 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00", - 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff", - 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff" - }.freeze + # TODO : use http://www.w3.org/TR/SVG11/types.html#ColorKeywords + HTML_COLORS = { + 'black' => "000000", 'green' => "008000", 'silver' => "c0c0c0", 'lime' => "00ff00", + 'gray' => "808080", 'olive' => "808000", 'white' => "ffffff", 'yellow' => "ffff00", + 'maroon' => "800000", 'navy' => "000080", 'red' => "ff0000", 'blue' => "0000ff", + 'purple' => "800080", 'teal' => "008080", 'fuchsia' => "ff00ff", 'aqua' => "00ffff" + }.freeze - RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*" - RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i + RGB_VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*" + RGB_REGEXP = /\Argb\(#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP},#{RGB_VALUE_REGEXP}\)\z/i - def color_to_hex(color_string) - color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_| - if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i) - break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}" - elsif color.match(/\A#[0-9a-f]{6}\z/i) - break color[1..6] - elsif hex = HTML_COLORS[color.downcase] - break hex - elsif m = color.match(RGB_REGEXP) - break (1..3).collect do |n| - value = m[n].to_f - value *= 2.55 if m[n][-1..-1] == '%' - "%02x" % clamp(value.round, 0, 255) - end.join - end - end - end + def color_to_hex(color_string) + color_string.scan(/([^(\s]+(\([^)]*\))?)/).detect do |color, *_| + if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i) + break "#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}" + elsif color.match(/\A#[0-9a-f]{6}\z/i) + break color[1..6] + elsif hex = HTML_COLORS[color.downcase] + break hex + elsif m = color.match(RGB_REGEXP) + break (1..3).collect do |n| + value = m[n].to_f + value *= 2.55 if m[n][-1..-1] == '%' + "%02x" % clamp(value.round, 0, 255) + end.join + end + end + end - def x(value) - (points(value, :x) - @x_offset) * scale - end + def x(value) + (points(value, :x) - @x_offset) * scale + end - def y(value) - (@actual_height - (points(value, :y) - @y_offset)) * scale - end + def y(value) + (@actual_height - (points(value, :y) - @y_offset)) * scale + end - def distance(value, axis = nil) - value && (points(value, axis) * scale) - end + def distance(value, axis = nil) + value && (points(value, axis) * scale) + end - def points(value, axis = nil) - if value.is_a?(String) - if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/) - send("#{match[1]}2pt", value.to_f) - elsif value[-1..-1] == "%" - value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0 - else - value.to_f + def points(value, axis = nil) + if value.is_a?(String) + if match = value.match(/\d(cm|dm|ft|in|m|mm|yd)$/) + send("#{match[1]}2pt", value.to_f) + elsif value[-1..-1] == "%" + value.to_f * (axis == :y ? @actual_height : @actual_width) / 100.0 + else + value.to_f + end + else + value.to_f + end end - else - value.to_f - end - end - def calculate_dimensions - if @options[:width] - @width = @options[:width] - @scale = @options[:width] / @actual_width.to_f - elsif @options[:height] - @height = @options[:height] - @scale = @options[:height] / @actual_height.to_f - else - @scale = 1 - end + def calculate_dimensions + if @options[:width] + @width = @options[:width] + @scale = @options[:width] / @actual_width.to_f + elsif @options[:height] + @height = @options[:height] + @scale = @options[:height] / @actual_height.to_f + else + @scale = 1 + end - @width ||= @actual_width * @scale - @height ||= @actual_height * @scale - end + @width ||= @actual_width * @scale + @height ||= @actual_height * @scale + end - def clamp(value, min_value, max_value) - [[value, min_value].max, max_value].min - end + def clamp(value, min_value, max_value) + [[value, min_value].max, max_value].min + end - def check_attrs_present(element, attrs) - missing_attrs = attrs - element.attributes.keys - if missing_attrs.any? - @warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag" + def check_attrs_present(element, attrs) + missing_attrs = attrs - element.attributes.keys + if missing_attrs.any? + @warnings << "Must have attributes #{missing_attrs.join(", ")} on tag #{element.name}; skipping tag" + end + missing_attrs.empty? + end end - missing_attrs.empty? end end