PDFの文字列オブジェクトの詳細仕様をAdobeReaderDCで確認してみた

# PDFファイル内部のリテラル文字列表現について

今回の対象は、丸括弧でくくられた文字列オブジェクトです。
[こちら](https://qiita.com/RT-EGG/items/18705b590c75c1bf15ca#string-objects)の解説が分かりやすいと思います。
~~なので、下記では説明をさぼっています。~~

# 確認環境

ヘッダに`%PDF-1.4`を指定して、Adobe Reader DC (ver 2019.021.20056)で動作を確認。

PDFの作成は
https://qiita.com/kob58im/items/8474bfd37a5bd464172c
をもとに実施。

# 確認したこと：エスケープシーケンス

`\075`のようにエスケープ文字`\`と直後に続く1～3桁の8進数で、任意の1byteを表現できる仕様がある。
[公式](https://www.google.co.jp/url?sa=t&rct=j&q=&esrc=s&source=web&cd=1&cad=rja&uact=8&ved=2ahUKEwicxcnatO3lAhX3xosBHXQxBWAQFjAAegQIBBAC&url=https%3A%2F%2Fwww.adobe.com%2Fcontent%2Fdam%2Facom%2Fen%2Fdevnet%2Fpdf%2Fpdfs%2FPDF32000_2008.pdf&usg=AOvVaw1-RSI9319qZLGt7YE-l-gc)の7.3.4.2章の例では、`(\53)`の`\53`は`\053`とみなされ、`+`になるとあるが、直後が終端以外だとどうなるかが明記されていないように見える。[^1]

## 試した結果：

PDFファイル上の文字列：`(Test\075Test\75Test\758Test\75)`

表示された文字列：`Test=Test=Test=8Test=`

結論：3文字以内に0～7以外が来たら、その時点で8進数として解釈するようである。[^2]
なお、8bitを超えるbitは無視するとの仕様記載があり、`\505`は`\105`とみなされる。

# リテラル文字列を解析するサンプルソース

こんな感じでよいはず。

※注意：作成中のツールのコードから切り出してきたのでコンパイル確認はしていません。

```csharp

public static byte[] ReadString(Stream st)
{
    byte[] ret = new byte[0];

    int nCount = 0;// Count of open '('
    bool escape = false;
    
    int b = st.ReadByte();
    if ( b != '(' ) {
        if ( b >= 0 ) { // EOFでなければ1文字戻す
            st.Position--;
        }
        return null;
    }

    nCount++;
    while ( nCount > 0 ) {
        b = st.ReadByte();
        if ( b < 0 ) { // EOF
            return null;
        }
        if (!escape) {
            switch(b) {
                case '(':
                    nCount++;
                    ArrayAppend(ref ret, (byte)b);
                    break;
                case ')':
                    nCount--;
                    if ( nCount > 0 ) { ArrayAppend(ref ret, (byte)b); }
                    break;
                case '\\':
                    escape = true;
                    break;
                default:
                    ArrayAppend(ref ret,(byte)b);
                    break;
            }
        }
        else {
            escape = false;
            
            if ('0'<=b && b<='7') {
                // \000 - \777 (8bitの最大値は\377だが、overflowを無視することとあるので\777を受け取ることとする)
                // (\53a) の扱いが明確には定義されていないが、(\53)を\053と扱えとあるので、(\53a)は"+a"とみなすこととする
                int b2 = st.ReadByte();
                if ( b2 < '0' || '7' < b2 ) { // out of range
                    // 1byte戻して、とれたとこまでを解釈して次の文字に移る
                    st.Position--;
                    ArrayAppend(ref ret, OctNum(0,0,b));
                    continue;
                }
                int b3 = st.ReadByte();
                if ( b3 < '0' || '7' < b3) {
                    // 1byte戻して、とれたとこまでを解釈して次の文字に移る
                    st.Position--;
                    ArrayAppend(ref ret, OctNum(0,b,b2));
                    continue;
                }
                ArrayAppend(ref ret, OctNum(b,b2,b3));
            }
            else {
                switch(b) {
                    case 't': ArrayAppend(ref ret, (byte)'\t'); break;
                    case 'r': ArrayAppend(ref ret, (byte)'\r'); break;
                    case 'n': ArrayAppend(ref ret, (byte)'\n'); break;
                    case 'b': ArrayAppend(ref ret, (byte)'\b'); break;
                    case 'f': ArrayAppend(ref ret, (byte)'\f'); break;
                    case '\r': // '\r'を読み捨てる
                        {
                            int b2 = st.ReadByte(); // "\r\n"を読み捨てる
                            if(b2 != '\n'){ st.Position--; } // '\r'だけだったら1byte戻す
                        }
                        break;
                    case '\n': break; // '\n'を読み捨てる
                    default:  ArrayAppend(ref ret, (byte)b);    break;
                }
            }
        }
    }
    return ret;
}

static byte OctNum(int b, int b2, int b3)
{
    return (byte)(((b-'0')<<6) | ((b2-'0')<<3) | (b3-'0') );
}

static void ArrayAppend<T>(ref T[] a, T newItem)
{
    int n = a.Length;
    Array.Resize(ref a, n+1);
    a[n] = newItem;
} 
```

# おまけ：16進文字列

`<`と`>`で16進文字列をくくると、同様に文字列オブジェクトとして扱われる。

PDFファイル上の文字列：`<35353 53 5 353\n 5353\t5355>`に対し、
表示された文字列：`555555555P`となった。
（35 35 35 35 35 35 35 35 35 50 と解釈されている。）
間に空白文字があっても<b>2桁ずつ取るわけではない</b>ので注意が必要。

また、1文字だけでも成立するようである。

PDFファイル上の文字列：`<A>`  ~~(0x0A = Lf)~~ (下位に0が補填されるので0xA0)
表示された文字列：`□` (表現できない文字として四角形が描画された。)

0文字のケースについて
`<>`に対しては当然ながら表示はされず、Adobe Reader DC上、開いてズームイン/アウトなどの操作をしてもエラーとはならなかったため、おそらく有効なデータ（空文字列）として扱われていると思う。

# 16進文字列を解析するサンプルソース

```csharp

public static byte[] ReadHexString(Stream st)
{
    byte[] ret = new byte[0];

    int b = st.ReadByte();
    if ( b != '<' ) {
        if ( b >= 0 ) { st.Position--; }// EOFでなければ1文字戻す
        return null;
    }

    while (true) {
        do { b = st.ReadByte(); } while (IsWhiteSpace(b)); // skip whitespaces
        if ( b == '>' ) {
            return ret;
        }
        int upperNibble = AsHexDigit(b);
        if ( upperNibble < 0 ) {
            return null;
        }

        do { b = st.ReadByte(); } while (IsWhiteSpace(b)); // skip whitespaces
        if ( b == '>' ) {
            // 奇数桁で終わっている場合は末尾に'0'を補填する( = (上位nibble<<4) + 0)
            ArrayAppend(ref ret, (byte)(upperNibble<<4));
            return ret;
        }
        int lowerNibble = AsHexDigit(b);
        if ( lowerNibble < 0 ) {
            return null;
        }
        ArrayAppend(ref ret, (byte)((upperNibble<<4) | lowerNibble));
    }
}

static bool IsWhiteSpace(int c) { return (c==0||c==9||c==10||c==12||c==13||c==32);}
static bool IsHexDigit(int c) { return (('0'<=c && c<='9')||('A'<=c && c<='F')||('a'<=c && c<='f')); }
static int AsHexDigit(int c)
{
    if ('0'<=c && c<='9') { return c-'0'; }
    if ('A'<=c && c<='F') { return (c-'A')+10; }
    if ('a'<=c && c<='f') { return (c-'a')+10; }
    return -1;
}
```

# おまけその２ `%%EOF` について

~~`%%EOF`は行頭でなくても認識される。~~
`%%EOF`がなくても読み込まれるようである。


[^1]: 1～3桁という記載はあるが、3桁になるようにleading 0をつけろという記載もある。

[^2]: Acrobat Reader DCでの結果であり、3rd party製のツールがどこまでまじめにやっているかは不明。