diff options
Diffstat (limited to 'app/bin/svgformat.c')
-rw-r--r-- | app/bin/svgformat.c | 674 |
1 files changed, 674 insertions, 0 deletions
diff --git a/app/bin/svgformat.c b/app/bin/svgformat.c new file mode 100644 index 0000000..3dfa621 --- /dev/null +++ b/app/bin/svgformat.c @@ -0,0 +1,674 @@ +/** \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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +*/ + +#define _USE_MATH_DEFINES +#include <math.h> +#include <stdarg.h> +#include <string.h> +#include <stdio.h> + +#include "dynstring.h" +#include "mxml.h" +#include "include/svgformat.h" +#include "include/utlist.h" +#include "common.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; + + CHECK(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); +} |