diff --git a/src-cpp/graphviz/CMakeLists.txt b/src-cpp/graphviz/CMakeLists.txt index 23e57a99..7af6bd1c 100644 --- a/src-cpp/graphviz/CMakeLists.txt +++ b/src-cpp/graphviz/CMakeLists.txt @@ -32,7 +32,10 @@ set_property(SOURCE main.cpp APPEND PROPERTY OBJECT_DEPENDS ${CMAKE_CURRENT_BINA # --- --- --- set(SRCS + main.hpp main.cpp + util.hpp + util.cpp ) include_directories( diff --git a/src-cpp/graphviz/main.cpp b/src-cpp/graphviz/main.cpp index 18b40dc1..260671c2 100644 --- a/src-cpp/graphviz/main.cpp +++ b/src-cpp/graphviz/main.cpp @@ -1,14 +1,8 @@ #include "main.hpp" #include -// #include #include #include -#include -// #include -// #include -// #include -// #include #include @@ -40,11 +34,11 @@ lt_symlist_t lt_preloaded_symbols[] = { {"gvplugin_core_LTX_library", &gvplugin_core_LTX_library}, {0, 0}}; -char lastErrorStr[1024]; +StringBuffer lastErrorStr; int vizErrorf(char *buf) { - strncpy(lastErrorStr, buf, sizeof(lastErrorStr) - 1); + lastErrorStr = buf; return 0; } @@ -67,6 +61,11 @@ Graphviz::Graphviz(int yInvert, int nop) { Y_invert = yInvert > 0 ? yInvert : origYInvert; Nop = nop > 0 ? nop : origNop; + + lastErrorStr = ""; + agseterr(AGERR); + agseterrf(vizErrorf); + agreadline(1); } Graphviz::~Graphviz() @@ -86,182 +85,84 @@ void Graphviz::createFile(const char *path, const char *data) path, data); } -const char *Graphviz::lastResult() -{ - return m_result.c_str(); -} - const char *Graphviz::layout(const char *src, const char *format, const char *engine) { - lastErrorStr[0] = '\0'; - m_result = ""; - - // const auto demand_loading = false; - // auto gvc = std::make_shared(lt_preloaded_symbols, demand_loading); - // auto g = std::make_shared(dot); - // const auto layout = GVC::GVLayout(gvc, g, engine); - // const auto result = layout.render(format); - // m_result = result.string_view(); - - // return m_result.c_str(); + layout_result = ""; GVC_t *gvc = gvContextPlugins(lt_preloaded_symbols, true); - agseterr(AGERR); - agseterrf(vizErrorf); - - agreadline(1); - - Agraph_t *graph; - char *data = NULL; - unsigned int length; - while ((graph = agmemread(src))) + Agraph_t *graph = agmemread(src); + if (graph) { - if (data == NULL) - { - gvLayout(gvc, graph, engine); - gvRenderData(gvc, graph, format, &data, &length); - gvFreeLayout(gvc, graph); - } - + char *data = NULL; + unsigned int length; + + gvLayout(gvc, graph, engine); + gvRenderData(gvc, graph, format, &data, &length); + layout_result = data; + gvFreeRenderData(data); + gvFreeLayout(gvc, graph); agclose(graph); - - src = ""; } - m_result = data; - gvFreeRenderData(data); gvFinalize(gvc); gvFreeContext(gvc); - return m_result.c_str(); -} - -int myindegree(Agnode_t *n) -{ - return agdegree(n->root, n, TRUE, FALSE); + return layout_result; } -/* need outdegree without selfarcs */ -int myoutdegree(Agnode_t *n) +bool Graphviz::acyclic(const char *src, bool doWrite, bool verbose) { - Agedge_t *e; - int rv = 0; + acyclic_outFile = ""; + acyclic_num_rev = 0; + bool retVal = false; - for (e = agfstout(n->root, n); e; e = agnxtout(n->root, e)) + Agraph_t *graph = agmemread(src); + if (graph) { - if (agtail(e) != aghead(e)) - rv++; + TempFileBuffer outFile; + graphviz_acyclic_options_t opts = {outFile, doWrite, verbose}; + retVal = graphviz_acyclic(graph, &opts, &acyclic_num_rev); + acyclic_outFile = outFile; + agclose(graph); } - return rv; -} - -bool isleaf(Agnode_t *n) -{ - return myindegree(n) + myoutdegree(n) == 1; + return retVal; } -bool ischainnode(Agnode_t *n) +void Graphviz::tred(const char *src, bool verbose, bool printRemovedEdges) { - return myindegree(n) == 1 && myoutdegree(n) == 1; -} - -void adjustlen(Agedge_t *e, Agsym_t *sym, int newlen) -{ - char buf[12]; + tred_out = ""; + tred_err = ""; - snprintf(buf, sizeof(buf), "%d", newlen); - agxset(e, sym, buf); + Agraph_t *graph = agmemread(src); + if (graph) + { + TempFileBuffer out; + TempFileBuffer err; + graphviz_tred_options_t opts = {verbose, printRemovedEdges, out, err}; + graphviz_tred(graph, &opts); + tred_out = out; + tred_err = err; + agclose(graph); + } } -Agsym_t *bindedgeattr(Agraph_t *g, const char *str) +const char *Graphviz::unflatten(const char *src, int maxMinlen, bool do_fans, int chainLimit) { - return agattr(g, AGEDGE, const_cast(str), ""); -} + unflatten_out = ""; -const char *Graphviz::unflatten(const char *src, unsigned int MaxMinlen, bool Do_fans, unsigned int ChainLimit) -{ - lastErrorStr[0] = '\0'; - m_result = ""; - Agraph_t *g = agmemread(src); - if (g) + Agraph_t *graph = agmemread(src); + if (graph) { + graphviz_unflatten_options_t opts = {do_fans, maxMinlen, chainLimit}; + graphviz_unflatten(graph, &opts); - Agnode_t *ChainNode; - unsigned int ChainSize = 0; - - Agnode_t *n; - Agedge_t *e; - char *str; - Agsym_t *m_ix, *s_ix; - int cnt, d; - - m_ix = bindedgeattr(g, "minlen"); - s_ix = bindedgeattr(g, "style"); - - for (n = agfstnode(g); n; n = agnxtnode(g, n)) - { - d = myindegree(n) + myoutdegree(n); - if (d == 0) - { - if (ChainLimit < 1) - continue; - if (ChainNode) - { - e = agedge(g, ChainNode, n, const_cast(""), TRUE); - agxset(e, s_ix, "invis"); - ChainSize++; - if (ChainSize < ChainLimit) - ChainNode = n; - else - { - ChainNode = NULL; - ChainSize = 0; - } - } - else - ChainNode = n; - } - else if (d > 1) - { - if (MaxMinlen < 1) - continue; - cnt = 0; - for (e = agfstin(g, n); e; e = agnxtin(g, e)) - { - if (isleaf(agtail(e))) - { - str = agxget(e, m_ix); - if (str[0] == 0) - { - adjustlen(e, m_ix, cnt % MaxMinlen + 1); - cnt++; - } - } - } - - cnt = 0; - for (e = agfstout(g, n); e; e = agnxtout(g, e)) - { - if (isleaf(e->node) || (Do_fans && ischainnode(e->node))) - { - str = agxget(e, m_ix); - if (str[0] == 0) - adjustlen(e, m_ix, cnt % MaxMinlen + 1); - cnt++; - } - } - } - } - FILE *fp = fopen("tmp.dot", "w"); - agwrite(g, fp); - fclose(fp); - std::ifstream file("tmp.dot"); - std::string graph_str((std::istreambuf_iterator(file)), std::istreambuf_iterator()); - remove("tmp.dot"); - m_result = graph_str; + TempFileBuffer tempFile; + agwrite(graph, tempFile); + unflatten_out = tempFile; + agclose(graph); } - return m_result.c_str(); + return unflatten_out; } // Include JS Glue --- diff --git a/src-cpp/graphviz/main.hpp b/src-cpp/graphviz/main.hpp index 624f0dd4..d60cfcc6 100644 --- a/src-cpp/graphviz/main.hpp +++ b/src-cpp/graphviz/main.hpp @@ -1,10 +1,9 @@ #include +#include "util.hpp" class Graphviz { private: - std::string m_result; - public: static const char *version(); static const char *lastError(); @@ -13,7 +12,18 @@ class Graphviz ~Graphviz(); void createFile(const char *path, const char *data); - const char *lastResult(); + + StringBuffer layout_result; const char *layout(const char *dot, const char *format, const char *engine); - const char *unflatten(const char *dot, unsigned int MaxMinlen = 0, bool Do_fans = false, unsigned int ChainLimit = 0); + + StringBuffer acyclic_outFile; + size_t acyclic_num_rev; + bool acyclic(const char *dot, bool doWrite = false, bool verbose = false); + + StringBuffer tred_out; + StringBuffer tred_err; + void tred(const char *dot, bool verbose = false, bool printRemovedEdges = false); + + StringBuffer unflatten_out; + const char *unflatten(const char *dot, int maxMinlen = 0, bool do_fans = false, int chainLimit = 0); }; diff --git a/src-cpp/graphviz/main.idl b/src-cpp/graphviz/main.idl index 8731b064..e6a71727 100644 --- a/src-cpp/graphviz/main.idl +++ b/src-cpp/graphviz/main.idl @@ -4,7 +4,17 @@ interface Graphviz [Const] static DOMString version(); [Const] static DOMString lastError(); void createFile([Const] DOMString file, [Const] DOMString data); - [Const] DOMString lastResult(); + + [Const] attribute DOMString layout_result; [Const] DOMString layout([Const] DOMString dot, [Const] DOMString format, [Const] DOMString engine); + + [Const] attribute DOMString acyclic_outFile; + attribute long acyclic_num_rev; + boolean acyclic([Const] DOMString dot, boolean doWrite, boolean verbose); + + [Const] attribute DOMString tred_out; + [Const] attribute DOMString tred_err; + void tred([Const] DOMString dot, boolean verbose, boolean printRemovedEdges); + [Const] DOMString unflatten([Const] DOMString dot, long MaxMinlen, boolean Do_fans, long ChainLimit); }; diff --git a/src-cpp/graphviz/util.cpp b/src-cpp/graphviz/util.cpp new file mode 100644 index 00000000..e019cdcb --- /dev/null +++ b/src-cpp/graphviz/util.cpp @@ -0,0 +1,55 @@ +#include "util.hpp" + +StringBuffer::operator std::string() const +{ + return m_buffer; +} + +StringBuffer::operator const char *() const +{ + return m_buffer.c_str(); +} + +StringBuffer &StringBuffer::operator=(const std::string &str) +{ + m_buffer = str; + return *this; +} + +TempFileBuffer::TempFileBuffer() +{ + if (std::tmpnam(tempFileName) == nullptr) + throw std::runtime_error("Failed to generate a unique temporary file name."); + + filePointer = std::fopen(tempFileName, "w+"); + if (filePointer == nullptr) + throw std::runtime_error("Failed to open temporary file for writing."); +} + +TempFileBuffer::~TempFileBuffer() +{ + if (filePointer != nullptr) + { + std::fclose(filePointer); + std::remove(tempFileName); + } +} + +TempFileBuffer::operator FILE *() const +{ + return filePointer; +} + +TempFileBuffer::operator std::string() const +{ + std::string content; + if (filePointer != nullptr) + { + std::rewind(filePointer); + char buffer[256]; + size_t bytesRead; + while ((bytesRead = std::fread(buffer, 1, sizeof(buffer), filePointer)) > 0) + content.append(buffer, bytesRead); + } + return content; +} diff --git a/src-cpp/graphviz/util.hpp b/src-cpp/graphviz/util.hpp new file mode 100644 index 00000000..d08391d8 --- /dev/null +++ b/src-cpp/graphviz/util.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include + +class StringBuffer +{ +private: + std::string m_buffer; + +public: + operator std::string() const; + operator const char *() const; + StringBuffer &operator=(const std::string &str); +}; + +class TempFileBuffer +{ +private: + FILE *filePointer = nullptr; + char tempFileName[L_tmpnam]; + +public: + TempFileBuffer(); + ~TempFileBuffer(); + + operator FILE *() const; + operator std::string() const; + operator const char *() const; +}; diff --git a/src-ts/__tests__/graphviz.ts b/src-ts/__tests__/graphviz.ts index 69b38876..779b8af0 100644 --- a/src-ts/__tests__/graphviz.ts +++ b/src-ts/__tests__/graphviz.ts @@ -285,6 +285,132 @@ describe("options", async function () { const stripWhitespaces = str => str.replace(/[\r\n\t\s]+/g, ""); +describe("acyclic", async function () { + it("simple", async function () { + Graphviz.unload(); + const graphviz = await Graphviz.load(); + const acyclicDot = `\ +digraph { + a -> b; + b -> c; + c -> d; + d -> a; + d -> e; + e -> d; +}`; + let retVal = graphviz.acyclic(acyclicDot); + expect(retVal.acyclic).to.equal(true); + expect(retVal.num_rev).to.equal(2); + expect(retVal.outFile).to.be.empty; + retVal = graphviz.acyclic(acyclicDot, true); + expect(retVal.acyclic).to.equal(true); + expect(retVal.num_rev).to.equal(2); + expect(retVal.outFile).to.not.be.empty; + retVal = graphviz.acyclic(acyclicDot, true, true); + expect(retVal.acyclic).to.equal(true); + expect(retVal.num_rev).to.equal(2); + expect(retVal.outFile).to.not.be.empty; + const notAcyclicDot = `\ +digraph { + a -> b; + b -> c; + c -> d; + d -> e; +}`; + retVal = graphviz.acyclic(notAcyclicDot); + expect(retVal.acyclic).to.equal(false); + expect(retVal.num_rev).to.equal(0); + expect(retVal.outFile).to.be.empty; + retVal = graphviz.acyclic(notAcyclicDot, true); + expect(retVal.acyclic).to.equal(false); + expect(retVal.num_rev).to.equal(0); + expect(retVal.outFile).to.not.be.empty; + retVal = graphviz.acyclic(notAcyclicDot, true, true); + expect(retVal.acyclic).to.equal(false); + expect(retVal.num_rev).to.equal(0); + expect(retVal.outFile).to.not.be.empty; + }); + + it("empty", async function () { + const graphviz = await Graphviz.load(); + const retVal = graphviz.acyclic(""); + expect(retVal.acyclic).to.equal(false); + expect(retVal.num_rev).to.equal(0); + expect(retVal.outFile).to.be.empty; + }); + + it("syntax error", async function () { + const graphviz = await Graphviz.load(); + try { + const retVal = graphviz.acyclic(badDot); + expect(true).to.be.false; + } catch (e: any) { + expect(typeof e.message).to.equal("string"); + expect(e.message).to.contain("syntax error in line"); + } + }); +}); + +describe("tred", async function () { + it("simple", async function () { + Graphviz.unload(); + const graphviz = await Graphviz.load(); + const dot = `\ +digraph { + a -> b; + b -> c; + c -> d; + d -> e; +}`; + + let retVal = graphviz.tred(dot); + expect(stripWhitespaces(retVal.out)).to.equal(stripWhitespaces(dot)); + expect(retVal.err).to.equal(""); + retVal = graphviz.tred(dot, true); + expect(stripWhitespaces(retVal.out)).to.equal(stripWhitespaces(dot)); + expect(retVal.err).to.not.be.empty; + retVal = graphviz.tred(dot, true, true); + expect(stripWhitespaces(retVal.out)).to.equal(stripWhitespaces(dot)); + expect(retVal.err).to.not.be.empty; + const acyclicDot = `\ +digraph { + a -> b; + b -> c; + c -> d; + d -> a; + d -> e; + e -> d; +}`; + retVal = graphviz.tred(acyclicDot); + expect(stripWhitespaces(retVal.out)).to.not.equal(stripWhitespaces(dot)); + expect(retVal.err).to.not.be.empty; + retVal = graphviz.tred(acyclicDot, true); + expect(stripWhitespaces(retVal.out)).to.not.equal(stripWhitespaces(dot)); + expect(retVal.err).to.not.be.empty; + retVal = graphviz.tred(acyclicDot, true, true); + expect(stripWhitespaces(retVal.out)).to.not.equal(stripWhitespaces(dot)); + expect(retVal.err).to.not.be.empty; + }); + + it("empty", async function () { + const graphviz = await Graphviz.load(); + const { out, err } = graphviz.tred(""); + expect(out).to.equal(""); + expect(err).to.equal(""); + }); + + it("syntax error", async function () { + const graphviz = await Graphviz.load(); + try { + const xxx = graphviz.tred(badDot); + expect(true).to.be.false; + } catch (e: any) { + expect(typeof e.message).to.equal("string"); + expect(e.message).to.contain("syntax error in line"); + } + }); +}); + describe("unflatten", async function () { it("simple", async function () { Graphviz.unload(); @@ -331,7 +457,7 @@ graph { it("syntax error", async function () { const graphviz = await Graphviz.load(); try { - const xxx = graphviz.dot(badDot, "svg"); + const xxx = graphviz.unflatten(badDot); expect(true, xxx).to.be.false; } catch (e: any) { expect(typeof e.message).to.equal("string"); diff --git a/src-ts/graphviz.ts b/src-ts/graphviz.ts index 143065df..78ee516a 100644 --- a/src-ts/graphviz.ts +++ b/src-ts/graphviz.ts @@ -123,6 +123,7 @@ export class Graphviz { version(): string { return this._module.Graphviz.prototype.version(); } + /** * Performs layout for the supplied _dotSource_, see [The DOT Language](https://graphviz.gitlab.io/doc/info/lang.html) for specification. * @@ -155,24 +156,90 @@ export class Graphviz { return retVal; } + /** + * acyclic is a filter that takes a directed graph as input and outputs a copy of the graph with sufficient edges reversed to make the graph acyclic. The reversed edge inherits all of the attributes of the original edge. The optional file argument specifies where the input graph is stored; by default. + * + * @param dotSource Required - graph definition in [DOT](https://graphviz.gitlab.io/doc/info/lang.html) language + * @param doWrite Enable output is produced, though the return value will indicate whether the graph is acyclic or not. + * @param verbose Print information about whether the file is acyclic, has a cycle or is undirected. + * @returns `{ acyclic: boolean, num_rev: number, outFile: string }` `acyclic` will be true if a cycle was found, `num_rev` will contain the number of reversed edges and `outFile` will (optionally) contain the output. + */ + acyclic(dotSource: string, doWrite: boolean = false, verbose: boolean = false): { acyclic: boolean, num_rev: number, outFile: string } { + if (!dotSource) return { acyclic: false, num_rev: 0, outFile: "" }; + const graphViz = new this._module.Graphviz(); + let acyclic: boolean = false; + let num_rev: number = 0; + let outFile: string = ""; + let errorMsg = ""; + try { + try { + acyclic = graphViz.acyclic(dotSource, doWrite, verbose); + num_rev = graphViz.acyclic_num_rev; + outFile = graphViz.acyclic_outFile; + } catch (e: any) { + errorMsg = e.message; + }; + errorMsg = graphViz.lastError() || errorMsg; + } finally { + this._module.destroy(graphViz); + } + if (errorMsg) { + Graphviz.unload(); + throw new Error(errorMsg); + } + return { acyclic, num_rev, outFile }; + } + + /** + * tred computes the transitive reduction of directed graphs, and prints the resulting graphs to standard output. This removes edges implied by transitivity. Nodes and subgraphs are not otherwise affected. The ‘‘meaning’’ and validity of the reduced graphs is application dependent. tred is particularly useful as a preprocessor to dot to reduce clutter in dense layouts. Undirected graphs are silently ignored. + * + * @param dotSource Required - graph definition in [DOT](https://graphviz.gitlab.io/doc/info/lang.html) language + * @param verbose Print additional information. + * @param printRemovedEdges Print information about removed edges. + * @returns `{ out: string, err: string }`. + */ + tred(dotSource: string, verbose: boolean = false, printRemovedEdges: boolean = false): { out: string, err: string } { + if (!dotSource) return { out: "", err: "" }; + const graphViz = new this._module.Graphviz(); + let out: string = ""; + let err: string = ""; + let errorMsg = ""; + try { + try { + graphViz.tred(dotSource, verbose, printRemovedEdges); + out = graphViz.tred_out; + err = graphViz.tred_err; + } catch (e: any) { + errorMsg = e.message; + }; + errorMsg = graphViz.lastError() || errorMsg; + } finally { + this._module.destroy(graphViz); + } + if (!out && errorMsg) { + Graphviz.unload(); + throw new Error(errorMsg); + } + return { out, err }; + } + /** * unflatten is a preprocessor to dot that is used to improve the aspect ratio of graphs having many leaves or disconnected nodes. The usual layout for such a graph is generally very wide or tall. unflatten inserts invisible edges or adjusts the minlen on edges to improve layout compaction. * * @param dotSource Required - graph definition in [DOT](https://graphviz.gitlab.io/doc/info/lang.html) language - * @param l The minimum length of leaf edges is staggered between 1 and len (a small integer). - * @param f Enables the staggering of the -l option to fanout nodes whose indegree and outdegree are both 1. This helps with structures such as a -> \{w x y \} -> b. This option only works if the -l flag is set. - * @param c Form disconnected nodes into chains of up to len nodes. + * @param maxMinlen The minimum length of leaf edges is staggered between 1 and len (a small integer). + * @param do_fans Enables the staggering of the -maxMinlen option to fanout nodes whose indegree and outdegree are both 1. This helps with structures such as a -> \{w x y \} -> b. This option only works if the -maxMinlen flag is set. + * @param chainLimit Form disconnected nodes into chains of up to len nodes. * @returns A string containing the "unflattened" dotSource. */ - - unflatten(dotSource: string, l: number = 0, f: boolean = false, c: number = 0): string { + unflatten(dotSource: string, maxMinlen: number = 0, do_fans: boolean = false, chainLimit: number = 0): string { if (!dotSource) return ""; const graphViz = new this._module.Graphviz(); let retVal = ""; let errorMsg = ""; try { try { - retVal = graphViz.unflatten(dotSource, l, f, c); + retVal = graphViz.unflatten(dotSource, maxMinlen, do_fans, chainLimit); } catch (e: any) { errorMsg = e.message; }; diff --git a/vcpkg-overlays/graphviz/portfile.cmake b/vcpkg-overlays/graphviz/portfile.cmake index a896dcf8..b839f226 100644 --- a/vcpkg-overlays/graphviz/portfile.cmake +++ b/vcpkg-overlays/graphviz/portfile.cmake @@ -2,8 +2,8 @@ vcpkg_from_gitlab( GITLAB_URL https://gitlab.com OUT_SOURCE_PATH SOURCE_PATH REPO graphviz/graphviz - REF ${VERSION} - SHA512 1edcf6aa232d38d1861a344c1a4a88aac51fd4656d667783ca1608ac694025199595a72a293c4eee2f7c7326ce54f22b787a5b7f4c44946f2de6096bd8f0e79d + REF 1cfb01454ca543f307c33bd8c34f20ee2004ddb6 + SHA512 7ec8e8fd7ea0fece8f65d217cee84ea6081844a941716c8182ad5a8c5c95fc627cbef1e0164eecc322626247e8eb839cd6d4246e193c70ecb4d909bca49779bf HEAD_REF main ) diff --git a/vcpkg-overlays/graphviz/vcpkg.json b/vcpkg-overlays/graphviz/vcpkg.json index 8c8f6916..ca4c0b81 100644 --- a/vcpkg-overlays/graphviz/vcpkg.json +++ b/vcpkg-overlays/graphviz/vcpkg.json @@ -1,6 +1,6 @@ { "name": "graphviz", - "version-semver": "9.0.0", + "version-semver": "10.0.0", "port-version": 0, "homepage": "https://graphviz.org/", "description": "Graphviz is open source graph visualization software. Graph visualization is a way of representing structural information as diagrams of abstract graphs and networks. It has important applications in networking, bioinformatics, software engineering, database and web design, machine learning, and in visual interfaces for other technical domains.",