Browse Source

android: support writing png/jpg/webp via android.graphics.Bitmap

rdb 7 years ago
parent
commit
6bd1976892

+ 2 - 1
makepanda/makepanda.py

@@ -5100,7 +5100,8 @@ if (not RTDIST and not RUNTIME and PkgSkip("PVIEW")==0 and GetTarget() != 'andro
 if (not RUNTIME and GetTarget() == 'android'):
   OPTS=['DIR:panda/src/android']
   TargetAdd('org/panda3d/android/NativeIStream.class', opts=OPTS, input='NativeIStream.java')
-  TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java', dep='org/panda3d/android/NativeIStream.class')
+  TargetAdd('org/panda3d/android/NativeOStream.class', opts=OPTS, input='NativeOStream.java')
+  TargetAdd('org/panda3d/android/PandaActivity.class', opts=OPTS, input='PandaActivity.java')
 
   TargetAdd('p3android_composite1.obj', opts=OPTS, input='p3android_composite1.cxx')
   TargetAdd('libp3android.dll', input='p3android_composite1.obj')

+ 52 - 0
panda/src/android/NativeOStream.java

@@ -0,0 +1,52 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file NativeOStream.java
+ * @author rdb
+ * @date 2018-02-10
+ */
+
+package org.panda3d.android;
+
+import java.io.OutputStream;
+
+/**
+ * An implementation of OutputStream that puts its data into a C++ ostream
+ * pointer, passed as long.
+ */
+public class NativeOStream extends OutputStream {
+    private long streamPtr = 0;
+
+    public NativeOStream(long ptr) {
+        streamPtr = ptr;
+    }
+
+    @Override
+    public void flush() {
+        nativeFlush(streamPtr);
+    }
+
+    @Override
+    public void write(int b) {
+        nativePut(streamPtr, b);
+    }
+
+    @Override
+    public void write(byte[] buffer) {
+        nativeWrite(streamPtr, buffer, 0, buffer.length);
+    }
+
+    @Override
+    public void write(byte[] buffer, int offset, int length) {
+        nativeWrite(streamPtr, buffer, offset, length);
+    }
+
+    private static native void nativeFlush(long ptr);
+    private static native void nativePut(long ptr, int b);
+    private static native void nativeWrite(long ptr, byte[] buffer, int offset, int length);
+}

+ 26 - 0
panda/src/android/PandaActivity.java

@@ -20,12 +20,29 @@ import android.widget.Toast;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
 import org.panda3d.android.NativeIStream;
+import org.panda3d.android.NativeOStream;
 
 /**
  * The entry point for a Panda-based activity.  Loads the Panda libraries and
  * also provides some utility functions.
  */
 public class PandaActivity extends NativeActivity {
+    private static final Bitmap.Config sConfigs[] = {
+            null,
+            Bitmap.Config.ALPHA_8,
+            null,
+            Bitmap.Config.RGB_565,
+            Bitmap.Config.ARGB_4444,
+            Bitmap.Config.ARGB_8888,
+            null, //Bitmap.Config.RGBA_F16,
+            null, //Bitmap.Config.HARDWARE,
+        };
+    private static final Bitmap.CompressFormat sFormats[] = {
+            Bitmap.CompressFormat.JPEG,
+            Bitmap.CompressFormat.PNG,
+            Bitmap.CompressFormat.WEBP,
+        };
+
     protected static BitmapFactory.Options readBitmapSize(long istreamPtr) {
         BitmapFactory.Options options = new BitmapFactory.Options();
         options.inJustDecodeBounds = true;
@@ -44,6 +61,15 @@ public class PandaActivity extends NativeActivity {
         return BitmapFactory.decodeStream(stream, null, options);
     }
 
+    protected static Bitmap createBitmap(int width, int height, int config, boolean hasAlpha) {
+        return Bitmap.createBitmap(width, height, sConfigs[config]);
+    }
+
+    protected static boolean compressBitmap(Bitmap bitmap, int format, int quality, long ostreamPtr) {
+        NativeOStream stream = new NativeOStream(ostreamPtr);
+        return bitmap.compress(sFormats[format], quality, stream);
+    }
+
     protected static String getCurrentThreadName() {
         return Thread.currentThread().getName();
     }

+ 52 - 5
panda/src/android/config_android.cxx

@@ -24,12 +24,24 @@ struct android_app *panda_android_app = NULL;
 jclass    jni_PandaActivity;
 jmethodID jni_PandaActivity_readBitmapSize;
 jmethodID jni_PandaActivity_readBitmap;
+jmethodID jni_PandaActivity_createBitmap;
+jmethodID jni_PandaActivity_compressBitmap;
 jmethodID jni_PandaActivity_showToast;
 
 jclass   jni_BitmapFactory_Options;
 jfieldID jni_BitmapFactory_Options_outWidth;
 jfieldID jni_BitmapFactory_Options_outHeight;
 
+#ifndef HAVE_JPEG
+static PNMFileTypeAndroid file_type_jpeg(PNMFileTypeAndroid::CF_jpeg);
+#endif
+#ifndef HAVE_PNG
+static PNMFileTypeAndroid file_type_png(PNMFileTypeAndroid::CF_png);
+#endif
+#if __ANDROID_API__ >= 14
+static PNMFileTypeAndroid file_type_webp(PNMFileTypeAndroid::CF_webp);
+#endif
+
 /**
  * Initializes the library.  This must be called at least once before any of
  * the functions or classes in this library can be used.  Normally, this is
@@ -37,10 +49,6 @@ jfieldID jni_BitmapFactory_Options_outHeight;
  */
 void
 init_libandroid() {
-  PNMFileTypeRegistry *tr = PNMFileTypeRegistry::get_global_ptr();
-  PNMFileTypeAndroid::init_type();
-  PNMFileTypeAndroid::register_with_read_factory();
-  tr->register_type(new PNMFileTypeAndroid);
 }
 
 /**
@@ -48,7 +56,7 @@ init_libandroid() {
  * references and the method IDs.
  */
 jint JNI_OnLoad(JavaVM *jvm, void *reserved) {
-  init_libandroid();
+  //init_libandroid();
 
   Thread *thread = Thread::get_current_thread();
   JNIEnv *env = thread->get_jni_env();
@@ -63,6 +71,12 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) {
   jni_PandaActivity_readBitmap = env->GetStaticMethodID(jni_PandaActivity,
                    "readBitmap", "(JI)Landroid/graphics/Bitmap;");
 
+  jni_PandaActivity_createBitmap = env->GetStaticMethodID(jni_PandaActivity,
+                   "createBitmap", "(IIIZ)Landroid/graphics/Bitmap;");
+
+  jni_PandaActivity_compressBitmap = env->GetStaticMethodID(jni_PandaActivity,
+                   "compressBitmap", "(Landroid/graphics/Bitmap;IIJ)Z");
+
   jni_PandaActivity_showToast = env->GetMethodID(jni_PandaActivity,
                    "showToast", "(Ljava/lang/String;I)V");
 
@@ -72,6 +86,25 @@ jint JNI_OnLoad(JavaVM *jvm, void *reserved) {
   jni_BitmapFactory_Options_outWidth = env->GetFieldID(jni_BitmapFactory_Options, "outWidth", "I");
   jni_BitmapFactory_Options_outHeight = env->GetFieldID(jni_BitmapFactory_Options, "outHeight", "I");
 
+  nassertr(jni_PandaActivity_readBitmapSize, -1);
+  nassertr(jni_PandaActivity_readBitmap, -1);
+  nassertr(jni_PandaActivity_createBitmap, -1);
+  nassertr(jni_PandaActivity_compressBitmap, -1);
+  nassertr(jni_PandaActivity_showToast, -1);
+
+  // We put this in JNI_OnLoad because it relies on Java classes, which
+  // are only available when launched from the Java VM.
+  PNMFileTypeRegistry *tr = PNMFileTypeRegistry::get_global_ptr();
+#ifndef HAVE_JPEG
+  tr->register_type(&file_type_jpeg);
+#endif
+#ifndef HAVE_PNG
+  tr->register_type(&file_type_png);
+#endif
+#if __ANDROID_API__ >= 14
+  tr->register_type(&file_type_webp);
+#endif
+
   return JNI_VERSION_1_4;
 }
 
@@ -86,6 +119,20 @@ void JNI_OnUnload(JavaVM *jvm, void *reserved) {
 
   env->DeleteGlobalRef(jni_PandaActivity);
   env->DeleteGlobalRef(jni_BitmapFactory_Options);
+
+  // These will no longer work without JNI, so unregister them.
+  PNMFileTypeRegistry *tr = PNMFileTypeRegistry::get_global_ptr();
+  if (tr != nullptr) {
+#ifndef HAVE_JPEG
+    tr->unregister_type(&file_type_jpeg);
+#endif
+#ifndef HAVE_PNG
+    tr->unregister_type(&file_type_png);
+#endif
+#if __ANDROID_API__ >= 14
+    tr->unregister_type(&file_type_webp);
+#endif
+  }
 }
 
 /**

+ 2 - 0
panda/src/android/config_android.h

@@ -31,6 +31,8 @@ extern EXPORT_CLASS struct android_app* panda_android_app;
 extern jclass    jni_PandaActivity;
 extern jmethodID jni_PandaActivity_readBitmapHeader;
 extern jmethodID jni_PandaActivity_readBitmap;
+extern jmethodID jni_PandaActivity_createBitmap;
+extern jmethodID jni_PandaActivity_compressBitmap;
 extern jmethodID jni_PandaActivity_showToast;
 
 extern jclass   jni_BitmapFactory_Options;

+ 54 - 0
panda/src/android/jni_NativeOStream.cxx

@@ -0,0 +1,54 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file jni_NativeOStream.cxx
+ * @author rdb
+ * @date 2018-02-10
+ */
+
+#include <jni.h>
+
+#include <ostream>
+
+#if __GNUC__ >= 4
+#define EXPORT_JNI extern "C" __attribute__((visibility("default")))
+#else
+#define EXPORT_JNI extern "C"
+#endif
+
+/**
+ * Flushes the stream.
+ */
+EXPORT_JNI void
+Java_org_panda3d_android_NativeOStream_nativeFlush(JNIEnv *env, jclass clazz, jlong ptr) {
+  std::ostream *stream = (std::ostream *)ptr;
+
+  stream->flush();
+}
+
+/**
+ * Writes a single character to the ostream.
+ */
+EXPORT_JNI void
+Java_org_panda3d_android_NativeOStream_nativePut(JNIEnv *env, jclass clazz, jlong ptr, int b) {
+  std::ostream *stream = (std::ostream *)ptr;
+
+  stream->put((char)(b & 0xff));
+}
+
+/**
+ * Writes an array of bytes to the ostream.
+ */
+EXPORT_JNI void
+Java_org_panda3d_android_NativeOStream_nativeWrite(JNIEnv *env, jclass clazz, jlong ptr, jbyteArray byte_array, jint offset, jint length) {
+  std::ostream *stream = (std::ostream *)ptr;
+
+  jbyte *buffer = (jbyte *)alloca(length);
+  env->GetByteArrayRegion(byte_array, offset, length, buffer);
+  stream->write((char *)buffer, length);
+}

+ 3 - 1
panda/src/android/p3android_composite1.cxx

@@ -1,4 +1,6 @@
 #include "config_android.cxx"
 #include "jni_NativeIStream.cxx"
+#include "jni_NativeOStream.cxx"
 #include "pnmFileTypeAndroid.cxx"
-#include "pnmFileTypeAndroidReader.cxx"
+#include "pnmFileTypeAndroidReader.cxx"
+#include "pnmFileTypeAndroidWriter.cxx"

+ 28 - 36
panda/src/android/pnmFileTypeAndroid.cxx

@@ -17,24 +17,11 @@
 
 #include "config_pnmimagetypes.h"
 
-#include "pnmFileTypeRegistry.h"
-#include "bamReader.h"
-
-static const char * const extensions_android[] = {
-  "jpg", "jpeg", "gif", "png",
-#if __ANDROID_API__ >= 14
-  "webp"
-#endif
-};
-static const int num_extensions_android = sizeof(extensions_android) / sizeof(const char *);
-
-TypeHandle PNMFileTypeAndroid::_type_handle;
-
 /**
  *
  */
 PNMFileTypeAndroid::
-PNMFileTypeAndroid() {
+PNMFileTypeAndroid(CompressFormat format) : _format(format) {
 }
 
 /**
@@ -51,7 +38,16 @@ get_name() const {
  */
 int PNMFileTypeAndroid::
 get_num_extensions() const {
-  return num_extensions_android;
+  switch (_format) {
+  case CF_jpeg:
+    return 3;
+  case CF_png:
+    return 1;
+  case CF_webp:
+    return 1;
+  default:
+    return 0;
+  }
 }
 
 /**
@@ -60,8 +56,17 @@ get_num_extensions() const {
  */
 string PNMFileTypeAndroid::
 get_extension(int n) const {
-  nassertr(n >= 0 && n < num_extensions_android, string());
-  return extensions_android[n];
+  static const char *const jpeg_extensions[] = {"jpg", "jpeg", "jpe"};
+  switch (_format) {
+  case CF_jpeg:
+    return jpeg_extensions[n];
+  case CF_png:
+    return "png";
+  case CF_webp:
+    return "webp";
+  default:
+    return 0;
+  }
 }
 
 /**
@@ -80,30 +85,17 @@ has_magic_number() const {
  */
 PNMReader *PNMFileTypeAndroid::
 make_reader(istream *file, bool owns_file, const string &magic_number) {
-  init_pnm();
   return new Reader(this, file, owns_file, magic_number);
 }
 
 /**
- * Registers the current object as something that can be read from a Bam file.
- */
-void PNMFileTypeAndroid::
-register_with_read_factory() {
-  BamReader::get_factory()->
-    register_factory(get_class_type(), make_PNMFileTypeAndroid);
-}
-
-/**
- * This method is called by the BamReader when an object of this type is
- * encountered in a Bam file; it should allocate and return a new object with
- * all the data read.
- *
- * In the case of the PNMFileType objects, since these objects are all shared,
- * we just pull the object from the registry.
+ * Allocates and returns a new PNMWriter suitable for reading from this file
+ * type, if possible.  If writing files of this type is not supported, returns
+ * NULL.
  */
-TypedWritable *PNMFileTypeAndroid::
-make_PNMFileTypeAndroid(const FactoryParams &params) {
-  return PNMFileTypeRegistry::get_global_ptr()->get_type_by_handle(get_class_type());
+PNMWriter *PNMFileTypeAndroid::
+make_writer(ostream *file, bool owns_file) {
+  return new Writer(this, file, owns_file, _format);
 }
 
 #endif  // ANDROID

+ 18 - 20
panda/src/android/pnmFileTypeAndroid.h

@@ -30,7 +30,13 @@
  */
 class EXPCL_PANDA_PNMIMAGETYPES PNMFileTypeAndroid : public PNMFileType {
 public:
-  PNMFileTypeAndroid();
+  enum CompressFormat : jint {
+    CF_jpeg = 0,
+    CF_png = 1,
+    CF_webp = 2,
+  };
+
+  PNMFileTypeAndroid(CompressFormat format);
 
   virtual string get_name() const;
 
@@ -41,6 +47,7 @@ public:
 
   virtual PNMReader *make_reader(istream *file, bool owns_file = true,
                                  const string &magic_number = string());
+  virtual PNMWriter *make_writer(ostream *file, bool owns_file = true);
 
 public:
   class Reader : public PNMReader {
@@ -60,29 +67,20 @@ public:
     int32_t _format;
   };
 
-  // The TypedWritable interface follows.
-public:
-  static void register_with_read_factory();
+  class Writer : public PNMWriter {
+  public:
+    Writer(PNMFileType *type, ostream *file, bool owns_file,
+           CompressFormat format);
 
-protected:
-  static TypedWritable *make_PNMFileTypeAndroid(const FactoryParams &params);
+    virtual int write_data(xel *array, xelval *alpha);
+    virtual bool supports_grayscale() const;
 
-public:
-  static TypeHandle get_class_type() {
-    return _type_handle;
-  }
-  static void init_type() {
-    PNMFileType::init_type();
-    register_type(_type_handle, "PNMFileTypeAndroid",
-                  PNMFileType::get_class_type());
-  }
-  virtual TypeHandle get_type() const {
-    return get_class_type();
-  }
-  virtual TypeHandle force_init_type() {init_type(); return get_class_type();}
+  private:
+    CompressFormat _format;
+  };
 
 private:
-  static TypeHandle _type_handle;
+  CompressFormat _format;
 };
 
 #endif  // ANDROID

+ 146 - 0
panda/src/android/pnmFileTypeAndroidWriter.cxx

@@ -0,0 +1,146 @@
+/**
+ * PANDA 3D SOFTWARE
+ * Copyright (c) Carnegie Mellon University.  All rights reserved.
+ *
+ * All use of this software is subject to the terms of the revised BSD
+ * license.  You should have received a copy of this license along
+ * with this source code in a file named "LICENSE."
+ *
+ * @file pnmFileTypeAndroidWriter.cxx
+ * @author rdb
+ * @date 2018-02-10
+ */
+
+#include "pnmFileTypeAndroid.h"
+
+#ifdef ANDROID
+
+#include "config_pnmimagetypes.h"
+
+#include <android/bitmap.h>
+#include <jni.h>
+
+// See android/graphics/Bitmap.java
+enum class BitmapConfig : jint {
+  ALPHA_8 = 1,
+  RGB_565 = 3,
+  ARGB_4444 = 4,
+  ARGB_8888 = 5,
+  RGBA_F16 = 6,
+  HARDWARE = 7,
+};
+
+/**
+ *
+ */
+PNMFileTypeAndroid::Writer::
+Writer(PNMFileType *type, ostream *file, bool owns_file,
+       CompressFormat format) :
+  PNMWriter(type, file, owns_file),
+  _format(format)
+{
+}
+
+/**
+ * Writes out an entire image all at once, including the header, based on the
+ * image data stored in the given _x_size * _y_size array and alpha pointers.
+ * (If the image type has no alpha channel, alpha is ignored.) Returns the
+ * number of rows correctly written.
+ *
+ * It is the user's responsibility to fill in the header data via calls to
+ * set_x_size(), set_num_channels(), etc., or copy_header_from(), before
+ * calling write_data().
+ *
+ * It is important to delete the PNMWriter class after successfully writing
+ * the data.  Failing to do this may result in some data not getting flushed!
+ *
+ * Derived classes need not override this if they instead provide
+ * supports_streaming() and write_row(), below.
+ */
+int PNMFileTypeAndroid::Writer::
+write_data(xel *array, xelval *alpha) {
+  size_t num_pixels = (size_t)_x_size * (size_t)_y_size;
+
+  Thread *current_thread = Thread::get_current_thread();
+  JNIEnv *env = current_thread->get_jni_env();
+  nassertr(env != nullptr, 0);
+
+  // Create a Bitmap object.
+  jobject bitmap =
+    env->CallStaticObjectMethod(jni_PandaActivity,
+                                jni_PandaActivity_createBitmap,
+                                (jint)_x_size, (jint)_y_size,
+                                BitmapConfig::ARGB_8888,
+                                (jboolean)has_alpha());
+  nassertr(bitmap != nullptr, 0);
+
+  // Get a writable pointer to write our pixel data to.
+  uint32_t *out;
+  int rc = AndroidBitmap_lockPixels(env, bitmap, (void **)&out);
+  if (rc != 0) {
+    android_cat.error()
+      << "Could not lock bitmap pixels (result code " << rc << ")\n";
+    return 0;
+  }
+
+  if (_maxval == 255) {
+    if (has_alpha() && alpha != nullptr) {
+      for (size_t i = 0; i < num_pixels; ++i) {
+        out[i] = (array[i].r)
+               | (array[i].g << 8u)
+               | (array[i].b << 16u)
+               | (alpha[i] << 24u);
+      }
+    } else {
+      for (size_t i = 0; i < num_pixels; ++i) {
+        out[i] = (array[i].r)
+               | (array[i].g << 8u)
+               | (array[i].b << 16u)
+               | 0xff000000u;
+      }
+    }
+  } else {
+    double ratio = 255.0 / _maxval;
+    if (has_alpha() && alpha != nullptr) {
+      for (size_t i = 0; i < num_pixels; ++i) {
+        out[i] = ((uint32_t)(array[i].r * ratio))
+               | ((uint32_t)(array[i].g * ratio) << 8u)
+               | ((uint32_t)(array[i].b * ratio) << 16u)
+               | ((uint32_t)(alpha[i] * ratio) << 24u);
+      }
+    } else {
+      for (size_t i = 0; i < num_pixels; ++i) {
+        out[i] = ((uint32_t)(array[i].r * ratio))
+               | ((uint32_t)(array[i].g * ratio) << 8u)
+               | ((uint32_t)(array[i].b * ratio) << 16u)
+               | 0xff000000u;
+      }
+    }
+  }
+
+  // Finally, unlock the pixel data and compress it to the ostream.
+  AndroidBitmap_unlockPixels(env, bitmap);
+  jboolean res =
+    env->CallStaticBooleanMethod(jni_PandaActivity,
+                                 jni_PandaActivity_compressBitmap,
+                                 bitmap, _format, 85, (jlong)_file);
+  if (!res) {
+    android_cat.error()
+      << "Failed to compress bitmap.\n";
+    return 0;
+  }
+  return _y_size;
+}
+
+/**
+ * Returns true if this particular PNMWriter understands grayscale images.  If
+ * this is false, then the rgb values of the xel array will be pre-filled with
+ * the same value across all three channels, to allow the writer to simply
+ * write out RGB data for a grayscale image.
+ */
+bool PNMFileTypeAndroid::Writer::
+supports_grayscale() const {
+  return false;
+}
+
+#endif  // ANDROID

+ 1 - 0
panda/src/android/pview_manifest.xml

@@ -6,6 +6,7 @@
         android:versionName="1.0">
 
     <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
     <uses-sdk android:minSdkVersion="21" />
     <uses-feature android:glEsVersion="0x00020000" android:required="true" />
 

+ 41 - 7
panda/src/pnmimage/pnmFileTypeRegistry.cxx

@@ -49,16 +49,19 @@ register_type(PNMFileType *type) {
   }
 
   // Make sure we haven't already registered this type.
-  Handles::iterator hi = _handles.find(type->get_type());
-  if (hi != _handles.end()) {
-    pnmimage_cat->warning()
-      << "Attempt to register PNMFileType " << type->get_name()
-      << " (" << type->get_type() << ") more than once.\n";
-    return;
+  TypeHandle handle = type->get_type();
+  if (handle != PNMFileType::get_class_type()) {
+    Handles::iterator hi = _handles.find(handle);
+    if (hi != _handles.end()) {
+      pnmimage_cat->warning()
+        << "Attempt to register PNMFileType " << type->get_name()
+       << " (" << type->get_type() << ") more than once.\n";
+      return;
+    }
+    _handles.insert(Handles::value_type(handle, type));
   }
 
   _types.push_back(type);
-  _handles.insert(Handles::value_type(type->get_type(), type));
 
   // Collect the unique extensions associated with the type.
   pset<string> unique_extensions;
@@ -82,6 +85,37 @@ register_type(PNMFileType *type) {
   _requires_sort = true;
 }
 
+/**
+ * Removes a PNMFileType previously passed to register_type.
+ */
+void PNMFileTypeRegistry::
+unregister_type(PNMFileType *type) {
+  if (pnmimage_cat->is_debug()) {
+    pnmimage_cat->debug()
+      << "Unregistering image type " << type->get_name() << "\n";
+  }
+
+  TypeHandle handle = type->get_type();
+  if (handle != PNMFileType::get_class_type()) {
+    Handles::iterator hi = _handles.find(handle);
+    if (hi != _handles.end()) {
+      _handles.erase(hi);
+    }
+  }
+
+  _types.erase(std::remove(_types.begin(), _types.end(), type),
+               _types.end());
+
+  Extensions::iterator ei;
+  for (ei = _extensions.begin(); ei != _extensions.end(); ++ei) {
+    Types &types = ei->second;
+    types.erase(std::remove(types.begin(), types.end(), type),
+                types.end());
+  }
+
+  _requires_sort = true;
+}
+
 /**
  * Returns the total number of types registered.
  */

+ 1 - 0
panda/src/pnmimage/pnmFileTypeRegistry.h

@@ -33,6 +33,7 @@ public:
   ~PNMFileTypeRegistry();
 
   void register_type(PNMFileType *type);
+  void unregister_type(PNMFileType *type);
 
 PUBLISHED:
   int get_num_types() const;