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
がなくても読み込まれるようである。