diff --git a/lib/prawn-svg.rb b/lib/prawn-svg.rb index 69cbcbd..ed120d7 100644 --- a/lib/prawn-svg.rb +++ b/lib/prawn-svg.rb @@ -26,6 +26,7 @@ require 'prawn/svg/css/font_family_parser' require 'prawn/svg/css/selector_parser' require 'prawn/svg/css/stylesheets' +require 'prawn/svg/css/values_parser' require 'prawn/svg/ttf' require 'prawn/svg/font' require 'prawn/svg/gradients' diff --git a/lib/prawn/svg/color.rb b/lib/prawn/svg/color.rb index c467bf1..bf76aa9 100644 --- a/lib/prawn/svg/color.rb +++ b/lib/prawn/svg/color.rb @@ -2,7 +2,7 @@ class Prawn::SVG::Color RGB = Struct.new(:value) CMYK = Struct.new(:value) - RGB_DEFAULT_COLOR = RGB.new("000000") + RGB_DEFAULT_COLOR = RGB.new('000000') CMYK_DEFAULT_COLOR = CMYK.new([0, 0, 0, 100]) HTML_COLORS = { @@ -155,90 +155,91 @@ class Prawn::SVG::Color 'yellowgreen' => '9acd32' }.freeze - VALUE_REGEXP = "\s*(-?[0-9.]+%?)\s*" - RGB_REGEXP = /\Argb\(#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP}\)\z/i - CMYK_REGEXP = /\Adevice-cmyk\(#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP},#{VALUE_REGEXP}\)\z/i - URL_REGEXP = /\Aurl\(([^)]*)\)\z/i + class << self + def parse(color_string, gradients = nil, color_mode = :rgb) + url_specified = false - def self.parse(color_string, gradients = nil, color_mode = :rgb) - url_specified = false + values = ::Prawn::SVG::CSS::ValuesParser.parse(color_string) - components = color_string.to_s.strip.scan(/([^(\s]+(\([^)]*\))?)/) + result = values.map do |value| + case value + in ['rgb', args] + hex = (0..2).collect do |n| + number = args[n].to_f + number *= 2.55 if args[n][-1..] == '%' + format('%02x', clamp(number.round, 0, 255)) + end.join - result = components.map do |color, *_| - if m = color.match(/\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i) - RGB.new("#{m[1] * 2}#{m[2] * 2}#{m[3] * 2}") + RGB.new(hex) - elsif color.match(/\A#[0-9a-f]{6}\z/i) - RGB.new(color[1..6]) + in ['device-cmyk', args] + cmyk = (0..3).collect do |n| + number = args[n].to_f + number *= 100 unless args[n][-1..] == '%' + clamp(number, 0, 100) + end - elsif hex = HTML_COLORS[color.downcase] - hex_color(hex, color_mode) + CMYK.new(cmyk) - elsif m = color.match(RGB_REGEXP) - hex = (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 + in ['url', [url]] + url_specified = true + if url[0] == '#' && gradients && (gradient = gradients[url[1..]]) + gradient + end - RGB.new(hex) + in /\A#([0-9a-f])([0-9a-f])([0-9a-f])\z/i + RGB.new("#{$1 * 2}#{$2 * 2}#{$3 * 2}") - elsif m = color.match(CMYK_REGEXP) - cmyk = (1..4).collect do |n| - value = m[n].to_f - value *= 100 unless m[n][-1..-1] == '%' - clamp(value, 0, 100) - end + in /\A#[0-9a-f]{6}\z/i + RGB.new(value[1..]) - CMYK.new(cmyk) + in String => color + if (hex = HTML_COLORS[color.downcase]) + hex_color(hex, color_mode) + end - elsif matches = color.match(URL_REGEXP) - url_specified = true - url = matches[1] - if url[0] == "#" && gradients && gradient = gradients[url[1..-1]] - gradient + else + nil end end - end - # Generally, we default to black if the colour was unparseable. - # http://www.w3.org/TR/SVG/painting.html section 11.2 says if a URL was - # supplied without a fallback, that's an error. - result << default_color(color_mode) unless url_specified + # Generally, we default to black if the colour was unparseable. + # http://www.w3.org/TR/SVG/painting.html section 11.2 says if a URL was + # supplied without a fallback, that's an error. + result << default_color(color_mode) unless url_specified - result.compact - end + result.compact + end - def self.css_color_to_prawn_color(color) - result = parse(color).detect {|result| result.is_a?(RGB) || result.is_a?(CMYK)} - result.value if result - end + def css_color_to_prawn_color(color) + parse(color).detect { |value| value.is_a?(RGB) || value.is_a?(CMYK) }&.value + end - protected + def default_color(color_mode) + color_mode == :cmyk ? CMYK_DEFAULT_COLOR : RGB_DEFAULT_COLOR + end - def self.clamp(value, min_value, max_value) - [[value, min_value].max, max_value].min - end + private - def self.default_color(color_mode) - color_mode == :cmyk ? CMYK_DEFAULT_COLOR : RGB_DEFAULT_COLOR - end + def clamp(value, min_value, max_value) + [[value, min_value].max, max_value].min + end - def self.hex_color(hex, color_mode) - if color_mode == :cmyk - r, g, b = [hex[0..1], hex[2..3], hex[4..5]].map { |h| h.to_i(16) / 255.0 } - k = 1 - [r, g, b].max - if k == 1 - CMYK.new([0, 0, 0, 100]) + def hex_color(hex, color_mode) + if color_mode == :cmyk + r, g, b = [hex[0..1], hex[2..3], hex[4..5]].map { |h| h.to_i(16) / 255.0 } + k = 1 - [r, g, b].max + if k == 1 + CMYK.new([0, 0, 0, 100]) + else + c = (1 - r - k) / (1 - k) + m = (1 - g - k) / (1 - k) + y = (1 - b - k) / (1 - k) + CMYK.new([c, m, y, k].map { |v| (v * 100).round }) + end else - c = (1 - r - k) / (1 - k) - m = (1 - g - k) / (1 - k) - y = (1 - b - k) / (1 - k) - CMYK.new([c, m, y, k].map { |v| (v * 100).round }) + RGB.new(hex) end - else - RGB.new(hex) end end end diff --git a/lib/prawn/svg/css/values_parser.rb b/lib/prawn/svg/css/values_parser.rb new file mode 100644 index 0000000..1778531 --- /dev/null +++ b/lib/prawn/svg/css/values_parser.rb @@ -0,0 +1,68 @@ +module Prawn::SVG::CSS + class ValuesParser + class << self + def parse(values) + result = [] + + while values + value, remainder = parse_next(values) + break unless value + + result << value + values = remainder + end + + result + end + + private + + def parse_next(values) + values = values.strip + return if values.empty? + + if (matches = values.match(/\A([a-z-]+)\(\s*(.+)/i)) + parse_function_call(matches[1].downcase, matches[2]) + else + values.split(/\s+/, 2) + end + end + + # Note this does not support space-separated arguments. + # I don't think CSS 2 has any, but in case it does here is the place to add them. + def parse_function_call(name, rest) + arguments = [] + in_quote = nil + in_escape = false + current = '' + + rest.chars.each.with_index do |char, index| + if in_escape + current << char + in_escape = false + elsif %w[" '].include?(char) + if in_quote == char + in_quote = nil + elsif in_quote.nil? + in_quote = char + else + current << char + end + elsif char == '\\' + in_escape = true + elsif in_quote.nil? && char == ',' + arguments << current.strip + current = '' + elsif in_quote.nil? && char == ')' + arguments << current.strip + return [[name, arguments], rest[index + 1..]] + else + current << char + end + end + + [rest, nil] + end + end + end +end diff --git a/lib/prawn/svg/elements/base.rb b/lib/prawn/svg/elements/base.rb index f5a1004..a2c91f8 100644 --- a/lib/prawn/svg/elements/base.rb +++ b/lib/prawn/svg/elements/base.rb @@ -261,10 +261,16 @@ def require_positive_value(*args) end end - def extract_element_from_url_id_reference(value, expected_type = nil) - matches = value.strip.match(/\Aurl\(\s*#(\S+)\s*\)\z/i) if value - element = document.elements_by_id[matches[1]] if matches - element if element && (expected_type.nil? || element.name == expected_type) + def extract_element_from_url_id_reference(values, expected_type = nil) + Prawn::SVG::CSS::ValuesParser.parse(values).detect do |value| + case value + in ['url', [url]] + element = document.elements_by_id[url[1..]] if url.start_with?('#') + break element if element && (expected_type.nil? || element.name == expected_type) + else + nil + end + end end def href_attribute diff --git a/spec/prawn/svg/css/values_parser_spec.rb b/spec/prawn/svg/css/values_parser_spec.rb new file mode 100644 index 0000000..d594308 --- /dev/null +++ b/spec/prawn/svg/css/values_parser_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +RSpec.describe Prawn::SVG::CSS::ValuesParser do + it 'parses specified values' do + values = 'hello world url("#myid") no-quote(very good) escaping(")\\")ok") rgb( 1,4, 5 )' + + expect(described_class.parse(values)).to eq [ + 'hello', + 'world', + ['url', ['#myid']], + ['no-quote', ['very good']], + ['escaping', [')")ok']], + ['rgb', %w[1 4 5]] + ] + end +end