Skip to content

Instantly share code, notes, and snippets.

@vidarh
Created September 24, 2013 00:51
Show Gist options
  • Save vidarh/6678989 to your computer and use it in GitHub Desktop.
Save vidarh/6678989 to your computer and use it in GitHub Desktop.
Rack middleware for inline Graphviz graphs
require 'digest'
module GViz
CACHE_PATH = "/tmp/gviz/"
ENGINES = %w{dot neato twopi circo fdp sfdp}
XSLTPROC = `which xsltproc`.chomp
notugly = File.dirname(__FILE__)+"/notugly.xsl"
NOTUGLY = (File.exists?(notugly) && XSLTPROC != "") ? notugly : nil
CONVERT = `which convert`
class Graph
def initialize(graph)
@graph = graph
end
def slug
# The MD5 hash is not for security, since I'm not accepting posted graphs,
# nor keeping any private graphs.
@slug ||= Digest::MD5.hexdigest(@graph)
end
def write(fname)
`mkdir -p #{CACHE_PATH}`
File.open(fname,"w") do |f|
f.write(@graph)
end
end
def cache
fname = "#{CACHE_PATH}#{slug}.dot"
write(fname) if !File.exists?(fname)
slug
end
def self.dot_file(slug)
"#{CACHE_PATH}#{slug}.dot"
end
end
class Cache
def initialize hash,engine,format
@hash = hash
@engine = engine
@format = format
raise "Unsupported layout" if !ENGINES.member?(@engine.to_s)
end
def layout(format, src, dest)
system("#{@engine.to_s} -T#{format.to_s} #{src} -o #{dest}")
end
def xsl(src,dest)
system("#{XSLTPROC} --nonet #{NOTUGLY} #{src} >#{dest}")
end
def convert(src,dest)
system("convert #{src} #{dest}")
end
def target_file(format=nil)
format ||= @format
CACHE_PATH+@hash+".#{format.to_s}"
end
def render(format=nil)
format ||= @format
src = Graph.dot_file(@hash)
dest = target_file(format)
if (format.to_sym == :svg && NOTUGLY)
layout(format,src,dest +".tmp")
xsl(dest+".tmp",dest)
elsif NOTUGLY && CONVERT != ""
layout(:svg, src, dest+".tmp")
xsl(dest+".tmp",dest+".tmp.svg")
convert(dest+".tmp.svg",dest)
else
layout(format,src,dest)
end
File.read(dest) rescue nil
end
def url(url_base)
"#{url_base}#{@engine}_#{@hash}.#{@format}"
end
def img_link(url_base)
"<img src='#{url(url_base)}' class='gviz' />"
end
def to_html(url_base="/images/gviz/")
return img_link(url_base) if @format != :inline_svg
svg = render(:svg)
svg = svg.split("\n")[2..-1] # Strip xml declaration and doctype
svg.join("\n")
end
def file
file = File.read(target_file) rescue nil
return file if file
render
end
def xsl(src,dest)
system("#{XSLTPROC} --nonet #{NOTUGLY} #{src} >#{dest}")
end
def convert(src,dest)
system("convert #{src} #{dest}")
end
def target_file(format=nil)
format ||= @format
CACHE_PATH+@hash+".#{format.to_s}"
end
def render(format=nil)
format ||= @format
src = Graph.dot_file(@hash)
dest = target_file(format)
if (format.to_sym == :svg && NOTUGLY)
layout(format,src,dest +".tmp")
xsl(dest+".tmp",dest)
elsif NOTUGLY && CONVERT != ""
layout(:svg, src, dest+".tmp")
xsl(dest+".tmp",dest+".tmp.svg")
convert(dest+".tmp.svg",dest)
else
layout(format,src,dest)
end
File.read(dest) rescue nil
end
def url(url_base)
"#{url_base}#{@engine}_#{@hash}.#{@format}"
end
def img_link(url_base)
"<img src='#{url(url_base)}' class='gviz' />"
end
def to_html(url_base="/images/gviz/")
return img_link(url_base) if @format != :inline_svg
svg = render(:svg)
svg = svg.split("\n")[2..-1] # Strip xml declaration and doctype
svg.join("\n")
end
def file
file = File.read(target_file) rescue nil
return file if file
render
end
end
#
# Cache the graph, and return either a link, or inline SVG depending
# on the preferred format
#
def self.graph_to_html(engine,graph,preferred_format=:inline_svg, url_base="/images/gviz/")
hash = Graph.new(graph).cache
Cache.new(hash,engine,preferred_format).to_html(url_base)
end
end
require 'lib/gviz'
require 'rack'
module GViz
class Controller
def initialize app, base = "/images/gviz/"
@app = app
@base = base
@uri_regexp = /^(#{ENGINES.join("|")})_([0-9a-f]{32}).(png|svg)$/
end
def call env
uri = env["REQUEST_URI"]
if (uri[0.. @base.size-1] != @base)
return @app.call(env) if @app
return Rack::Response.new("(empty)").finish
end
match = uri[@base.size .. -1].match(@uri_regexp)
if !match
return Rack::Response.new("Invalid URL",403)
end
engine = match[1]
hash = match[2]
format = match[3]
cache = GViz::Cache.new(hash,engine,format)
file = cache.file
return Rack::Response.new("No such graph",404) if !file
r = Rack::Response.new
r["Content-Type"] = format == "png" ? "image/png" : "image/svg+xml"
r.write(file)
r.finish
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment