xref: /haiku/src/apps/icon-o-matic/import_export/svg/SVGExporter.cpp (revision e1c4049fed1047bdb957b0529e1921e97ef94770)
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
37 static status_t
38 write_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
50 SVGExporter::SVGExporter()
51 	: Exporter(),
52 	  fGradientCount(0),
53 	  fOriginalEntry(NULL)
54 {
55 }
56 
57 // destructor
58 SVGExporter::~SVGExporter()
59 {
60 	delete fOriginalEntry;
61 }
62 
63 // Export
64 status_t
65 SVGExporter::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
133 void
134 SVGExporter::SetOriginalEntry(const entry_ref* ref)
135 {
136 	if (ref) {
137 		delete fOriginalEntry;
138 		fOriginalEntry = new entry_ref(*ref);
139 	}
140 }
141 
142 // DisplayWarning
143 bool
144 SVGExporter::_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
162 const char*
163 convert_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
181 const char*
182 convert_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
202 status_t
203 SVGExporter::_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
366 status_t
367 SVGExporter::_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
447 void
448 SVGExporter::_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);
462 double matrix[Transformable::matrix_size];
463 object->StoreTo(matrix);
464 append_float(string, matrix[0]);
465 string << ",";
466 append_float(string, matrix[1]);
467 string << ",";
468 append_float(string, matrix[2]);
469 string << ",";
470 append_float(string, matrix[3]);
471 string << ",";
472 append_float(string, matrix[4]);
473 string << ",";
474 append_float(string, matrix[5]);
475 	string << ")\"";
476 }
477 
478 // _GetFill
479 status_t
480 SVGExporter::_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