private function conv($str)
    {
        // prefer mb_convert_encoding if available
        if ($this->supportsExtMbstring) {
            return mb_convert_encoding($str, 'UTF-8', 'UTF-16BE');
        }

        // Otherwise convert each byte pair to its Unicode code point and
        // then manually encode as UTF-8 bytes.
        return preg_replace_callback('/(?:[\xD8-\xDB]...)|(?:..)/s', function ($m) {
            if (isset($m[0][3])) {
                // U+10000 - U+10FFFF uses four UTF-16 bytes and 4 UTF-8 bytes
                // get code point from higher and lower surrogate and convert
                list(, $higher, $lower) = unpack('n*', $m[0]);
                $code = (($higher & 0x03FF) << 10) + ($lower & 0x03FF) + 0x10000;

                return pack(
                    'c*',
                    $code >> 18 | 0xF0,
                    $code >> 12 & 0x3F | 0x80,
                    $code >> 6 & 0x3F | 0x80,
                    $code & 0x3F | 0x80
                );
            }

            list(, $code) = unpack('n', $m[0]);
            if ($code < 0x80) {
                // U+0000 - U+007F encodes as single ASCII/UTF-8 byte
                return chr($code);
            } elseif ($code < 0x0800) {
                // U+0080 - U+07FF encodes as two UTF-8 bytes
                return chr($code >> 6 | 0xC0) . chr($code & 0x3F | 0x80);
            } else {
                // U+0800 - U+FFFF encodes as three UTF-8 bytes
                return chr($code >> 12 | 0xE0) . chr($code >> 6 & 0x3F | 0x80) . chr($code & 0x3F | 0x80);
            }
            return '?';
        }, $str);
    }