1 ///////////////////////////////////////////////////////////////////////////////
2 // Copyright (C) 2004-2010 by The Allacrost Project
3 // All Rights Reserved
4 //
5 // This code is licensed under the GNU GPL version 2. It is free software
6 // and you may modify it and/or redistribute it under the terms of this license.
7 // See http://www.gnu.org/copyleft/gpl.html for details.
8 ///////////////////////////////////////////////////////////////////////////////
9
10 /** ****************************************************************************
11 *** \file image_base.cpp
12 *** \author Tyler Olsen, roots@allacrost.org
13 *** \brief Source file for image base classes
14 *** ***************************************************************************/
15
16 #include <cstdarg>
17 #include <math.h>
18
19 #include "image_base.h"
20 #include "video.h"
21
22 using namespace std;
23 using namespace hoa_utils;
24
25 namespace hoa_video {
26
27 namespace private_video {
28
29 // -----------------------------------------------------------------------------
30 // ImageMemory class
31 // -----------------------------------------------------------------------------
32
ImageMemory()33 ImageMemory::ImageMemory() :
34 width(0),
35 height(0),
36 pixels(NULL),
37 rgb_format(false)
38 {}
39
40
41
~ImageMemory()42 ImageMemory::~ImageMemory() {
43 // Winter Knight - I commented this out because it was causing double free
44 // segfaults when ImageMemory objects were copied via copy constructor.
45 if (pixels != NULL) {
46 IF_PRINT_WARNING(VIDEO_DEBUG) << "pixels member was not NULL upon object destruction" << endl;
47 // free(pixels);
48 // pixels = NULL;
49 }
50 }
51
52
53
LoadImage(const string & filename)54 bool ImageMemory::LoadImage(const string& filename) {
55 if (pixels != NULL) {
56 IF_PRINT_WARNING(VIDEO_DEBUG) << "pixels member was not NULL upon function invocation" << endl;
57 free(pixels);
58 pixels = NULL;
59 }
60
61 // Isolate the extension
62 size_t ext_position = filename.rfind('.');
63
64 if (ext_position == string::npos) {
65 IF_PRINT_WARNING(VIDEO_DEBUG) << "could not decipher file extension for filename: " << filename << endl;
66 return false;
67 }
68
69 string extension = string(filename, ext_position, filename.length() - ext_position);
70
71 // NOTE: We could technically try uppercase forms of the file extension, or also include the .jpeg extension name,
72 // but Allacrost's file standard states that only the .png and .jpg image file extensions are suppported.
73 if (extension == ".png")
74 return _LoadPngImage(filename);
75 else if (extension == ".jpg")
76 return _LoadJpgImage(filename);
77
78 IF_PRINT_WARNING(VIDEO_DEBUG) << "unsupported file extension: \"" << extension << "\" for filename: " << filename << endl;
79 return false;
80 }
81
82
83
SaveImage(const string & filename,bool png_image)84 bool ImageMemory::SaveImage(const string& filename, bool png_image) {
85 if (pixels == NULL) {
86 IF_PRINT_WARNING(VIDEO_DEBUG) << "pixels member was NULL upon function invocation for file: " << filename << endl;
87 return false;
88 }
89
90 if (png_image) {
91 return _SavePngImage(filename);
92 }
93 else {
94 // JPG images don't have alpha information, so we must convert the data to RGB format first
95 if (rgb_format == false)
96 RGBAToRGB();
97 return _SaveJpgImage(filename);
98 }
99 }
100
101
102
ConvertToGrayscale()103 void ImageMemory::ConvertToGrayscale() {
104 if (width <= 0 || height <= 0) {
105 IF_PRINT_WARNING(VIDEO_DEBUG) << "width and/or height members were invalid (<= 0)" << endl;
106 return;
107 }
108
109 if (pixels == NULL) {
110 IF_PRINT_WARNING(VIDEO_DEBUG) << "no image data (pixels == NULL)" << endl;
111 return;
112 }
113
114 uint8 format_bytes = (rgb_format ? 3 : 4);
115 uint8* end_position = static_cast<uint8*>(pixels) + (width * height * format_bytes);
116
117 for (uint8* i = static_cast<uint8*>(pixels); i < end_position; i += format_bytes) {
118 // Compute the grayscale value for this pixel based on RGB values: 0.30R + 0.59G + 0.11B
119 uint8 value = static_cast<uint8>((30 * *(i) + 59 * *(i + 1) + 11 * *(i + 2)) * 0.01f);
120 *i = value;
121 *(i + 1) = value;
122 *(i + 2) = value;
123 // *(i + 3) for RGBA is the alpha value and is left unmodified
124 }
125 }
126
127
RGBAToRGB()128 void ImageMemory::RGBAToRGB() {
129 if (width <= 0 || height <= 0) {
130 IF_PRINT_WARNING(VIDEO_DEBUG) << "width and/or height members were invalid (<= 0)" << endl;
131 return;
132 }
133
134 if (pixels == NULL) {
135 IF_PRINT_WARNING(VIDEO_DEBUG) << "no image data (pixels == NULL)" << endl;
136 return;
137 }
138
139 if (rgb_format == true) {
140 IF_PRINT_WARNING(VIDEO_DEBUG) << "image data was said to already be in RGB format" << endl;
141 return;
142 }
143
144 uint8* pixel_index = static_cast<uint8*>(pixels);
145 uint8* pixel_source = pixel_index;
146
147 for (int32 i = 0; i < height * width; i++, pixel_index += 4) {
148 int32 index = 3 * i;
149 pixel_source[index] = *pixel_index;
150 pixel_source[index + 1] = *(pixel_index + 1);
151 pixel_source[index + 2] = *(pixel_index + 2);
152 }
153
154 // Reduce the memory consumed by 1/4 since we no longer need to contain alpha data
155 pixels = realloc(pixels, width * height * 3);
156 rgb_format = true;
157 }
158
159
160
CopyFromTexture(TexSheet * texture)161 void ImageMemory::CopyFromTexture(TexSheet* texture) {
162 if (pixels != NULL)
163 free(pixels);
164 pixels = NULL;
165
166 // Get the texture as a buffer
167 height = texture->height;
168 width = texture->width;
169 pixels = malloc(height * width * (rgb_format ? 3 : 4));
170 if (pixels == NULL) {
171 PRINT_ERROR << "failed to malloc enough memory to copy the texture" << endl;
172 }
173
174 TextureManager->_BindTexture(texture->tex_id);
175 glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
176 }
177
178
179
CopyFromImage(BaseTexture * img)180 void ImageMemory::CopyFromImage(BaseTexture* img) {
181 // First copy the image's entire texture sheet to memory
182 CopyFromTexture(img->texture_sheet);
183
184 // Check that the image to copy is smaller than its texture sheet (usually true).
185 // If so, then copy over only the sub-rectangle area of the image from its texture
186 if (height > img->height || width > img->width) {
187 uint8 format_bytes = (rgb_format ? 3 : 4);
188 uint32 src_bytes = width * format_bytes;
189 uint32 dst_bytes = img->width * format_bytes;
190 uint32 src_offset = img->y * width * format_bytes + img->x * format_bytes;
191 void* img_pixels = malloc(img->width * img->height * format_bytes);
192 if (img_pixels == NULL) {
193 PRINT_ERROR << "failed to malloc enough memory to copy the image" << endl;
194 return;
195 }
196
197 for (int32 i = 0; i < img->height; i++) {
198 memcpy((uint8*)img_pixels + i * dst_bytes, (uint8*)pixels + i * src_bytes + src_offset, dst_bytes);
199 }
200
201 // Delete the memory used for the texture sheet and replace it with the memory for the image
202 if (pixels)
203 free(pixels);
204
205 height = img->height;
206 width = img->width;
207 pixels = img_pixels;
208 }
209 }
210
211
212
_LoadPngImage(const string & filename)213 bool ImageMemory::_LoadPngImage(const string& filename) {
214 // open up the PNG
215 FILE* fp = fopen(filename.c_str(), "rb");
216
217 if (fp == NULL)
218 return false;
219
220 // check the signature to make sure it's a PNG
221 uint8 test_buffer[8];
222
223 fread(test_buffer, 1, 8, fp);
224 if (png_sig_cmp(test_buffer, 0, 8)) {
225 fclose(fp);
226 return false;
227 }
228
229 // load the actual PNG
230 png_structp png_ptr = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)NULL, NULL, NULL);
231
232 if (png_ptr == NULL) {
233 fclose(fp);
234 return false;
235 }
236
237 // get the info structure
238 png_infop info_ptr = png_create_info_struct(png_ptr);
239
240 if (info_ptr == NULL) {
241 png_destroy_read_struct(&png_ptr, NULL, (png_infopp)NULL);
242 fclose(fp);
243 return false;
244 }
245
246 // error handling
247 if (setjmp(png_jmpbuf(png_ptr))) {
248 png_destroy_read_struct(&png_ptr, NULL, (png_infopp)NULL);
249 fclose(fp);
250 return false;
251 }
252
253 // read the PNG
254 png_init_io(png_ptr, fp);
255 png_set_sig_bytes(png_ptr, 8);
256 png_read_png(png_ptr, info_ptr, PNG_TRANSFORM_STRIP_16 | PNG_TRANSFORM_PACKING | PNG_TRANSFORM_EXPAND, NULL);
257
258 // and get an array of pointers, one for each row of the PNG
259 uint8** row_pointers = png_get_rows(png_ptr, info_ptr);
260
261 // copy metadata
262 width = png_get_image_width(png_ptr, info_ptr);
263 height = png_get_image_height(png_ptr, info_ptr);
264 pixels = malloc(width * height * 4);
265
266 // check that we were able to allocate enough memory for the PNG
267 if (pixels == NULL) {
268 png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
269 fclose(fp);
270 PRINT_ERROR << "failed to malloc sufficient memory for .png file: " << filename << endl;
271 return false;
272 }
273
274 // convert the damn thing so that it works in our format
275 // this is mostly just byteswapping and adding extra data - we want everything in four channels
276 // for the moment, anyway
277 uint32 bpp = png_get_channels(png_ptr, info_ptr);
278 uint8* img_pixel = NULL;
279 uint8* dst_pixel = NULL;
280
281 if (png_get_color_type(png_ptr, info_ptr) == PNG_COLOR_TYPE_PALETTE) {
282 // colours come from a palette - for this colour type, we have to look up the colour from the palette
283 png_colorp palette;
284 int num_palette;
285 png_get_PLTE(png_ptr, info_ptr, &palette, &num_palette);
286 png_color c;
287 for (uint32 y = 0; y < height; y++) {
288 for (uint32 x = 0; x < width; x++) {
289 img_pixel = row_pointers[y] + (x * bpp);
290 dst_pixel = ((uint8*)pixels) + ((y * width) + x) * 4;
291 c = palette[img_pixel[0]];
292
293 dst_pixel[0] = c.red;
294 dst_pixel[1] = c.green;
295 dst_pixel[2] = c.blue;
296 dst_pixel[3] = 0xFF;
297 }
298 }
299 }
300 else if (bpp == 1) {
301 for (uint32 y = 0; y < height; y++) {
302 for (uint32 x = 0; x < width; x++) {
303 img_pixel = row_pointers[y] + (x * bpp);
304 dst_pixel = ((uint8*)pixels) + ((y * width) + x) * 4;
305 dst_pixel[0] = img_pixel[0];
306 dst_pixel[1] = img_pixel[0];
307 dst_pixel[2] = img_pixel[0];
308 dst_pixel[3] = 0xFF;
309 }
310 }
311 }
312 else if (bpp == 3) {
313 for (uint32 y = 0; y < height; y++) {
314 for (uint32 x = 0; x < width; x++) {
315 img_pixel = row_pointers[y] + (x * bpp);
316 dst_pixel = ((uint8*)pixels) + ((y * width) + x) * 4;
317 dst_pixel[0] = img_pixel[0];
318 dst_pixel[1] = img_pixel[1];
319 dst_pixel[2] = img_pixel[2];
320 dst_pixel[3] = 0xFF;
321 }
322 }
323 }
324 else if (bpp == 4) {
325 for (uint32 y = 0; y < height; y++) {
326 for (uint32 x = 0; x < width; x++) {
327 img_pixel = row_pointers[y] + (x * bpp);
328 dst_pixel = ((uint8*)pixels) + ((y * width) + x) * 4;
329 dst_pixel[0] = img_pixel[0];
330 dst_pixel[1] = img_pixel[1];
331 dst_pixel[2] = img_pixel[2];
332 dst_pixel[3] = img_pixel[3];
333 }
334 }
335 }
336 else {
337 png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
338 fclose(fp);
339 PRINT_ERROR << "failed to load .png file (bytes per pixel not supported): " << filename << endl;
340 return false;
341 }
342
343 // clean everything up
344 png_destroy_read_struct(&png_ptr, &info_ptr, (png_infopp)NULL);
345 fclose(fp);
346
347 rgb_format = false;
348 return true;
349 } // bool ImageMemory::_LoadPngImage(const string& filename)
350
351
352
_LoadJpgImage(const string & filename)353 bool ImageMemory::_LoadJpgImage(const string& filename) {
354 // open the file
355 FILE* fp;
356 uint8** buffer;
357
358 if ((fp = fopen(filename.c_str(), "rb")) == NULL)
359 return false;
360
361 // create the error-handing stuff and the main decompression object
362 jpeg_decompress_struct cinfo;
363 jpeg_error_mgr jerr;
364
365 cinfo.err = jpeg_std_error(&jerr);
366 jpeg_create_decompress(&cinfo);
367
368 // tell it where to read, and read the header
369 jpeg_stdio_src(&cinfo, fp);
370 jpeg_read_header(&cinfo, TRUE);
371
372 // here we go, here we go, here we go!
373 jpeg_start_decompress(&cinfo);
374
375 // how much space does each row take up?
376 JDIMENSION row_stride = cinfo.output_width * cinfo.output_components;
377
378 // let's get us some MEMORY - but we use the jpeg library to do it so that it comes out of that memory pool
379 buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
380
381 // metadata
382 width = cinfo.output_width;
383 height = cinfo.output_height;
384 pixels = malloc(cinfo.output_width * cinfo.output_height * 3);
385
386 // swizzle everything so it's in the format we want
387 uint32 bpp = cinfo.output_components;
388 uint8* img_pixel = NULL;
389 uint8* dst_pixel = NULL;
390
391 if (bpp == 3) {
392 for (uint32 y = 0; y < cinfo.output_height; y++) {
393 jpeg_read_scanlines(&cinfo, buffer, 1);
394
395 for(uint32 x = 0; x < cinfo.output_width; x++) {
396 img_pixel = buffer[0] + (x * bpp);
397 dst_pixel = ((uint8 *)pixels) + ((y * cinfo.output_width) + x) * 3;
398
399 dst_pixel[0] = img_pixel[0];
400 dst_pixel[1] = img_pixel[1];
401 dst_pixel[2] = img_pixel[2];
402 }
403 }
404 }
405 else if (bpp == 4) {
406 for (uint32 y = 0; y < cinfo.output_height; y++) {
407 jpeg_read_scanlines(&cinfo, buffer, 1);
408
409 for (uint32 x = 0; x < cinfo.output_width; x++) {
410 img_pixel = buffer[0] + (x * bpp);
411 dst_pixel = ((uint8 *)pixels) + ((y * cinfo.output_width) + x) * 3;
412
413 dst_pixel[0] = img_pixel[0];
414 dst_pixel[1] = img_pixel[1];
415 dst_pixel[2] = img_pixel[2];
416 }
417 }
418 }
419 else {
420 jpeg_finish_decompress(&cinfo);
421 jpeg_destroy_decompress(&cinfo);
422 fclose(fp);
423 PRINT_ERROR << "failed to load .jpg file (bytes per pixel not supported): " << filename << endl;
424 return false;
425 }
426
427 // clean up
428 jpeg_finish_decompress(&cinfo);
429 jpeg_destroy_decompress(&cinfo);
430
431 fclose(fp);
432 rgb_format = true;
433 return true;
434 } // bool ImageMemory::_LoadJpgImage(const string& filename)
435
436
437
_SavePngImage(const std::string & filename) const438 bool ImageMemory::_SavePngImage(const std::string& filename) const {
439 // open up the file for writing
440 FILE* fp = fopen(filename.c_str(), "wb");
441
442 if (fp == NULL) {
443 IF_PRINT_WARNING(VIDEO_DEBUG) << "could not open file: " << filename << endl;
444 return false;
445 }
446
447 // aah, RGB data! We can only handle RGBA at the moment
448 if (rgb_format == true) {
449 IF_PRINT_WARNING(VIDEO_DEBUG) << "attempting to save RGB format image data as a RGBA format PNG image" << endl;
450 }
451
452 // grab a write structure
453 png_structp png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, (png_voidp)NULL, NULL, NULL);
454
455 if (!png_ptr) {
456 IF_PRINT_WARNING(VIDEO_DEBUG) << "png_create_write_struct() failed for file: " << filename << endl;
457 fclose(fp);
458 return false;
459 }
460
461 // and a place to store the metadata
462 png_infop info_ptr = png_create_info_struct(png_ptr);
463
464 if (!info_ptr) {
465 IF_PRINT_WARNING(VIDEO_DEBUG) << "png_create_info_struct() failed for file: " << filename << endl;
466 png_destroy_write_struct(&png_ptr, NULL);
467 fclose(fp);
468 return false;
469 }
470
471 // prepare for error handling!
472 if (setjmp(png_jmpbuf(png_ptr))) {
473 IF_PRINT_WARNING(VIDEO_DEBUG) << "setjmp returned non-zero for file: " << filename << endl;
474 png_destroy_write_struct(&png_ptr, &info_ptr);
475 fclose(fp);
476 return false;
477 }
478
479 // tell it where to look
480 png_init_io(png_ptr, fp);
481
482 // write the header
483 png_set_IHDR(png_ptr, info_ptr, width, height, 8, PNG_COLOR_TYPE_RGB_ALPHA,
484 PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
485
486 // get the row array from our data
487 png_byte** row_pointers = new png_byte*[height];
488 int32 bytes_per_row = width * 4;
489 for (int32 i = 0; i < height; i++) {
490 row_pointers[i] = (png_byte*)pixels + bytes_per_row * i;
491 }
492
493 // tell it what the rows are
494 png_set_rows(png_ptr, info_ptr, row_pointers);
495 // and write the PNG
496 png_write_png (png_ptr, info_ptr, PNG_TRANSFORM_IDENTITY, NULL);
497 png_write_image(png_ptr, row_pointers);
498 // clean up
499 png_write_end(png_ptr, info_ptr);
500 png_destroy_write_struct(&png_ptr, &info_ptr);
501
502 // free the memory we ate
503 delete[] row_pointers;
504
505 // peace and love for all
506 return true;
507 } // bool ImageMemory::_SavePngImage(const std::string& filename) const
508
509
510
_SaveJpgImage(const std::string & filename) const511 bool ImageMemory::_SaveJpgImage(const std::string& filename) const {
512 FILE* fp = fopen(filename.c_str(), "wb");
513 if (fp == NULL) {
514 IF_PRINT_WARNING(VIDEO_DEBUG) << "could not open file: " << filename << endl;
515 return false;
516 }
517
518 // we don't support RGBA because JPEGs don't support alpha
519 if (rgb_format == false) {
520 IF_PRINT_WARNING(VIDEO_DEBUG) << "attempting to save non-RGB format pixel data as a RGB format JPG image" << endl;
521 }
522
523 // compression object and error handling
524 jpeg_compress_struct cinfo;
525 jpeg_error_mgr jerr;
526
527 cinfo.err = jpeg_std_error(&jerr);
528 jpeg_create_compress(&cinfo);
529
530 // tell libjpeg how we do things in this town
531 cinfo.in_color_space = JCS_RGB;
532 cinfo.image_width = width;
533 cinfo.image_height = height;
534 cinfo.input_components = 3;
535
536 // everything else can be default
537 jpeg_set_defaults(&cinfo);
538 // tell it where to look
539 jpeg_stdio_dest(&cinfo, fp);
540 // compress it
541 jpeg_start_compress(&cinfo, TRUE);
542
543 JSAMPROW row_pointer; // A pointer to a single row
544 uint32 row_stride = width * 3; // The physical row width in the buffer (RGB)
545
546 // Note that the lines have to be stored from top to bottom
547 while (cinfo.next_scanline < cinfo.image_height) {
548 row_pointer = (uint8*)pixels + cinfo.next_scanline * row_stride;
549 jpeg_write_scanlines(&cinfo, &row_pointer, 1);
550 }
551
552 // compression is DONE, we are HAPPY
553 // now finish it and clean up
554 jpeg_finish_compress(&cinfo);
555 jpeg_destroy_compress(&cinfo);
556
557 fclose(fp);
558 return true;
559 } // bool ImageMemory::_SaveJpgImage(const std::string& file_name) const
560
561 // -----------------------------------------------------------------------------
562 // BaseTexture class
563 // -----------------------------------------------------------------------------
564
BaseTexture()565 BaseTexture::BaseTexture() :
566 texture_sheet(NULL),
567 width(0),
568 height(0),
569 x(0),
570 y(0),
571 u1(0.0f),
572 v1(0.0f),
573 u2(0.0f),
574 v2(0.0f),
575 smooth(false),
576 ref_count(0)
577 {}
578
579
580
BaseTexture(int32 width_,int32 height_)581 BaseTexture::BaseTexture(int32 width_, int32 height_) :
582 texture_sheet(NULL),
583 width(width_),
584 height(height_),
585 x(0),
586 y(0),
587 u1(0.0f),
588 v1(0.0f),
589 u2(0.0f),
590 v2(0.0f),
591 smooth(false),
592 ref_count(0)
593 {}
594
595
596
BaseTexture(TexSheet * texture_sheet_,int32 width_,int32 height_)597 BaseTexture::BaseTexture(TexSheet* texture_sheet_, int32 width_, int32 height_) :
598 texture_sheet(texture_sheet_),
599 width(width_),
600 height(height_),
601 x(0),
602 y(0),
603 u1(0.0f),
604 v1(0.0f),
605 u2(0.0f),
606 v2(0.0f),
607 smooth(false),
608 ref_count(0)
609 {}
610
611
612
~BaseTexture()613 BaseTexture::~BaseTexture() {
614 if (ref_count > 0) {
615 IF_PRINT_WARNING(VIDEO_DEBUG) << "destructor invoked when the object had a reference count greater than zero: " << ref_count << endl;
616 }
617 }
618
619
620
RemoveReference()621 bool BaseTexture::RemoveReference() {
622 ref_count--;
623
624 if (ref_count < 0) {
625 IF_PRINT_WARNING(VIDEO_DEBUG) << "texture ref_count member is now negative: " << ref_count << endl;
626 return true;
627 }
628 else if (ref_count == 0)
629 return true;
630 else
631 return false;
632 }
633
634 // -----------------------------------------------------------------------------
635 // ImageTexture class
636 // -----------------------------------------------------------------------------
637
ImageTexture(const string & filename_,const string & tags_,int32 width_,int32 height_)638 ImageTexture::ImageTexture(const string& filename_, const string& tags_, int32 width_, int32 height_) :
639 BaseTexture(width_, height_),
640 filename(filename_),
641 tags(tags_)
642 {
643 if (VIDEO_DEBUG) {
644 if (TextureManager->_IsImageTextureRegistered(filename + tags))
645 PRINT_WARNING << "constructor invoked when ImageTexture was already referenced for: " << filename << tags << endl;
646 }
647
648 TextureManager->_RegisterImageTexture(this);
649 }
650
651
652
ImageTexture(TexSheet * texture_sheet_,const string & filename_,const string & tags_,int32 width_,int32 height_)653 ImageTexture::ImageTexture(TexSheet* texture_sheet_, const string& filename_, const string& tags_, int32 width_, int32 height_) :
654 BaseTexture(texture_sheet_, width_, height_),
655 filename(filename_),
656 tags(tags_)
657 {
658 if (VIDEO_DEBUG) {
659 if (TextureManager->_IsImageTextureRegistered(filename + tags))
660 PRINT_WARNING << "constructor invoked when ImageTexture was already referenced for: " << filename << tags << endl;
661 }
662
663 TextureManager->_RegisterImageTexture(this);
664 }
665
666
667
~ImageTexture()668 ImageTexture::~ImageTexture() {
669 // Remove this instance from the texture manager
670 TextureManager->_UnregisterImageTexture(this);
671 }
672
673 } // namespace private_video
674
675 } // namespace hoa_video
676