1/*
2 * Copyright 2006, 2023, Haiku. All rights reserved.
3 * Distributed under the terms of the MIT License.
4 *
5 * Authors:
6 *		Stephan A��mus <superstippi@gmx.de>
7 *		Zardshard
8 */
9
10#include <stdio.h>
11
12#include <Alert.h>
13#include <Catalog.h>
14#include <DataIO.h>
15#include <File.h>
16#include <Locale.h>
17#include <String.h>
18
19#include "support.h"
20
21#include "Icon.h"
22#include "GradientTransformable.h"
23#include "PathSourceShape.h"
24#include "Shape.h"
25#include "StrokeTransformer.h"
26#include "Style.h"
27#include "VectorPath.h"
28
29#include "SVGExporter.h"
30
31
32#undef B_TRANSLATION_CONTEXT
33#define B_TRANSLATION_CONTEXT "Icon-O-Matic-SVGExport"
34
35
36// write_line
37static status_t
38write_line(BPositionIO* stream, BString& string)
39{
40	ssize_t written = stream->Write(string.String(), string.Length());
41	if (written > B_OK && written < string.Length())
42		written = B_ERROR;
43	string.SetTo("");
44	return written;
45}
46
47// #pragma mark -
48
49// constructor
50SVGExporter::SVGExporter()
51	: Exporter(),
52	  fGradientCount(0),
53	  fOriginalEntry(NULL)
54{
55}
56
57// destructor
58SVGExporter::~SVGExporter()
59{
60	delete fOriginalEntry;
61}
62
63// Export
64status_t
65SVGExporter::Export(const Icon* icon, BPositionIO* stream)
66{
67	if (!icon || !stream)
68		return B_BAD_VALUE;
69
70//	if (fOriginalEntry && *fOriginalEntry == *refToFinalFile) {
71//		if (!_DisplayWarning()) {
72//			return B_CANCELED;
73//		} else {
74//			delete fOriginalEntry;
75//			fOriginalEntry = NULL;
76//		}
77//	}
78
79	BString helper;
80
81	fGradientCount = 0;
82
83	// header
84	helper << "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n";
85	status_t ret = write_line(stream, helper);
86
87	// image size
88	if (ret >= B_OK) {
89		helper << "<svg version=\"1.1\" width=\"" << 64 << "\""
90			   << " height=\"" << 64 << "\""
91			   << " color-interpolation=\"linearRGB\"\n"
92			   << "     xmlns:svg=\"http://www.w3.org/2000/svg\""
93// Should be needed when exporting inline images:
94//			   << " xmlns:xlink=\"http://www.w3.org/1999/xlink\""
95			   << " xmlns=\"http://www.w3.org/2000/svg\">\n";
96		ret = write_line(stream, helper);
97	}
98
99	if (ret >= B_OK) {
100		// start (the only) layer
101		helper << " <g>\n";
102		ret = write_line(stream, helper);
103
104		// export all shapes
105		if (ret >= B_OK) {
106			int32 count = icon->Shapes()->CountItems();
107			for (int32 i = 0; i < count; i++) {
108				Shape* shape = icon->Shapes()->ItemAtFast(i);
109				ret = _ExportShape(shape, stream);
110				if (ret < B_OK)
111					break;
112			}
113		}
114
115		// finish (the only) layer
116		if (ret >= B_OK) {
117			helper << " </g>\n";
118			ret = write_line(stream, helper);
119		}
120	}
121
122	// footer
123	if (ret >= B_OK) {
124		helper << "</svg>\n";
125		ret = write_line(stream, helper);
126	}
127	return ret;
128}
129
130// #pragma mark -
131
132// SetOriginalEntry
133void
134SVGExporter::SetOriginalEntry(const entry_ref* ref)
135{
136	if (ref) {
137		delete fOriginalEntry;
138		fOriginalEntry = new entry_ref(*ref);
139	}
140}
141
142// DisplayWarning
143bool
144SVGExporter::_DisplayWarning() const
145{
146	BAlert* alert = new BAlert(B_TRANSLATE("warning"),
147							   B_TRANSLATE("Icon-O-Matic might not have "
148							   "interpreted all data from the SVG "
149							   "when it was loaded. "
150							   "By overwriting the original "
151							   "file, this information would now "
152							   "be lost."),
153							   B_TRANSLATE("Cancel"),
154							   B_TRANSLATE("Overwrite"));
155	alert->SetShortcut(0, B_ESCAPE);
156	return alert->Go() == 1;
157}
158
159// #pragma mark -
160
161// convert_join_mode_svg
162const char*
163convert_join_mode_svg(agg::line_join_e mode)
164{
165	const char* svgMode = "miter";
166	switch (mode) {
167		case agg::round_join:
168			svgMode = "round";
169			break;
170		case agg::bevel_join:
171			svgMode = "bevel";
172			break;
173		case agg::miter_join:
174		default:
175			break;
176	}
177	return svgMode;
178}
179
180// convert_cap_mode_svg
181const char*
182convert_cap_mode_svg(agg::line_cap_e mode)
183{
184	const char* svgMode = "butt";
185	switch (mode) {
186		case agg::square_cap:
187			svgMode = "square";
188			break;
189		case agg::round_cap:
190			svgMode = "round";
191			break;
192		case agg::butt_cap:
193		default:
194			break;
195	}
196	return svgMode;
197}
198
199// #pragma mark -
200
201// _ExportShape
202status_t
203SVGExporter::_ExportShape(const Shape* shape, BPositionIO* stream)
204{
205	if (!shape->Visible(1.0)) {
206		// don't export shapes which are not visible at the
207		// default scale
208		return B_OK;
209	}
210
211	// only render PathSourceShapes
212	const PathSourceShape* pathSourceShape = dynamic_cast<const PathSourceShape*>(shape);
213	if (pathSourceShape == NULL)
214		return B_OK;
215
216
217	const Style* style = pathSourceShape->Style();
218
219	char color[64];
220	status_t ret = _GetFill(style, color, stream);
221
222	if (ret < B_OK)
223		return ret;
224
225	// The transformation matrix is extracted again in order to
226	// maintain the effect of a distorted outline. There is of
227	// course a difference when applying the transformation to
228	// the points of the path, then stroking the transformed
229	// path with an outline of a certain width, opposed to applying
230	// the transformation to the points of the generated stroke
231	// as well. Adobe SVG Viewer is supporting this fairly well,
232	// though I have come across SVG software that doesn't (InkScape).
233
234	// start new shape and write transform matrix
235	BString helper;
236	helper << "  <path ";
237
238	// hack to see if this is an outline shape
239	StrokeTransformer* stroke
240		= dynamic_cast<StrokeTransformer*>(pathSourceShape->Transformers()->ItemAt(0));
241	if (stroke) {
242		helper << "style=\"fill:none; stroke:" << color;
243		if (!style->Gradient() && style->Color().alpha < 255) {
244			helper << "; stroke-opacity:";
245			append_float(helper, style->Color().alpha / 255.0);
246		}
247		helper << "; stroke-width:";
248		append_float(helper, stroke->width());
249
250		if (stroke->line_cap() != agg::butt_cap) {
251			helper << "; stroke-linecap:";
252			helper << convert_cap_mode_svg(stroke->line_cap());
253		}
254
255		if (stroke->line_join() != agg::miter_join) {
256			helper << "; stroke-linejoin:";
257			helper << convert_join_mode_svg(stroke->line_join());
258		}
259
260		helper << "\"\n";
261	} else {
262		helper << "style=\"fill:" << color;
263
264		if (!style->Gradient() && style->Color().alpha < 255) {
265			helper << "; fill-opacity:";
266			append_float(helper, style->Color().alpha / 255.0);
267		}
268
269//		if (pathSourceShape->FillingRule() == FILL_MODE_EVEN_ODD &&
270//			pathSourceShape->Paths()->CountPaths() > 1)
271//			helper << "; fill-rule:evenodd";
272
273		helper << "\"\n";
274	}
275
276	helper << "        d=\"";
277	ret = write_line(stream, helper);
278
279	if (ret < B_OK)
280		return ret;
281
282	int32 count = pathSourceShape->Paths()->CountItems();
283	for (int32 i = 0; i < count; i++) {
284		VectorPath* path = pathSourceShape->Paths()->ItemAtFast(i);
285
286		if (i > 0) {
287			helper << "\n           ";
288		}
289
290		BPoint a, aIn, aOut;
291		if (path->GetPointAt(0, a)) {
292			helper << "M";
293			append_float(helper, a.x, 2);
294			helper << " ";
295			append_float(helper, a.y, 2);
296		}
297
298		int32 pointCount = path->CountPoints();
299		for (int32 j = 0; j < pointCount; j++) {
300
301			if (!path->GetPointsAt(j, a, aIn, aOut))
302				break;
303
304			BPoint b, bIn, bOut;
305			if ((j + 1 < pointCount && path->GetPointsAt(j + 1, b, bIn, bOut))
306				|| (path->IsClosed() && path->GetPointsAt(0, b, bIn, bOut))) {
307
308				if (aOut == a && bIn == b) {
309					if (a.x == b.x) {
310						// vertical line
311						helper << "V";
312						append_float(helper, b.y, 2);
313					} else if (a.y == b.y) {
314						// horizontal line
315						helper << "H";
316						append_float(helper, b.x, 2);
317					} else {
318						// line
319						helper << "L";
320						append_float(helper, b.x, 2);
321						helper << " ";
322						append_float(helper, b.y, 2);
323					}
324				} else {
325					// cubic curve
326					helper << "C";
327					append_float(helper, aOut.x, 2);
328					helper << " ";
329					append_float(helper, aOut.y, 2);
330					helper << " ";
331					append_float(helper, bIn.x, 2);
332					helper << " ";
333					append_float(helper, bIn.y, 2);
334					helper << " ";
335					append_float(helper, b.x, 2);
336					helper << " ";
337					append_float(helper, b.y, 2);
338				}
339			}
340		}
341		if (path->IsClosed())
342			helper << "z";
343
344		ret = write_line(stream, helper);
345		if (ret < B_OK)
346			break;
347	}
348	helper << "\"\n";
349
350	if (!pathSourceShape->IsIdentity()) {
351		helper << "        transform=\"";
352		_AppendMatrix(pathSourceShape, helper);
353		helper << "\n";
354	}
355
356	if (ret >= B_OK) {
357		helper << "  />\n";
358
359		ret = write_line(stream, helper);
360	}
361
362	return ret;
363}
364
365// _ExportGradient
366status_t
367SVGExporter::_ExportGradient(const Gradient* gradient, BPositionIO* stream)
368{
369	BString helper;
370
371	// start new gradient tag
372	if (gradient->Type() == GRADIENT_CIRCULAR) {
373		helper << "  <radialGradient ";
374	} else {
375		helper << "  <linearGradient ";
376	}
377
378	// id
379	BString gradientName("gradient");
380	gradientName << fGradientCount;
381
382	helper << "id=\"" << gradientName << "\" gradientUnits=\"userSpaceOnUse\"";
383
384	// write gradient transformation
385	if (gradient->Type() == GRADIENT_CIRCULAR) {
386		helper << " cx=\"0\" cy=\"0\" r=\"64\"";
387		if (!gradient->IsIdentity()) {
388			helper << " gradientTransform=\"";
389			_AppendMatrix(gradient, helper);
390		}
391	} else {
392		double x1 = -64.0;
393		double y1 = -64.0;
394		double x2 = 64.0;
395		double y2 = -64.0;
396		gradient->Transform(&x1, &y1);
397		gradient->Transform(&x2, &y2);
398
399		helper << " x1=\"";
400		append_float(helper, x1, 2);
401		helper << "\"";
402		helper << " y1=\"";
403		append_float(helper, y1, 2);
404		helper << "\"";
405		helper << " x2=\"";
406		append_float(helper, x2, 2);
407		helper << "\"";
408		helper << " y2=\"";
409		append_float(helper, y2, 2);
410		helper << "\"";
411	}
412
413	helper << ">\n";
414
415	// write stop tags
416	char color[16];
417	for (int32 i = 0; BGradient::ColorStop* stop = gradient->ColorAt(i); i++) {
418
419		sprintf(color, "%.2x%.2x%.2x", stop->color.red,
420									   stop->color.green,
421									   stop->color.blue);
422		color[6] = 0;
423
424		helper << "   <stop offset=\"";
425		append_float(helper, stop->offset);
426		helper << "\" stop-color=\"#" << color << "\"";
427
428		if (stop->color.alpha < 255) {
429			helper << " stop-opacity=\"";
430			append_float(helper, (float)stop->color.alpha / 255.0);
431			helper << "\"";
432		}
433		helper << "/>\n";
434	}
435
436	// finish gradient tag
437	if (gradient->Type() == GRADIENT_CIRCULAR) {
438		helper << "  </radialGradient>\n";
439	} else {
440		helper << "  </linearGradient>\n";
441	}
442
443	return write_line(stream, helper);
444}
445
446// _AppendMatrix
447void
448SVGExporter::_AppendMatrix(const Transformable* object, BString& string) const
449{
450	string << "matrix(";
451//	append_float(string, object->sx);
452//	string << ",";
453//	append_float(string, object->shy);
454//	string << ",";
455//	append_float(string, object->shx);
456//	string << ",";
457//	append_float(string, object->sy);
458//	string << ",";
459//	append_float(string, object->tx);
460//	string << ",";
461//	append_float(string, object->ty);
462double matrix[Transformable::matrix_size];
463object->StoreTo(matrix);
464append_float(string, matrix[0]);
465string << ",";
466append_float(string, matrix[1]);
467string << ",";
468append_float(string, matrix[2]);
469string << ",";
470append_float(string, matrix[3]);
471string << ",";
472append_float(string, matrix[4]);
473string << ",";
474append_float(string, matrix[5]);
475	string << ")\"";
476}
477
478// _GetFill
479status_t
480SVGExporter::_GetFill(const Style* style, char* string,
481					  BPositionIO* stream)
482{
483	status_t ret = B_OK;
484	if (Gradient* gradient = style->Gradient()) {
485		ret = _ExportGradient(gradient, stream);
486		sprintf(string, "url(#gradient%" B_PRId32 ")", fGradientCount++);
487	} else {
488		sprintf(string, "#%.2x%.2x%.2x", style->Color().red,
489										 style->Color().green,
490										 style->Color().blue);
491	}
492	return ret;
493}
494