Skip to content

Instantly share code, notes, and snippets.

@aphyr
Created June 2, 2016 21:44
Show Gist options
  • Save aphyr/6ec1edbfbbf0f6a1b4f9b80188f9ca75 to your computer and use it in GitHub Desktop.
Save aphyr/6ec1edbfbbf0f6a1b4f9b80188f9ca75 to your computer and use it in GitHub Desktop.
Script to fix the root viewbox on simple svg diagrams
#!/usr/bin/env ruby
require 'xml'
require 'pp'
require 'bigdecimal'
unless ARGV.first
puts "No file"
exit 1
end
parser = XML::Parser.file(ARGV.first)
doc = parser.parse
# All bounding boxes are {:x1, :x2, :y1, :y2}.
# Merge two bounding boxes
def merge2(a, b)
{x1: [a[:x1], b[:x1]].min,
x2: [a[:x2], b[:x2]].max,
y1: [a[:y1], b[:y1]].min,
y2: [a[:y2], b[:y2]].max}
end
# Merge a bunch of boxes
def merge(boxes)
boxes.reject(&:nil?).reduce { |a, b| merge2(a, b) }
end
# Make a decimal
def d(x)
BigDecimal.new x
end
# Computes the bounding box of the given node.
def bbox(node, offset)
x1, x2, y1, y2 = nil
case node.name
when "defs"
return nil
when "g"
if t = node["transform"]
# Parse a translation out of there, god this is such a hack
if t =~ /translate\(([\d\.]+),([\d\.]+)\)/
offset = {x: offset[:x] + d($1),
y: offset[:y] + d($2)}
p "offset", offset
end
end
return merge(node.children.map { |n| bbox(n, offset) })
when 'line'
x1 = d(node[:x1])
x2 = d(node[:x2])
y1 = d(node[:y1])
y2 = d(node[:y2])
when 'rect'
x1 = d(node[:x])
x2 = d(node[:x]) + d(node[:width])
y1 = d(node[:y])
y2 = d(node[:y]) + d(node[:height])
when "script"
return nil
when "svg"
return merge(node.children.map { |n| bbox(n, offset) })
when 'text'
# cool story: this is literally impossible
x = d(node[:x])
y = d(node[:y])
# guess font height and width
node[:style] =~ /font-size: ([\d\.]+)/
height = d($1) # font sizes are an em: roughly descender-to-ascender
width = node.content.length * 0.6 * height # greetings
node[:style] =~ /text-anchor: (\w+)/
case $1
when 'middle'
x1 = x - (width / 2)
x2 = x + (width / 2)
when 'left', 'start', nil
x1 = x
x2 = x + width
when 'right', 'end'
x1 = x - width
x2 = x
else
pp node
raise RuntimeError, "what kind of anchor is #{$1}?"
end
node[:style] =~ /alignment-baseline: (\w+)/
case $1
when 'middle'
y1 = y - (height / 2)
y2 = y + (height / 2)
when 'top'
y1 = y
y2 = y + height
when 'bottom' # not part of spec, maybe I shouldn't emit this
y1 = y - height
y2 = y
when nil
# ehhhh maybe?
y1 = y - (height * 0.7)
y2 = y + (height * 0.3)
else
pp node
raise RuntimeError, "what kind of baseline is #{$1}?"
end
else
pp node
raise RuntimeError, "what's a #{node.name}?"
end
{x1: x1 + offset[:x],
x2: x2 + offset[:x],
y1: y1 + offset[:y],
y2: y2 + offset[:y]}
end
b = bbox(doc.root, {x: 0, y: 0})
doc.root['viewBox'] = [b[:x1].to_f,
b[:y1].to_f,
(b[:x2] - b[:x1]).to_f,
(b[:y2] - b[:y1]).to_f].join(' ')
doc.save(ARGV.first, indent: true)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment