Class: Postsvg::SvgBuilder
- Inherits:
-
Object
- Object
- Postsvg::SvgBuilder
- Defined in:
- lib/postsvg/svg_builder.rb
Overview
Append-only SVG emitter. The Renderer / Visitors call methods on a builder instance; #to_s finalizes and returns the SVG string.
Determinism invariants:
- Two runs of the same input produce byte-equal output.
- IDs (clip1, grad1, pattern1) start at 1 on every SvgBuilder.new.
- Floats formatted through FormatNumber.
block orders clipPaths, gradients, then patterns by registration order so output is reproducible.
Constant Summary collapse
- CLIP_PREFIX =
"clip".freeze
- GRADIENT_PREFIX =
"grad".freeze
- PATTERN_PREFIX =
"pattern".freeze
Instance Method Summary collapse
-
#buffer_size ⇒ Object
Expose internal buffers for testing only.
- #close_group ⇒ Object
- #close_svg ⇒ Object
- #comment(text) ⇒ Object
- #image(href:, x:, y:, width:, height:, transform: nil) ⇒ Object
-
#initialize ⇒ SvgBuilder
constructor
A new instance of SvgBuilder.
- #open_group(transform: nil, clip_path_id: nil) ⇒ Object
- #open_svg(viewbox:, width:, height:) ⇒ Object
- #open_y_flip_group ⇒ Object
-
#path(d:, mode:, color: nil, stroke_color: nil, stroke_width: nil, line_cap: nil, line_join: nil, dash: nil, clip_path_id: nil, fill_id: nil) ⇒ Object
Emit a
. -
#register_clip_path(path_d) ⇒ Object
Register a clip path.
- #register_linear_gradient(stops:, x1:, y1:, x2:, y2:) ⇒ Object
- #register_pattern(width:, height:, body_blocks:) ⇒ Object
- #register_radial_gradient(stops:, cx:, cy:, r:, fx: nil, fy: nil) ⇒ Object
- #registered_clip_count ⇒ Object
- #registered_gradient_count ⇒ Object
- #text(content:, x:, y:, font_family:, font_size:, color:, transform: nil) ⇒ Object
- #to_s ⇒ Object
Constructor Details
#initialize ⇒ SvgBuilder
Returns a new instance of SvgBuilder.
18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# File 'lib/postsvg/svg_builder.rb', line 18 def initialize @buffer = String.new(capacity: 4096) @defs_buffer = String.new(capacity: 1024) @body_buffer = String.new(capacity: 4096) @clip_paths = {} # path_d -> id @gradients = {} # signature -> id @patterns = {} # signature -> id @next_clip_id = 1 @next_gradient_id = 1 @next_pattern_id = 1 @svg_open = false @group_depth = 0 @header_height = nil end |
Instance Method Details
#buffer_size ⇒ Object
Expose internal buffers for testing only.
205 206 207 |
# File 'lib/postsvg/svg_builder.rb', line 205 def buffer_size @buffer.bytesize + @defs_buffer.bytesize + @body_buffer.bytesize end |
#close_group ⇒ Object
58 59 60 61 62 63 64 |
# File 'lib/postsvg/svg_builder.rb', line 58 def close_group raise RenderError, "no open group" if @group_depth.zero? @body_buffer << %(</g>\n) @group_depth -= 1 self end |
#close_svg ⇒ Object
185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/postsvg/svg_builder.rb', line 185 def close_svg raise RenderError, "svg not open" unless @svg_open raise RenderError, "unclosed groups: #{@group_depth}" if @group_depth.positive? @buffer << "<defs>\n" unless @clip_paths.empty? && @gradients.empty? && @patterns.empty? @buffer << @defs_buffer @buffer << "</defs>\n" unless @clip_paths.empty? && @gradients.empty? && @patterns.empty? @buffer << @body_buffer @buffer << "</svg>\n" @svg_open = false self end |
#comment(text) ⇒ Object
180 181 182 183 |
# File 'lib/postsvg/svg_builder.rb', line 180 def comment(text) @body_buffer << %(<!-- #{escape(text)} -->\n) self end |
#image(href:, x:, y:, width:, height:, transform: nil) ⇒ Object
170 171 172 173 174 175 176 177 178 |
# File 'lib/postsvg/svg_builder.rb', line 170 def image(href:, x:, y:, width:, height:, transform: nil) attrs = +"" attrs << %( transform="#{transform}") if transform attrs << %( x="#{num(x)}" y="#{num(y)}") attrs << %( width="#{num(width)}" height="#{num(height)}") attrs << %( href="#{escape(href)}") @body_buffer << %(<image#{attrs} />\n) self end |
#open_group(transform: nil, clip_path_id: nil) ⇒ Object
49 50 51 52 53 54 55 56 |
# File 'lib/postsvg/svg_builder.rb', line 49 def open_group(transform: nil, clip_path_id: nil) attrs = +"" attrs << %( transform="#{transform}") if transform attrs << %( clip-path="url(##{clip_path_id})") if clip_path_id @body_buffer << %(<g#{attrs}>\n) @group_depth += 1 self end |
#open_svg(viewbox:, width:, height:) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 |
# File 'lib/postsvg/svg_builder.rb', line 33 def open_svg(viewbox:, width:, height:) raise RenderError, "svg already open" if @svg_open @svg_open = true @header_height = height @buffer << %(<?xml version="1.0" encoding="UTF-8"?>\n) @buffer << %(<svg xmlns="http://www.w3.org/2000/svg" ) @buffer << %(viewBox="#{viewbox}" ) @buffer << %(width="#{num(width)}" height="#{num(height)}">\n) self end |
#open_y_flip_group ⇒ Object
45 46 47 |
# File 'lib/postsvg/svg_builder.rb', line 45 def open_y_flip_group open_group(transform: "translate(0 #{num(@header_height)}) scale(1 -1)") end |
#path(d:, mode:, color: nil, stroke_color: nil, stroke_width: nil, line_cap: nil, line_join: nil, dash: nil, clip_path_id: nil, fill_id: nil) ⇒ Object
Emit a mode is one of :fill, :stroke, :fill_and_stroke.
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/postsvg/svg_builder.rb', line 132 def path(d:, mode:, color: nil, stroke_color: nil, stroke_width: nil, line_cap: nil, line_join: nil, dash: nil, clip_path_id: nil, fill_id: nil) attrs = +"d=\"#{d}\"" attrs << case mode when :fill fill_attr(color, fill_id) when :stroke %( fill="none" stroke="#{color_attr(stroke_color || color)}") when :fill_and_stroke "#{fill_attr(color, fill_id)} stroke=\"#{color_attr(stroke_color || color)}\"" else %( fill="none" stroke="none") end attrs << %( stroke-width="#{num(stroke_width)}") if stroke_width && stroke_width != 1.0 attrs << %( stroke-linecap="#{line_cap}") if line_cap && line_cap != :butt attrs << %( stroke-linejoin="#{line_join}") if line_join && line_join != :miter attrs << %( stroke-dasharray="#{dash}") if dash attrs << %( clip-path="url(##{clip_path_id})") if clip_path_id @body_buffer << %(<path #{attrs} />\n) self end |
#register_clip_path(path_d) ⇒ Object
Register a clip path. Returns the id used. If path_d was
previously registered, returns the existing id (dedup).
68 69 70 71 72 73 74 75 76 |
# File 'lib/postsvg/svg_builder.rb', line 68 def register_clip_path(path_d) return @clip_paths[path_d] if @clip_paths.key?(path_d) id = "#{CLIP_PREFIX}#{@next_clip_id}" @next_clip_id += 1 @clip_paths[path_d] = id @defs_buffer << %(<clipPath id="#{id}"><path d="#{path_d}" /></clipPath>\n) id end |
#register_linear_gradient(stops:, x1:, y1:, x2:, y2:) ⇒ Object
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 |
# File 'lib/postsvg/svg_builder.rb', line 78 def register_linear_gradient(stops:, x1:, y1:, x2:, y2:) sig = gradient_signature("linear", stops, [x1, y1, x2, y2]) return @gradients[sig] if @gradients.key?(sig) id = "#{GRADIENT_PREFIX}#{@next_gradient_id}" @next_gradient_id += 1 @gradients[sig] = id @defs_buffer << %(<linearGradient id="#{id}" ) @defs_buffer << %(x1="#{num(x1)}" y1="#{num(y1)}" ) @defs_buffer << %(x2="#{num(x2)}" y2="#{num(y2)}">\n) stops.each do |stop| @defs_buffer << %(<stop offset="#{num(stop[:offset])}" ) @defs_buffer << %(stop-color="#{stop[:color].to_svg}" />\n) end @defs_buffer << %(</linearGradient>\n) id end |
#register_pattern(width:, height:, body_blocks:) ⇒ Object
116 117 118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/postsvg/svg_builder.rb', line 116 def register_pattern(width:, height:, body_blocks:) sig = "pattern:#{width}:#{height}:#{body_blocks.join('|')}" return @patterns[sig] if @patterns.key?(sig) id = "#{PATTERN_PREFIX}#{@next_pattern_id}" @next_pattern_id += 1 @patterns[sig] = id @defs_buffer << %(<pattern id="#{id}" ) @defs_buffer << %(width="#{num(width)}" height="#{num(height)}" ) @defs_buffer << %(patternUnits="userSpaceOnUse">\n) body_blocks.each { |b| @defs_buffer << b } @defs_buffer << %(</pattern>\n) id end |
#register_radial_gradient(stops:, cx:, cy:, r:, fx: nil, fy: nil) ⇒ Object
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/postsvg/svg_builder.rb', line 96 def register_radial_gradient(stops:, cx:, cy:, r:, fx: nil, fy: nil) fx ||= cx fy ||= cy sig = gradient_signature("radial", stops, [cx, cy, r, fx, fy]) return @gradients[sig] if @gradients.key?(sig) id = "#{GRADIENT_PREFIX}#{@next_gradient_id}" @next_gradient_id += 1 @gradients[sig] = id @defs_buffer << %(<radialGradient id="#{id}" ) @defs_buffer << %(cx="#{num(cx)}" cy="#{num(cy)}" r="#{num(r)}" ) @defs_buffer << %(fx="#{num(fx)}" fy="#{num(fy)}">\n) stops.each do |stop| @defs_buffer << %(<stop offset="#{num(stop[:offset])}" ) @defs_buffer << %(stop-color="#{stop[:color].to_svg}" />\n) end @defs_buffer << %(</radialGradient>\n) id end |
#registered_clip_count ⇒ Object
209 210 211 |
# File 'lib/postsvg/svg_builder.rb', line 209 def registered_clip_count @clip_paths.size end |
#registered_gradient_count ⇒ Object
213 214 215 |
# File 'lib/postsvg/svg_builder.rb', line 213 def registered_gradient_count @gradients.size end |
#text(content:, x:, y:, font_family:, font_size:, color:, transform: nil) ⇒ Object
158 159 160 161 162 163 164 165 166 167 168 |
# File 'lib/postsvg/svg_builder.rb', line 158 def text(content:, x:, y:, font_family:, font_size:, color:, transform: nil) attrs = +"" attrs << %( transform="#{transform}") if transform attrs << %( x="#{num(x)}" y="#{num(y)}") attrs << %( font-family="#{escape(font_family)}") attrs << %( font-size="#{num(font_size)}") attrs << %( fill="#{color_attr(color)}") @body_buffer << %(<text#{attrs}>#{escape(content)}</text>\n) self end |
#to_s ⇒ Object
198 199 200 201 202 |
# File 'lib/postsvg/svg_builder.rb', line 198 def to_s raise RenderError, "svg not closed" if @svg_open @buffer.dup end |