diff --git a/makepanda/makepanda.py b/makepanda/makepanda.py index 69c8c93a37..ad974d38d0 100755 --- a/makepanda/makepanda.py +++ b/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') diff --git a/panda/src/android/NativeOStream.java b/panda/src/android/NativeOStream.java new file mode 100644 index 0000000000..ddd76e485a --- /dev/null +++ b/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); +} diff --git a/panda/src/android/PandaActivity.java b/panda/src/android/PandaActivity.java index c9985986f6..a4413a2816 100644 --- a/panda/src/android/PandaActivity.java +++ b/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(); } diff --git a/panda/src/android/config_android.cxx b/panda/src/android/config_android.cxx index 019443be4b..afbaf243ff 100644 --- a/panda/src/android/config_android.cxx +++ b/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 + } } /** diff --git a/panda/src/android/config_android.h b/panda/src/android/config_android.h index 74599ec3d9..2224dbd065 100644 --- a/panda/src/android/config_android.h +++ b/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; diff --git a/panda/src/android/jni_NativeOStream.cxx b/panda/src/android/jni_NativeOStream.cxx new file mode 100644 index 0000000000..c2779d0174 --- /dev/null +++ b/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 + +#include + +#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); +} diff --git a/panda/src/android/p3android_composite1.cxx b/panda/src/android/p3android_composite1.cxx index 14b48ab99f..ea14d2b311 100644 --- a/panda/src/android/p3android_composite1.cxx +++ b/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" \ No newline at end of file +#include "pnmFileTypeAndroidReader.cxx" +#include "pnmFileTypeAndroidWriter.cxx" diff --git a/panda/src/android/pnmFileTypeAndroid.cxx b/panda/src/android/pnmFileTypeAndroid.cxx index c661c6b3b0..aa25b2c777 100644 --- a/panda/src/android/pnmFileTypeAndroid.cxx +++ b/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. + * 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. */ -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. - */ -TypedWritable *PNMFileTypeAndroid:: -make_PNMFileTypeAndroid(const FactoryParams ¶ms) { - 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 diff --git a/panda/src/android/pnmFileTypeAndroid.h b/panda/src/android/pnmFileTypeAndroid.h index cc49fd8f4f..f27cc4f23d 100644 --- a/panda/src/android/pnmFileTypeAndroid.h +++ b/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 ¶ms); + 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 diff --git a/panda/src/android/pnmFileTypeAndroidWriter.cxx b/panda/src/android/pnmFileTypeAndroidWriter.cxx new file mode 100644 index 0000000000..4677a25cbd --- /dev/null +++ b/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 +#include + +// 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 diff --git a/panda/src/android/pview_manifest.xml b/panda/src/android/pview_manifest.xml index 6b14579a80..30125c673f 100644 --- a/panda/src/android/pview_manifest.xml +++ b/panda/src/android/pview_manifest.xml @@ -6,6 +6,7 @@ android:versionName="1.0"> + diff --git a/panda/src/pnmimage/pnmFileTypeRegistry.cxx b/panda/src/pnmimage/pnmFileTypeRegistry.cxx index 261f311f11..96e24ef1bb 100644 --- a/panda/src/pnmimage/pnmFileTypeRegistry.cxx +++ b/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 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. */ diff --git a/panda/src/pnmimage/pnmFileTypeRegistry.h b/panda/src/pnmimage/pnmFileTypeRegistry.h index aec9327d49..9c8702a961 100644 --- a/panda/src/pnmimage/pnmFileTypeRegistry.h +++ b/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;