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()->CountShapes(); 107 for (int32 i = 0; i < count; i++) { 108 Shape* shape = icon->Shapes()->ShapeAtFast(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->TransformerAt(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()->CountPaths(); 283 for (int32 i = 0; i < count; i++) { 284 VectorPath* path = pathSourceShape->Paths()->PathAtFast(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