root/src/apps/icon-o-matic/import_export/svg/SVGExporter.cpp
/*
 * Copyright 2006, 2023, Haiku. All rights reserved.
 * Distributed under the terms of the MIT License.
 *
 * Authors:
 *              Stephan Aßmus <superstippi@gmx.de>
 *              Zardshard
 */

#include <stdio.h>

#include <Alert.h>
#include <Catalog.h>
#include <DataIO.h>
#include <File.h>
#include <Locale.h>
#include <String.h>

#include "support.h"

#include "Icon.h"
#include "GradientTransformable.h"
#include "PathSourceShape.h"
#include "Shape.h"
#include "StrokeTransformer.h"
#include "Style.h"
#include "VectorPath.h"

#include "SVGExporter.h"


#undef B_TRANSLATION_CONTEXT
#define B_TRANSLATION_CONTEXT "Icon-O-Matic-SVGExport"


// write_line
static status_t
write_line(BPositionIO* stream, BString& string)
{
        ssize_t written = stream->Write(string.String(), string.Length());
        if (written > B_OK && written < string.Length())
                written = B_ERROR;
        string.SetTo("");
        return written;
}

// #pragma mark -

// constructor
SVGExporter::SVGExporter()
        : Exporter(),
          fShownUnsupportedGradientWarning(false),
          fGradientCount(0),
          fOriginalEntry(NULL)
{
}

// destructor
SVGExporter::~SVGExporter()
{
        delete fOriginalEntry;
}

// Export
status_t
SVGExporter::Export(const Icon* icon, BPositionIO* stream)
{
        if (!icon || !stream)
                return B_BAD_VALUE;

//      if (fOriginalEntry && *fOriginalEntry == *refToFinalFile) {
//              if (!_DisplayWarning()) {
//                      return B_CANCELED;
//              } else {
//                      delete fOriginalEntry;
//                      fOriginalEntry = NULL;
//              }
//      }

        BString helper;

        fGradientCount = 0;

        // header
        helper << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
        status_t ret = write_line(stream, helper);

        // image size
        if (ret >= B_OK) {
                helper << "<svg version=\"1.1\" width=\"" << 64 << "\""
                           << " height=\"" << 64 << "\""
                           << " color-interpolation=\"linearRGB\"\n"
                           << "     xmlns:svg=\"http://www.w3.org/2000/svg\""
// Should be needed when exporting inline images:
//                         << " xmlns:xlink=\"http://www.w3.org/1999/xlink\""
                           << " xmlns=\"http://www.w3.org/2000/svg\">\n";
                ret = write_line(stream, helper);
        }

        if (ret >= B_OK) {
                // start (the only) layer
                helper << " <g>\n";
                ret = write_line(stream, helper);

                // export all shapes
                if (ret >= B_OK) {
                        int32 count = icon->Shapes()->CountItems();
                        for (int32 i = 0; i < count; i++) {
                                Shape* shape = icon->Shapes()->ItemAtFast(i);
                                ret = _ExportShape(shape, stream);
                                if (ret < B_OK)
                                        break;
                        }
                }

                // finish (the only) layer
                if (ret >= B_OK) {
                        helper << " </g>\n";
                        ret = write_line(stream, helper);
                }
        }

        // footer
        if (ret >= B_OK) {
                helper << "</svg>\n";
                ret = write_line(stream, helper);
        }
        return ret;
}

// #pragma mark -

// SetOriginalEntry
void
SVGExporter::SetOriginalEntry(const entry_ref* ref)
{
        if (ref) {
                delete fOriginalEntry;
                fOriginalEntry = new entry_ref(*ref);
        }
}

// DisplayWarning
bool
SVGExporter::_DisplayWarning() const
{
        BString text(B_TRANSLATE("%appname% might not have "
                                                           "interpreted all data from the SVG "
                                                           "when it was loaded. "
                                                           "By overwriting the original "
                                                           "file, this information would now "
                                                           "be lost."));
        text.ReplaceFirst("%appname%", B_TRANSLATE_SYSTEM_NAME("Icon-O-Matic"));
        BAlert* alert = new BAlert(B_TRANSLATE("warning"), text,
                                                           B_TRANSLATE("Cancel"), 
                                                           B_TRANSLATE("Overwrite"));
        alert->SetShortcut(0, B_ESCAPE);
        return alert->Go() == 1;
}

// _DisplayUnsupportedGradientWarning
void
SVGExporter::_DisplayUnsupportedGradientWarning() const
{
        if (fShownUnsupportedGradientWarning)
                return;

        BAlert* alert = new BAlert(
                B_TRANSLATE("SVG export"),
                B_TRANSLATE("SVG does not support some gradient types "
                            "(conic, diamond, xy, sqrt_xy).\n\n"
                            "They will be exported as linear approximations and "
                            "may look different in other viewers."),
                B_TRANSLATE("OK")
        );

        alert->Go();
        fShownUnsupportedGradientWarning = true;
}


// #pragma mark -

// convert_join_mode_svg
const char*
convert_join_mode_svg(agg::line_join_e mode)
{
        const char* svgMode = "miter";
        switch (mode) {
                case agg::round_join:
                        svgMode = "round";
                        break;
                case agg::bevel_join:
                        svgMode = "bevel";
                        break;
                case agg::miter_join:
                default:
                        break;
        }
        return svgMode;
}

// convert_cap_mode_svg
const char*
convert_cap_mode_svg(agg::line_cap_e mode)
{
        const char* svgMode = "butt";
        switch (mode) {
                case agg::square_cap:
                        svgMode = "square";
                        break;
                case agg::round_cap:
                        svgMode = "round";
                        break;
                case agg::butt_cap:
                default:
                        break;
        }
        return svgMode;
}

// #pragma mark -

// _ExportShape
status_t
SVGExporter::_ExportShape(const Shape* shape, BPositionIO* stream)
{
        if (!shape->Visible(1.0)) {
                // don't export shapes which are not visible at the
                // default scale
                return B_OK;
        }

        // only render PathSourceShapes
        const PathSourceShape* pathSourceShape = dynamic_cast<const PathSourceShape*>(shape);
        if (pathSourceShape == NULL)
                return B_OK;


        const Style* style = pathSourceShape->Style();

        char color[64];
        status_t ret = _GetFill(style, color, stream);

        if (ret < B_OK)
                return ret;

        // The transformation matrix is extracted again in order to
        // maintain the effect of a distorted outline. There is of
        // course a difference when applying the transformation to
        // the points of the path, then stroking the transformed
        // path with an outline of a certain width, opposed to applying
        // the transformation to the points of the generated stroke
        // as well. Adobe SVG Viewer is supporting this fairly well,
        // though I have come across SVG software that doesn't (InkScape).

        // start new shape and write transform matrix
        BString helper;
        helper << "  <path ";

        // hack to see if this is an outline shape
        StrokeTransformer* stroke
                = dynamic_cast<StrokeTransformer*>(pathSourceShape->Transformers()->ItemAt(0));
        if (stroke) {
                helper << "style=\"fill:none; stroke:" << color;
                if (!style->Gradient() && style->Color().alpha < 255) {
                        helper << "; stroke-opacity:";
                        append_float(helper, style->Color().alpha / 255.0);
                }
                helper << "; stroke-width:";
                append_float(helper, stroke->width());

                if (stroke->line_cap() != agg::butt_cap) {
                        helper << "; stroke-linecap:";
                        helper << convert_cap_mode_svg(stroke->line_cap());
                }

                if (stroke->line_join() != agg::miter_join) {
                        helper << "; stroke-linejoin:";
                        helper << convert_join_mode_svg(stroke->line_join());
                }

                helper << "\"\n";
        } else {
                helper << "style=\"fill:" << color;

                if (!style->Gradient() && style->Color().alpha < 255) {
                        helper << "; fill-opacity:";
                        append_float(helper, style->Color().alpha / 255.0);
                }

//              if (pathSourceShape->FillingRule() == FILL_MODE_EVEN_ODD &&
//                      pathSourceShape->Paths()->CountPaths() > 1)
//                      helper << "; fill-rule:evenodd";

                helper << "\"\n";
        }

        helper << "        d=\"";
        ret = write_line(stream, helper);

        if (ret < B_OK)
                return ret;

        int32 count = pathSourceShape->Paths()->CountItems();
        for (int32 i = 0; i < count; i++) {
                VectorPath* path = pathSourceShape->Paths()->ItemAtFast(i);

                if (i > 0) {
                        helper << "\n           ";
                }

                BPoint a, aIn, aOut;
                if (path->GetPointAt(0, a)) {
                        helper << "M";
                        append_float(helper, a.x, 2);
                        helper << " ";
                        append_float(helper, a.y, 2);
                }

                int32 pointCount = path->CountPoints();
                for (int32 j = 0; j < pointCount; j++) {

                        if (!path->GetPointsAt(j, a, aIn, aOut))
                                break;

                        BPoint b, bIn, bOut;
                        if ((j + 1 < pointCount && path->GetPointsAt(j + 1, b, bIn, bOut))
                                || (path->IsClosed() && path->GetPointsAt(0, b, bIn, bOut))) {

                                if (aOut == a && bIn == b) {
                                        if (a.x == b.x) {
                                                // vertical line
                                                helper << "V";
                                                append_float(helper, b.y, 2);
                                        } else if (a.y == b.y) {
                                                // horizontal line
                                                helper << "H";
                                                append_float(helper, b.x, 2);
                                        } else {
                                                // line
                                                helper << "L";
                                                append_float(helper, b.x, 2);
                                                helper << " ";
                                                append_float(helper, b.y, 2);
                                        }
                                } else {
                                        // cubic curve
                                        helper << "C";
                                        append_float(helper, aOut.x, 2);
                                        helper << " ";
                                        append_float(helper, aOut.y, 2);
                                        helper << " ";
                                        append_float(helper, bIn.x, 2);
                                        helper << " ";
                                        append_float(helper, bIn.y, 2);
                                        helper << " ";
                                        append_float(helper, b.x, 2);
                                        helper << " ";
                                        append_float(helper, b.y, 2);
                                }
                        }
                }
                if (path->IsClosed())
                        helper << "z";

                ret = write_line(stream, helper);
                if (ret < B_OK)
                        break;
        }
        helper << "\"\n";

        if (!pathSourceShape->IsIdentity()) {
                helper << "        transform=\"";
                _AppendMatrix(pathSourceShape, helper);
                helper << "\n";
        }

        if (ret >= B_OK) {
                helper << "  />\n";

                ret = write_line(stream, helper);
        }

        return ret;
}

// _ExportGradient
status_t
SVGExporter::_ExportGradient(const Gradient* gradient, BPositionIO* stream)
{
        BString helper;

        // display a warning if the gradient is unsupported
        if (gradient->Type() == GRADIENT_CONIC
                || gradient->Type() == GRADIENT_DIAMOND
                || gradient->Type() == GRADIENT_XY
                || gradient->Type() == GRADIENT_SQRT_XY)
                _DisplayUnsupportedGradientWarning();

        // start new gradient tag
        if (gradient->Type() == GRADIENT_CIRCULAR) {
                helper << "  <radialGradient ";
        } else {
                helper << "  <linearGradient ";
        }

        // id
        BString gradientName("gradient");
        gradientName << fGradientCount;

        helper << "id=\"" << gradientName << "\" gradientUnits=\"userSpaceOnUse\"";

        // write gradient transformation
        if (gradient->Type() == GRADIENT_CIRCULAR) {
                helper << " cx=\"0\" cy=\"0\" r=\"64\"";
                if (!gradient->IsIdentity()) {
                        helper << " gradientTransform=\"";
                        _AppendMatrix(gradient, helper);
                }
        } else {
                double x1 = -64.0;
                double y1 = -64.0;
                double x2 = 64.0;
                double y2 = -64.0;
                gradient->Transform(&x1, &y1);
                gradient->Transform(&x2, &y2);

                helper << " x1=\"";
                append_float(helper, x1, 2);
                helper << "\"";
                helper << " y1=\"";
                append_float(helper, y1, 2);
                helper << "\"";
                helper << " x2=\"";
                append_float(helper, x2, 2);
                helper << "\"";
                helper << " y2=\"";
                append_float(helper, y2, 2);
                helper << "\"";
        }

        helper << ">\n";

        // write stop tags
        char color[16];
        for (int32 i = 0; BGradient::ColorStop* stop = gradient->ColorAt(i); i++) {

                sprintf(color, "%.2x%.2x%.2x", stop->color.red,
                                                                           stop->color.green,
                                                                           stop->color.blue);
                color[6] = 0;

                helper << "   <stop offset=\"";
                append_float(helper, stop->offset);
                helper << "\" stop-color=\"#" << color << "\"";

                if (stop->color.alpha < 255) {
                        helper << " stop-opacity=\"";
                        append_float(helper, (float)stop->color.alpha / 255.0);
                        helper << "\"";
                }
                helper << "/>\n";
        }

        // finish gradient tag
        if (gradient->Type() == GRADIENT_CIRCULAR) {
                helper << "  </radialGradient>\n";
        } else {
                helper << "  </linearGradient>\n";
        }

        return write_line(stream, helper);
}

// _AppendMatrix
void
SVGExporter::_AppendMatrix(const Transformable* object, BString& string) const
{
        string << "matrix(";
//      append_float(string, object->sx);
//      string << ",";
//      append_float(string, object->shy);
//      string << ",";
//      append_float(string, object->shx);
//      string << ",";
//      append_float(string, object->sy);
//      string << ",";
//      append_float(string, object->tx);
//      string << ",";
//      append_float(string, object->ty);
double matrix[Transformable::matrix_size];
object->StoreTo(matrix);
append_float(string, matrix[0]);
string << ",";
append_float(string, matrix[1]);
string << ",";
append_float(string, matrix[2]);
string << ",";
append_float(string, matrix[3]);
string << ",";
append_float(string, matrix[4]);
string << ",";
append_float(string, matrix[5]);
        string << ")\"";
}

// _GetFill
status_t
SVGExporter::_GetFill(const Style* style, char* string,
                                          BPositionIO* stream)
{
        status_t ret = B_OK;
        if (Gradient* gradient = style->Gradient()) {
                ret = _ExportGradient(gradient, stream);
                sprintf(string, "url(#gradient%" B_PRId32 ")", fGradientCount++);
        } else {
                sprintf(string, "#%.2x%.2x%.2x", style->Color().red,
                                                                                 style->Color().green,
                                                                                 style->Color().blue);
        }
        return ret;
}