Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

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

今回の対象は、丸括弧でくくられた文字列オブジェクトです。
こちらの解説が分かりやすいと思います。
なので、下記では説明をさぼっています。

確認環境

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

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

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

\075のようにエスケープ文字\と直後に続く1~3桁の8進数で、任意の1byteを表現できる仕様がある。
公式の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とみなされる。

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

こんな感じでよいはず。

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

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 と解釈されている。)
間に空白文字があっても2桁ずつ取るわけではないので注意が必要。

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

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

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

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

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;
}

おまけその2 %%EOF について

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


  1. 1~3桁という記載はあるが、3桁になるようにleading 0をつけろという記載もある。 

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

kob58im
趣味でC#で色々試してます。 置いてるほとんどのC#サンプルコードは、Windows7以降デフォで入ってる環境でコンパイルできます。 最近はCodePen使ってJavaScriptも書いてます。
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away