/****************************************************************************** * * Copyright (C) 1997-2019 by Dimitri van Heesch. * * Permission to use, copy, modify, and distribute this software and its * documentation under the terms of the GNU General Public License is hereby * granted. No representations are made about the suitability of this software * for any purpose. It is provided "as is" without express or implied warranty. * See the GNU General Public License for more details. * * Documents produced by Doxygen are derivative works derived from the * input used in their production; they are not affected by this license. * */ #include "dotfilepatcher.h" #include "dotrunner.h" #include "qstring.h" #include "config.h" #include "qdir.h" #include "message.h" #include "ftextstream.h" #include "docparser.h" #include "doxygen.h" #include "util.h" #include "dot.h" static const char svgZoomHeader[] = "\n" "\n" "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n" "\n" ; static QCString replaceRef(const QCString &buf,const QCString relPath, bool urlOnly,const QCString &context,const QCString &target=QCString()) { // search for href="...", store ... part in link QCString href = "href"; //bool isXLink=FALSE; int len = 6; int indexS = buf.find("href=\""), indexE; bool targetAlreadySet = buf.find("target=")!=-1; if (indexS>5 && buf.find("xlink:href=\"")!=-1) // XLink href (for SVG) { indexS-=6; len+=6; href.prepend("xlink:"); //isXLink=TRUE; } if (indexS>=0 && (indexE=buf.find('"',indexS+len))!=-1) { QCString link = buf.mid(indexS+len,indexE-indexS-len); QCString result; if (urlOnly) // for user defined dot graphs { if (link.left(5)=="\\ref " || link.left(5)=="@ref ") // \ref url { result=href+"=\""; // fake ref node to resolve the url DocRef *df = new DocRef( (DocNode*) 0, link.mid(5), context ); result+=externalRef(relPath,df->ref(),TRUE); if (!df->file().isEmpty()) result += df->file().data() + Doxygen::htmlFileExtension; if (!df->anchor().isEmpty()) result += "#" + df->anchor(); delete df; result += "\""; } else { result = href+"=\"" + link + "\""; } } else // ref$url (external ref via tag file), or $url (local ref) { int marker = link.find('$'); if (marker!=-1) { QCString ref = link.left(marker); QCString url = link.mid(marker+1); if (!ref.isEmpty()) { result = externalLinkTarget(true); if (!result.isEmpty())targetAlreadySet=true; } result+= href+"=\""; result+=externalRef(relPath,ref,TRUE); result+= url + "\""; } else // should not happen, but handle properly anyway { result = href+"=\"" + link + "\""; } } if (!target.isEmpty() && !targetAlreadySet) { result+=" target=\""+target+"\""; } QCString leftPart = buf.left(indexS); QCString rightPart = buf.mid(indexE+1); //printf("replaceRef(\n'%s'\n)->\n'%s+%s+%s'\n", // buf.data(),leftPart.data(),result.data(),rightPart.data()); return leftPart + result + rightPart; } else { return buf; } } /*! converts the rectangles in a client site image map into a stream * \param t the stream to which the result is written. * \param mapName the name of the map file. * \param relPath the relative path to the root of the output directory * (used in case CREATE_SUBDIRS is enabled). * \param urlOnly if FALSE the url field in the map contains an external * references followed by a $ and then the URL. * \param context the context (file, class, or namespace) in which the * map file was found * \returns TRUE if successful. */ bool DotFilePatcher::convertMapFile(FTextStream &t,const char *mapName, const QCString relPath, bool urlOnly, const QCString &context) { QFile f(mapName); if (!f.open(IO_ReadOnly)) { err("problems opening map file %s for inclusion in the docs!\n" "If you installed Graphviz/dot after a previous failing run, \n" "try deleting the output directory and rerun doxygen.\n",mapName); return FALSE; } const int maxLineLen=10240; while (!f.atEnd()) // foreach line { QCString buf(maxLineLen); int numBytes = f.readLine(buf.rawData(),maxLineLen); if (numBytes>0) { buf.resize(numBytes+1); if (buf.left(5)=="0 && (indexE=replBuf.find('"',indexS+4))!=-1) { t << replBuf.left(indexS-1) << replBuf.right(replBuf.length() - indexE - 1); } else { t << replBuf; } } } } return TRUE; } DotFilePatcher::DotFilePatcher(const char *patchFile) : m_patchFile(patchFile) { m_maps.setAutoDelete(TRUE); } bool DotFilePatcher::isSVGFile() const { return m_patchFile.right(4)==".svg"; } int DotFilePatcher::addMap(const QCString &mapFile,const QCString &relPath, bool urlOnly,const QCString &context,const QCString &label) { int id = m_maps.count(); Map *map = new Map; map->mapFile = mapFile; map->relPath = relPath; map->urlOnly = urlOnly; map->context = context; map->label = label; map->zoomable = FALSE; map->graphId = -1; m_maps.append(map); return id; } int DotFilePatcher::addFigure(const QCString &baseName, const QCString &figureName,bool heightCheck) { int id = m_maps.count(); Map *map = new Map; map->mapFile = figureName; map->urlOnly = heightCheck; map->label = baseName; map->zoomable = FALSE; map->graphId = -1; m_maps.append(map); return id; } int DotFilePatcher::addSVGConversion(const QCString &relPath,bool urlOnly, const QCString &context,bool zoomable, int graphId) { int id = m_maps.count(); Map *map = new Map; map->relPath = relPath; map->urlOnly = urlOnly; map->context = context; map->zoomable = zoomable; map->graphId = graphId; m_maps.append(map); return id; } int DotFilePatcher::addSVGObject(const QCString &baseName, const QCString &absImgName, const QCString &relPath) { int id = m_maps.count(); Map *map = new Map; map->mapFile = absImgName; map->relPath = relPath; map->label = baseName; map->zoomable = FALSE; map->graphId = -1; m_maps.append(map); return id; } bool DotFilePatcher::run() const { //printf("DotFilePatcher::run(): %s\n",m_patchFile.data()); bool interactiveSVG_local = Config_getBool(INTERACTIVE_SVG); bool isSVGFile = m_patchFile.right(4)==".svg"; int graphId = -1; QCString relPath; if (isSVGFile) { Map *map = m_maps.at(0); // there is only one 'map' for a SVG file interactiveSVG_local = interactiveSVG_local && map->zoomable; graphId = map->graphId; relPath = map->relPath; //printf("DotFilePatcher::addSVGConversion: file=%s zoomable=%d\n", // m_patchFile.data(),map->zoomable); } QCString tmpName = m_patchFile+".tmp"; QCString patchFile = m_patchFile; if (!QDir::current().rename(patchFile,tmpName)) { err("Failed to rename file %s to %s!\n",m_patchFile.data(),tmpName.data()); return FALSE; } QFile fi(tmpName); QFile fo(patchFile); if (!fi.open(IO_ReadOnly)) { err("problem opening file %s for patching!\n",tmpName.data()); QDir::current().rename(tmpName,patchFile); return FALSE; } if (!fo.open(IO_WriteOnly)) { err("problem opening file %s for patching!\n",m_patchFile.data()); QDir::current().rename(tmpName,patchFile); return FALSE; } FTextStream t(&fo); const int maxLineLen=100*1024; int lineNr=1; int width,height; bool insideHeader=FALSE; bool replacedHeader=FALSE; bool foundSize=FALSE; while (!fi.atEnd()) // foreach line { QCString line(maxLineLen); int numBytes = fi.readLine(line.rawData(),maxLineLen); if (numBytes<=0) { break; } line.resize(numBytes+1); //printf("line=[%s]\n",line.stripWhiteSpace().data()); int i; ASSERT(numBytes500 || height>450); if (foundSize) insideHeader=TRUE; } else if (insideHeader && !replacedHeader && line.find("")!=-1) { if (foundSize) { // insert special replacement header for interactive SVGs t << "<!--zoomable " << height << " -->\n"; t << svgZoomHeader; t << "var viewWidth = " << width << ";\n"; t << "var viewHeight = " << height << ";\n"; if (graphId>=0) { t << "var sectionId = 'dynsection-" << graphId << "';\n"; } t << "</script>\n"; t << "<script xlink:href=\"" << relPath << "svgpan.js\"/>\n"; t << "<svg id=\"graph\" class=\"graph\">\n"; t << "<g id=\"viewport\">\n"; } insideHeader=FALSE; replacedHeader=TRUE; } } if (!insideHeader || !foundSize) // copy SVG and replace refs, // unless we are inside the header of the SVG. // Then we replace it with another header. { Map *map = m_maps.at(0); // there is only one 'map' for a SVG file t << replaceRef(line,map->relPath,map->urlOnly,map->context,"_top"); } } else if ((i=line.find("<!-- SVG"))!=-1 || (i=line.find("[!-- SVG"))!=-1) { //printf("Found marker at %d\n",i); int mapId=-1; t << line.left(i); int n = sscanf(line.data()+i+1,"!-- SVG %d",&mapId); if (n==1 && mapId>=0 && mapId<(int)m_maps.count()) { int e = QMAX(line.find("--]"),line.find("-->")); Map *map = m_maps.at(mapId); //printf("DotFilePatcher::writeSVGFigure: file=%s zoomable=%d\n", // m_patchFile.data(),map->zoomable); if (!writeSVGFigureLink(t,map->relPath,map->label,map->mapFile)) { err("Problem extracting size from SVG file %s\n",map->mapFile.data()); } if (e!=-1) t << line.mid(e+3); } else // error invalid map id! { err("Found invalid SVG id in file %s!\n",m_patchFile.data()); t << line.mid(i); } } else if ((i=line.find("<!-- MAP"))!=-1) { int mapId=-1; t << line.left(i); int n = sscanf(line.data()+i,"<!-- MAP %d",&mapId); if (n==1 && mapId>=0 && mapId<(int)m_maps.count()) { QGString result; FTextStream tt(&result); Map *map = m_maps.at(mapId); //printf("patching MAP %d in file %s with contents of %s\n", // mapId,m_patchFile.data(),map->mapFile.data()); convertMapFile(tt,map->mapFile,map->relPath,map->urlOnly,map->context); if (!result.isEmpty()) { t << "<map name=\"" << correctId(map->label) << "\" id=\"" << correctId(map->label) << "\">" << endl; t << result; t << "</map>" << endl; } } else // error invalid map id! { err("Found invalid MAP id in file %s!\n",m_patchFile.data()); t << line.mid(i); } } else if ((i=line.find("% FIG"))!=-1) { int mapId=-1; int n = sscanf(line.data()+i+2,"FIG %d",&mapId); //printf("line='%s' n=%d\n",line.data()+i,n); if (n==1 && mapId>=0 && mapId<(int)m_maps.count()) { Map *map = m_maps.at(mapId); //printf("patching FIG %d in file %s with contents of %s\n", // mapId,m_patchFile.data(),map->mapFile.data()); if (!writeVecGfxFigure(t,map->label,map->mapFile)) { err("problem writing FIG %d figure!\n",mapId); return FALSE; } } else // error invalid map id! { err("Found invalid bounding FIG %d in file %s!\n",mapId,m_patchFile.data()); t << line; } } else { t << line; } lineNr++; } fi.close(); if (isSVGFile && interactiveSVG_local && replacedHeader) { QCString orgName=m_patchFile.left(m_patchFile.length()-4)+"_org.svg"; t << substitute(svgZoomFooter,"$orgname",stripPath(orgName)); fo.close(); // keep original SVG file so we can refer to it, we do need to replace // dummy link by real ones fi.setName(tmpName); fo.setName(orgName); if (!fi.open(IO_ReadOnly)) { err("problem opening file %s for reading!\n",tmpName.data()); return FALSE; } if (!fo.open(IO_WriteOnly)) { err("problem opening file %s for writing!\n",orgName.data()); return FALSE; } FTextStream to(&fo); while (!fi.atEnd()) // foreach line { QCString line(maxLineLen); int numBytes = fi.readLine(line.rawData(),maxLineLen); if (numBytes<=0) { break; } line.resize(numBytes+1); Map *map = m_maps.at(0); // there is only one 'map' for a SVG file to << replaceRef(line,map->relPath,map->urlOnly,map->context,"_top"); } fi.close(); fo.close(); } // remove temporary file QDir::current().remove(tmpName); return TRUE; } //--------------------------------------------------------------------------------------------- // extract size from a dot generated SVG file static bool readSVGSize(const QCString &fileName,int *width,int *height) { bool found=FALSE; QFile f(fileName); if (!f.open(IO_ReadOnly)) { return FALSE; } const int maxLineLen=4096; char buf[maxLineLen]; while (!f.atEnd() && !found) { int numBytes = f.readLine(buf,maxLineLen-1); // read line if (numBytes>0) { buf[numBytes]='\0'; if (qstrncmp(buf,"<!--zoomable ",13)==0) { *width=-1; *height=-1; sscanf(buf,"<!--zoomable %d",height); //printf("Found zoomable for %s!\n",fileName.data()); found=TRUE; } else if (sscanf(buf,"<svg width=\"%dpt\" height=\"%dpt\"",width,height)==2) { //printf("Found fixed size %dx%d for %s!\n",*width,*height,fileName.data()); found=TRUE; } } else // read error! { //printf("Read error %d!\n",numBytes); return FALSE; } } return TRUE; } static void writeSVGNotSupported(FTextStream &out) { out << "<p><b>This browser is not able to show SVG: try Firefox, Chrome, Safari, or Opera instead.</b></p>"; } /// Check if a reference to a SVG figure can be written and do so if possible. /// Returns FALSE if not possible (for instance because the SVG file is not yet generated). bool DotFilePatcher::writeSVGFigureLink(FTextStream &out,const QCString &relPath, const QCString &baseName,const QCString &absImgName) { int width=600,height=600; if (!readSVGSize(absImgName,&width,&height)) { return FALSE; } if (width==-1) { if (height<=60) height=300; else height+=300; // add some extra space for zooming if (height>600) height=600; // clip to maximum height of 600 pixels out << "<div class=\"zoom\">"; //out << "<object type=\"image/svg+xml\" data=\"" //out << "<embed type=\"image/svg+xml\" src=\"" out << "<iframe scrolling=\"no\" frameborder=\"0\" src=\"" << relPath << baseName << ".svg\" width=\"100%\" height=\"" << height << "\">"; } else { //out << "<object type=\"image/svg+xml\" data=\"" //out << "<embed type=\"image/svg+xml\" src=\"" out << "<iframe scrolling=\"no\" frameborder=\"0\" src=\"" << relPath << baseName << ".svg\" width=\"" << ((width*96+48)/72) << "\" height=\"" << ((height*96+48)/72) << "\">"; } writeSVGNotSupported(out); //out << "</object>"; //out << "</embed>"; out << "</iframe>"; if (width==-1) { out << "</div>"; } return TRUE; } bool DotFilePatcher::writeVecGfxFigure(FTextStream &out,const QCString &baseName, const QCString &figureName) { int width=400,height=550; if (Config_getBool(USE_PDFLATEX)) { if (!DotRunner::readBoundingBox(figureName+".pdf",&width,&height,FALSE)) { //printf("writeVecGfxFigure()=0\n"); return FALSE; } } else { if (!DotRunner::readBoundingBox(figureName+".eps",&width,&height,TRUE)) { //printf("writeVecGfxFigure()=0\n"); return FALSE; } } //printf("Got PDF/EPS size %d,%d\n",width,height); int maxWidth = 350; /* approx. page width in points, excl. margins */ int maxHeight = 550; /* approx. page height in points, excl. margins */ out << "\\nopagebreak\n" "\\begin{figure}[H]\n" "\\begin{center}\n" "\\leavevmode\n"; if (width>maxWidth || height>maxHeight) // figure too big for page { // c*width/maxWidth > c*height/maxHeight, where c=maxWidth*maxHeight>0 if (width*maxHeight>height*maxWidth) { out << "\\includegraphics[width=" << maxWidth << "pt]"; } else { out << "\\includegraphics[height=" << maxHeight << "pt]"; } } else { out << "\\includegraphics[width=" << width << "pt]"; } out << "{" << baseName << "}\n" "\\end{center}\n" "\\end{figure}\n"; //printf("writeVecGfxFigure()=1\n"); return TRUE; }