KiCad PCB EDA Suite
Loading...
Searching...
No Matches
glb_utils.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include "glb_utils.h"
25
26#include <cstdint>
27#include <cstring>
28#include <fstream>
29#include <map>
30#include <vector>
31
32#include <json_common.h>
33
34#include <kiplatform/io.h>
35
36
37static constexpr uint32_t GLB_MAGIC = 0x46546C67; // "glTF" in little-endian
38static constexpr uint32_t GLB_CHUNK_JSON = 0x4E4F534A; // "JSON" in little-endian
39static constexpr int GLTF_LINES_MODE = 1;
40
41
42bool FixGlbLinesPrimitives( const wxString& aFilePath )
43{
44 std::ifstream inFile( aFilePath.ToStdString(), std::ios::binary );
45
46 if( !inFile )
47 return false;
48
49 std::vector<uint8_t> fileData( ( std::istreambuf_iterator<char>( inFile ) ),
50 std::istreambuf_iterator<char>() );
51 inFile.close();
52
53 if( fileData.size() < 20 )
54 return false;
55
56 uint32_t magic = 0;
57 uint32_t version = 0;
58 uint32_t totalLength = 0;
59 std::memcpy( &magic, &fileData[0], 4 );
60 std::memcpy( &version, &fileData[4], 4 );
61 std::memcpy( &totalLength, &fileData[8], 4 );
62
63 // A declared length larger than the file means the GLB is truncated. Bounding the later
64 // chunk copy by totalLength then keeps any trailing junk out of the rewritten file.
65 if( magic != GLB_MAGIC || version != 2 || totalLength > fileData.size() )
66 return false;
67
68 uint32_t jsonChunkLength = 0;
69 uint32_t jsonChunkType = 0;
70 std::memcpy( &jsonChunkLength, &fileData[12], 4 );
71 std::memcpy( &jsonChunkType, &fileData[16], 4 );
72
73 // The JSON chunk is 4-byte aligned by spec; a misaligned length signals a malformed GLB.
74 if( jsonChunkType != GLB_CHUNK_JSON || jsonChunkLength % 4 != 0 )
75 return false;
76
77 size_t jsonStart = 20;
78 size_t jsonEnd = jsonStart + jsonChunkLength;
79
80 if( jsonEnd > totalLength )
81 return false;
82
83 std::string jsonStr( fileData.begin() + jsonStart, fileData.begin() + jsonEnd );
84
85 nlohmann::json gltf;
86
87 try
88 {
89 gltf = nlohmann::json::parse( jsonStr );
90 }
91 catch( const nlohmann::json::parse_error& )
92 {
93 return false;
94 }
95
96 if( !gltf.contains( "meshes" ) || !gltf.contains( "accessors" ) )
97 return true;
98
99 bool modified = false;
100
101 try
102 {
103 if( !gltf["meshes"].is_array() || !gltf["accessors"].is_array() )
104 return true;
105
106 auto& accessors = gltf["accessors"];
107
108 // An indices accessor may be shared by several primitives. Reducing its count in
109 // place would corrupt the others, so count references first and clone-on-write.
110 std::map<size_t, int> accessorRefCount;
111
112 for( auto& mesh : gltf["meshes"] )
113 {
114 if( !mesh.is_object() || !mesh.contains( "primitives" )
115 || !mesh["primitives"].is_array() )
116 continue;
117
118 for( auto& prim : mesh["primitives"] )
119 {
120 if( prim.is_object() && prim.contains( "indices" )
121 && prim["indices"].is_number_unsigned() )
122 accessorRefCount[prim["indices"].get<size_t>()]++;
123 }
124 }
125
126 for( auto& mesh : gltf["meshes"] )
127 {
128 if( !mesh.is_object() || !mesh.contains( "primitives" )
129 || !mesh["primitives"].is_array() )
130 continue;
131
132 auto& primitives = mesh["primitives"];
133
134 for( auto primIt = primitives.begin(); primIt != primitives.end(); )
135 {
136 auto& prim = *primIt;
137
138 if( !prim.is_object() )
139 {
140 ++primIt;
141 continue;
142 }
143
144 int mode = prim.value( "mode", 4 );
145
146 if( mode != GLTF_LINES_MODE || !prim.contains( "indices" )
147 || !prim["indices"].is_number_unsigned() )
148 {
149 ++primIt;
150 continue;
151 }
152
153 size_t accessorIdx = prim["indices"].get<size_t>();
154
155 if( accessorIdx >= accessors.size() || !accessors[accessorIdx].is_object()
156 || !accessors[accessorIdx]["count"].is_number_unsigned() )
157 {
158 ++primIt;
159 continue;
160 }
161
162 size_t count = accessors[accessorIdx]["count"].get<size_t>();
163
164 if( count % 2 == 0 )
165 {
166 ++primIt;
167 continue;
168 }
169
170 // A single dangling index forms no segment at all, so the primitive is
171 // useless; the spec also forbids a zero count. Drop the whole primitive, but
172 // only while the mesh keeps another, since the spec requires a non-empty
173 // primitives array. Bail out untouched in the (OCCT-never-seen) lone case
174 // rather than emit an equally invalid empty mesh.
175 if( count < 2 )
176 {
177 if( primitives.size() <= 1 )
178 return false;
179
180 primIt = primitives.erase( primIt );
181 modified = true;
182 continue;
183 }
184
185 // Dropping the dangling index leaves a valid even count. When the accessor
186 // is shared, clone it so only this primitive's line list is shortened.
187 if( accessorRefCount[accessorIdx] > 1 )
188 {
189 accessorRefCount[accessorIdx]--;
190 nlohmann::json clone = accessors[accessorIdx];
191 clone["count"] = count - 1;
192 accessors.push_back( std::move( clone ) );
193 prim["indices"] = accessors.size() - 1;
194 }
195 else
196 {
197 accessors[accessorIdx]["count"] = count - 1;
198 }
199
200 modified = true;
201 ++primIt;
202 }
203 }
204 }
205 catch( const nlohmann::json::exception& )
206 {
207 return false;
208 }
209
210 if( !modified )
211 return true;
212
213 std::string fixedJson = gltf.dump();
214
215 // GLB spec requires JSON chunk to be padded to 4-byte alignment with spaces
216 while( fixedJson.size() % 4 != 0 )
217 fixedJson.push_back( ' ' );
218
219 size_t newJsonChunkLength = fixedJson.size();
220
221 // Reconstruct the GLB as header + JSON chunk header + JSON + the remaining chunks. The
222 // BIN chunk and any trailing chunks are copied verbatim, bounded by the declared length.
223 size_t binChunkStart = jsonEnd;
224 size_t remainingSize = totalLength - binChunkStart;
225 size_t newTotalLength = 12 + 8 + newJsonChunkLength + remainingSize;
226
227 if( newTotalLength > UINT32_MAX )
228 return false;
229
230 std::vector<uint8_t> outData;
231 outData.reserve( newTotalLength );
232
233 uint32_t newJsonChunkLength32 = static_cast<uint32_t>( newJsonChunkLength );
234 uint32_t newTotalLength32 = static_cast<uint32_t>( newTotalLength );
235
236 // GLB header
237 outData.resize( 12 );
238 std::memcpy( &outData[0], &magic, 4 );
239 std::memcpy( &outData[4], &version, 4 );
240 std::memcpy( &outData[8], &newTotalLength32, 4 );
241
242 // JSON chunk header
243 size_t pos = outData.size();
244 outData.resize( pos + 8 );
245 std::memcpy( &outData[pos], &newJsonChunkLength32, 4 );
246 std::memcpy( &outData[pos + 4], &jsonChunkType, 4 );
247
248 // JSON data
249 outData.insert( outData.end(), fixedJson.begin(), fixedJson.end() );
250
251 // Remaining chunks (BIN chunk, etc.)
252 if( remainingSize > 0 )
253 outData.insert( outData.end(), fileData.begin() + binChunkStart,
254 fileData.begin() + totalLength );
255
256 // Replace the file atomically so a partial write or crash can never leave a corrupt GLB.
257 return KIPLATFORM::IO::AtomicWriteFile( aFilePath, outData.data(), outData.size() );
258}
static constexpr uint32_t GLB_CHUNK_JSON
Definition glb_utils.cpp:38
static constexpr uint32_t GLB_MAGIC
Definition glb_utils.cpp:37
static constexpr int GLTF_LINES_MODE
Definition glb_utils.cpp:39
bool FixGlbLinesPrimitives(const wxString &aFilePath)
Fix LINES primitives in a GLB file that have odd index counts.
Definition glb_utils.cpp:42
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...