/*========================================================================= Program: Visualization Toolkit Module: vtkSVGContextDevice2D.cxx Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen All rights reserved. See Copyright.txt or http://www.kitware.com/Copyright.htm for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notice for more information. =========================================================================*/ #include "vtkSVGContextDevice2D.h" #include "vtkAssume.h" #include "vtkBase64OutputStream.h" #include "vtkBrush.h" #include "vtkColor.h" #include "vtkFloatArray.h" #include "vtkFreeTypeTools.h" #include "vtkImageCast.h" #include "vtkImageData.h" #include "vtkIntArray.h" #include "vtkMath.h" #include "vtkMatrix3x3.h" #include "vtkMatrix4x4.h" #include "vtkNew.h" #include "vtkObjectFactory.h" #include "vtkPNGWriter.h" #include "vtkPath.h" #include "vtkPen.h" #include "vtkPointData.h" #include "vtkRenderWindow.h" #include "vtkRenderer.h" #include "vtkTextProperty.h" #include "vtkTextRenderer.h" #include "vtkTransform.h" #include "vtkUnsignedCharArray.h" #include "vtkVector.h" #include "vtkVectorOperators.h" #include "vtkXMLDataElement.h" #include #include #include #include #include #include #include #include #include namespace { std::string ColorToString(const unsigned char* rgb) { std::ostringstream out; out << "#"; for (int i = 0; i < 3; ++i) { out << std::setw(2) << std::right << std::setfill('0') << std::hex << static_cast(rgb[i]); } return out.str(); } // Bbox is xmin, xmax, ymin, ymax. Writes: // "xmin,ymin,xmax,ymax" std::string BBoxToString(const std::array& bbox) { std::ostringstream out; out << bbox[0] << "," << bbox[2] << "," << bbox[1] << "," << bbox[3]; return out.str(); } std::string Transform2DToString(const std::array xform) { std::ostringstream out; out << "matrix(" << xform[0] << "," << xform[3] << "," << xform[1] << "," << xform[4] << "," << xform[2] << "," << xform[5] << ")"; return out.str(); } struct EllipseHelper { EllipseHelper(float cx, float cy, float rx, float ry) : X(0.f) , Y(0.f) , Cx(cx) , Cy(cy) , Rx(rx) , Ry(ry) { } void UpdateDegrees(float degrees) { this->UpdateRadians(vtkMath::RadiansFromDegrees(degrees)); } void UpdateRadians(float radians) { this->X = this->Cx + std::cos(radians) * this->Rx; this->Y = this->Cy + std::sin(radians) * this->Ry; } float X; float Y; private: float Cx; float Cy; float Rx; float Ry; }; struct FontKey { vtkSmartPointer TextProperty; explicit FontKey(vtkTextProperty* tprop) : TextProperty(vtkSmartPointer::New()) { // Clone into an internal tprop. The property will likely be modified by // the time we get around to writing out definitions. this->TextProperty->ShallowCopy(tprop); // Blank out properties that we don't care about for raw outlines: this->TextProperty->SetFontSize(0); this->TextProperty->SetOrientation(0.); } FontKey(const FontKey& o) = default; bool operator<(const FontKey& other) const { const int thisFontFamily = this->TextProperty->GetFontFamily(); const int otherFontFamily = other.TextProperty->GetFontFamily(); if (thisFontFamily < otherFontFamily) { return true; } else if (thisFontFamily > otherFontFamily) { return false; } const bool thisBold = this->TextProperty->GetBold() != 0; const bool otherBold = other.TextProperty->GetBold() != 0; if (thisBold < otherBold) { return true; } else if (thisBold > otherBold) { return false; } const bool thisItalic = this->TextProperty->GetItalic() != 0; const bool otherItalic = other.TextProperty->GetItalic() != 0; if (thisItalic < otherItalic) { return true; } else if (thisItalic > otherItalic) { return false; } if (thisFontFamily == VTK_FONT_FILE) { const char* thisFile = this->TextProperty->GetFontFile(); const char* otherFile = other.TextProperty->GetFontFile(); if (thisFile < otherFile) { return true; } else if (thisFile > otherFile) { return false; } } return false; } }; struct FontInfo { using CharType = vtkTypeUInt32; using KerningPairType = std::pair; explicit FontInfo(const std::string& svgId) : SVGId(svgId) { } void ProcessString(const std::string& str) { if (!str.empty()) { std::string::const_iterator it = str.begin(); std::string::const_iterator end = str.end(); vtkTypeUInt32 value = utf8::next(it, end); this->Chars.insert(value); while (it != end) { vtkTypeUInt32 next = utf8::next(it, end); this->Chars.insert(next); this->KerningPairs.insert(std::make_pair(value, next)); value = next; } } } std::string SVGId; std::set Chars; std::set KerningPairs; private: FontInfo(const FontInfo&) = delete; void operator=(const FontInfo&) = delete; }; struct ImageInfo { explicit ImageInfo(vtkImageData* img) { vtkNew pngWriter; pngWriter->WriteToMemoryOn(); pngWriter->SetCompressionLevel(0); pngWriter->SetInputData(img); pngWriter->Write(); vtkUnsignedCharArray* png = pngWriter->GetResult(); if (!png || png->GetNumberOfValues() == 0) { return; } std::ostringstream base64Stream; base64Stream << "data:image/png;base64,"; vtkNew base64Encoder; base64Encoder->SetStream(&base64Stream); if (!base64Encoder->StartWriting() || !base64Encoder->Write(png->GetPointer(0), png->GetNumberOfValues()) || !base64Encoder->EndWriting()) { return; } int* dims = img->GetDimensions(); this->Size[0] = dims[0]; this->Size[1] = dims[1]; this->PNGBase64 = base64Stream.str(); } ImageInfo(ImageInfo&& o) noexcept : Size(o.Size) , Id(std::move(o.Id)) , PNGBase64(std::move(o.PNGBase64)) { } bool operator<(const ImageInfo& other) const { if (this->Size[0] < other.Size[0]) { return true; } else if (this->Size[0] > other.Size[0]) { return false; } if (this->Size[1] < other.Size[1]) { return true; } else if (this->Size[1] > other.Size[1]) { return false; } if (this->PNGBase64 < other.PNGBase64) { return true; } return false; } std::array Size; std::string Id; std::string PNGBase64; private: ImageInfo(const ImageInfo&) = delete; void operator=(const ImageInfo&) = delete; }; struct PatternInfo { explicit PatternInfo(const ImageInfo& img, int textureProperty) // We only care about Repeat and Stretch, since SVG doesn't allow control // over Nearest/Linear interpolation. : TextureProperty(textureProperty & (vtkBrush::Repeat | vtkBrush::Stretch)) , ImageSize(img.Size) , ImageId(img.Id) { } PatternInfo(PatternInfo&& o) noexcept : TextureProperty(o.TextureProperty) , ImageSize(o.ImageSize) , ImageId(std::move(o.ImageId)) , PatternId(std::move(o.PatternId)) { } bool operator<(const PatternInfo& other) const { if (this->TextureProperty < other.TextureProperty) { return true; } else if (this->TextureProperty > other.TextureProperty) { return false; } if (this->ImageId < other.ImageId) { return true; } return false; } int TextureProperty; std::array ImageSize; std::string ImageId; std::string PatternId; private: PatternInfo(const PatternInfo&) = delete; void operator=(const PatternInfo&) = delete; }; struct ClipRectInfo { explicit ClipRectInfo(const std::array& rect) : Rect(rect) { } ClipRectInfo(ClipRectInfo&& o) noexcept : Rect(o.Rect) , Id(std::move(o.Id)) { } bool operator<(const ClipRectInfo& other) const { for (size_t i = 0; i < this->Rect.size(); ++i) { if (this->Rect[i] < other.Rect[i]) { return true; } else if (this->Rect[i] > other.Rect[i]) { return false; } } return false; } std::array Rect; // x, y, w, h std::string Id; private: ClipRectInfo(const ClipRectInfo&) = delete; void operator=(const ClipRectInfo&) = delete; }; // SVG's y axis is inverted compared to VTK's: struct YConverter { float Height; explicit YConverter(float height) : Height(height) { } float operator()(float inY) { return this->Height - inY; } }; } // end anon namespace // Need to be able to use vtkColor4f in a std::map. Must be outside of the anon // namespace to work. static bool operator<(const vtkColor4f& a, const vtkColor4f& b) { for (int i = 0; i < 4; ++i) { if (a[i] < b[i]) { return true; } else if (a[i] > b[i]) { return false; } } return false; } struct vtkSVGContextDevice2D::Details { using FontMapType = std::map; using ImageSetType = std::set; using PatternSetType = std::set; using ClipRectSetType = std::set; FontMapType FontMap; ImageSetType ImageSet; PatternSetType PatternSet; ClipRectSetType ClipRectSet; ~Details() { this->FreeFontMap(); } void FreeFontMap() { for (auto& it : this->FontMap) { delete it.second; } this->FontMap.clear(); } const ImageInfo& GetImageInfo(vtkImageData* img) { ImageInfo newInfo(img); auto insertResult = this->ImageSet.insert(std::move(newInfo)); const ImageInfo& info = *insertResult.first; if (insertResult.second) { std::ostringstream id; id << "vtkEmbeddedImage" << this->ImageSet.size(); // This is safe; setting the id won't change the sort order in the set. const_cast(info).Id = id.str(); } return info; } const PatternInfo& GetPatternInfo(vtkImageData* texture, int textureProperty) { const ImageInfo& imageInfo = this->GetImageInfo(texture); PatternInfo newInfo(imageInfo, textureProperty); auto insertResult = this->PatternSet.insert(std::move(newInfo)); const PatternInfo& info = *insertResult.first; if (insertResult.second) { std::ostringstream id; id << "vtkPattern" << this->PatternSet.size(); // This is safe; setting the id won't change the sort order in the set. const_cast(info).PatternId = id.str(); } return info; } const ClipRectInfo& GetClipRectInfo(const std::array& rect) { ClipRectInfo newInfo(rect); auto insertResult = this->ClipRectSet.insert(std::move(newInfo)); const ClipRectInfo& info = *insertResult.first; if (insertResult.second) { std::ostringstream id; id << "vtkClipRect" << this->ClipRectSet.size(); // This is safe; setting the id won't change the sort order in the set. const_cast(info).Id = id.str(); } return info; } FontInfo& GetFontInfo(vtkTextProperty* tprop) { FontKey key(tprop); FontMapType::const_iterator it = this->FontMap.find(key); if (it == this->FontMap.end()) { std::ostringstream tmp; tmp << "vtkExportedFont-" << std::hex << this << "_" << std::dec << this->FontMap.size() << "_" << tprop->GetFontFamilyAsString(); std::string id = tmp.str(); auto result = this->FontMap.insert(std::make_pair(key, new FontInfo(id))); it = result.first; } return *it->second; } }; //------------------------------------------------------------------------------ vtkStandardNewMacro(vtkSVGContextDevice2D); vtkCxxSetObjectMacro(vtkSVGContextDevice2D, Viewport, vtkViewport); //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::PrintSelf(std::ostream& os, vtkIndent indent) { this->Superclass::PrintSelf(os, indent); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetSVGContext(vtkXMLDataElement* context, vtkXMLDataElement* defs) { this->ContextNode = context; this->ActiveNode = context; this->DefinitionNode = defs; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::GenerateDefinitions() { if (this->EmbedFonts) { this->WriteFonts(); } this->WriteImages(); this->WritePatterns(); // Must come after images this->WriteClipRects(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::Begin(vtkViewport* vp) { // Recreate the pen/brush to reset state: this->Pen->Delete(); this->Pen = vtkPen::New(); this->Brush->Delete(); this->Brush = vtkBrush::New(); this->SetViewport(vp); this->CanvasHeight = static_cast(vp->GetVTKWindow()->GetSize()[1]); std::fill(this->ClipRect.begin(), this->ClipRect.end(), 0); std::fill(this->ActiveNodeClipRect.begin(), this->ActiveNodeClipRect.end(), 0); std::fill(this->ActiveNodeTransform.begin(), this->ActiveNodeTransform.end(), 0.); this->ActiveNodeTransform[0] = 1.; this->ActiveNodeTransform[4] = 1.; this->ActiveNodeTransform[8] = 1.; this->Matrix->Identity(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::End() { this->SetViewport(nullptr); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawPoly(float* points, int n, unsigned char* colors, int nc_comps) { if (!colors) { vtkNew polyLine; polyLine->SetName("polyline"); this->ActiveNode->AddNestedElement(polyLine); this->ApplyPenStateToNode(polyLine); YConverter y(this->CanvasHeight); std::ostringstream verts; verts << "\n"; for (int i = 0; i < n; ++i) { verts << points[i * 2] << "," << y(points[i * 2 + 1]) << "\n"; } polyLine->SetAttribute("points", verts.str().c_str()); } else { this->PushGraphicsState(); this->ApplyPenStippleToNode(this->ActiveNode); this->ApplyPenWidthToNode(this->ActiveNode); bool useAlpha = nc_comps == 4; if (!useAlpha) { this->ApplyPenOpacityToNode(this->ActiveNode); } for (int i = 0; i < n - 1; ++i) { const vtkVector2f p1(points + i * 2); const vtkColor4ub c1(colors + i * nc_comps); const vtkVector2f p2(points + (i + 1) * 2); const vtkColor4ub c2(colors + (i + 1) * nc_comps); this->DrawLineGradient(p1, c1, p2, c2, useAlpha); } this->PopGraphicsState(); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawLines(float* points, int n, unsigned char* colors, int nc_comps) { if (!colors) { // Use path instead of lines for a more efficient/compact representation: vtkNew path; path->SetName("path"); this->ActiveNode->AddNestedElement(path); this->ApplyPenStateToNode(path); YConverter y(this->CanvasHeight); std::ostringstream d; d << "\n"; int numLines = n / 2; for (int i = 0; i < numLines; ++i) { const float* p1 = points + i * 4; const float* p2 = points + i * 4 + 2; d << "M" << p1[0] << "," << y(p1[1]) << "L" << p2[0] << "," << y(p2[1]) << "\n"; } path->SetAttribute("d", d.str().c_str()); } else { this->PushGraphicsState(); this->ApplyPenStippleToNode(this->ActiveNode); this->ApplyPenWidthToNode(this->ActiveNode); bool useAlpha = nc_comps == 4; if (!useAlpha) { this->ApplyPenOpacityToNode(this->ActiveNode); } const int numLines = n / 2; for (int i = 0; i < numLines; ++i) { const vtkVector2f p1(points + i * 4); const vtkVector2f p2(points + i * 4 + 2); const vtkColor4ub c1(colors + i * 2 * nc_comps); const vtkColor4ub c2(colors + (i * 2 + 1) * nc_comps); this->DrawLineGradient(p1, c1, p2, c2, useAlpha); } this->PopGraphicsState(); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawPoints(float* points, int n, unsigned char* colors, int nc_comps) { if (!colors) { // Use path instead of rects for a more efficient/compact representation. vtkNew path; path->SetName("path"); this->ActiveNode->AddNestedElement(path); this->ApplyPenAsFillColorToNode(path); this->ApplyPenAsFillOpacityToNode(path); YConverter y(this->CanvasHeight); float deltaX; float deltaY; this->GetScaledPenWidth(deltaX, deltaY); deltaX *= 0.5f; deltaY *= 0.5f; std::ostringstream d; d << "\n"; for (int i = 0; i < n; ++i) { const float* p = points + i * 2; d << "M" << p[0] - deltaX << "," << y(p[1] - deltaY) << "\n" "L" << p[0] + deltaX << "," << y(p[1] - deltaY) << "\n" << p[0] + deltaX << "," << y(p[1] + deltaY) << "\n" << p[0] - deltaX << "," << y(p[1] + deltaY) << "\nz\n"; } path->SetAttribute("d", d.str().c_str()); } else { const float width = this->GetScaledPenWidth(); const float halfWidth = width * 0.5f; const bool useAlpha = nc_comps == 4; if (!useAlpha) { this->PushGraphicsState(); this->ApplyPenAsFillOpacityToNode(this->ActiveNode); } YConverter y(this->CanvasHeight); for (int i = 0; i < n; ++i) { const float* p = points + i * 2; const unsigned char* c = colors + i * nc_comps; vtkNew point; this->ActiveNode->AddNestedElement(point); point->SetName("rect"); point->SetFloatAttribute("x", p[0] - halfWidth); point->SetFloatAttribute("y", y(p[1] - halfWidth)); point->SetFloatAttribute("width", width); point->SetFloatAttribute("height", width); point->SetAttribute("fill", ColorToString(c).c_str()); if (useAlpha && c[3] != 255) { point->SetFloatAttribute("fill-opacity", c[3] / 255.f); } } if (!useAlpha) { this->PopGraphicsState(); } } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawPointSprites( vtkImageData* spriteIn, float* points, int n, unsigned char* colors, int nc_comps) { if (nc_comps != 3 && nc_comps != 4) { vtkErrorMacro("Unsupported number of components: " << nc_comps); return; } vtkImageData* rgba = this->PreparePointSprite(spriteIn); if (!rgba) { vtkErrorMacro("Unsupported point sprite format."); return; } assert(rgba->GetScalarType() == VTK_UNSIGNED_CHAR); assert(rgba->GetNumberOfScalarComponents() == 4); int dims[3]; rgba->GetDimensions(dims); vtkIdType numPoints = rgba->GetNumberOfPoints(); vtkUnsignedCharArray* colorArray = vtkArrayDownCast(rgba->GetPointData()->GetScalars()); const float sizeFactor = this->GetScaledPenWidth() / static_cast(std::max(dims[0], dims[1])); const float spriteWidth = dims[0] * sizeFactor; const float spriteHeight = dims[1] * sizeFactor; const float halfWidth = spriteWidth * 0.5f; const float halfHeight = spriteHeight * 0.5f; const float brushAlpha = this->Brush->GetOpacity() / 255.f; YConverter y(this->CanvasHeight); typedef std::map SpriteMap; SpriteMap spriteMap; for (int i = 0; i < n; ++i) { const float* p = points + 2 * i; vtkColor4f color; if (colors) { unsigned char* c = colors + nc_comps * i; switch (nc_comps) { case 3: color.Set(c[0] / 255.f, c[1] / 255.f, c[2] / 255.f, brushAlpha); break; case 4: color.Set(c[0] / 255.f, c[1] / 255.f, c[2] / 255.f, c[3] / 255.f); break; default: vtkErrorMacro("Unsupported number of color components: " << nc_comps); continue; } } else { vtkColor4ub penColor = this->Pen->GetColorObject(); color = vtkColor4f( penColor[0] / 255.f, penColor[1] / 255.f, penColor[2] / 255.f, penColor[3] / 255.f); } std::string sprite; SpriteMap::iterator it = spriteMap.find(color); if (it != spriteMap.end()) { sprite = it->second; } else { vtkNew spriteColor; spriteColor->SetNumberOfComponents(4); spriteColor->SetNumberOfTuples(numPoints); for (vtkIdType t = 0; t < numPoints; ++t) { // This is what the OpenGL implementation does: for (int c = 0; c < 4; ++c) { spriteColor->SetTypedComponent( t, c, static_cast(colorArray->GetTypedComponent(t, c) * color[c] + .5f)); } } vtkNew spriteImage; spriteImage->ShallowCopy(rgba); spriteImage->GetPointData()->SetScalars(spriteColor); const ImageInfo& info = this->Impl->GetImageInfo(spriteImage); sprite = info.Id; spriteMap.insert(std::make_pair(color, sprite)); } const float xScale = spriteWidth / dims[0]; const float yScale = spriteHeight / dims[1]; // Offset the coordinates to center the sprite on the anchor: const float anchorX = p[0] - halfWidth; const float anchorY = y(p[1] - halfHeight); // Construct a matrix representing the following transformation: // // [X] = [T3] [T2] [S] [T1] // // [X] = final transform // [T1] = translate(-pos.X, -pos.Y); Move to origin to prepare for scaling. // [S] = scale(xScale, yScale); Resize the image to match the input rect. // [T2] = translate(0, -pos.H); Anchor at bottom corner instead of top // [T3] = translate(pos.X, pos.Y); Move back to anchor point std::ostringstream xform; xform << "matrix(" << xScale << ",0,0," << yScale << "," << anchorX - xScale * anchorX << "," << anchorY - (yScale * anchorY + spriteHeight) << ")"; vtkNew use; this->ActiveNode->AddNestedElement(use); use->SetName("use"); use->SetFloatAttribute("x", anchorX); use->SetFloatAttribute("y", anchorY); // YConverter already applied use->SetFloatAttribute("width", spriteWidth); use->SetFloatAttribute("height", spriteHeight); use->SetAttribute("transform", xform.str().c_str()); use->SetAttribute("xlink:href", (std::string("#") + sprite).c_str()); } rgba->UnRegister(this); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawMarkers( int shape, bool highlight, float* points, int n, unsigned char* colors, int nc_comps) { bool fill = false; bool stroke = false; float strokeWidth = 0.f; std::string markerId; switch (shape) { case VTK_MARKER_CROSS: markerId = this->AddCrossSymbol(highlight); stroke = true; strokeWidth = highlight ? 1.5f : 1.f; break; default: // default is here for consistency with old impl -- defaults to plus for // unrecognized shapes. VTK_FALLTHROUGH; case VTK_MARKER_PLUS: markerId = this->AddPlusSymbol(highlight); stroke = true; strokeWidth = highlight ? 1.5f : 1.f; break; case VTK_MARKER_SQUARE: markerId = this->AddSquareSymbol(highlight); fill = true; break; case VTK_MARKER_CIRCLE: markerId = this->AddCircleSymbol(highlight); fill = true; break; case VTK_MARKER_DIAMOND: markerId = this->AddDiamondSymbol(highlight); fill = true; break; } const float width = this->GetScaledPenWidth(); const float halfWidth = width * 0.5f; YConverter y(this->CanvasHeight); // Adjust stroke width for scaling. Symbols are defined in a unit square. strokeWidth /= width; markerId = std::string("#") + markerId; if (!colors) { this->PushGraphicsState(); if (stroke) { this->ApplyPenColorToNode(this->ActiveNode); this->ApplyPenOpacityToNode(this->ActiveNode); this->ApplyPenStippleToNode(this->ActiveNode); this->ActiveNode->SetFloatAttribute("stroke-width", strokeWidth); } if (fill) { this->ApplyPenAsFillColorToNode(this->ActiveNode); this->ApplyPenAsFillOpacityToNode(this->ActiveNode); } for (int i = 0; i < n; ++i) { const float* p = points + i * 2; vtkNew node; this->ActiveNode->AddNestedElement(node); node->SetName("use"); node->SetFloatAttribute("x", p[0] - halfWidth); node->SetFloatAttribute("y", y(p[1]) - halfWidth); node->SetFloatAttribute("width", width); node->SetFloatAttribute("height", width); node->SetAttribute("xlink:href", markerId.c_str()); } this->PopGraphicsState(); } else { const bool useAlpha = nc_comps == 4; if (!useAlpha) { this->PushGraphicsState(); if (stroke) { this->ApplyPenOpacityToNode(this->ActiveNode); } if (fill) { this->ApplyPenAsFillOpacityToNode(this->ActiveNode); } } for (int i = 0; i < n; ++i) { const float* p = points + i * 2; const unsigned char* c = colors + i * nc_comps; const std::string colStr = ColorToString(c); vtkNew node; this->ActiveNode->AddNestedElement(node); node->SetName("use"); node->SetFloatAttribute("x", p[0] - halfWidth); node->SetFloatAttribute("y", y(p[1]) - halfWidth); node->SetFloatAttribute("width", width); node->SetFloatAttribute("height", width); node->SetAttribute("xlink:href", markerId.c_str()); if (stroke) { node->SetAttribute("stroke", colStr.c_str()); node->SetFloatAttribute("stroke-width", strokeWidth); } if (fill) { node->SetAttribute("fill", colStr.c_str()); } if (useAlpha && c[3] != 255) { const float a = c[3] / 255.f; if (stroke) { node->SetFloatAttribute("stroke-opacity", a); } if (fill) { node->SetFloatAttribute("fill-opacity", a); } } } if (!useAlpha) { this->PopGraphicsState(); } } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawQuad(float* points, int n) { this->DrawPolygon(points, n); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawQuadStrip(float* points, int n) { if (n < 4 || n % 2 != 0) { // Must be at least one quad, and a whole number of quads. return; } // Combine all into a path that traces the exterior (Even verts on one side, // odd verts on the other): vtkNew path; path->SetName("path"); this->ActiveNode->AddNestedElement(path); this->ApplyBrushStateToNode(path); YConverter y(this->CanvasHeight); std::ostringstream d; d << "\nM" << points[0] << "," << y(points[1]) << "\nL\n"; for (int i = 2; i < n; i += 2) { d << points[i * 2] << "," << y(points[i * 2 + 1]) << "\n"; } for (int i = n - 1; i >= 0; i -= 2) { d << points[i * 2] << "," << y(points[i * 2 + 1]) << "\n"; } d << "z"; path->SetAttribute("d", d.str().c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawPolygon(float* points, int n) { vtkNew path; path->SetName("path"); this->ActiveNode->AddNestedElement(path); this->ApplyBrushStateToNode(path); YConverter y(this->CanvasHeight); std::ostringstream d; d << "\nM" << points[0] << "," << y(points[1]) << "\nL"; for (int i = 1; i < n; ++i) { d << points[i * 2] << "," << y(points[i * 2 + 1]) << "\n"; } d << "z"; path->SetAttribute("d", d.str().c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawColoredPolygon( float* points, int numPoints, unsigned char* colors, int nc_comps) { assert(numPoints > 0); assert(nc_comps >= 3 && nc_comps <= 4); assert(points != nullptr); // Just use the standard draw method if there is a texture or colors are not // specified: if (this->Brush->GetTexture() != nullptr || nc_comps == 0) { this->DrawPolygon(points, numPoints); return; } // If all of the points have the same color, use a more compact method to // draw the poly: bool sameColor = true; for (int i = 1; i < numPoints && sameColor; ++i) { sameColor = std::equal(colors, colors + nc_comps, colors + (i * nc_comps)); } if (sameColor) { const vtkColor4ub oldBrush = this->Brush->GetColorObject(); switch (nc_comps) { case 4: this->Brush->SetOpacity(colors[3]); VTK_FALLTHROUGH; case 3: this->Brush->SetColor(colors); break; default: vtkWarningMacro("Unsupported number of color components: " << nc_comps); return; } this->DrawPolygon(points, numPoints); this->Brush->SetColor(oldBrush); return; } const bool useAlpha = nc_comps == 4; const vtkVector2f p0(points); const vtkColor4ub c0(colors); // We may have 3 or 4 components, so initialize these with a sane alpha value: vtkColor4ub c1{ 0, 0, 0, 255 }; vtkColor4ub c2{ 0, 0, 0, 255 }; for (int i = 1; i < numPoints - 1; ++i) { const vtkVector2f p1(points + 2 * i); const vtkVector2f p2(points + 2 * (i + 1)); std::copy_n(colors + nc_comps * i, nc_comps, c1.GetData()); std::copy_n(colors + nc_comps * (i + 1), nc_comps, c2.GetData()); this->DrawTriangleGradient(p0, c0, p1, c1, p2, c2, useAlpha); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawEllipseWedge(float cx, float cy, float outRx, float outRy, float inRx, float inRy, float startAngle, float stopAngle) { if (stopAngle < startAngle) { std::swap(startAngle, stopAngle); } const float arcLength = stopAngle - startAngle; const bool isArc = arcLength < 359.99f; const bool isFilled = inRx == 0.f && inRy == 0.f; const bool isCircle = inRx == inRy && outRx == outRy; const int largeArcFlag = (arcLength >= 180.f) ? 1 : 0; const int sweepFlag = 0; YConverter y(this->CanvasHeight); if (!isArc) { if (isFilled) { // Easy case: full ellipse/circle: if (isCircle) { vtkNew circle; this->ActiveNode->AddNestedElement(circle); this->ApplyBrushStateToNode(circle); circle->SetName("circle"); circle->SetFloatAttribute("cx", cx); circle->SetFloatAttribute("cy", y(cy)); circle->SetFloatAttribute("r", outRx); } else // !isCircle { vtkNew ellipse; this->ActiveNode->AddNestedElement(ellipse); this->ApplyBrushStateToNode(ellipse); ellipse->SetName("ellipse"); ellipse->SetFloatAttribute("cx", cx); ellipse->SetFloatAttribute("cy", y(cy)); ellipse->SetFloatAttribute("rx", outRx); ellipse->SetFloatAttribute("ry", outRy); } } else // !isFilled { vtkNew path; this->ActiveNode->AddNestedElement(path); this->ApplyBrushStateToNode(path); path->SetName("path"); path->SetAttribute("fill-rule", "evenodd"); std::ostringstream d; // Outer ellipse: EllipseHelper helper(cx, cy, outRx, outRy); helper.UpdateDegrees(0.f); d << "M" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(180.f); d << "A" << outRx << "," << outRy << " 0 1 1 " << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(360.f); d << "A" << outRx << "," << outRy << " 0 1 1 " << helper.X << "," << y(helper.Y) << "\nz\n"; // Inner ellipse: helper = EllipseHelper(cx, cy, inRx, inRy); helper.UpdateDegrees(0.f); d << "M" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(180.f); d << "A" << inRx << "," << inRy << " 0 1 1 " << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(360.f); d << "A" << inRx << "," << inRy << " 0 1 1 " << helper.X << "," << y(helper.Y) << "\nz\n"; path->SetAttribute("d", d.str().c_str()); } } else // isArc { if (isFilled) { vtkNew path; this->ActiveNode->AddNestedElement(path); this->ApplyBrushStateToNode(path); path->SetName("path"); std::ostringstream d; EllipseHelper helper(cx, cy, outRx, outRy); d << "M" << cx << "," << y(cy) << "\n"; helper.UpdateDegrees(startAngle); d << "L" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(stopAngle); d << "A" << outRx << "," << outRy << " 0 " << largeArcFlag << " " << sweepFlag << " " << helper.X << "," << y(helper.Y) << "\nz\n"; path->SetAttribute("d", d.str().c_str()); } else // !isFilled { vtkNew path; this->ActiveNode->AddNestedElement(path); this->ApplyBrushStateToNode(path); path->SetName("path"); path->SetAttribute("fill-rule", "evenodd"); std::ostringstream d; // Outer ellipse EllipseHelper helper(cx, cy, outRx, outRy); helper.UpdateDegrees(startAngle); d << "M" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(stopAngle); d << "A" << outRx << "," << outRy << " 0 " << largeArcFlag << " " << sweepFlag << " " << helper.X << "," << y(helper.Y) << "\n"; path->SetAttribute("d", d.str().c_str()); // Inner ellipse const int innerSweepFlag = 1; helper = EllipseHelper(cx, cy, inRx, inRy); helper.UpdateDegrees(stopAngle); d << "L" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(startAngle); d << "A" << inRx << "," << inRy << " 0 " << largeArcFlag << " " << innerSweepFlag << " " << helper.X << "," << y(helper.Y) << "\nz\n"; path->SetAttribute("d", d.str().c_str()); } } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawEllipticArc( float cx, float cy, float rX, float rY, float startAngle, float stopAngle) { if (stopAngle < startAngle) { std::swap(startAngle, stopAngle); } const float arcLength = stopAngle - startAngle; const bool isArc = arcLength < 360.f; const bool isCircle = rX == rY; const int largeArcFlag = (arcLength >= 180.f) ? 1 : 0; const int sweepFlag = 0; YConverter y(this->CanvasHeight); if (!isArc) { // Easy case: full ellipse/circle: if (isCircle) { vtkNew circle; this->ActiveNode->AddNestedElement(circle); this->ApplyPenStateToNode(circle); this->ApplyBrushStateToNode(circle); circle->SetName("circle"); circle->SetFloatAttribute("cx", cx); circle->SetFloatAttribute("cy", y(cy)); circle->SetFloatAttribute("r", rX); } else // !isCircle { vtkNew ellipse; this->ActiveNode->AddNestedElement(ellipse); this->ApplyPenStateToNode(ellipse); this->ApplyBrushStateToNode(ellipse); ellipse->SetName("ellipse"); ellipse->SetFloatAttribute("cx", cx); ellipse->SetFloatAttribute("cy", y(cy)); ellipse->SetFloatAttribute("rx", rX); ellipse->SetFloatAttribute("ry", rY); } } else // isArc { vtkNew path; this->ActiveNode->AddNestedElement(path); this->ApplyPenStateToNode(path); this->ApplyBrushStateToNode(path); path->SetName("path"); std::ostringstream d; EllipseHelper helper(cx, cy, rX, rY); helper.UpdateDegrees(startAngle); d << "M" << helper.X << "," << y(helper.Y) << "\n"; helper.UpdateDegrees(stopAngle); d << "A" << rX << "," << rY << " 0 " << largeArcFlag << " " << sweepFlag << " " << helper.X << "," << y(helper.Y) << "\n"; path->SetAttribute("d", d.str().c_str()); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawString(float* point, const vtkStdString& string) { vtkTextRenderer* tren = vtkTextRenderer::GetInstance(); if (!tren) { vtkErrorMacro("vtkTextRenderer unavailable. Link to vtkRenderingFreeType " "to get the default implementation."); return; } int backend = this->TextAsPath ? vtkTextRenderer::Default : tren->DetectBackend(string); if (backend == vtkTextRenderer::FreeType) { // Embed freetype text and fonts in the SVG: FontInfo& info = this->Impl->GetFontInfo(this->TextProp); info.ProcessString(string); vtkNew text; this->ActiveNode->AddNestedElement(text); text->SetName("text"); this->ApplyTextPropertyStateToNode(text, point[0], point[1]); // Position is encoded in the transform: text->SetFloatAttribute("x", 0.f); text->SetFloatAttribute("y", 0.f); text->SetCharacterData(string.c_str(), static_cast(string.length())); } else { // Export other text (e.g. MathText) as a path: vtkNew tPath; int dpi = this->Viewport->GetVTKWindow()->GetDPI(); if (!tren->StringToPath(this->TextProp, string, tPath, dpi, backend)) { vtkErrorMacro("Error generating path for MathText string '" << string << "'."); return; } vtkNew path; this->ActiveNode->AddNestedElement(path); path->SetName("path"); this->ApplyTextPropertyStateToNodeForPath(path, point[0], point[1]); std::ostringstream d; this->DrawPath(tPath, d); path->SetAttribute("d", d.str().c_str()); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ComputeStringBounds(const vtkStdString& string, float bounds[4]) { vtkTextRenderer* tren = vtkTextRenderer::GetInstance(); if (!tren) { vtkErrorMacro("vtkTextRenderer unavailable. Link to vtkRenderingFreeType " "to get the default implementation."); std::fill(bounds, bounds + 4, 0.f); return; } assert(this->Viewport && this->Viewport->GetVTKWindow()); int dpi = this->Viewport->GetVTKWindow()->GetDPI(); vtkTextRenderer::Metrics m; if (!tren->GetMetrics(this->TextProp, string, m, dpi)) { vtkErrorMacro("Error computing bbox for string '" << string << "'."); std::fill(bounds, bounds + 4, 0.f); return; } bounds[0] = 0.f; bounds[1] = 0.f; bounds[2] = static_cast(m.BoundingBox[1] - m.BoundingBox[0] + 1); bounds[3] = static_cast(m.BoundingBox[3] - m.BoundingBox[2] + 1); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ComputeJustifiedStringBounds(const char* string, float bounds[4]) { this->ComputeStringBounds(vtkStdString(string), bounds); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawMathTextString(float* point, const vtkStdString& str) { this->DrawString(point, str); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawImage(float p[2], float scale, vtkImageData* image) { int dims[3]; image->GetDimensions(dims); dims[0] *= scale; dims[1] *= scale; this->DrawImage(vtkRectf(p[0], p[1], dims[0], dims[1]), image); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawImage(const vtkRectf& pos, vtkImageData* image) { const ImageInfo& info = this->Impl->GetImageInfo(image); YConverter y(this->CanvasHeight); const float xScale = pos.GetWidth() / info.Size[0]; const float yScale = pos.GetHeight() / info.Size[1]; // Construct a matrix representing the following transformation: // // [X] = [T3] [T2] [S] [T1] // // [X] = final transform // [T1] = translate(-pos.X, -pos.Y); Move to origin to prepare for scaling. // [S] = scale(xScale, yScale); Resize the image to match the input rect. // [T2] = translate(0, -pos.H); Anchor at bottom corner instead of top // [T3] = translate(pos.X, pos.Y); Move back to anchor point std::ostringstream xform; xform << "matrix(" << xScale << ",0,0," << yScale << "," << pos.GetX() - xScale * pos.GetX() << "," << y(pos.GetY()) - (yScale * y(pos.GetY()) + pos.GetHeight()) << ")"; vtkNew use; this->ActiveNode->AddNestedElement(use); use->SetName("use"); use->SetFloatAttribute("x", pos.GetX()); use->SetFloatAttribute("y", y(pos.GetY())); use->SetFloatAttribute("width", pos.GetWidth()); use->SetFloatAttribute("height", pos.GetHeight()); use->SetAttribute("transform", xform.str().c_str()); use->SetAttribute("xlink:href", (std::string("#") + info.Id).c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetColor4(unsigned char[]) { // This is how the OpenGL2 impl handles this... vtkErrorMacro("color cannot be set this way."); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetTexture(vtkImageData* image, int properties) { this->Brush->SetTexture(image); this->Brush->SetTextureProperties(properties); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetPointSize(float size) { this->Pen->SetWidth(size); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetLineWidth(float width) { this->Pen->SetWidth(width); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetLineType(int type) { this->Pen->SetLineType(type); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetMatrix(vtkMatrix3x3* m) { // Adjust the transform to account for the fact that SVG's y-axis is reversed: std::array mat3; vtkSVGContextDevice2D::AdjustMatrixForSVG(m->GetData(), mat3.data()); std::array mat4; vtkSVGContextDevice2D::Matrix3ToMatrix4(mat3.data(), mat4.data()); this->Matrix->SetMatrix(mat4.data()); this->ApplyTransform(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::GetMatrix(vtkMatrix3x3* mat3) { vtkSVGContextDevice2D::Matrix4ToMatrix3(this->Matrix->GetMatrix()->GetData(), mat3->GetData()); vtkSVGContextDevice2D::AdjustMatrixForSVG(mat3->GetData(), mat3->GetData()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::MultiplyMatrix(vtkMatrix3x3* m) { // Adjust the transform to account for the fact that SVG's y-axis is reversed: std::array mat3; vtkSVGContextDevice2D::AdjustMatrixForSVG(m->GetData(), mat3.data()); std::array mat4; vtkSVGContextDevice2D::Matrix3ToMatrix4(mat3.data(), mat4.data()); this->Matrix->Concatenate(mat4.data()); this->ApplyTransform(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::PushMatrix() { this->Matrix->Push(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::PopMatrix() { this->Matrix->Pop(); this->ApplyTransform(); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetClipping(int* x) { if (!std::equal(this->ClipRect.begin(), this->ClipRect.end(), x)) { std::copy(x, x + this->ClipRect.size(), this->ClipRect.begin()); this->SetupClippingAndTransform(); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::EnableClipping(bool enable) { if (enable != this->IsClipping) { this->IsClipping = enable; this->SetupClippingAndTransform(); } } //------------------------------------------------------------------------------ vtkSVGContextDevice2D::vtkSVGContextDevice2D() : Impl(new Details) , Viewport(nullptr) , ContextNode(nullptr) , ActiveNode(nullptr) , DefinitionNode(nullptr) , CanvasHeight(0.f) , SubdivisionThreshold(1.f) , IsClipping(false) , ActiveNodeIsClipping(false) , EmbedFonts(false) , TextAsPath(true) { std::fill(this->ClipRect.begin(), this->ClipRect.end(), 0); std::fill(this->ActiveNodeClipRect.begin(), this->ActiveNodeClipRect.end(), 0); std::fill(this->ActiveNodeTransform.begin(), this->ActiveNodeTransform.end(), 0.); this->ActiveNodeTransform[0] = 1.; this->ActiveNodeTransform[4] = 1.; this->ActiveNodeTransform[8] = 1.; } //------------------------------------------------------------------------------ vtkSVGContextDevice2D::~vtkSVGContextDevice2D() { this->SetViewport(nullptr); delete this->Impl; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::PushGraphicsState() { vtkNew newGState; newGState->SetName("g"); this->ActiveNode->AddNestedElement(newGState); this->ActiveNode = newGState; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::PopGraphicsState() { if (this->ActiveNode == this->ContextNode) { vtkErrorMacro("Internal error: Attempting to pop graphics state past " "context node. This likely means there's a pop with no " "corresponding push."); return; } vtkXMLDataElement* oldActive = this->ActiveNode; this->ActiveNode = this->ActiveNode->GetParent(); // If the old active node is empty, remove it completely: if (oldActive->GetNumberOfNestedElements() == 0) { this->ActiveNode->RemoveNestedElement(oldActive); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::SetupClippingAndTransform() { // To manage transforms and clipping, we don't push/pop/concatenate transforms // in the output, and instead only push a single element under the // ContextNode with the current transform and clipping formation. Any other // calls to PushGraphicsState (for instance, setting a common color to a // collection of primitives) should be popped before changing transform or // clipping info. // If we're more than one node nested under ContextNode, that's an error. // See above. if (this->ContextNode != this->ActiveNode && this->ContextNode != this->ActiveNode->GetParent()) { vtkErrorMacro("This method must only be called when there is, at most, one " " element between ActiveNode and ContextNode."); return; } // Have the transform/clipping settings actually changed? double* mat4 = this->Matrix->GetMatrix()->GetData(); const bool isClippingChanged = this->IsClipping == this->ActiveNodeIsClipping; const bool clipRectChanged = !std::equal(this->ClipRect.begin(), this->ClipRect.end(), this->ActiveNodeClipRect.begin()); const bool transformChanged = vtkSVGContextDevice2D::Transform2DEqual(this->ActiveNodeTransform.data(), mat4); if (!isClippingChanged && (!this->IsClipping || !clipRectChanged) && !transformChanged) { return; } // Sync the cached values: vtkSVGContextDevice2D::Matrix4ToMatrix3(mat4, this->ActiveNodeTransform.data()); std::copy(this->ClipRect.begin(), this->ClipRect.end(), this->ActiveNodeClipRect.begin()); this->ActiveNodeIsClipping = this->IsClipping; // Strip the old transform/clip node out if needed: if (this->ActiveNode != this->ContextNode) { this->PopGraphicsState(); } assert(this->ActiveNode == this->ContextNode); // If no clipping or transform is present, no need for a new element, // just add new primitives to the ContextNode directly. const std::array ident = { { 1., 0., 0., 0., 1., 0., 0., 0., 1. } }; const bool isIdentity = vtkSVGContextDevice2D::Transform2DEqual(ident.data(), mat4); if (!this->IsClipping && isIdentity) { return; } // Finally, add new gstate with transform and clipping info. this->PushGraphicsState(); if (!isIdentity) { this->ActiveNode->SetAttribute( "transform", Transform2DToString(this->ActiveNodeTransform).c_str()); } if (this->IsClipping) { const ClipRectInfo& info = this->Impl->GetClipRectInfo(this->ClipRect); this->ActiveNode->SetAttribute("clip-path", (std::string("url(#") + info.Id + ")").c_str()); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenStateToNode(vtkXMLDataElement* node) { this->ApplyPenColorToNode(node); this->ApplyPenOpacityToNode(node); this->ApplyPenWidthToNode(node); this->ApplyPenStippleToNode(node); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenColorToNode(vtkXMLDataElement* node) { node->SetAttribute("stroke", ColorToString(this->Pen->GetColor()).c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenOpacityToNode(vtkXMLDataElement* node) { if (this->Pen->GetOpacity() != 255) { node->SetFloatAttribute("stroke-opacity", this->Pen->GetOpacity() / 255.f); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenWidthToNode(vtkXMLDataElement* node) { float width = this->GetScaledPenWidth(); if (std::fabs(width - 1.f) > 1e-5) { node->SetFloatAttribute("stroke-width", width); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenStippleToNode(vtkXMLDataElement* node) { // These match the OpenGL2 implementation: switch (this->Pen->GetLineType()) { default: vtkErrorMacro("Unknown line type: " << this->Pen->GetLineType()); VTK_FALLTHROUGH; case vtkPen::NO_PEN: node->SetAttribute("stroke-dasharray", "0,10"); break; case vtkPen::SOLID_LINE: node->RemoveAttribute("stroke-dasharray"); break; case vtkPen::DASH_LINE: node->SetAttribute("stroke-dasharray", "8"); break; case vtkPen::DOT_LINE: node->SetAttribute("stroke-dasharray", "1,7"); break; case vtkPen::DASH_DOT_LINE: node->SetAttribute("stroke-dasharray", "4,6,2,4"); break; case vtkPen::DASH_DOT_DOT_LINE: // This is dash-dot-dash, but eh. It matches the OpenGL2 0x1C47 pattern. node->SetAttribute("stroke-dasharray", "3,3,1,3,3,3"); break; case vtkPen::DENSE_DOT_LINE: node->SetAttribute("stroke-dasharray", "1,3"); break; } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenAsFillColorToNode(vtkXMLDataElement* node) { node->SetAttribute("fill", ColorToString(this->Pen->GetColor()).c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyPenAsFillOpacityToNode(vtkXMLDataElement* node) { if (this->Pen->GetOpacity() != 255) { node->SetFloatAttribute("fill-opacity", this->Pen->GetOpacity() / 255.f); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyBrushStateToNode(vtkXMLDataElement* node) { if (!this->Brush->GetTexture()) { this->ApplyBrushColorToNode(node); this->ApplyBrushOpacityToNode(node); } else { // Do not apply brush opacity; this matches the OpenGL2 implementation. this->ApplyBrushTextureToNode(node); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyBrushColorToNode(vtkXMLDataElement* node) { node->SetAttribute("fill", ColorToString(this->Brush->GetColor()).c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyBrushOpacityToNode(vtkXMLDataElement* node) { if (this->Brush->GetOpacity() != 255) { node->SetFloatAttribute("fill-opacity", this->Brush->GetOpacity() / 255.f); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyBrushTextureToNode(vtkXMLDataElement* node) { vtkImageData* img = this->Brush->GetTexture(); int prop = this->Brush->GetTextureProperties(); const PatternInfo& info = this->Impl->GetPatternInfo(img, prop); std::ostringstream fill; fill << "url(#" << info.PatternId << ")"; node->SetAttribute("fill", fill.str().c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyTextPropertyStateToNode(vtkXMLDataElement* node, float x, float y) { vtkFreeTypeTools* ftt = vtkFreeTypeTools::GetInstance(); if (!ftt) { vtkErrorMacro("Error embedding fonts: No vtkFreeTypeTools instance " "available."); return; } YConverter yConv(this->CanvasHeight); using FaceMetrics = vtkFreeTypeTools::FaceMetrics; FaceMetrics faceMetrics = ftt->GetFaceMetrics(this->TextProp); vtkVector3d colord; this->TextProp->GetColor(colord.GetData()); vtkColor3ub color = { static_cast((colord[0] * 255.) + 0.5), static_cast((colord[1] * 255.) + 0.5), static_cast((colord[2] * 255.) + 0.5), }; std::ostringstream xform; xform << "translate(" << x << "," << yConv(y) << ")"; if (this->TextProp->GetOrientation() != 0.) { xform << "rotate(" << this->TextProp->GetOrientation() << ") "; } std::ostringstream fontSize; fontSize << this->TextProp->GetFontSize() << "pt"; node->SetAttribute("fill", ColorToString(color.GetData()).c_str()); node->SetFloatAttribute("fill-opacity", static_cast(this->TextProp->GetOpacity())); node->SetAttribute("font-family", faceMetrics.FamilyName.c_str()); node->SetAttribute("font-size", fontSize.str().c_str()); node->SetAttribute("font-style", this->TextProp->GetItalic() != 0 ? "italic" : "normal"); node->SetAttribute("font-weight", this->TextProp->GetBold() != 0 ? "bold" : "normal"); switch (this->TextProp->GetJustification()) { default: case VTK_TEXT_LEFT: break; case VTK_TEXT_CENTERED: node->SetAttribute("text-anchor", "middle"); break; case VTK_TEXT_RIGHT: node->SetAttribute("text-anchor", "right"); break; } switch (this->TextProp->GetVerticalJustification()) { default: case VTK_TEXT_BOTTOM: node->SetAttribute("alignment-baseline", "bottom"); break; case VTK_TEXT_CENTERED: if (this->TextProp->GetUseTightBoundingBox()) { node->SetAttribute("alignment-baseline", "middle"); } else { node->SetAttribute("alignment-baseline", "central"); } break; case VTK_TEXT_TOP: node->SetAttribute("alignment-baseline", "top"); break; } node->SetAttribute("transform", xform.str().c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyTextPropertyStateToNodeForPath( vtkXMLDataElement* node, float x, float y) { vtkVector3d colord; this->TextProp->GetColor(colord.GetData()); vtkColor3ub color = { static_cast((colord[0] * 255.) + 0.5), static_cast((colord[1] * 255.) + 0.5), static_cast((colord[2] * 255.) + 0.5), }; YConverter yConv(this->CanvasHeight); std::ostringstream xform; xform << "translate(" << x << "," << yConv(y) << ")"; node->SetAttribute("fill", ColorToString(color.GetData()).c_str()); node->SetFloatAttribute("fill-opacity", static_cast(this->TextProp->GetOpacity())); node->SetAttribute("transform", xform.str().c_str()); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::ApplyTransform() { this->SetupClippingAndTransform(); } //------------------------------------------------------------------------------ std::string vtkSVGContextDevice2D::AddCrossSymbol(bool) { std::ostringstream idStream; idStream << "Cross"; std::string id = idStream.str(); if (!this->DefinitionNode->FindNestedElementWithNameAndId("symbol", id.c_str())) { vtkNew symbol; this->DefinitionNode->AddNestedElement(symbol); symbol->SetName("symbol"); symbol->SetId(id.c_str()); symbol->SetAttribute("id", id.c_str()); symbol->SetAttribute("viewBox", "0,0 1,1"); vtkNew path; symbol->AddNestedElement(path); path->SetName("path"); path->SetAttribute("d", "M0,0L1,1M0,1L1,0"); } return id; } //------------------------------------------------------------------------------ std::string vtkSVGContextDevice2D::AddPlusSymbol(bool) { std::ostringstream idStream; idStream << "Plus"; std::string id = idStream.str(); if (!this->DefinitionNode->FindNestedElementWithNameAndId("symbol", id.c_str())) { vtkNew symbol; this->DefinitionNode->AddNestedElement(symbol); symbol->SetName("symbol"); symbol->SetId(id.c_str()); symbol->SetAttribute("id", id.c_str()); symbol->SetAttribute("viewBox", "0,0 1,1"); vtkNew path; symbol->AddNestedElement(path); path->SetName("path"); path->SetAttribute("d", "M0.5,0L0.5,1M0,0.5L1,0.5"); } return id; } //------------------------------------------------------------------------------ std::string vtkSVGContextDevice2D::AddSquareSymbol(bool) { std::ostringstream idStream; idStream << "Square"; std::string id = idStream.str(); if (!this->DefinitionNode->FindNestedElementWithNameAndId("symbol", id.c_str())) { vtkNew symbol; this->DefinitionNode->AddNestedElement(symbol); symbol->SetName("symbol"); symbol->SetId(id.c_str()); symbol->SetAttribute("id", id.c_str()); symbol->SetAttribute("viewBox", "0,0 1,1"); vtkNew rect; symbol->AddNestedElement(rect); rect->SetName("rect"); rect->SetFloatAttribute("x", 0.f); rect->SetFloatAttribute("y", 0.f); rect->SetFloatAttribute("width", 1.f); rect->SetFloatAttribute("height", 1.f); } return id; } //------------------------------------------------------------------------------ std::string vtkSVGContextDevice2D::AddCircleSymbol(bool) { std::ostringstream idStream; idStream << "Circle"; std::string id = idStream.str(); if (!this->DefinitionNode->FindNestedElementWithNameAndId("symbol", id.c_str())) { vtkNew symbol; this->DefinitionNode->AddNestedElement(symbol); symbol->SetName("symbol"); symbol->SetId(id.c_str()); symbol->SetAttribute("id", id.c_str()); symbol->SetAttribute("viewBox", "0,0 1,1"); vtkNew circle; symbol->AddNestedElement(circle); circle->SetName("circle"); circle->SetFloatAttribute("cx", 0.5f); circle->SetFloatAttribute("cy", 0.5f); circle->SetFloatAttribute("r", 0.5f); } return id; } //------------------------------------------------------------------------------ std::string vtkSVGContextDevice2D::AddDiamondSymbol(bool) { std::ostringstream idStream; idStream << "Diamond"; std::string id = idStream.str(); if (!this->DefinitionNode->FindNestedElementWithNameAndId("symbol", id.c_str())) { vtkNew symbol; this->DefinitionNode->AddNestedElement(symbol); symbol->SetName("symbol"); symbol->SetId(id.c_str()); symbol->SetAttribute("id", id.c_str()); symbol->SetAttribute("viewBox", "0,0 1,1"); vtkNew path; symbol->AddNestedElement(path); path->SetName("path"); path->SetAttribute("d", "M0,.5L.5,1 1,.5 .5,0z"); } return id; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawPath(vtkPath* path, std::ostream& out) { // The text renderer always uses floats to generate paths, so we'll optimize // a bit here: vtkFloatArray* points = vtkArrayDownCast(path->GetPoints()->GetData()); vtkIntArray* codes = path->GetCodes(); if (!points) { vtkErrorMacro("This method expects the path point precision to be floats."); return; } vtkIdType numTuples = points->GetNumberOfTuples(); if (numTuples != codes->GetNumberOfTuples() || codes->GetNumberOfComponents() != 1 || points->GetNumberOfComponents() != 3) { vtkErrorMacro("Invalid path data."); return; } if (numTuples == 0) { // Nothing to do. return; } // Use a lambda to invert the y positions for SVG: auto y = [](float yIn) -> float { return -yIn; }; typedef vtkPath::ControlPointType CodeEnum; typedef vtkIntArray::ValueType CodeType; CodeType* code = codes->GetPointer(0); CodeType* codeEnd = code + numTuples; typedef vtkFloatArray::ValueType PointType; PointType* point = points->GetPointer(0); // These are only used in an assertion, ifdef silences warning on non-debug // builds #ifndef NDEBUG PointType* pointBegin = point; CodeType* codeBegin = code; #endif // Track the last code so we can save a little space by chaining draw commands int lastCode = -1; while (code < codeEnd) { assert("Sanity check" && (code - codeBegin) * 3 == point - pointBegin); switch (static_cast(*code)) { case vtkPath::MOVE_TO: if (lastCode != *code) { lastCode = *code; out << "M"; } out << point[0] << "," << y(point[1]) << "\n"; point += 3; ++code; break; case vtkPath::LINE_TO: if (lastCode != *code) { lastCode = *code; out << "L"; } out << point[0] << "," << y(point[1]) << "\n"; point += 3; ++code; break; case vtkPath::CONIC_CURVE: assert(CodeEnum(code[1]) == vtkPath::CONIC_CURVE); if (lastCode != *code) { lastCode = *code; out << "Q"; } out << point[0] << "," << y(point[1]) << " " << point[3] << "," << y(point[4]) << "\n"; point += 6; code += 2; break; case vtkPath::CUBIC_CURVE: assert(CodeEnum(code[1]) == vtkPath::CUBIC_CURVE); assert(CodeEnum(code[2]) == vtkPath::CUBIC_CURVE); if (lastCode != *code) { lastCode = *code; out << "C"; } out << point[0] << "," << y(point[1]) << " " << point[3] << "," << y(point[4]) << " " << point[6] << "," << y(point[7]) << "\n"; point += 9; code += 3; break; default: vtkErrorMacro("Unknown control code."); } } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawLineGradient(const vtkVector2f& p1, const vtkColor4ub& c1, const vtkVector2f& p2, const vtkColor4ub& c2, bool useAlpha) { const vtkColor4ub aveColor = { static_cast(static_cast(c1[0] + c2[0]) / 2), static_cast(static_cast(c1[1] + c2[1]) / 2), static_cast(static_cast(c1[2] + c2[2]) / 2), static_cast(static_cast(c1[3] + c2[3]) / 2) }; // If the colors are more or less the same, go ahead and draw this segment. // Same if the segment is small enough to fit on a single pixel. if (this->LengthLessThanTolerance(p1, p2) || this->ColorsAreClose(c1, c2, useAlpha)) { YConverter y(this->CanvasHeight); vtkNew line; this->ActiveNode->AddNestedElement(line); line->SetName("line"); line->SetFloatAttribute("x1", p1[0]); line->SetFloatAttribute("y1", y(p1[1])); line->SetFloatAttribute("x2", p2[0]); line->SetFloatAttribute("y2", y(p2[1])); this->ApplyPenWidthToNode(line); line->SetAttribute("stroke", ColorToString(aveColor.GetData()).c_str()); if (useAlpha && aveColor[3] != 255) { line->SetFloatAttribute("stroke-opacity", aveColor[3] / 255.f); } // FIXME: Disable gradient stipple for now, we'd need to account for offsets // this->ApplyPenStippleToNode(node); return; } // Otherwise, subdivide into two more line segments: const vtkVector2f avePos = (p1 + p2) * 0.5; this->DrawLineGradient(p1, c1, avePos, aveColor, useAlpha); this->DrawLineGradient(avePos, aveColor, p2, c2, useAlpha); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::DrawTriangleGradient(const vtkVector2f& p1, const vtkColor4ub& c1, const vtkVector2f& p2, const vtkColor4ub& c2, const vtkVector2f& p3, const vtkColor4ub& c3, bool useAlpha) { // If the colors are more or less the same, go ahead and draw this triangle. // Same if the triangle is small enough to fit on a single pixel. if (this->AreaLessThanTolerance(p1, p2, p3) || this->ColorsAreClose(c1, c2, c3, useAlpha)) { YConverter y(this->CanvasHeight); const vtkColor4ub aveColor = { static_cast( static_cast(c1[0] + c2[0] + c3[0]) / 3), static_cast(static_cast(c1[1] + c2[1] + c3[1]) / 3), static_cast(static_cast(c1[2] + c2[2] + c3[2]) / 3), static_cast(static_cast(c1[3] + c2[3] + c3[3]) / 3) }; vtkNew polygon; this->ActiveNode->AddNestedElement(polygon); polygon->SetName("polygon"); polygon->SetAttribute("fill", ColorToString(aveColor.GetData()).c_str()); if (useAlpha && aveColor[3] != 255) { polygon->SetFloatAttribute("fill-opacity", aveColor[3] / 255.f); } // This should disable antialiasing on supported viewers (works on webkit). // Helps prevent visible boundaries between polygons: polygon->SetAttribute("shape-rendering", "crispEdges"); std::ostringstream points; points << p1[0] << "," << y(p1[1]) << " " << p2[0] << "," << y(p2[1]) << " " << p3[0] << "," << y(p3[1]); polygon->SetAttribute("points", points.str().c_str()); return; } // Otherwise, subdivide into 4 triangles: // v1 // + // /| // / | // / | // / | // v12 +----+ v13 // /| /| // / | / | // / | / | // / |/ | // +----+----+ // v2 v23 v3 const vtkVector2f p12 = (p1 + p2) * 0.5; const vtkVector2f p23 = (p2 + p3) * 0.5; const vtkVector2f p13 = (p1 + p3) * 0.5; const vtkColor4ub c12 = { static_cast(static_cast(c1[0] + c2[0]) / 2), static_cast(static_cast(c1[1] + c2[1]) / 2), static_cast(static_cast(c1[2] + c2[2]) / 2), static_cast(static_cast(c1[3] + c2[3]) / 2) }; const vtkColor4ub c23 = { static_cast(static_cast(c2[0] + c3[0]) / 2), static_cast(static_cast(c2[1] + c3[1]) / 2), static_cast(static_cast(c2[2] + c3[2]) / 2), static_cast(static_cast(c2[3] + c3[3]) / 2) }; const vtkColor4ub c13 = { static_cast(static_cast(c1[0] + c3[0]) / 2), static_cast(static_cast(c1[1] + c3[1]) / 2), static_cast(static_cast(c1[2] + c3[2]) / 2), static_cast(static_cast(c1[3] + c3[3]) / 2) }; this->DrawTriangleGradient(p1, c1, p12, c12, p13, c13, useAlpha); this->DrawTriangleGradient(p2, c2, p12, c12, p23, c23, useAlpha); this->DrawTriangleGradient(p3, c3, p13, c13, p23, c23, useAlpha); this->DrawTriangleGradient(p12, c12, p13, c13, p23, c23, useAlpha); } //------------------------------------------------------------------------------ bool vtkSVGContextDevice2D::AreaLessThanTolerance( const vtkVector2f& p1, const vtkVector2f& p2, const vtkVector2f& p3) { return this->LengthLessThanTolerance(p1, p2) && this->LengthLessThanTolerance(p1, p3) && this->LengthLessThanTolerance(p2, p3); } //------------------------------------------------------------------------------ bool vtkSVGContextDevice2D::LengthLessThanTolerance(const vtkVector2f& p1, const vtkVector2f& p2) { return (p2 - p1).SquaredNorm() < this->SubdivisionThreshold; } //------------------------------------------------------------------------------ bool vtkSVGContextDevice2D::ColorsAreClose( const vtkColor4ub& c1, const vtkColor4ub& c2, bool useAlpha) { const std::array tol = { { 16, 8, 32, 32 } }; int comps = useAlpha ? 4 : 3; for (int i = 0; i < comps; ++i) { if (std::abs(c1[i] - c2[i]) > tol[i]) { return false; } } return true; } //------------------------------------------------------------------------------ bool vtkSVGContextDevice2D::ColorsAreClose( const vtkColor4ub& c1, const vtkColor4ub& c2, const vtkColor4ub& c3, bool useAlpha) { return (this->ColorsAreClose(c1, c2, useAlpha) && this->ColorsAreClose(c2, c3, useAlpha) && this->ColorsAreClose(c1, c3, useAlpha)); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::WriteFonts() { vtkFreeTypeTools* ftt = vtkFreeTypeTools::GetInstance(); if (!ftt) { vtkErrorMacro("Error embedding fonts: No vtkFreeTypeTools instance " "available."); return; } using FaceMetrics = vtkFreeTypeTools::FaceMetrics; using GlyphOutline = vtkFreeTypeTools::GlyphOutline; for (const auto& fontEntry : this->Impl->FontMap) { const FontKey& key = fontEntry.first; const FontInfo* info = fontEntry.second; FaceMetrics faceMetrics = ftt->GetFaceMetrics(key.TextProperty); // We only embed scalable fonts for now. if (!faceMetrics.Scalable) { vtkWarningMacro("Cannot embed non-scalable fonts (referring to font file: " << key.TextProperty->GetFontFile() << ")"); continue; } vtkNew font; this->DefinitionNode->AddNestedElement(font); font->SetName("font"); font->SetAttribute("id", info->SVGId.c_str()); font->SetIntAttribute("horiz-adv-x", faceMetrics.HorizAdvance); vtkNew face; font->AddNestedElement(face); face->SetName("font-face"); face->SetAttribute("font-family", faceMetrics.FamilyName.c_str()); face->SetAttribute("font-style", faceMetrics.Italic ? "italic" : "normal"); face->SetAttribute("font-weight", faceMetrics.Bold ? "bold" : "normal"); face->SetAttribute("font-size", "all"); face->SetIntAttribute("units-per-em", faceMetrics.UnitsPerEM); face->SetIntAttribute("ascent", faceMetrics.Ascender); face->SetIntAttribute("descent", faceMetrics.Descender); face->SetAttribute("bbox", BBoxToString(faceMetrics.BoundingBox).c_str()); face->SetAttribute("alphabetic", "0"); for (auto value : info->Chars) { GlyphOutline glyphInfo = ftt->GetUnscaledGlyphOutline(key.TextProperty, value); std::string text; utf8::append(value, std::back_inserter(text)); vtkNew glyph; face->AddNestedElement(glyph); glyph->SetName("glyph"); glyph->SetAttributeEncoding(VTK_ENCODING_UTF_8); glyph->SetAttribute("unicode", text.c_str()); glyph->SetIntAttribute("horiz-adv-x", glyphInfo.HorizAdvance); std::ostringstream d; this->DrawPath(glyphInfo.Path, d); glyph->SetAttribute("d", d.str().c_str()); } for (auto charPair : info->KerningPairs) { std::array kerning = ftt->GetUnscaledKerning(key.TextProperty, charPair.first, charPair.second); if (kerning[0] != 0) { std::string left; utf8::append(charPair.first, std::back_inserter(left)); std::string right; utf8::append(charPair.second, std::back_inserter(right)); vtkNew hkern; font->AddNestedElement(hkern); hkern->SetName("hkern"); hkern->SetAttributeEncoding(VTK_ENCODING_UTF_8); hkern->SetAttribute("u1", left.c_str()); hkern->SetAttribute("u2", right.c_str()); hkern->SetIntAttribute("k", -kerning[0]); } } } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::WriteImages() { for (const ImageInfo& info : this->Impl->ImageSet) { vtkNew image; this->DefinitionNode->AddNestedElement(image); image->SetName("image"); image->SetAttribute("id", info.Id.c_str()); image->SetIntAttribute("width", info.Size[0]); image->SetIntAttribute("height", info.Size[1]); image->SetAttribute("xlink:href", info.PNGBase64.c_str()); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::WritePatterns() { for (const PatternInfo& info : this->Impl->PatternSet) { vtkNew pattern; this->DefinitionNode->AddNestedElement(pattern); pattern->SetName("pattern"); pattern->SetAttribute("id", info.PatternId.c_str()); // We only care about Repeat and Stretch, since SVG doesn't allow control // over Nearest/Linear interpolation. const bool isTiled = ((info.TextureProperty & vtkBrush::Repeat) != 0); if (isTiled) { pattern->SetIntAttribute("width", info.ImageSize[0]); pattern->SetIntAttribute("height", info.ImageSize[1]); pattern->SetAttribute("patternUnits", "userSpaceOnUse"); } else // Stretched { std::ostringstream viewBox; viewBox << "0,0," << info.ImageSize[0] << "," << info.ImageSize[1]; pattern->SetIntAttribute("width", 1); pattern->SetIntAttribute("height", 1); pattern->SetAttribute("viewBox", viewBox.str().c_str()); pattern->SetAttribute("preserveAspectRatio", "none"); } vtkNew use; pattern->AddNestedElement(use); use->SetName("use"); use->SetFloatAttribute("x", 0); use->SetFloatAttribute("y", 0); use->SetIntAttribute("width", info.ImageSize[0]); use->SetIntAttribute("height", info.ImageSize[1]); use->SetAttribute("xlink:href", (std::string("#") + info.ImageId).c_str()); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::WriteClipRects() { for (const auto& info : this->Impl->ClipRectSet) { vtkNew clipPath; this->DefinitionNode->AddNestedElement(clipPath); clipPath->SetName("clipPath"); clipPath->SetAttribute("id", info.Id.c_str()); // Get rect vtkNew rect; clipPath->AddNestedElement(rect); rect->SetName("rect"); rect->SetAttribute("fill", "#000"); rect->SetIntAttribute("x", info.Rect[0]); rect->SetIntAttribute("y", info.Rect[1]); rect->SetIntAttribute("width", info.Rect[2]); rect->SetIntAttribute("height", info.Rect[3]); } } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::AdjustMatrixForSVG(const double in[9], double out[9]) { // Adjust the transform to account for the fact that SVG's y-axis is reversed: // // [S] = [T]^-1 [V] [T] // // [S] is the transform in SVG space (stored in this->Matrix). // [V] is the transform in VTK space (inputs from Context2D API). // | 1 0 0 | // [T] = | 0 -1 h | where h = viewport height. // | 0 0 1 | [T] flips the y axis. // Also, [T] = [T]^-1 in this case. std::array tmpMat3; std::array VTKToSVG; vtkSVGContextDevice2D::GetSVGMatrix(VTKToSVG.data()); vtkMatrix3x3::Multiply3x3(VTKToSVG.data(), in, tmpMat3.data()); vtkMatrix3x3::Multiply3x3(tmpMat3.data(), VTKToSVG.data(), out); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::GetSVGMatrix(double svg[9]) { svg[0] = 1.; svg[1] = 0.; svg[2] = 0.; svg[3] = 0.; svg[4] = -1.; svg[5] = this->CanvasHeight; svg[6] = 0.; svg[7] = 0.; svg[8] = 1.; } //------------------------------------------------------------------------------ bool vtkSVGContextDevice2D::Transform2DEqual(const double mat3[9], const double mat4[16]) { const double tol = 1e-5; const std::array mat3Map = { { 0, 1, 2, 3, 4, 5 } }; const std::array mat4Map = { { 0, 1, 3, 4, 5, 7 } }; for (size_t i = 0; i < 6; ++i) { if (std::fabs(mat3[mat3Map[i]] - mat4[mat4Map[i]]) > tol) { return false; } } return true; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::Matrix3ToMatrix4(const double mat3[9], double mat4[16]) { mat4[0] = mat3[0]; mat4[1] = mat3[1]; mat4[2] = 0.; mat4[3] = mat3[2]; mat4[4] = mat3[3]; mat4[5] = mat3[4]; mat4[6] = 0.; mat4[7] = mat3[5]; mat4[8] = 0.; mat4[9] = 0.; mat4[10] = 1.; mat4[11] = 0.; mat4[12] = 0.; mat4[13] = 0.; mat4[14] = 0.; mat4[15] = 1.; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::Matrix4ToMatrix3(const double mat4[16], double mat3[9]) { mat3[0] = mat4[0]; mat3[1] = mat4[1]; mat3[2] = mat4[3]; mat3[3] = mat4[4]; mat3[4] = mat4[5]; mat3[5] = mat4[7]; mat3[6] = 0.; mat3[7] = 0.; mat3[8] = 1.; } //------------------------------------------------------------------------------ float vtkSVGContextDevice2D::GetScaledPenWidth() { float x, y; this->GetScaledPenWidth(x, y); return (x + y) * 0.5f; } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::GetScaledPenWidth(float& x, float& y) { x = y = this->Pen->GetWidth(); this->TransformSize(x, y); } //------------------------------------------------------------------------------ void vtkSVGContextDevice2D::TransformSize(float& x, float& y) { // Get current 3x3 SVG transform: std::array m; vtkSVGContextDevice2D::Matrix4ToMatrix3(this->Matrix->GetMatrix()->GetData(), m.data()); // Invert it (we want to go from local space --> global space) vtkMatrix3x3::Invert(m.data(), m.data()); // Extract the scale values: const double xScale = std::copysign(std::sqrt(m[0] * m[0] + m[1] * m[1]), m[0]); const double yScale = std::copysign(std::sqrt(m[3] * m[3] + m[4] * m[4]), m[4]); x *= static_cast(xScale); y *= static_cast(yScale); } //------------------------------------------------------------------------------ vtkImageData* vtkSVGContextDevice2D::PreparePointSprite(vtkImageData* in) { int numComps = in->GetNumberOfScalarComponents(); // We'll only handle RGB / RGBA: if (numComps != 3 && numComps != 4) { vtkWarningMacro("Images with " << numComps << " components not supported."); return nullptr; } // Need to convert scalar type? if (in->GetScalarType() != VTK_UNSIGNED_CHAR) { vtkNew cast; cast->SetInputData(in); cast->SetOutputScalarTypeToUnsignedChar(); cast->Update(); in = cast->GetOutput(); in->Register(this); } else { in->Register(this); // Keep refcounts consistent } if (in->GetNumberOfScalarComponents() == 3) { // If RGB, append a constant alpha. vtkNew rgba; rgba->ShallowCopy(in); vtkUnsignedCharArray* data = vtkArrayDownCast(rgba->GetPointData()->GetScalars()); if (!data) { vtkErrorMacro("Internal error: vtkImageCast failed."); in->UnRegister(this); return nullptr; } vtkIdType numTuples = data->GetNumberOfTuples(); vtkNew newData; newData->SetNumberOfComponents(4); newData->SetNumberOfTuples(numTuples); // Give the compiler an explicit hint about array strides so it can optimize // the memory accesses below: VTK_ASSUME(data->GetNumberOfComponents() == 3); VTK_ASSUME(newData->GetNumberOfComponents() == 4); for (vtkIdType t = 0; t < numTuples; ++t) { newData->SetTypedComponent(t, 0, data->GetTypedComponent(t, 0)); newData->SetTypedComponent(t, 1, data->GetTypedComponent(t, 1)); newData->SetTypedComponent(t, 2, data->GetTypedComponent(t, 2)); newData->SetTypedComponent(t, 3, 255); } rgba->GetPointData()->SetScalars(newData); in->UnRegister(this); in = rgba; in->Register(this); } return in; }