1 /*
2 * Copyright (C) 2012 Alberto Irurueta Carro (alberto@irurueta.com)
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16 package com.irurueta.geometry.io;
17
18 import java.io.File;
19 import java.io.IOException;
20 import java.io.OutputStream;
21
22 /**
23 * Abstract class that defines the interface for writers. A MeshWriter will be
24 * able to transcode a given 3D file into another format.
25 */
26 public abstract class MeshWriter {
27
28 /**
29 * Loader to load a file to be trans-coded.
30 */
31 protected final Loader loader;
32
33 /**
34 * Stream where trans-coded data will be written to.
35 */
36 protected final OutputStream stream;
37
38 /**
39 * Listener to be notified when transcoding process starts, stops or when
40 * progress changes.
41 */
42 protected MeshWriterListener listener;
43
44 /**
45 * Indicates if mesh writer is locked because a file is being processed.
46 */
47 protected boolean locked;
48
49 /**
50 * Indicates if texture validation will be ignored or not. If not ignored,
51 * it will be ensured that textures are valid image files.
52 */
53 protected boolean ignoreTextureValidation = false;
54
55 /**
56 * Internal class implementing specific listeners for some specific loader
57 * implementations.
58 */
59 final Listeners internalListeners;
60
61 /**
62 * Constructor.
63 *
64 * @param loader loader to load a 3D file.
65 * @param stream stream where trans-coded data will be written to.
66 */
67 protected MeshWriter(final Loader loader, final OutputStream stream) {
68 this.loader = loader;
69 this.stream = stream;
70 listener = null;
71 locked = false;
72
73 internalListeners = new Listeners(this);
74 }
75
76 /**
77 * Constructor.
78 *
79 * @param loader loader to load a 3D file.
80 * @param stream stream where trans-coded data will be written to.
81 * @param listener listener to be notified of progress changes or when
82 * transcoding process starts or finishes.
83 */
84 protected MeshWriter(final Loader loader, final OutputStream stream, final MeshWriterListener listener) {
85 this.loader = loader;
86 this.stream = stream;
87 this.listener = listener;
88 locked = false;
89
90 internalListeners = new Listeners(this);
91 }
92
93 /**
94 * Boolean indicating whether this mesh writer is locked because a file is
95 * being processed.
96 *
97 * @return true if this instance is locked, false otherwise.
98 */
99 public boolean isLocked() {
100 return locked;
101 }
102
103 /**
104 * Returns stream where trans-coded data will be written to.
105 *
106 * @return stream where trans-coded data will be written to.
107 */
108 public OutputStream getStream() {
109 return stream;
110 }
111
112 /**
113 * Returns listener to be notified when transcoding process starts, stops or
114 * when progress changes.
115 *
116 * @return listener of this mesh writer.
117 */
118 public MeshWriterListener getListener() {
119 return listener;
120 }
121
122 /**
123 * Sets listener to be notified when transcoding process starts, stops or
124 * when progress changes.
125 *
126 * @param listener listener to be set.
127 * @throws LockedException if this instance is locked because this mesh
128 * writer is already processing a file.
129 */
130 public void setListener(final MeshWriterListener listener) throws LockedException {
131 if (isLocked()) {
132 throw new LockedException();
133 }
134 this.listener = listener;
135 }
136
137 /**
138 * Indicates if this mesh writer is ready because a file and a loader have
139 * been provided.
140 *
141 * @return true if mesh writer is ready, false otherwise.
142 */
143 public boolean isReady() {
144 return (stream != null) && (loader != null);
145 }
146
147 /**
148 * Internal class implementing listeners for different specific loaders.
149 */
150 private class Listeners implements LoaderListener, LoaderListenerOBJ, LoaderListenerBinary, MaterialLoaderListener {
151
152 /**
153 * Reference to mesh writer.
154 */
155 private final MeshWriter writer;
156
157 /**
158 * Constructor.
159 *
160 * @param writer reference to mesh writer.
161 */
162 public Listeners(final MeshWriter writer) {
163 this.writer = writer;
164 }
165
166 //Loader listener
167
168 /**
169 * Method called when the loader starts processing a file.
170 *
171 * @param loader reference to loader.
172 */
173 @Override
174 public void onLoadStart(final Loader loader) {
175 //no action needed
176 }
177
178 /**
179 * Method called when the loader ends processing a file.
180 *
181 * @param loader reference to loader.
182 */
183 @Override
184 public void onLoadEnd(final Loader loader) {
185 //no action needed
186 }
187
188 /**
189 * Method called when loading progress changes enough to be notified.
190 *
191 * @param loader reference to loader.
192 * @param progress progress amount as a value between 0.0 and 1.0.
193 */
194 @Override
195 public void onLoadProgressChange(final Loader loader, final float progress) {
196 if (listener != null) {
197 listener.onWriteProgressChange(writer, progress);
198 }
199 }
200
201 // OBJ loader listener (requesting a material loader for a given path)
202
203 /**
204 * Called when an OBJ loader requires a material loader to load the
205 * associated material (MTL) file.
206 *
207 * @param loader reference to loader.
208 * @param path path where MTL file should be found. It's up to the
209 * final implementation to determine where the file will be finally
210 * found.
211 * @return an instance of a material loader.
212 */
213 @Override
214 public MaterialLoaderOBJ onMaterialLoaderRequested(final LoaderOBJ loader, final String path) {
215 if (listener != null) {
216 final var materialFile = listener.onMaterialFileRequested(writer, path);
217
218 if (materialFile == null) {
219 return null;
220 }
221
222 try {
223 // when instantiating material loader it checks if file
224 // exists, if not we return null
225 return new MaterialLoaderOBJ(materialFile, this);
226 } catch (final IOException ex) {
227 return null;
228 }
229 }
230
231 return null;
232 }
233
234 /**
235 * Called when a texture has been found.
236 *
237 * @param loader reference to loader.
238 * @param textureId id assigned to the texture that has been found.
239 * @param textureImageWidth width of texture image that has been found.
240 * @param textureImageHeight height of texture image that has been found.
241 * @return File where texture data will be copied to.
242 */
243 @Override
244 public File onTextureReceived(final LoaderBinary loader, final int textureId, final int textureImageWidth,
245 final int textureImageHeight) {
246 if (listener != null) {
247 return listener.onTextureReceived(writer, textureImageWidth, textureImageHeight);
248 }
249
250 return null;
251 }
252
253 /**
254 * Called when texture data is available to be retrieved.
255 *
256 * @param loader reference to loader.
257 * @param textureFile reference to a File where texture data has been
258 * temporarily copied.
259 * @param textureId id assigned to the texture.
260 * @param textureImageWidth width of texture image.
261 * @param textureImageHeight height of texture image.
262 * @return converted image trans-coded into JPG format or resized if
263 * needed, or input textureFile if no changes are needed.
264 */
265 @Override
266 public boolean onTextureDataAvailable(final LoaderBinary loader, final File textureFile, final int textureId,
267 final int textureImageWidth, final int textureImageHeight) {
268 var valid = true;
269 var convertedFile = textureFile;
270 if (textureFile != null) {
271 if (listener != null) {
272 // notify that texture data has been copied into texture file
273 // in case it needs to be handled elsewhere (to transform it
274 // into another format)
275 convertedFile = listener.onTextureDataAvailable(writer, textureFile, textureImageWidth,
276 textureImageHeight);
277 }
278
279 if (convertedFile != null) {
280 final var tex = new Texture(textureId);
281 tex.setWidth(textureImageWidth);
282 tex.setHeight(textureImageHeight);
283 tex.setValid(true);
284
285 try {
286 processTextureFile(tex, convertedFile);
287 } catch (final IOException ex) {
288 valid = false;
289 }
290
291 if (listener != null) {
292 listener.onTextureDataProcessed(writer, textureFile, textureId, textureImageHeight);
293 }
294 }
295 }
296 return valid;
297 }
298
299 // Material loader listener
300
301 /**
302 * Called when material loader starts processing materials.
303 *
304 * @param loader reference to loader.
305 */
306 @Override
307 public void onLoadStart(final MaterialLoader loader) {
308 // no action required
309 }
310
311 /**
312 * Called when material loader finishes processing materials.
313 *
314 * @param loader reference to loader.
315 */
316 @Override
317 public void onLoadEnd(final MaterialLoader loader) {
318 // no action required
319 }
320
321 /**
322 * Called when a texture assigned to a material must be validated to
323 * ensure that texture is valid.
324 *
325 * @param loader reference to loader.
326 * @param texture texture reference to be validated.
327 * @return true if texture is valid, false otherwise.
328 */
329 @Override
330 public boolean onValidateTexture(final MaterialLoader loader, final Texture texture) {
331 var valid = true;
332 if (listener != null && !ignoreTextureValidation) {
333 final var textureFile = listener.onValidateTexture(writer, texture);
334 valid = texture.isValid();
335
336 if (valid && textureFile != null) {
337 try {
338 processTextureFile(texture, textureFile);
339 } catch (final IOException ex) {
340 valid = false;
341 }
342
343 // notify that texture was validated in case that texture
344 // file needs to be removed
345 listener.onDidValidateTexture(writer, textureFile);
346 }
347 }
348 return valid;
349 }
350 }
351
352 /**
353 * Abstract method to process input file and write it into output stream.
354 *
355 * @throws LoaderException if 3D file loading fails.
356 * @throws IOException if an I/O error occurs.
357 * @throws NotReadyException if mesh writer is not ready because either a
358 * loader has not been provided or an output stream has not been provided.
359 * @throws LockedException if this mesh writer is locked processing a file.
360 */
361 public abstract void write() throws LoaderException, IOException, NotReadyException, LockedException;
362
363 /**
364 * Abstract method to processes texture file. Usually this will imply
365 * validating that image file is not corrupt and has proper size (power of
366 * 2). If not image will be resized.
367 *
368 * @param texture reference to texture.
369 * @param textureFile file where texture is temporarily copied.
370 * @throws IOException if an I/O error occurs.
371 */
372 protected abstract void processTextureFile(final Texture texture, final File textureFile) throws IOException;
373 }