diff options
Diffstat (limited to 'app/bin/svgformat.c')
| -rw-r--r-- | app/bin/svgformat.c | 677 | 
1 files changed, 677 insertions, 0 deletions
diff --git a/app/bin/svgformat.c b/app/bin/svgformat.c new file mode 100644 index 0000000..2c57da0 --- /dev/null +++ b/app/bin/svgformat.c @@ -0,0 +1,677 @@ +/** \file svgformat.c +* Formatting of SVG commands and parameters. +*/ + +/*  XTrkCad - Model Railroad CAD +*  Copyright (C)2021 Martin Fischer +* +*  This program is free software; you can redistribute it and/or modify +*  it under the terms of the GNU General Public License as published by +*  the Free Software Foundation; either version 2 of the License, or +*  (at your option) any later version. +* +*  This program is distributed in the hope that it will be useful, +*  but WITHOUT ANY WARRANTY; without even the implied warranty of +*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +*  GNU General Public License for more details. +* +*  You should have received a copy of the GNU General Public License +*  along with this program; if not, write to the Free Software +*  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#define _USE_MATH_DEFINES +#include <math.h> +#include <stdarg.h> +#include <string.h> +#include <stdio.h> + +#ifdef HAVE_MALLOC_H +    #include <malloc.h> +#endif + +#include "dynstring.h" +#include "mxml.h" +#include "include/svgformat.h" +#include "include/utlist.h" + +#define SVGDPIFACTOR 90.0					/**< the assumed resolution of the svg, 90 is what Inkscape uses */ +#define ROUND2PIXEL( value ) ((int)floor(value * SVGDPIFACTOR + 0.5)) + +typedef struct sCssStyle { +    DynString name; +    DynString style; +    struct sCssStyle *next; +} sCssStyle; + +static sCssStyle *styleCache = NULL; +static unsigned cacheCount; + +static char *lineStyleCSS[] = {				/**< The css class names for line styles */ +    NULL,										// no style needed for solid line +    "linedash", +    "linedot", +    "linedashdot", +    "linedashdot", +    "linecenter", +    "linephantom" +}; + +#define LINESTYLECLASSES \ +    ".linedash{ stroke-dasharray: 25px 15px; } \n" \ +    ".linedot{ stroke-dasharray: 5px 10px; } \n" \ +    ".linedashdot{ stroke-dasharray: 25px 10px 5px 10px; } \n" \ +    ".linedashdotdot{ stroke-dasharray: 25px 10px 5px 10px 5px 10px; } \n" \ +    ".linecenter{ stroke-dasharray: 40px 15px 25px 15px; } \n" \ +    ".linephantom{ stroke-dasharray: 40px 15px 25px 15px 25px 15px; } \n" + + +/** + * Initialize style cache. Memory is allocated and the default style added + */ + +static void +SvgInitStyleCache(void) +{ +    sCssStyle *style; + +    style = malloc(sizeof(sCssStyle)); +    DynStringMalloc(&(style->name), 2); +    DynStringCatCStr(&(style->name), "*"); + +    DynStringMalloc(&(style->style), 16); +    DynStringCatCStr(&(style->style), "stroke-width:1; stroke:#000000; fill:none;"); +    LL_APPEND(styleCache, style); + +    cacheCount = 1; +} + +static int +CompareStyle(sCssStyle *a, sCssStyle *b) +{ +    return (strcmp(DynStringToCStr(&(a->style)), DynStringToCStr(&(b->style)))); +} + +/** + * Add style to cache. If an identical style definition can be found in the + * cache, the class name is returned. + * If no previous definition is in the cache, a new one is contructed and + * stored in the cache. The new class name is returned + * + * \param [in] styleDef style definition. + * + * \returns Null if default style can be used, else the class name + */ + +static char * +SvgAddStyleToCache(DynString *styleDef) +{ +    sCssStyle *style; +    sCssStyle *result; + +    style = malloc(sizeof(sCssStyle)); +    style->style = *styleDef; + +    LL_SEARCH(styleCache, result, style, CompareStyle); +    if (result) { +        if (!strcmp(DynStringToCStr(&(result->name)), "*")) { +            return (NULL); +        } else { +            return (DynStringToCStr(&(result->name)) + 1); +        } +    } else { +        DynString className; +        DynStringMalloc(&className, 16); +        DynStringPrintf(&className, ".xtc%u", cacheCount++); +        style->name = className; +        LL_APPEND(styleCache, style); +        return (DynStringToCStr(&className) + 1);		//skip leading dot in class name +    } +} + +/** + * destroy style cache freeing all memory allocated + */ + +static void +SvgDestroyStyleCache(void) +{ +    sCssStyle *style; +    sCssStyle *tmp; + +    LL_FOREACH_SAFE(styleCache, style, tmp) { +        DynStringFree(&(style->name)); +        DynStringFree(&(style->style)); +        free(style); +    } + +    styleCache = NULL; +} + +/** + * Svg create style, add to the cache and the associated CSS class name to the element + * + * \param element	SVG element + * \param colorRGB	RGB value + * \param width     line width + * \param fill		true to fill + */ + +static void +SvgCreateStyle(mxml_node_t *element, unsigned long colorRGB, double width, +               bool fill, unsigned lineStyle) +{ +    DynString style; +    char color[10]; +    char *className = NULL; +    char *classLineStyle = NULL; + +    assert(lineStyle < 7); + +    sprintf(color, "#%2.2x%2.2x%2.2x", ((unsigned int)colorRGB >> 16) & 0xFF, +            ((unsigned int)colorRGB >> 8) & 0xFF, (unsigned int)colorRGB & 0xFF); + +    DynStringMalloc(&style, 32); + +    DynStringPrintf(&style, +                    "stroke-width:%d; stroke:%s; fill:%s;", +                    (int)(width + 0.5), +                    color, +                    (fill ? color: "none")); + +    className = SvgAddStyleToCache(&style); +    classLineStyle = lineStyleCSS[lineStyle]; + +    if (className && classLineStyle) { +        mxmlElementSetAttrf(element, "class", "%s %s", className, classLineStyle); +    } else { +        if (className || classLineStyle) { +            mxmlElementSetAttr(element, "class", (className? className:classLineStyle)); +        } else { +            // strict default, nothing to add +        } +    } +} + +/** + * add real unit, ie. units that are specified in pixels. Rounding is performed + * + * \param [in,out] node  If non-null, the node. + * \param [in,out] name  If non-null, the name. + * \param 		   value the dimension in pixels + */ + +static void +SvgAddRealUnit(mxml_node_t *node, char *name, double value) +{ +    mxmlElementSetAttrf(node, name, "%d", (int)(value+0.5)); +} + +/** +* Format a dimension and add to XML node as an attribute. +* A fictional value for the resolution is assumed. As final +* rendering is done by the client, this is not really relevant. +* +* \PARAM [in, out]	node	the XML node +* \PARAM [in]		name	name of attribute +* \param [in]		value	size +*/ + +static void +SvgAddCoordinate(mxml_node_t *node, char *name, double value) +{ +    mxmlElementSetAttrf(node, name, "%d", ROUND2PIXEL(value)); +} + +/** + * Svg line command + * \param [in] svg		 the svg parent. + * \param 	   x0		 The x coordinate 0. + * \param 	   y0		 The y coordinate 0. + * \param 	   x1		 The first x value. + * \param 	   y1		 The first y value. + * \param 	   w		 A wDrawWidth to process. + * \param 	   c		 RGB color definition. + * \param 	   lineStyle line style. + */ + +void +SvgLineCommand(SVGParent *svg, double x0, +               double y0, double x1, double y1, double w, long c, unsigned lineStyle) +{ +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(svg, "line"); + +    // line end points +    SvgAddCoordinate(xmlData, "x1", x0); +    SvgAddCoordinate(xmlData, "y1", y0); +    SvgAddCoordinate(xmlData, "x2", x1); +    SvgAddCoordinate(xmlData, "y2", y1); + +    SvgCreateStyle(xmlData, c, w, false, lineStyle); +} + +/** + * Svg rectangle command + * + * \param [in]    svg	   If non-null, the svg. + * \param 		   x0	   The x coordinate 0. + * \param 		   y0	   The y coordinate 0. + * \param 		   x1	   The first x value. + * \param 		   y1	   The first y value. + * \param 		   color   The color. + * \param 		   fill		Specifies the fill options. + */ + +void +SvgRectCommand(SVGParent *svg, double x0, double y0, double x1, double y1, +               int color, unsigned lineStyle) +{ +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(svg, "rect"); + +    // line end points +    SvgAddCoordinate(xmlData, "x1", x0); +    SvgAddCoordinate(xmlData, "y1", y0); +    SvgAddCoordinate(xmlData, "x2", x1); +    SvgAddCoordinate(xmlData, "y2", y1); + +    SvgCreateStyle(xmlData, color, 1, false, lineStyle); + +} + +/** + * Svg polygon line command + * + * \param [in] svg		 If non-null, the svg. + * \param 	   cnt		 Number of point. + * \param [in] points    If non-null, the points. + * \param 	   color	 The line and fill color. + * \param 	   width	 The line width. + * \param 	   lineStyle The line style. + * \param 	   fill		 True to fill. + */ + +void +SvgPolyLineCommand(SVGParent *svg, int cnt, double *points, int color, +                   double width, bool fill, unsigned lineStyle) +{ +    mxml_node_t *xmlData; +    DynString pointList; +    DynString pos; + +    DynStringMalloc(&pointList, 64); +    DynStringMalloc(&pos, 20); + + +    for (int i = 0; i < cnt; i++) { +        DynStringPrintf(&pos, +                        "%d,%d ", +                        (int)floor(points[i * 2] * SVGDPIFACTOR + 0.5), +                        (int)floor(points[ i * 2 + 1] * SVGDPIFACTOR + 0.5)); + +        DynStringCatStr(&pointList, &pos); +        DynStringClear(&pos); +    } + +    xmlData = mxmlNewElement(svg, "polyline"); +    mxmlElementSetAttr(xmlData, "points", DynStringToCStr(&pointList)); + +    SvgCreateStyle(xmlData, color, width, fill, lineStyle); + +    DynStringFree(&pos); +    DynStringFree(&pointList); +} + +/** + * Format a complete CIRCLE command + * + * \param [in]     svg		 OUT buffer for the completed command. + * \param 		   x		 x position of center. + * \param 		   y		 y position of center point. + * \param 		   r		 radius. + * \param 		   w		 width + * \param 		   c		 color + * \param 		   lineStyle The line style. + * \param 		   fill		 True to fill. + */ + +void +SvgCircleCommand(SVGParent *svg, double x, +                 double y, double r, double w, long c, bool fill, unsigned lineStyle) +{ +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(svg, "circle"); + +    // line end points +    SvgAddCoordinate(xmlData, "cx", x); +    SvgAddCoordinate(xmlData, "cy", y); + +    SvgAddCoordinate(xmlData, "r", r); + +    SvgCreateStyle(xmlData, c, w, fill, lineStyle); + +} + +/** + * Polar to cartesian + * + * \param 		cx	  x coordinate of center. + * \param 		cy	  y coordinate of center + * \param 		radius radius. + * \param 		angle  angle. + * \param [out] px	  resulting x coordinate + * \param [out] py	  resulting y coordinate + */ + +static void +PolarToCartesian(double cx, double cy, double radius, double angle, double *px, +                 double *py) +{ +    double angleInRadians = ((angle) * M_PI) / 180.0; + +    *px = cx + (radius * cos(angleInRadians)); +    *py = cy + (radius * sin(angleInRadians)); +} + +/** + * Format an arc as a SVG path See + * https://stackoverflow.com/questions/5736398/how-to-calculate-the-svg-path-for-an-arc-of-a-circle + * + * \param [in]     svg		 the svg document. + * \param 		   x		 y IN center point. + * \param 		   y		 The y coordinate. + * \param 		   r		 IN radius. + * \param 		   a0		 IN starting angle. + * \param 		   a1		 IN ending angle. + * \param 		   center    IN draw center mark if true. + * \param 		   w		 line width. + * \param 		   c		 line color. + * \param 		   lineStyle line style. + */ + +void +SvgArcCommand(SVGParent *svg, double x, double y, +              double r, double a0, double a1, bool center, double w, long c, +              unsigned lineStyle) +{ +    double startX; +    double startY; +    double endX; +    double endY; +    char largeArcFlag = (a1 - a0 <= 180 ? '0' : '1'); +    DynString pathArc; +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(svg, "path"); + +    PolarToCartesian(x, y, r, a0+a1-90, &startX, &startY); +    PolarToCartesian(x, y, r, a0-90, &endX, &endY); + +    DynStringMalloc(&pathArc, 64); +    DynStringPrintf(&pathArc, +                    "M %d %d A %d %d 0 %c 0 %d %d", +                    ROUND2PIXEL(startX), +                    ROUND2PIXEL(startY), +                    ROUND2PIXEL(r), +                    ROUND2PIXEL(r), +                    largeArcFlag, +                    ROUND2PIXEL(endX), +                    ROUND2PIXEL(endY)); + +    mxmlElementSetAttr(xmlData, "d", DynStringToCStr(&pathArc)); + +    DynStringFree(&pathArc); + +    SvgCreateStyle(xmlData, c, w, false, lineStyle); +} + +/** + *  Create SVG text command + * + * \param [in] svg  If non-null, the svg. + * \param 		   x    The x coordinate. + * \param 		   y    The y coordinate. + * \param 		   size The fontsize. + * \param 		   c    the text color + * \param [in]	   text text in UTF-8 format + */ + +void +SvgTextCommand(SVGParent *svg, double x, +               double y, double size, long c, char *text) +{ +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(svg, "text"); +    // starting point +    SvgAddCoordinate(xmlData, "x", x); +    SvgAddCoordinate(xmlData, "y", y); + +    SvgCreateStyle(xmlData, c, 1, 1, 0); + +    SvgAddRealUnit(xmlData, "font-size", size); + +    mxmlNewText(xmlData, false, text); +} + +/** + * Add title to SVG document + * + * \param [in] svg   svg + * \param [in] title If non-null, the title. + */ + +void +SvgAddTitle(SVGParent *svg, char *title) +{ +    mxml_node_t *titleNode; +    if (title) { +        titleNode = mxmlNewElement(MXML_NO_PARENT, "title"); +        mxmlNewText(titleNode, false, title); + +        mxmlAdd(svg, MXML_ADD_BEFORE, MXML_ADD_TO_PARENT, titleNode); +    } +} + +/** + * Add CSS style definitions to the SVG file. CSS definitions are + * created from the options of the drawing commands. As a final step + * in creation of the SVG file, these definitions have to be added. + * For compatibility reasons the styles have to be defined before + * first use. + * + * \param [in] svg the svg. + */ + +void +SvgAddCSSStyle(SVGParent *svg) +{ +    mxml_node_t *cssNode; +    DynString cssDefs; +    DynString tmp; +    sCssStyle *style; + +    cssNode = mxmlNewElement(MXML_NO_PARENT, "style"); +    mxmlElementSetAttr(cssNode, "type", "text/css"); + +    DynStringMalloc(&cssDefs, 64); +    DynStringMalloc(&tmp, 64); +    LL_FOREACH(styleCache, style) { +        DynStringPrintf(&tmp, "%s { %s }\n", +                        DynStringToCStr(&(style->name)), +                        DynStringToCStr(&(style->style))); + +        DynStringCatStr(&cssDefs, &tmp); +    } + +    DynStringCatCStr(&cssDefs, LINESTYLECLASSES); +    mxmlNewCDATA(cssNode, DynStringToCStr(&cssDefs)); + +    mxmlAdd(svg, MXML_ADD_BEFORE, MXML_ADD_TO_PARENT, cssNode); +    DynStringFree(&tmp); +    DynStringFree(&cssDefs); +} + +/** + * Svg create document + * + * \returns An XMLDocument. + */ + +SVGDocument * +SvgCreateDocument() +{ +    SvgInitStyleCache(); + +    return ((SVGDocument *)mxmlNewXML("1.0")); +} + +/** + * Svg destroy document freeing the memory used by the XML tree + * + * \param [in] xml the XML document + */ + +void +SvgDestroyDocument(SVGDocument *xml) +{ +    mxmlDelete((mxml_node_t *)xml); + +    SvgDestroyStyleCache(); +} + +/** + * Create the complete prologue for a SVG file. + * + * \param [in]     parent	  the document handle. + * \param [in]     id		  If non-null, the identifier. + * \param 		   layerCount IN count of defined layers. + * \param 		   x0		  y0 IN minimum (left bottom) position. + * \param 		   y0		  y1 IN maximum (top right) position. + * \param 		   x1		  The first x value. + * \param 		   y1		  The first y value. + * + * \returns Null if it fails, else a pointer to a SVGParent. + */ + +SVGParent * +SvgPrologue(SVGDocument *parent, char *id, int layerCount, double x0, double y0, +            double x1, +            double y1) +{ +    mxml_node_t *xmlData; + +    xmlData = mxmlNewElement(parent, +                             "!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\"" +                             " \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\""); +    xmlData = mxmlNewElement(parent, "svg"); +    mxmlElementSetAttr(xmlData, "xmlns", "http://www.w3.org/2000/svg"); + +    if (id) { +        mxmlElementSetAttr(xmlData, "id", id); +    } +    SvgAddCoordinate(xmlData, "x", x0); +    SvgAddCoordinate(xmlData, "y", y0); +    SvgAddCoordinate(xmlData, "width", x1); +    SvgAddCoordinate(xmlData, "height", y1); + +    return ((SVGParent *)xmlData); +} + +/** + * Add formatting to the resulting document by adding whitespace + * + * \param  node to be formatted + * \param  see minixml docu, position in XML tag + * + * \returns Null if it no character to add, else a pointer to the additional chars. + */ + +const char * +whitespace_cb(mxml_node_t *node, int where) +{ +    const char *element; + +    /* +     * We can conditionally break to a new line before or after +     * any element.  These are just common HTML elements... +     */ + +    element = mxmlGetElement(node); + +    if (!strcmp(element, "svg") || +            !strncmp(element, "!DOCTYPE", strlen("!DOCTYPE"))) { +        /* +         * Newlines before open and after close... +         */ + +        if (where == MXML_WS_BEFORE_OPEN || +                where == MXML_WS_BEFORE_CLOSE) { +            return ("\n"); +        } +    } else { +        if (!strcmp(element, "line") || +                !strcmp(element, "circle") || +                !strcmp(element, "path") || +                !strcmp(element, "polyline")) { +            if (where == MXML_WS_BEFORE_OPEN || +                    where == MXML_WS_AFTER_CLOSE) { +                return ("\n\t"); +            } +        } else { +            if (!strcmp(element, "style") || +                    !strcmp(element, "title") || +                    !strcmp(element, "text")) { +                if (where == MXML_WS_BEFORE_OPEN) { +                    return ("\n\t"); +                } else { +                    if (where == MXML_WS_AFTER_OPEN) { +                        return ("\n\t\t"); +                    } else { +                        if (where == MXML_WS_AFTER_CLOSE) { +                            return (""); +                        } else { +                            return ("\n\t"); +                        } +                    } +                } + +            } +        } +    } + +    /* +     * Otherwise return NULL for no added whitespace... +     */ + +    return (NULL); +} + +/** + * Svg save file + * + * \param [in] svg	    the svg document. + * \param [in] filename filename of the file. + * + * \returns True if it succeeds, false if it fails. + */ + +bool +SvgSaveFile(SVGDocument *svg, char *filename) +{ +    FILE *svgF; + +    svgF = fopen(filename, "w"); +    if (svgF) { +        mxmlSetWrapMargin(0); +        mxmlSaveFile(svg, svgF, whitespace_cb); +        fclose(svgF); + +        return (true); +    } +    return (false); +}  | 
