C#

Quoted-Printable

バイト配列をQuoted-Printable形式のASCIIテキストに変換

/// <summary>
/// バイト配列をQuoted-Printable形式のASCIIテキストに変換します。
/// </summary>
/// <param name="inArray">バイト配列を指定します。</param>
/// <param name="offset">inArray のオフセットを指定します。</param>
/// <param name="length">変換する inArray の要素の数を指定します。。</param>
/// <returns></returns>
/// <remarks>Copyright (C) 2021, Kaigo, hesperus.net</remarks>
public static string ToQuotedPrintableString(byte[] inArray, int offset, int length)
{
    if (inArray == null)
    {
        return null;
    }

    // 16進数の表現に使用する文字
    const string HexdecimalNumbers = "0123456789ABCDEF";

    const ulong CrLfMask = 0xffff;
    const ulong CrLfCode = 0x0d0a;


    StringBuilder Builder = new StringBuilder();
    ulong ByteSeq = 0;
    int LineLength = 0;


    // 行が長くなるときに、soft line breakを追加
    Action<int> SoftLineBreakIfNeeded = (int CharCount) =>
    {
        if (LineLength + CharCount > 75)
        {
            Builder.Append("=\r\n");
            LineLength = 0;
        }
    };

    // バイト値をQuoted-Printableにして追加
    Action<byte> AppendAsQuoted = (byte Value) =>
    {
        SoftLineBreakIfNeeded(3);

        Builder.Append('=');
        Builder.Append(HexdecimalNumbers[Value >> 4]);
        Builder.Append(HexdecimalNumbers[Value & 0x0f]);
        LineLength += 3;
    };

    // hard line breakを追加する前の調整
    Action PreHardLineBreak = () =>
    {
        // 行末のTABやSPACEをQuoted-Printableに変換
        switch (ByteSeq & 0xff0000)
        {
            case 0x090000:
            case 0x200000:
                Builder.Length--;
                LineLength--;
                AppendAsQuoted((byte)((ByteSeq >> 16) & 0xff));
                return;
        }

        // この行の長さが0または1で、前の行がhard line breakされていないなら、
        // つまりsoft line breakされているなら、soft line breakを取り除いて、
        // この行を前の行の末尾に送ることができる。

        if (LineLength < 2)
        {
            int _Count = LineLength + 2;

            if (Builder.Length > _Count)
            {
                if (((ByteSeq >> (_Count << 3)) & CrLfMask) != CrLfCode)
                {
                    Builder.Remove(Builder.Length - 1 - _Count, 3);
                }
            }
        }
    };


    int Index = offset;
    int Limit = offset + length;

    while (Index < Limit)
    {
        byte CurByte = inArray[Index++];
        ByteSeq <<= 8;
        ByteSeq |= CurByte;

        if ((ByteSeq & CrLfMask) == CrLfCode)
        {
            // "\r" は "=0D" に変換されているはずなので、末尾の3字を削除
            Builder.Length -= 3;
            LineLength -= 3;

            PreHardLineBreak();

            Builder.Append("\r\n");
            LineLength = 0;
        }
        else if ((CurByte == 0x09) ||
            ((CurByte >= 0x20) && (CurByte <= 0x3c)) ||
            ((CurByte >= 0x3e) && (CurByte <= 0x7e)))
        {
            SoftLineBreakIfNeeded(1);

            Builder.Append((char)CurByte);
            LineLength++;
        }
        else
        {
            AppendAsQuoted(CurByte);
        }
    }

    ByteSeq <<= 16;
    PreHardLineBreak();

    return Builder.ToString();
}


/// <summary>
/// バイト配列をQuoted-Printable形式のASCIIテキストに変換します。
/// </summary>
/// <param name="inArray">バイト配列を指定します。</param>
/// <returns></returns>
/// <remarks>Copyright (C) 2021, Kaigo, hesperus.net</remarks>
public static string ToQuotedPrintableString(byte[] inArray)
{
    return ToQuotedPrintableString(inArray, 0, inArray.Length);
}

Quoted-Printable形式のASCIIテキストをバイト配列に変換

/// <summary>
/// Quoted-Printable形式のASCIIテキストをバイト配列に変換します。
/// </summary>
/// <param name="s">Quoted-Printable形式のASCIIテキストを指定します。</param>
/// <returns></returns>
/// <remarks>Copyright (C) 2021, Kaigo, hesperus.net</remarks>
public static byte[] FromQuotedPrintableString(string s)
{
    if (s == null)
    {
        return null;
    }

    // 行末にあるTABやSPACEはパディングなので削除
    s = Regex.Replace(s, "[\t ]+(\r?)$", "$1", RegexOptions.Multiline);

    // soft line breakを削除
    s = s.Replace("=\r\n", string.Empty);

    using (MemoryStream TempStream = new MemoryStream())
    {
        // 16進数の表現に使用する文字
        const string HexdecimalNumbers = "0123456789ABCDEF";

        int Index = 0;

        Action<int> TransferTo = (int Limit) =>
        {
            while (Index < Limit)
            {
                TempStream.WriteByte((byte)(s[Index++]));
            }
        };

        Match m = Regex.Match(s, "=[" + HexdecimalNumbers + "]{2}");

        while (m.Success)
        {
            TransferTo(m.Index);

            int HighNibble = HexdecimalNumbers.IndexOf(m.Value[1]);
            int LowNibble = HexdecimalNumbers.IndexOf(m.Value[2]);
            TempStream.WriteByte((byte)((HighNibble << 4) | LowNibble));

            Index += m.Length;

            m = m.NextMatch();
        }

        TransferTo(s.Length);

        return TempStream.ToArray();
    }
}

Base64はConvert.ToBase64String()とConvert.FromBase64String()で変換できるが、Quoted-Printableは .Net Framework のライブラリにないようだ。電子メールを読み取るときに、Quoted-Printableを読み取る機能が必要になった。

Convert.ToBase64String()とConvert.FromBase64String()の仕様に似せた。

Quoted-Printableのルールは、RFC 2045の6.7章に記載されている。

(2021/03/05 初稿)
(2021/03/07 更新)