module Prawn module SVG class Renderer attr_reader :prawn, :document, :options # # Creates a Prawn::SVG object. # # +data+ is the SVG data to convert. +prawn+ is your Prawn::Document object. # # See README.md for the options that can be passed to this method. # def initialize(prawn, document, options) @prawn = prawn @document = document @options = options end # # Draws the SVG to the Prawn::Document object. # def draw if sizing.invalid? document.warnings << 'Zero or negative sizing data means this SVG cannot be rendered' return end document.warnings.clear prawn.save_font do prawn.bounding_box(position, width: sizing.output_width, height: sizing.output_height) do prawn.save_graphics_state do clip_rectangle 0, 0, sizing.output_width, sizing.output_height calls = [] root_element = Prawn::SVG::Elements::Root.new(document, document.root, calls) root_element.process proc_creator(prawn, calls).call end end end end def sizing document.sizing end def position options[:at] || [x_based_on_requested_alignment, y_based_on_requested_alignment] end private def x_based_on_requested_alignment case options[:position] when :left, nil 0 when :center, :centre (sizing.bounds[0] - sizing.output_width) / 2.0 when :right sizing.bounds[0] - sizing.output_width when Numeric options[:position] else raise ArgumentError, 'options[:position] must be one of nil, :left, :right, :center or a number' end end def y_based_on_requested_alignment case options[:vposition] when nil prawn.cursor when :top sizing.bounds[1] when :center, :centre sizing.bounds[1] - ((sizing.bounds[1] - sizing.output_height) / 2.0) when :bottom sizing.output_height when Numeric sizing.bounds[1] - options[:vposition] else raise ArgumentError, 'options[:vposition] must be one of nil, :top, :right, :bottom or a number' end end def proc_creator(prawn, calls) proc { issue_prawn_command(prawn, calls) } end def issue_prawn_command(prawn, calls) calls.each do |call, arguments, kwarguments, children| skip = false rewrite_call_arguments(prawn, call, arguments, kwarguments) do issue_prawn_command(prawn, children) if children.any? skip = true end if skip # the call has been overridden elsif children.empty? && call != 'transparent' # some prawn calls complain if they aren't supplied a block if RUBY_VERSION >= '2.7' || !kwarguments.empty? prawn.send(call, *arguments, **kwarguments) else prawn.send(call, *arguments) end elsif RUBY_VERSION >= '2.7' || !kwarguments.empty? prawn.send(call, *arguments, **kwarguments, &proc_creator(prawn, children)) else prawn.send(call, *arguments, &proc_creator(prawn, children)) end end end def rewrite_call_arguments(prawn, call, arguments, kwarguments) case call when 'text_group' @cursor = [0, sizing.output_height] yield when 'draw_text' text = arguments.first options = kwarguments at = options.fetch(:at) at[0] = @cursor[0] if at[0] == :relative at[1] = @cursor[1] if at[1] == :relative case options.delete(:dominant_baseline) when 'middle' height = prawn.font.height at[1] -= height / 2.0 @cursor = [at[0], at[1]] end if (offset = options.delete(:offset)) at[0] += offset[0] at[1] -= offset[1] end width = prawn.width_of(text, options.merge(kerning: true)) if (stretch_to_width = options.delete(:stretch_to_width)) factor = stretch_to_width.to_f * 100 / width.to_f prawn.add_content "#{factor} Tz" width = stretch_to_width.to_f end if (pad_to_width = options.delete(:pad_to_width)) padding_required = pad_to_width.to_f - width.to_f padding_per_character = padding_required / text.length.to_f prawn.add_content "#{padding_per_character} Tc" width = pad_to_width.to_f end case options.delete(:text_anchor) when 'middle' at[0] -= width / 2 @cursor = [at[0] + (width / 2), at[1]] when 'end' at[0] -= width @cursor = at.dup else @cursor = [at[0] + width, at[1]] end decoration = options.delete(:decoration) if decoration == 'underline' prawn.save_graphics_state do prawn.line_width 1 prawn.line [at[0], at[1] - 1.25], [at[0] + width, at[1] - 1.25] prawn.stroke end end when 'transformation_matrix' left = prawn.bounds.absolute_left top = prawn.bounds.absolute_top arguments[4] += left - ((left * arguments[0]) + (top * arguments[2])) arguments[5] += top - ((left * arguments[1]) + (top * arguments[3])) when 'clip' prawn.add_content 'W n' # clip to path yield when 'save' prawn.save_graphics_state yield when 'restore' prawn.restore_graphics_state yield when 'end_path' yield prawn.add_content 'n' # end path when 'fill_and_stroke' yield # prawn (as at 2.0.1 anyway) uses 'b' for its fill_and_stroke. 'b' is 'h' (closepath) + 'B', and we # never want closepath to be automatically run as it stuffs up many drawing operations, such as dashes # and line caps, and makes paths close that we didn't ask to be closed when fill is specified. even_odd = kwarguments[:fill_rule] == :even_odd content = even_odd ? 'B*' : 'B' prawn.add_content content when 'noop' yield when 'svg:render_sub_document' sub_document = arguments.first sub_options = inheritable_options.merge({ at: [0, 0] }) Renderer.new(prawn, sub_document, sub_options).draw document.warnings.concat(sub_document.warnings) yield when 'svg:render_gradient' type = arguments.first GradientRenderer.new(prawn, type, **kwarguments).draw yield end end def inheritable_options (options || {}).slice(Prawn::SVG::Interface::INHERITABLE_OPTIONS) end def clip_rectangle(x, y, width, height) prawn.move_to x, y prawn.line_to x + width, y prawn.line_to x + width, y + height prawn.line_to x, y + height prawn.close_path prawn.add_content 'W n' # clip to path end end end end