Strony

czwartek, 28 stycznia 2010

hacking cvs2svn ;)

Several months ago my BOSS asked me to transfer our ancient CVS repository to the brand new Subversion system. Not a big deal -I thought - there is lovely tool which does it for me: cvs2svn converter made by Tigris. But just after that I realized it wouldn't be such easy task, as our current repository is rather huge and holds several years of our projects history. Some fraction of packages was obsolete and additionally there was a request to split it to three different SVN repos and keep in a newly Subversion repos only recent year of history.

Not a big deal once again - I said after reading of cvs2svn documentation - we've got a database holding all the cvs tags used to built our software, so I'd make a few queries to know what to keep and that's all, except... OMG! There is no "include" pattern for tags!!!

You could in a easy way exclude symbols from cvs, which shouldn't be converted, but I've got completely opposite situation. Now, smart guys, please try to write me a reqexp which is nagative to a pattern... Oups!

What I did it was a little modification of cvs2svn tool in cvs2svn/cvs2svn_lib/symbol_strategy.py:

class IncludeRegexpStrategyRule(StrategyRule):
"""Include symbols matching pattern."""
def __init__(self, pattern):
try:
self.regexp = re.compile('^' + pattern + '$')
except re.error:
raise FatalError("%r is not a valid regexp." % (pattern,))
def log(self, symbol):
Log().verbose(
'Excluding symbol %s because it doesn\'t match regexp "%s".'
% (symbol, self.regexp.pattern,)
)
def get_symbol(self, symbol, stats):
if ( hasattr( symbol, "name" ) ):
if self.regexp.match( symbol.name ):
return symbol
else:
self.log( symbol )
return ExcludedSymbol(symbol)
else:
return symbol
view raw cvs2svn.py hosted with ❤ by GitHub
Then of course I wrote a script which:

  • asked our tag db for packages and their tags/branches that should be migrated
  • generated cvs2svn option file
  • and run cvs2svn tool


Several testing later a big migration day arrived on agenda. I've started to migrate about 9:00 AM and before 6:00 PM ca. 2k packages were moved generating initially about 190k revisions. And one day later we've opened new repos to the whole collaboration.

poniedziałek, 25 stycznia 2010

SVN hooks once again, this time tags, branches, log messages etc.

This one hook I wrote quite long ago is forcing users of Subversion repository to:

  • provide log message on every commit (class logChecker)
  • block removing of "/tags", "/branches" and "/trunk" directories from repo (class pathChecker)
  • forcing branch and tag naming convention
  • making subdirectories under /tags read-only


My company is running Subversion for storing all software packages and our repo hasn't got the default structure (/tags, /trunk and /branches as a top directories) — instead of that every package has got it's own /trunk, /tags and /branches subdirectories. So the repository structure is something like this:

/Path/
PackageA/
trunk/
src/
doc/
tags/
PackageA-00-00-00/
src/
doc/
...
branches/
PackageA-00-00-00-branch/
src/
doc/

PackageB/
...
/OtherPath/
PackageC/
...


Looks rather complicated, but it isn't. ;)

Also we've got tags and branches naming convention. Every tag should be made of package name, then a three (or four for tags made over a branch) groups of digits, e.g.: PackageName-ii-jj-kk or PackageName-ii-jj-kk-ll.
Every branch name is very similar to tag name, except there is additional (-branch) string at the end.

