Skip to content

Commit

Permalink
Implement <image> support for SVG documents
Browse files Browse the repository at this point in the history
This allows <image> elements to reference SVG documents so they can be
embedded inside the top-level SVG document.

Implements #137 and #144
  • Loading branch information
mogest committed Jan 28, 2024
1 parent 811cf71 commit a906de4
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 220 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ prawn-svg supports most but not all of the full SVG 1.1 specification. It curre

- <tt>&lt;style&gt;</tt> (see CSS section below)

- `<image>` referencing a JPEG or PNG image, with `http:`, `https:`, `data:image/jpeg;base64`, `data:image/png;base64` and `file:` schemes
(`file:` is disabled by default for security reasons, see Options section above)
- `<image>` referencing a JPEG, PNG, or SVG image, with `http:`, `https:`, `data:image/jpeg;base64`,
`data:image/png;base64`, `data:image/svg+xml;base64` and `file:` schemes (`file:` is disabled by default for
security reasons, see Options section above)

- <tt>&lt;clipPath&gt;</tt>

Expand Down
1 change: 1 addition & 0 deletions lib/prawn-svg.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
require 'prawn/svg/pathable'
require 'prawn/svg/elements'
require 'prawn/svg/extension'
require 'prawn/svg/renderer'
require 'prawn/svg/interface'
require 'prawn/svg/css/font_family_parser'
require 'prawn/svg/css/selector_parser'
Expand Down
7 changes: 5 additions & 2 deletions lib/prawn/svg/document.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ class Prawn::SVG::Document
:element_styles,
:color_mode

def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new)
def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser::Parser.new, attribute_overrides: {})
@root = REXML::Document.new(data).root

if @root.nil?
Expand All @@ -41,7 +41,10 @@ def initialize(data, bounds, options, font_registry: nil, css_parser: CssParser:
enable_file_with_root: options[:enable_file_requests_with_root]
)

@sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, @root.attributes)
attributes = @root.attributes.dup
attribute_overrides.each { |key, value| attributes.add(REXML::Attribute.new(key, value)) }

@sizing = Prawn::SVG::Calculators::DocumentSizing.new(bounds, attributes)
calculate_sizing(requested_width: options[:width], requested_height: options[:height])

@element_styles = Prawn::SVG::CSS::Stylesheets.new(css_parser, root).load
Expand Down
72 changes: 53 additions & 19 deletions lib/prawn/svg/elements/image.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,32 @@ class FakeIO
def initialize(data)
@data = data
end

def read
@data
end
def rewind
end

def rewind; end
end

ImageData = Struct.new(:dimensions, :document)

def parse
require_attributes 'width', 'height'

raise SkipElementQuietly if state.computed_properties.display == "none"
raise SkipElementQuietly if state.computed_properties.display == 'none'

@url = href_attribute
if @url.nil?
raise SkipElementError, "image tag must have an href or xlink:href"
end
raise SkipElementError, 'image tag must have an href or xlink:href' if @url.nil?

x = x(attributes['x'] || 0)
y = y(attributes['y'] || 0)
width = x_pixels(attributes['width'])
height = y_pixels(attributes['height'])
preserveAspectRatio = attributes['preserveAspectRatio']

raise SkipElementQuietly if width.zero? || height.zero?

require_positive_value width, height

@image = begin
Expand All @@ -34,7 +37,9 @@ def parse
raise SkipElementError, "Error retrieving URL #{@url}: #{e.message}"
end

@aspect = Prawn::SVG::Calculators::AspectRatio.new(attributes['preserveAspectRatio'], [width, height], image_dimensions(@image))
@image_data = process_image(@image, width, height, preserveAspectRatio)

@aspect = Prawn::SVG::Calculators::AspectRatio.new(preserveAspectRatio, [width, height], @image_data.dimensions)

@clip_x = x
@clip_y = y
Expand All @@ -49,15 +54,21 @@ def parse

def apply
if @aspect.slice?
add_call "save"
add_call "rectangle", [@clip_x, @clip_y], @clip_width, @clip_height
add_call "clip"
add_call 'save'
add_call 'rectangle', [@clip_x, @clip_y], @clip_width, @clip_height
add_call 'clip'
end

options = {:width => @width, :height => @height, :at => [@x, @y]}
if (document = @image_data.document)
add_call_and_enter 'translate', @x, @y
add_call 'svg:render_sub_document', document
else
options = { width: @width, height: @height, at: [@x, @y] }

add_call 'image', FakeIO.new(@image), options
end

add_call "image", FakeIO.new(@image), options
add_call "restore" if @aspect.slice?
add_call 'restore' if @aspect.slice?
end

def bounding_box
Expand All @@ -66,15 +77,38 @@ def bounding_box

protected

def image_dimensions(data)
unless (handler = find_image_handler(data))
raise SkipElementError, 'Unsupported image type supplied to image tag'
def process_image(data, width, height, preserveAspectRatio)
if (handler = find_image_handler(data))
image = handler.new(data)
ImageData.new([image.width.to_f, image.height.to_f], nil)

elsif potentially_svg?(data)
document = Prawn::SVG::Document.new(
data, [width, height], { width: width, height: height },
attribute_overrides: { 'preserveAspectRatio' => preserveAspectRatio }
)

dimensions = [document.sizing.output_width, document.sizing.output_height]
ImageData.new(dimensions, document)

else
raise_invalid_image_type
end
image = handler.new(data)
[image.width.to_f, image.height.to_f]
rescue Prawn::SVG::Document::InvalidSVGData
raise_invalid_image_type
end

def find_image_handler(data)
Prawn.image_handler.find(data) rescue nil
Prawn.image_handler.find(data)
rescue StandardError
nil
end

def potentially_svg?(data)
data.include?('<svg')
end

def raise_invalid_image_type
raise SkipElementError, 'Unsupported image type supplied to image tag'
end
end
Loading

0 comments on commit a906de4

Please sign in to comment.