1 /*=========================================================================
2
3 Program: Visualization Toolkit
4 Module: vtkGLTFExporter.cxx
5
6 Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
7 All rights reserved.
8 See Copyright.txt or http://www.kitware.com/Copyright.htm for details.
9
10 This software is distributed WITHOUT ANY WARRANTY; without even
11 the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
12 PURPOSE. See the above copyright notice for more information.
13
14 =========================================================================*/
15 #include "vtkGLTFExporter.h"
16 #include "vtkGLTFWriterUtils.h"
17
18 #include <cstdio>
19 #include <memory>
20 #include <sstream>
21
22 #include "vtk_jsoncpp.h"
23
24 #include "vtkAssemblyPath.h"
25 #include "vtkBase64OutputStream.h"
26 #include "vtkCamera.h"
27 #include "vtkCollectionRange.h"
28 #include "vtkCompositeDataIterator.h"
29 #include "vtkCompositeDataSet.h"
30 #include "vtkFloatArray.h"
31 #include "vtkImageData.h"
32 #include "vtkImageFlip.h"
33 #include "vtkMapper.h"
34 #include "vtkMatrix4x4.h"
35 #include "vtkObjectFactory.h"
36 #include "vtkPNGWriter.h"
37 #include "vtkPointData.h"
38 #include "vtkPolyData.h"
39 #include "vtkProperty.h"
40 #include "vtkRenderWindow.h"
41 #include "vtkRendererCollection.h"
42 #include "vtkTexture.h"
43 #include "vtkTriangleFilter.h"
44 #include "vtkTrivialProducer.h"
45 #include "vtkUnsignedCharArray.h"
46 #include "vtkUnsignedIntArray.h"
47
48 #include "vtksys/FStream.hxx"
49 #include "vtksys/SystemTools.hxx"
50
51 vtkStandardNewMacro(vtkGLTFExporter);
52
vtkGLTFExporter()53 vtkGLTFExporter::vtkGLTFExporter()
54 {
55 this->FileName = nullptr;
56 this->InlineData = false;
57 this->SaveNormal = false;
58 this->SaveBatchId = false;
59 }
60
~vtkGLTFExporter()61 vtkGLTFExporter::~vtkGLTFExporter()
62 {
63 delete[] this->FileName;
64 }
65
66 namespace
67 {
68
findPolyData(vtkDataObject * input)69 vtkPolyData* findPolyData(vtkDataObject* input)
70 {
71 // do we have polydata?
72 vtkPolyData* pd = vtkPolyData::SafeDownCast(input);
73 if (pd)
74 {
75 return pd;
76 }
77 vtkCompositeDataSet* cd = vtkCompositeDataSet::SafeDownCast(input);
78 if (cd)
79 {
80 vtkSmartPointer<vtkCompositeDataIterator> iter;
81 iter.TakeReference(cd->NewIterator());
82 for (iter->InitTraversal(); !iter->IsDoneWithTraversal(); iter->GoToNextItem())
83 {
84 pd = vtkPolyData::SafeDownCast(iter->GetCurrentDataObject());
85 if (pd)
86 {
87 return pd;
88 }
89 }
90 }
91 return nullptr;
92 }
93
WriteMesh(Json::Value & accessors,Json::Value & buffers,Json::Value & bufferViews,Json::Value & meshes,Json::Value & nodes,vtkPolyData * pd,vtkActor * aPart,const char * fileName,bool inlineData,bool saveNormal,bool saveBatchId)94 void WriteMesh(Json::Value& accessors, Json::Value& buffers, Json::Value& bufferViews,
95 Json::Value& meshes, Json::Value& nodes, vtkPolyData* pd, vtkActor* aPart, const char* fileName,
96 bool inlineData, bool saveNormal, bool saveBatchId)
97 {
98 vtkNew<vtkTriangleFilter> trif;
99 trif->SetInputData(pd);
100 trif->Update();
101 vtkPolyData* tris = trif->GetOutput();
102
103 // write the point locations
104 int pointAccessor = 0;
105 {
106 vtkDataArray* da = tris->GetPoints()->GetData();
107 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
108
109 // write the accessor
110 Json::Value acc;
111 acc["bufferView"] = bufferViews.size() - 1;
112 acc["byteOffset"] = 0;
113 acc["type"] = "VEC3";
114 acc["componentType"] = GL_FLOAT;
115 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfTuples());
116 double range[6];
117 tris->GetPoints()->GetBounds(range);
118 Json::Value mins;
119 mins.append(range[0]);
120 mins.append(range[2]);
121 mins.append(range[4]);
122 Json::Value maxs;
123 maxs.append(range[1]);
124 maxs.append(range[3]);
125 maxs.append(range[5]);
126 acc["min"] = mins;
127 acc["max"] = maxs;
128 pointAccessor = accessors.size();
129 accessors.append(acc);
130 }
131
132 std::vector<vtkDataArray*> arraysToSave;
133 if (saveBatchId)
134 {
135 vtkDataArray* a;
136 if ((a = pd->GetPointData()->GetArray("_BATCHID")))
137 {
138 arraysToSave.push_back(a);
139 }
140 }
141 if (saveNormal)
142 {
143 vtkDataArray* a;
144 if ((a = pd->GetPointData()->GetArray("NORMAL")))
145 {
146 arraysToSave.push_back(a);
147 }
148 }
149 int userAccessorsStart = accessors.size();
150 for (size_t i = 0; i < arraysToSave.size(); ++i)
151 {
152 vtkDataArray* da = arraysToSave[i];
153 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
154
155 // write the accessor
156 Json::Value acc;
157 acc["bufferView"] = bufferViews.size() - 1;
158 acc["byteOffset"] = 0;
159 acc["type"] = da->GetNumberOfComponents() == 3 ? "VEC3" : "SCALAR";
160 acc["componentType"] = GL_FLOAT;
161 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfTuples());
162 accessors.append(acc);
163 }
164
165 // if we have vertex colors then write them out
166 int vertColorAccessor = -1;
167 aPart->GetMapper()->MapScalars(tris, 1.0);
168 if (aPart->GetMapper()->GetColorMapColors())
169 {
170 vtkUnsignedCharArray* da = aPart->GetMapper()->GetColorMapColors();
171 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
172
173 // write the accessor
174 Json::Value acc;
175 acc["bufferView"] = bufferViews.size() - 1;
176 acc["byteOffset"] = 0;
177 acc["type"] = "VEC4";
178 acc["componentType"] = GL_UNSIGNED_BYTE;
179 acc["normalized"] = true;
180 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfTuples());
181 vertColorAccessor = accessors.size();
182 accessors.append(acc);
183 }
184
185 // if we have tcoords then write them out
186 // first check for colortcoords
187 int tcoordAccessor = -1;
188 vtkFloatArray* tcoords = aPart->GetMapper()->GetColorCoordinates();
189 if (!tcoords)
190 {
191 tcoords = vtkFloatArray::SafeDownCast(tris->GetPointData()->GetTCoords());
192 }
193 if (tcoords)
194 {
195 vtkFloatArray* da = tcoords;
196 vtkGLTFWriterUtils::WriteBufferAndView(tcoords, fileName, inlineData, buffers, bufferViews);
197
198 // write the accessor
199 Json::Value acc;
200 acc["bufferView"] = bufferViews.size() - 1;
201 acc["byteOffset"] = 0;
202 acc["type"] = da->GetNumberOfComponents() == 3 ? "VEC3" : "VEC2";
203 acc["componentType"] = GL_FLOAT;
204 acc["normalized"] = false;
205 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfTuples());
206 tcoordAccessor = accessors.size();
207 accessors.append(acc);
208 }
209
210 // to store the primitives
211 Json::Value prims;
212
213 // write out the verts
214 if (tris->GetVerts() && tris->GetVerts()->GetNumberOfCells())
215 {
216 Json::Value aprim;
217 aprim["mode"] = 0;
218 Json::Value attribs;
219
220 vtkCellArray* da = tris->GetVerts();
221 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
222
223 // write the accessor
224 Json::Value acc;
225 acc["bufferView"] = bufferViews.size() - 1;
226 acc["byteOffset"] = 0;
227 acc["type"] = "SCALAR";
228 acc["componentType"] = GL_UNSIGNED_INT;
229 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfCells());
230 aprim["indices"] = accessors.size();
231 accessors.append(acc);
232
233 attribs["POSITION"] = pointAccessor;
234 int userAccessor = userAccessorsStart;
235 for (size_t i = 0; i < arraysToSave.size(); ++i)
236 {
237 attribs[arraysToSave[i]->GetName()] = userAccessor++;
238 }
239 if (vertColorAccessor >= 0)
240 {
241 attribs["COLOR_0"] = vertColorAccessor;
242 }
243 if (tcoordAccessor >= 0)
244 {
245 attribs["TEXCOORD_0"] = tcoordAccessor;
246 }
247 aprim["attributes"] = attribs;
248 prims.append(aprim);
249 }
250
251 // write out the lines
252 if (tris->GetLines() && tris->GetLines()->GetNumberOfCells())
253 {
254 Json::Value aprim;
255 aprim["mode"] = 1;
256 Json::Value attribs;
257
258 vtkCellArray* da = tris->GetLines();
259 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
260
261 // write the accessor
262 Json::Value acc;
263 acc["bufferView"] = bufferViews.size() - 1;
264 acc["byteOffset"] = 0;
265 acc["type"] = "SCALAR";
266 acc["componentType"] = GL_UNSIGNED_INT;
267 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfCells() * 2);
268 aprim["indices"] = accessors.size();
269 accessors.append(acc);
270
271 attribs["POSITION"] = pointAccessor;
272 int userAccessor = userAccessorsStart;
273 for (size_t i = 0; i < arraysToSave.size(); ++i)
274 {
275 attribs[arraysToSave[i]->GetName()] = userAccessor++;
276 }
277 if (vertColorAccessor >= 0)
278 {
279 attribs["COLOR_0"] = vertColorAccessor;
280 }
281 if (tcoordAccessor >= 0)
282 {
283 attribs["TEXCOORD_0"] = tcoordAccessor;
284 }
285 aprim["attributes"] = attribs;
286 prims.append(aprim);
287 }
288
289 // write out the triangles
290 if (tris->GetPolys() && tris->GetPolys()->GetNumberOfCells())
291 {
292 Json::Value aprim;
293 aprim["mode"] = 4;
294 Json::Value attribs;
295
296 vtkCellArray* da = tris->GetPolys();
297 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
298
299 // write the accessor
300 Json::Value acc;
301 acc["bufferView"] = bufferViews.size() - 1;
302 acc["byteOffset"] = 0;
303 acc["type"] = "SCALAR";
304 acc["componentType"] = GL_UNSIGNED_INT;
305 acc["count"] = static_cast<Json::Value::Int64>(da->GetNumberOfCells() * 3);
306 aprim["indices"] = accessors.size();
307 accessors.append(acc);
308
309 attribs["POSITION"] = pointAccessor;
310 int userAccessor = userAccessorsStart;
311 for (size_t i = 0; i < arraysToSave.size(); ++i)
312 {
313 attribs[arraysToSave[i]->GetName()] = userAccessor++;
314 }
315 if (vertColorAccessor >= 0)
316 {
317 attribs["COLOR_0"] = vertColorAccessor;
318 }
319 if (tcoordAccessor >= 0)
320 {
321 attribs["TEXCOORD_0"] = tcoordAccessor;
322 }
323 aprim["attributes"] = attribs;
324 prims.append(aprim);
325 }
326
327 Json::Value amesh;
328 char meshNameBuffer[32];
329 sprintf(meshNameBuffer, "mesh%d", meshes.size());
330 amesh["name"] = meshNameBuffer;
331 amesh["primitives"] = prims;
332 meshes.append(amesh);
333
334 // write out an actor
335 Json::Value child;
336 vtkMatrix4x4* amat = aPart->GetMatrix();
337 if (!amat->IsIdentity())
338 {
339 for (int i = 0; i < 4; ++i)
340 {
341 for (int j = 0; j < 4; ++j)
342 {
343 child["matrix"].append(amat->GetElement(j, i));
344 }
345 }
346 }
347 child["mesh"] = meshes.size() - 1;
348 child["name"] = meshNameBuffer;
349 nodes.append(child);
350 }
351
WriteCamera(Json::Value & cameras,vtkRenderer * ren)352 void WriteCamera(Json::Value& cameras, vtkRenderer* ren)
353 {
354 vtkCamera* cam = ren->GetActiveCamera();
355 Json::Value acamera;
356 Json::Value camValues;
357 camValues["znear"] = cam->GetClippingRange()[0];
358 camValues["zfar"] = cam->GetClippingRange()[1];
359 if (cam->GetParallelProjection())
360 {
361 acamera["type"] = "orthographic";
362 camValues["xmag"] = cam->GetParallelScale() * ren->GetTiledAspectRatio();
363 camValues["ymag"] = cam->GetParallelScale();
364 acamera["orthographic"] = camValues;
365 }
366 else
367 {
368 acamera["type"] = "perspective";
369 camValues["yfov"] = vtkMath::RadiansFromDegrees(cam->GetViewAngle());
370 camValues["aspectRatio"] = ren->GetTiledAspectRatio();
371 acamera["perspective"] = camValues;
372 }
373 cameras.append(acamera);
374 }
375
WriteTexture(Json::Value & buffers,Json::Value & bufferViews,Json::Value & textures,Json::Value & samplers,Json::Value & images,vtkPolyData * pd,vtkActor * aPart,const char * fileName,bool inlineData,std::map<vtkUnsignedCharArray *,unsigned int> & textureMap)376 void WriteTexture(Json::Value& buffers, Json::Value& bufferViews, Json::Value& textures,
377 Json::Value& samplers, Json::Value& images, vtkPolyData* pd, vtkActor* aPart,
378 const char* fileName, bool inlineData, std::map<vtkUnsignedCharArray*, unsigned int>& textureMap)
379 {
380 // do we have a texture
381 aPart->GetMapper()->MapScalars(pd, 1.0);
382 vtkImageData* id = aPart->GetMapper()->GetColorTextureMap();
383 vtkTexture* t = nullptr;
384 if (!id && aPart->GetTexture())
385 {
386 t = aPart->GetTexture();
387 id = t->GetInput();
388 }
389
390 vtkUnsignedCharArray* da = nullptr;
391 if (id && id->GetPointData()->GetScalars())
392 {
393 da = vtkUnsignedCharArray::SafeDownCast(id->GetPointData()->GetScalars());
394 }
395 if (!da)
396 {
397 return;
398 }
399
400 unsigned int textureSource = 0;
401
402 if (textureMap.find(da) == textureMap.end())
403 {
404 textureMap[da] = textures.size();
405
406 // flip Y
407 vtkNew<vtkTrivialProducer> triv;
408 triv->SetOutput(id);
409 vtkNew<vtkImageFlip> flip;
410 flip->SetFilteredAxis(1);
411 flip->SetInputConnection(triv->GetOutputPort());
412
413 // convert to png
414 vtkNew<vtkPNGWriter> png;
415 png->SetCompressionLevel(5);
416 png->SetInputConnection(flip->GetOutputPort());
417 png->WriteToMemoryOn();
418 png->Write();
419 da = png->GetResult();
420
421 vtkGLTFWriterUtils::WriteBufferAndView(da, fileName, inlineData, buffers, bufferViews);
422
423 // write the image
424 Json::Value img;
425 img["bufferView"] = bufferViews.size() - 1;
426 img["mimeType"] = "image/png";
427 images.append(img);
428
429 textureSource = images.size() - 1;
430 }
431 else
432 {
433 textureSource = textureMap[da];
434 }
435
436 // write the sampler
437 Json::Value smp;
438 smp["magFilter"] = GL_NEAREST;
439 smp["minFilter"] = GL_NEAREST;
440 smp["wrapS"] = GL_CLAMP_TO_EDGE;
441 smp["wrapT"] = GL_CLAMP_TO_EDGE;
442 if (t)
443 {
444 smp["wrapS"] = t->GetRepeat() ? GL_REPEAT : GL_CLAMP_TO_EDGE;
445 smp["wrapT"] = t->GetRepeat() ? GL_REPEAT : GL_CLAMP_TO_EDGE;
446 smp["magFilter"] = t->GetInterpolate() ? GL_LINEAR : GL_NEAREST;
447 smp["minFilter"] = t->GetInterpolate() ? GL_LINEAR : GL_NEAREST;
448 }
449 samplers.append(smp);
450
451 Json::Value texture;
452 texture["source"] = textureSource;
453 texture["sampler"] = samplers.size() - 1;
454 textures.append(texture);
455 }
456
WriteMaterial(Json::Value & materials,int textureIndex,bool haveTexture,vtkActor * aPart)457 void WriteMaterial(Json::Value& materials, int textureIndex, bool haveTexture, vtkActor* aPart)
458 {
459 Json::Value mat;
460 Json::Value model;
461
462 if (haveTexture)
463 {
464 Json::Value tex;
465 tex["texCoord"] = 0; // TEXCOORD_0
466 tex["index"] = textureIndex;
467 model["baseColorTexture"] = tex;
468 }
469
470 vtkProperty* prop = aPart->GetProperty();
471 double dcolor[3];
472 prop->GetDiffuseColor(dcolor);
473 model["baseColorFactor"].append(dcolor[0]);
474 model["baseColorFactor"].append(dcolor[1]);
475 model["baseColorFactor"].append(dcolor[2]);
476 model["baseColorFactor"].append(prop->GetOpacity());
477 model["metallicFactor"] = prop->GetSpecular();
478 model["roughnessFactor"] = 1.0 / (1.0 + prop->GetSpecular() * 0.2 * prop->GetSpecularPower());
479 mat["pbrMetallicRoughness"] = model;
480 materials.append(mat);
481 }
482
483 }
484
WriteToString()485 std::string vtkGLTFExporter::WriteToString()
486 {
487 std::ostringstream result;
488
489 this->WriteToStream(result);
490
491 return result.str();
492 }
493
WriteData()494 void vtkGLTFExporter::WriteData()
495 {
496 vtksys::ofstream output;
497
498 // make sure the user specified a FileName or FilePointer
499 if (this->FileName == nullptr)
500 {
501 vtkErrorMacro(<< "Please specify FileName to use");
502 return;
503 }
504
505 // try opening the files
506 output.open(this->FileName);
507 if (!output.is_open())
508 {
509 vtkErrorMacro("Unable to open file for gltf output.");
510 return;
511 }
512
513 this->WriteToStream(output);
514 output.close();
515 }
516
WriteToStream(ostream & output)517 void vtkGLTFExporter::WriteToStream(ostream& output)
518 {
519 Json::Value cameras;
520 Json::Value bufferViews;
521 Json::Value buffers;
522 Json::Value accessors;
523 Json::Value nodes;
524 Json::Value meshes;
525 Json::Value textures;
526 Json::Value images;
527 Json::Value samplers;
528 Json::Value materials;
529
530 std::vector<unsigned int> topNodes;
531
532 // support sharing texture maps
533 std::map<vtkUnsignedCharArray*, unsigned int> textureMap;
534
535 for (auto ren : vtk::Range(this->RenderWindow->GetRenderers()))
536 {
537 if (this->ActiveRenderer && ren != this->ActiveRenderer)
538 {
539 // If ActiveRenderer is specified then ignore all other renderers
540 continue;
541 }
542 if (!ren->GetDraw())
543 {
544 continue;
545 }
546
547 // setup the camera data in case we need to use it later
548 Json::Value anode;
549 anode["camera"] = cameras.size(); // camera node
550 vtkMatrix4x4* mat = ren->GetActiveCamera()->GetModelViewTransformMatrix();
551 for (int i = 0; i < 4; ++i)
552 {
553 for (int j = 0; j < 4; ++j)
554 {
555 anode["matrix"].append(mat->GetElement(j, i));
556 }
557 }
558 anode["name"] = "Camera Node";
559
560 // setup renderer group node
561 Json::Value rendererNode;
562 rendererNode["name"] = "Renderer Node";
563
564 vtkPropCollection* pc;
565 vtkProp* aProp;
566 pc = ren->GetViewProps();
567 vtkCollectionSimpleIterator pit;
568 bool foundVisibleProp = false;
569 for (pc->InitTraversal(pit); (aProp = pc->GetNextProp(pit));)
570 {
571 if (!aProp->GetVisibility())
572 {
573 continue;
574 }
575 vtkNew<vtkActorCollection> ac;
576 aProp->GetActors(ac);
577 vtkActor* anActor;
578 vtkCollectionSimpleIterator ait;
579 for (ac->InitTraversal(ait); (anActor = ac->GetNextActor(ait));)
580 {
581 vtkAssemblyPath* apath;
582 vtkActor* aPart;
583 for (anActor->InitPathTraversal(); (apath = anActor->GetNextPath());)
584 {
585 aPart = static_cast<vtkActor*>(apath->GetLastNode()->GetViewProp());
586 if (aPart->GetVisibility() && aPart->GetMapper() &&
587 aPart->GetMapper()->GetInputAlgorithm())
588 {
589 aPart->GetMapper()->GetInputAlgorithm()->Update();
590 vtkPolyData* pd = findPolyData(aPart->GetMapper()->GetInputDataObject(0, 0));
591 if (pd && pd->GetNumberOfCells() > 0)
592 {
593 foundVisibleProp = true;
594 WriteMesh(accessors, buffers, bufferViews, meshes, nodes, pd, aPart, this->FileName,
595 this->InlineData, this->SaveNormal, this->SaveBatchId);
596 rendererNode["children"].append(nodes.size() - 1);
597 unsigned int oldTextureCount = textures.size();
598 WriteTexture(buffers, bufferViews, textures, samplers, images, pd, aPart,
599 this->FileName, this->InlineData, textureMap);
600 meshes[meshes.size() - 1]["primitives"][0]["material"] = materials.size();
601 WriteMaterial(materials, oldTextureCount, oldTextureCount != textures.size(), aPart);
602 }
603 }
604 }
605 }
606 }
607 // only write the camera if we had visible nodes
608 if (foundVisibleProp)
609 {
610 WriteCamera(cameras, ren);
611 nodes.append(anode);
612 rendererNode["children"].append(nodes.size() - 1);
613 nodes.append(rendererNode);
614 topNodes.push_back(nodes.size() - 1);
615 }
616 }
617
618 Json::Value root;
619 Json::Value asset;
620 asset["generator"] = "VTK";
621 asset["version"] = "2.0";
622 root["asset"] = asset;
623
624 root["scene"] = 0;
625 root["cameras"] = cameras;
626 root["nodes"] = nodes;
627 root["meshes"] = meshes;
628 root["buffers"] = buffers;
629 root["bufferViews"] = bufferViews;
630 root["accessors"] = accessors;
631 if (!images.empty())
632 root["images"] = images;
633 if (!textures.empty())
634 root["textures"] = textures;
635 if (!samplers.empty())
636 root["samplers"] = samplers;
637 root["materials"] = materials;
638
639 Json::Value ascene;
640 ascene["name"] = "Layer 0";
641 Json::Value noderefs;
642 for (auto i : topNodes)
643 {
644 noderefs.append(i);
645 }
646 ascene["nodes"] = noderefs;
647 Json::Value scenes;
648 scenes.append(ascene);
649 root["scenes"] = scenes;
650
651 Json::StreamWriterBuilder builder;
652 builder["commentStyle"] = "None";
653 builder["indentation"] = " ";
654 std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
655 writer->write(root, &output);
656 }
657
PrintSelf(ostream & os,vtkIndent indent)658 void vtkGLTFExporter::PrintSelf(ostream& os, vtkIndent indent)
659 {
660 this->Superclass::PrintSelf(os, indent);
661
662 os << "InlineData: " << this->InlineData << "\n";
663 if (this->FileName)
664 {
665 os << indent << "FileName: " << this->FileName << "\n";
666 }
667 else
668 {
669 os << indent << "FileName: (null)\n";
670 }
671 }
672