So here is the hook itself:
#!/usr/bin/env python
##
# @file svn-policy.py
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
# @date 06/05/2009
# @brief pre-commit hook
#
import os
import sys
import getopt
try:
my_getopt = getopt.gnu_getopt
except AttributeError:
my_getopt = getopt.getopt
import re
import svn
import svn.fs
import svn.repos
import svn.core
import StringIO
from subprocess import Popen, PIPE
## txnChecker
# @class txnChecker
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
# @brief base class for txn hook
class txnChecker( object ):
## c'tor
# @param self "Me, myself and Irene"
# @param txn txn
# @param fs fs
def __init__( self, txn, fs ):
self.txn = txn
self.fs = fs
## get txn property
def txnProp( self, prop_name ):
return svn.fs.svn_fs_txn_prop( self.txn, prop_name )
## block
def block( self, msg ):
sys.stderr.write( str( msg ) )
return False
## allow
def allow( self ):
return True
## logChecker
# @class logChecker
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmali.com)
# @brief blocks commit if empty "svn:log" property
class logChecker( txnChecker ):
## c'tor
# @param self "Me, myself and Irene"
# @param txn
def __init__( self, txn, fs ):
txnChecker.__init__( self, txn, fs )
## check if svn:log isn't empty
# @param self "Me, myself and Irene"
def check( self ):
logMsg = self.txnProp( "svn:log" )
if ( logMsg and logMsg.strip() != "" ): return self.allow()
return self.block( "\nSVN POLICY: Cannot enter in empty commit message.\n" )
## pathChecker
# @class pathChecker
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
# @brief blocks commit if changed path outside "trunk/tags/branches" for ordinary user
class pathChecker( txnChecker ):
## c'tor
# @param self "Me, myself and Irene"
# @param txn_name
def __init__( self, txn, fs ):
txnChecker.__init__( self, txn, fs )
## checker
# @param self "Me, myself and Irene"
def check( self ):
rePath = re.compile("(/tags)|(/branches)|(/trunk)" )
if ( self.txnProp("svn:author") == "librarian" ): return self.allow()
self.txn_root = svn.fs.svn_fs_txn_root( self.txn )
self.changed_paths = svn.fs.paths_changed( self.txn_root )
for path, change in self.changed_paths.iteritems():
if ( not rePath.search( path ) ):
msg = "\nSVN POLICY: invalid path '" + path + "' in commit.\n\n"
msg += "Only librarian could modify paths outside package's '/tags', '/branches' or '/trunk' subdirectories.\n\n"
msg += "Contact librarian ('librarian@mail') for details.\n"
return self.block( msg )
if ( change.change_kind == svn.fs.path_change_delete ):
if ( path.endswith("/tags/") or path.endswith("/tags") ):
msg = "\nSVN POLICY: Only librarian is able to remove package's '/tags' directory!\n\n"
msg += "This directory is required for normal package operations.\n\n"
msg += "Contact librarian ('librarian@mail') for details.\n"
return self.block( msg )
if ( path.endswith("/branches/") or path.endswith("/branches") ):
msg = "\nSVN POLICY: Only librarian is able to remove package's '/branches' directory!\n\n"
msg += "This directory is required for normal package operations.\n\n"
msg += "Contact librarian ('librariran@mail') for details.\n"
return self.block( msg )
if ( path.endswith("/trunk/") or path.endswith("/trunk") ):
msg = "\nSVN POLICY: Only librarian is able to remove package's '/trunk' directory!\n\n"
msg += "This directory is required for normal package operations.\n\n"
msg += "Contact librarian ('librarian@mail') for details.\n"
return self.block( msg )
return self.allow()
## branchChecker
# @class brancheChecker
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
# @brief blocks commit to branch if wrong branch name
class branchChecker( txnChecker ):
path_is_in_branches = re.compile( "(?i)\/branches\/" )
path_is_a_branch = re.compile( "(?i)\/branches\/[^\/]+\/?$" )
# modify this RE to match your branch naming convention,
# this one is PackageName-ii-jj-kk-branch
is_branch_name_valid = re.compile( "(^[A-Za-z0-9_]+-[0-9]{2}-[0-9]{2}-[0-9]{2}-branch$)" )
## c'tor
# @param self "Me, myself and Irene"
# @param txn txn
# @param fs fs
# @param repos_path path to the repo in server fs
def __init__( self, txn, fs, repos_path ):
txnChecker.__init__( self, txn, fs )
self.repos_path = repos_path
## check
# @param self "Me, myself and Irene"
def check( self ):
txn_root = svn.fs.svn_fs_txn_root( self.txn )
changed_paths = svn.fs.paths_changed( txn_root )
for path, change in changed_paths.iteritems():
if ( self.path_is_in_branches.search( path ) ):
if ( self.path_is_a_branch.search( path ) ):
if ( change.change_kind == svn.fs.path_change_delete and
self.txnProp("svn:author") not in ["librarian", "root" ] ):
msg = "\nSVN POLICY: Only an administrator can delete a branch.\n\n"
msg += "Contact librarian (librarian@mail) for details."
return self.block( msg )
if ( change.change_kind == svn.fs.path_change_add ):
words = path.split("/branches/")
if ( len(words) == 2 ):
new_branch = words[-1]
package_name_from_path = words[0].split("/")[-1]
package_name_from_branch = new_branch.split("-")[0]
if ( self.txnProp("svn:author") not in ["librarian", "root" ] ):
if ( package_name_from_branch != package_name_from_path or
not self.is_branch_name_valid.match( new_branch ) ):
msg = "\nSVN POLICY: invalid branch name '%s' for package %s\n\n" % ( new_branch , package_name_from_path )
msg += "The branch naming policy requires a branch of form "
msg += "'%s-ii-jj-kk-branch, where i,j and k are digits.\n" % package_name_from_path
return self.block( msg )
return self.allow()
## tagChecker
# @class tagChecker
# @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAgmail.com)
# @brief tag checking hook
class tagChecker( txnChecker ):
path_is_in_tags = re.compile( "(?i)\/tags\/" )
path_is_a_tag = re.compile( "(?i)\/tags\/[^\/]+\/?$" )
# tag naming policy RE
# modify it to match your own
# this one is
# PackageName-ii-jj-kk or PackageName-ii-jj-kk-ll
is_tag_name_valid = re.compile("(^[A-Za-z0-9_]+-[0-9]{2}-[0-9]{2}-[0-9]{2}$)|(^[A-Za-z0-9_]+-[0-9]{2}-[0-9]{2}-[0-9]{2}-[0-9]{2}$)")
## c'tor
# @param self "Me, myself and Irene"
# @param txn transacation
# @param fs fs
# @param repos_path path in local file system to the repository
def __init__( self, txn, fs, repos_path ):
txnChecker.__init__( self, txn, fs )
self.repos_path = repos_path
## check tags
# @param self "Me, myself and Irene"
def check( self ):
txn_root = svn.fs.svn_fs_txn_root( self.txn )
changed_paths = svn.fs.paths_changed( txn_root )
for path, change in changed_paths.iteritems():
if ( self.path_is_in_tags.search( path ) ):
tagPath = path.split("/")
tagPath = "/".join( tagPath[0:tagPath.index("tags")+2])
self.pool = svn.core.svn_pool_create(None)
self.repos_ptr = svn.repos.open( self.repos_path, self.pool )
self.fs_ptr = svn.repos.fs( self.repos_ptr )
youngest_rev = svn.fs.svn_fs_youngest_rev( self.fs_ptr )
self.rev_root = svn.fs.revision_root( self.fs_ptr, youngest_rev, self.pool)
kind = svn.fs.svn_fs_check_path( self.rev_root, tagPath )
if ( kind != 0 and self.txnProp("svn:author") not in [ "librarian", "root"] ):
msg = "\nSVN POLICY: Unable to modify %s path.\n\n" % tagPath
msg += "It is within a '/tags' directory and tags are read-only.\n\n"
msg += "Possible reasons:\n"
msg += "[1] Maybe you forget to switch the context (svn switch) after check out of a tag?\n"
msg += "[2] This tag already exists in repository and you have tried to create it once again?\n\n"
msg += "Contact librarian ('librarian@mail') for details."
return self.block( msg )
if ( self.path_is_a_tag.search( path ) ):
#log.write(path + " is a tag!\n")
if ( change.change_kind == svn.fs.path_change_add ):
words = path.split("/tags/")
if ( len(words) == 2 ):
new_tag = words[-1]
package_name_from_path = words[0].split("/")[-1]
package_name_from_tag = new_tag.split("-")[0]
if ( self.txnProp("svn:author") not in [ "librarian", "root" ] ):
if ( package_name_from_tag != package_name_from_path or
not self.is_tag_name_valid.match( new_tag ) ):
msg = "\nSVN POLICY: invalid tag name '%s' for package %s\n\n" % ( new_tag , package_name_from_path )
msg += "The tag naming policy requires a tag of form:\n"
msg += " [01] %s-ii-jj-kk (regular tags)\n" % package_name_from_path
msg += " [02] %s-ii-jj-kk-ll (tags created on branches)\n" % package_name_from_path
msg += "where i,j,k and l are digits.\n"
return self.block( msg )
if ( change.change_kind == svn.fs.path_change_delete and
self.txnProp("svn:author") not in [ "alibrari", "root" ] ):
msg = "\nSVN POLICY: Only an administrator can delete a tag.\n\n"
msg += "You have tried to remove " + tagPath + " tag from repository, if you need so\n"
msg += "contact librarian ('librarian@mail') for details."
return self.block( msg )
return self.allow()
## write usage eand exit
def usage_and_exit(error_msg=None):
import os.path
stream = error_msg and sys.stderr or sys.stdout
if error_msg:
stream.write("ERROR: %s\n\n" % error_msg)
stream.write("USAGE: %s -t TXN_NAME REPOS\n"
% (os.path.basename(sys.argv[0])))
sys.exit(error_msg and 1 or 0)
## start processing
if __name__ == '__main__':
repos_path = None
txn_name = None
try:
opts, args = my_getopt( sys.argv[1:], 't:h?', ["help"])
except:
usage_and_exit("problem processing arguments / options.")
for opt, value in opts:
if opt == '--help' or opt == '-h' or opt == '-?':
usage_and_exit()
elif opt == '-t':
txn_name = value
else:
usage_and_exit("unknown option '%s'." % opt)
if txn_name is None:
usage_and_exit("must provide -t argument")
if len(args) != 1:
usage_and_exit("only one argument allowed (the repository).")
repos_path = svn.core.svn_path_canonicalize(args[0])
fs = svn.repos.svn_repos_fs( svn.repos.svn_repos_open(repos_path) )
txn = svn.fs.svn_fs_open_txn(fs, txn_name)
checks = [ logChecker( txn, fs ),
pathChecker( txn, fs ),
branchChecker( txn, fs, repos_path ),
tagChecker( txn, fs, repos_path ) ]
for check in checks:
if ( not check.check() ): sys.exit(1)
sys.exit(0)
view raw svn-policy.py hosted with ❤ by GitHub


