Created
June 2, 2016 21:44
-
-
Save aphyr/6ec1edbfbbf0f6a1b4f9b80188f9ca75 to your computer and use it in GitHub Desktop.
Script to fix the root viewbox on simple svg diagrams
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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