From 6bd98701fd2a5f4fdf5df5db3ee1119e51cca6f8 Mon Sep 17 00:00:00 2001 From: Mog Nesbitt Date: Sun, 16 Jun 2024 14:14:10 +1200 Subject: [PATCH] Rebuild CSS values parser to be more standards compliant CSS values parsing was pretty basic before. The CSS 2 graamar spec doesn't actually spell out exactly how quoting should work, but it's referenced elsewhere in the document, so this commit does its best to implement quote parsing. It also implements escaping as per the spec. Fixes #165 --- lib/prawn-svg.rb | 1 + lib/prawn/svg/color.rb | 129 ++++++++++++----------- lib/prawn/svg/css/values_parser.rb | 68 ++++++++++++ lib/prawn/svg/elements/base.rb | 14 ++- spec/prawn/svg/css/values_parser_spec.rb | 16 +++ 5 files changed, 160 insertions(+), 68 deletions(-) create mode 100644 lib/prawn/svg/css/values_parser.rb create mode 100644 spec/prawn/svg/css/values_parser_spec.rb 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