If you want to use this one, you have to modify it a little:

  • replace 'librarian' or/and 'root' account name with your own repository admin
  • put repository admin mail to block messages - replace this fake 'librarian@mail' address
  • change regexp ffor tags and branches to match your criteria
  • put svn-policy.py script into your repository hook directory ($REPO/hooks)
  • modify "$REPO/hooks/pre-commit" script to switch this one on

    #!/bin/sh
    REPOS="$1"
    TXN="$2"
    SVNPOLICY=/path/to/repo/hooks/svn-policy.py # modify this line
    #SVN policy
    $SVNPOLICY -t "$TXN" "$REPOS" || exit 1
    # All checks passed, so allow the commit.
    exit 0



Maybe the code needs some cleaning and better structure but anyway happy using, comments and questions are welcome!

Cheers,
Krzysztof

piątek, 22 stycznia 2010

rendering graphs in PHP using GraphViz

Well known and very popular graph rendering software Graphviz has got many different language bindings and is widely used in many different areas of maths and computing. I don't have to speak more. It just renders all those graphs in easy way using very simple but powerful language.

Amongst various tools which are using Graphviz there is also a PHP package in PEAR — Image_GraphViz, which has got only one pitfall — it cannot render cluster (a subgraph) inside a cluster (at least not in 1.2.1 version). So, I modified it a bit to make it possible:

