Quoted Printable とは

QuotedPrintable(QP encoding) とはエンコード方式の1種です。Base64 と同じように、データを印字可能なデータ(Ascii文字列)にエンコードします。メールのエンコード等で使われています。

QuotedPrintableは、任意の1バイトを "=" の後ろにそのバイトを表す16進数2桁 =FF の形にエンコードします。

特徴

QuotedPrintableは、Base64と同じように印字可能な文字(Ascii文字)にエンコードしますが、Base64と違い印字可能なAscii文字はそのままにします。また、印字可能なAscii文字についてはこの変換を行わないため、元データがAscii文字を多く含む場合、エンコードされた文字列をほぼそのまま読むことができ、エンコード後のデータサイズも小さくなります。。

一方、元データにAscii文字をあまり含まないデータについては、エンコード後の文字列はほぼ意味をなさない文字列になることに加え、データサイズも非常に大きくなってしまいます。

これは、Base64が一律のデータ効率(約33%増)となる点と比べ、大きく異なる点です。

アルゴリズムと仕様

www.ietf.org/rfc/rfc2045.txt

QuotedEncodingは、メールのエンコーディングに使われるため Content-Transfer-Encoding(MIME) として定義されています。上記URLはその定義文書です。

上記文書を参考にエンコードのルール以下に記します。

  1. 任意の1バイト(8ビット)は、"=" の後に続く2桁の16進数で表します。16進数は大文字を使います。
    • 例: =3D
  2. 0x21("!")から0x3C("<")まで、および 0x3E(">")から 0x7E("~") は符号化せずそのままにする。
    • 0x3D("=") は符号化します。
  3. 0x09(タブ文字) と 0x20(スペース) は符号化しません。ただし改行直前に来る場合は符号化します。
  4. 改行は CRLF を使います。テキストの場合、CR(0x0D)とLF(0x0A)は符号化しません。
  5. エンコードされたテキストは1行あたり76文字以下でないといけません。76文字を超える行はソフト改行(=CRLF)を入れます。
    • ソフト改行とは元データにとって無意味な改行、つまりエンコードの仕様上挟まれる改行のことです。
    • =と改行(CRLF)の間のスペースとタブはデコード時に無視されます。
    • 改行直前の意味のあるタブとスペースは 3. にある通り、符号化しておく必要があります。

サンプルコード

C#

public string Encode(string text, Encoding encoding)
{
    var sb = new StringBuilder();

    // 対象文字列のバイトデータ取得
    var bytes = encoding.GetBytes(text);

    // 1行当たりの文字数カウンタ
    var characterCount = 0;

    for (int i = 0; i < bytes.Length; i++)
    {
        var octet = bytes[i];
        // =00 の形に変換するかどうか
        var isConverting = false;

        switch (octet)
        {
            case 0x09: // タブ
            case 0x20: // スペース
                // 次に改行(CRLF)が続く場合は変換
                // それ以外はそのまま出力
                isConverting = i < bytes.Length - 2
                    && bytes[i + 1] == 0x0D
                    && bytes[i + 2] == 0x0A;
                break;
            case 0x0A: // LF
            case 0x0D: // CR
                // 変換せずそのまま出力
                isConverting = false;
                break;
            default:
                // 次の範囲の文字は変換して出力
                // 31   0x1f  US(ユニット区切り)以前の制御文字
                // 127    0x7f  DEL(削除) 以降の文字
                // 61    0x3d  = 
                isConverting = octet <= 0x20 || 0x7f <= octet || octet == 0x3d;
                break;
        }

        // 出力文字列に追加
        if (isConverting)
        {
            sb.Append($"={octet.ToString("X2")}");
            characterCount += 3;
        }
        else
        {
            sb.Append((char)octet);
            characterCount += 1;
        }

        // エンコードされた行の長さが76文字以下になるようにする
        // 次の出力で "=00" と3文字出力されて77文字以上になるときはソフト改行
        if (characterCount > 73)
        {
            sb.Append("=\r\n"); // =CRLF
        }

        // 改行されていれば文字数カウンタリセット
        if (sb.ToString().EndsWith("\r\n"))
        {
            characterCount = 0; // カウンタリセット
        }
    }

    return sb.ToString();
}

public string Decode(string text, Encoding encoding)
{
    var bytes = new List<Byte>();

    // ソフト改行をすべて削除
    var reg = new Regex("=[ \t]*\r\n");
    text = reg.Replace(text, string.Empty);

    // 次に読む文字位置
    var index = 0;

    while (index < text.Length)
    {
        var c = text[index];

        if (c == '=')
        {
            // = が出たら次の2文字を合わせて読む
            // 範囲外参照になる場合は不正なQP
            var octetStr = text.Substring(index + 1, 2);
            // 16進数文字列としてByte型に変換
            var octet = Convert.ToByte(octetStr, 16);
            bytes.Add(octet);
            // 詠み込んだ2文字すすめる
            index += 2;
        }
        else
        {
            // = で始まらない文字はそのまま出力
            // Ascii文字なのでByte型にキャスト
            bytes.Add((byte)c);
        }

        index++;
    }

    var result = encoding.GetString(bytes.ToArray());
    return result;
}