require_once("System.php");
/**
* @class graphviz
* @author Krzysztof Daniel Ciba (Krzysztof.Ciba@NOSPAMgmail.com)
* @brief class for making Graphviz plots online
*/
class graphviz {
var $directed = True;
var $_returnFalseOnError = true;
var $graph = array( "name" => "G",
"__attrs__" => array(),
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() );
var $fmt = "svg";
var $dotCommand = "/usr/bin/dot";
var $neatoCommand = "/usr/bin/neato";
/**
* @brief c'tor
* @param $name graph name
* @param $fmt output format
*/
public function __construct( $name=null, $fmt=null, $attr=null ) {
if ( $attr == null ) $attr = array();
$this->graph["_attrs__"] = $attr;
if ( $name != null && is_string($name) ) {
$this->graph["name"] = $name;
} else {
$name = "G";
}
if ( $fmt != null && is_string($fmt) ) {
$this->fmt = $fmt;
}
$this->graph = array( "name" => $name,
"__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() );
}
/**
* @brief sets directed/undirected flag for the graph.
* @param $directed Directed (TRUE) or undirected (FALSE) graph.
*/
public function setDirected( $directed=True ) {
if (is_bool($directed)) {
$this->directed = $directed;
}
}
/**
* @brief cluster finder
* @param $clusterName name of cluster to find
* @param $where reference to array
*/
public function &findCluster( $clusterName, &$where=null) {
if ( $where == null ) {
$where =& $this->graph;
}
if ( isset( $where["__clusters__"][$clusterName] ) ) {
return $where["__clusters__"][$clusterName];
} else {
foreach ( $where["__clusters__"] as $key => &$cluster ) {
return $this->findCluster( $clusterName, &$cluster );
}
}
return null;
}
/**
* @brief add subcluster to graph
* @param $id new cluster id
* @parm $attr array with attributes
* @param $toCluster parent subcluster
*/
public function addCluster( $id, $attr=null, $toCluster=null ) {
if ( $attr == null) $attr = array();
if ( $toCluster == null ) {
if ( ! isset( $this->graph["__clusters__"][$id] ) ) {
$this->graph["__clusters__"][$id] = array( "__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() ) ;
}
} else {
$where =& $this->findCluster( $toCluster );
if ( $where != null ) {
$where["__clusters__"][$id] = array( "__attrs__" => $attr,
"__nodes__" => array(),
"__edges__" => array(),
"__clusters__" => array() ) ;
}
}
}
/**
* @brief add node to graph
* @param $id node id
* @param $attr array with node attributes
* @param $cluster parent cluster
*/
public function addNode( $id, $attr=null, $cluster=null ) {
if ( $attr == null ) $attr = array();
if ( $cluster != null ) {
$where =& $this->findCluster( $cluster );
if ( $where != null ) {
$where["__nodes__"][$id] = $attr;
} else {
$this->addCluster($cluster);
$this->graph["__clusters__"][$cluster]["__nodes__"][$id] = $attr;
}
} else {
$this->graph["__nodes__"][$id] = $attr;
}
}
/**
* @brief connect to nodes
* @param $from begin node
* @param $to end node
* @param $attr edge attributes
*/
public function addEdge( $from, $to, $attr=null ) {
$id = $from . "-->" . $to;
if ( !isset( $this->graph["__edges__"][$id]) ) {
$this->graph["__edges__"][$id] = array( "__from__" => $from, "__to__" => $to, "__attrs__" => array() );
if ( is_array( $attr ) ) {
$this->graph["__edges__"][$id]["__attrs__"] = $attr;
}
} else {
if ( is_array($attr) ) {
$this->graph["__edges__"][$id]["__attrs__"] = array_merge($attr, $this->graph["__edges__"][$id]["__attrs__"] );
}
}
}
/**
* @brief transform internal subclusters representation to dot language
* @param where location to parse
* @return string with GraphViz markup
*/
private function __parseClusters( $where = null ) {
$out = "";
if ( $where == null ) {
$where = $this->graph;
}
foreach ( $where["__clusters__"] as $clusterName => $cluster ) {
$out .= "subgraph cluster_".$clusterName. " {\n";
// attributes
foreach ( $cluster["__attrs__"] as $attr => $attrValue ) {
$attributeList[] = $attr.'="'.$attrValue.'"';
}
if (!empty($attributeList) ) {
$out .= implode(';', $attributeList).";\n";
}
// nodes
foreach ( $cluster["__nodes__"] as $nodeName => $attr ) {
foreach ( $attr as $attrName => $attrValue ) {
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\"";
}
$out .= $nodeName;
if ( !empty($nodeAttr) ) {
$out .= " [ " . implode(',', $nodeAttr) . " ]";
}
$out .= ";\n";
}
$out .= $this->__parseClusters( $cluster );
$out .= "}\n";
}
return $out;
}
/**
* @brief transform internal graph representation to dot language
* @return string GraphViz markup
*/
public function parse() {
$parsedGraph = $this->directed ? "digraph " : "graph ";
if ( isset($this->graph["name"]) &&
is_string($this->graph["name"]) ) {
$parsedGraph .= $this->graph["name"] . " {\n";
} else {
$parsedGraph .= "G {\n";
}
if (isset($this->graph["__attrs__"])) {
foreach ($this->graph["__attrs__"] as $key => $value) {
$attributeList[] = $key . '="' . $value . '"';
}
if ( !empty($attributeList) ) {
$parsedGraph .= "graph [ ".implode(",", $attributeList) . " ];\n";
}
}
// subclusters
$parsedGraph .= $this->__parseClusters();
foreach ( $this->graph["__nodes__"] as $nodeName => $attr ) {
foreach ( $attr as $attrName => $attrValue ) {
$nodeAttr[] = $attrName . "=\"" . $attrValue . "\"";
}
$parsedGraph .= $nodeName;
if ( !empty($nodeAttr) ) {
$parsedGraph .= " [ " . implode(',', $nodeAttr) . " ]";
}
$parsedGraph .= ";\n";
}
// edges
foreach ( $this->graph["__edges__"] as $id => $edge ) {
$from = $edge["__from__"];
$to = $edge["__to__"];
$egdeAttributes = $edge["__attrs__"];
foreach ( $edgeAttributes as $attrName => $attrValue ) {
$edgeAttrList[] = $attrName."=\"".$attrValue."\"";
}
$parsedGraph .= $from . " -> " . $to;
if ( !empty($edgeAttrList) ) {
$parsedGraph .= " [ " . implode(",", $edgeAttrList) . " ]";
}
$parsedGraph .= ";\n";
}
// end of graph
$parsedGraph .= "}\n";
return $parsedGraph;
}
function fetch($format = 'svg', $command = null) {
$file = $this->saveParsedGraph();
if (!$file || PEAR::isError($file)) {
return $file;
}
$outputfile = $file . '.' . $format;
$rendered = $this->renderDotFile($file, $outputfile, $format,
$command);
if ($rendered !== true) {
return $rendered;
}
@unlink($file);
$fp = fopen($outputfile, 'rb');
if (!$fp) {
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not read rendered file');
return $error;
}
$data = fread($fp, filesize($outputfile));
fclose($fp);
@unlink($outputfile);
return $data;
}
function saveParsedGraph($file = '') {
$parsedGraph = $this->parse();
if (!empty($parsedGraph)) {
if (empty($file)) {
$file = System::mktemp('graph_');
}
if ($fp = @fopen($file, 'wb')) {
@fputs($fp, $parsedGraph);
@fclose($fp);
return $file;
}
}
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not save graph');
return $error;
}
function renderDotFile($dotfile, $outputfile, $format = 'svg', $command = null) {
if (!file_exists($dotfile)) {
if ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError('Could not find dot file');
return $error;
}
$oldmtime = file_exists($outputfile) ? filemtime($outputfile) : 0;
switch ($command) {
case 'dot':
case 'neato':
break;
default:
$command = $this->directed ? 'dot' : 'neato';
}
$command_orig = $command;
$command = $this->binPath.(($command == 'dot') ? $this->dotCommand : $this->neatoCommand);
$command .= ' -T'.escapeshellarg($format) .' -o'.escapeshellarg($outputfile)
.' '.escapeshellarg($dotfile).' 2>&1';
exec($command, $msg, $return_val);
clearstatcache();
if (file_exists($outputfile) &&
filemtime($outputfile) > $oldmtime
&& $return_val == 0 ) {
return true;
} elseif ($this->_returnFalseOnError) {
return false;
}
$error = PEAR::raiseError($command_orig.' command failed: '.implode("\n", $msg));
return $error;
}
/**
*
* Outputs image of the graph in a given format
* This methods send HTTP headers
* @param string $format Format of the output image. This may be one
* of the formats supported by GraphViz.
* @param string $command "dot" or "neato"
*
* @return boolean TRUE on success, FALSE or PEAR_Error otherwise
*/
public function image( $format = 'svg', $command = null ) {
$file = $this->saveParsedGraph();
if (!$file || PEAR::isError($file)) {
return $file;
}
$outputfile = $file . '.' . $format;
$rendered = $this->renderDotFile($file, $outputfile, $format, $command);
if ($rendered !== true) { return $rendered; }
$sendContentLengthHeader = true;
switch (strtolower($format)) {
case 'gif':
case 'png':
case 'bmp':
case 'jpeg':
case 'tiff':
header('Content-Type: image/' . $format);
break;
case 'tif':
header('Content-Type: image/tiff');
break;
case 'jpg':
header('Content-Type: image/jpeg');
break;
case 'ico':
header('Content-Type: image/x-icon');
break;
case 'wbmp':
header('Content-Type: image/vnd.wap.wbmp');
break;
case 'pdf':
header('Content-Type: application/pdf');
break;
case 'mif':
header('Content-Type: application/vnd.mif');
break;
case 'vrml':
header('Content-Type: application/x-vrml');
break;
case 'svg':
header('Content-Type: image/svg+xml');
break;
case 'plain':
case 'plain-ext':
header('Content-Type: text/plain');
break;
default:
header('Content-Type: application/octet-stream');
$sendContentLengthHeader = false;
}
if ($sendContentLengthHeader) {
header('Content-Length: ' . filesize($outputfile));
}
$return = true;
if (readfile($outputfile) === false) {
$return = false;
}
@unlink($outputfile);
return $return;
}
}
class test_graphviz {
public function __construct($fmt=null) {
$this->gr = new graphviz( "a", "svg", array( "bgcolor" => "#ffff00") );
$this->gr->addCluster( "main", array( "label" => "main", "labelloc"=>"t", "bgcolor" => "#ff0000", "labeljust"=>"l" ), null );
$this->gr->addCluster( "sub", array( "label" => "sub", "labelloc"=>"t", "bgcolor" => "#0000ff", "labeljust"=>"l" ), "main" );
$this->gr->addNode( "node_in_graph", array("shape" => "box", "color" => "#00ff00") );
$this->gr->addNode( "node_in_main", array("shape" => "box"), "main" );
$this->gr->addNode( "node_in_sub", array("shape" => "box"), "sub" );
$this->gr->addEdge( "node_in_main", "node_in_graph" );
$this->gr->addEdge( "node_in_main", "node_in_sub" );
$this->gr->addEdge( "node_in_sub", "node_in_graph" );
if ( $fmt == null ) {
$this->gr->image();
} else {
echo "
---dot---\n";
print_r( $this->gr->graph );
echo "
";
echo "
---graph---\n";
echo $this->gr->parse();
echo "
";
}
}
}
if ( isset($_GET["dot"] ) ) {
$t = new test_graphviz( "dot" );
}
if ( isset($_GET["test"] ) ) {
$t = new test_graphviz();
}
view raw Render.php hosted with ❤ by GitHub


Here is the results of running above example on apache (it renders image in svg format, so not all web browsers are able to display it correctly):



Test Graph in SVG

I was very happy when I'd figured out that my changes went into the official Image_GraphViz 1.3.0RC3. Good luck folks!