Разбор PDF-файла (low level)

123
19 июня 2021, 14:10

Я решил попробовать разобрать PDF-файл средствами c# и у меня возник вопрос. Вопрос, наверное, больше про байты, чем про сам формат, но если вы работали над подобной задачей, то будет просто отлично.

Для начала, я создал в Word'e самый простой документ с текстом "test" и сохранил его в формате PDF. Исходя из спецификации, в самом начале мне нужно прочитать число из предпоследней строки (это количество байтов от начала документа, до определённого блока).
Для наглядности, если открыть файл pdf в VisualStudio, то можно увидеть текст вида:

какой-то текст
ещё текст
много текста
ТУТ ЧИСЛО БАЙТОВ
%%EOF

То есть, по логике, чтобы прочитать это число, мне нужно отступить от конца файла 11 байтов (5 байтов последней строки + 2 байта \r\n + 4 байта Int32). Я пробовал отступать и 10, и 12, и еще очень большой диапазон байтов, но считывается не то число, которое ожидалось (считывал функцией BinaryReader.ReadInt32). Если считывать как символы или строку(BinaryReader.ReadChars(n)), то считывается нормально.

Тут я начал подозревать, что pdf-файлик то не совсем двоичный, а это число вовсе не число, а текст. Открыл его в hex editor, содержимое было было таким (показан лишь конец):
То есть, мы видим, что "число" занимает 5 байтов. Выходит, что оно действительно записано в виде текста. Вопросы:

  • Правильно ли все вышенаписанное?
  • В спецификации написано, что PDF - это двоичный файл, тогда почему число записано как текст, а не как Int32 (про int32 тоже указано там)?
  • Как мне тогда правельнее считывать это число, ведь разрядов (соответственно и байтов) может быть разное количество и придётся как-то подбирать сдвиг с конца для FileStream?

P.S. Я преследую исключительно научный интерес, не нужно советовать библиотеки по работе с pdf.

Answer 1

Правильно ли все вышенаписанное?

В целом, да, но далеко не все. А именно:

Тут я начал подозревать, что pdf-файлик то не совсем двоичный, а это число вовсе не число, а текст.

Все файлы по сути бинарные (двоичные). То, что в нем можно увидеть строку не отменяет того факта, что он бинарный. Бинарный файл - это последовательность байтов. Строка - тоже последовательность байтов. Важно лишь то, каким образом с ней работать: как со строкой, как с массивом байтов, как с числом определенной разрядности и т.д.

В бинарный файл можно записывать и строку, и отдельно символ, и число, и др. (если вы работали с BinaryReader и BinaryWriter, то должны были это понять, потому что для разных типов данных есть разные методы для чтения/записи). Главное, чтобы человек, который будет писать код для чтения этого файла, понимал, где записана строка, где число.

Как мне тогда правельнее считывать это число, ведь разрядов (соответственно и байтов) может быть разное количество и придётся как-то подбирать сдвиг с конца для FileStream?

Я сам не работал с чтением pdf-файла вручную, но, судя по спецификации, вам необходимо читать этот файл как текстовый (по крайней мере, trailer-часть). Можно воспользоваться методом File.ReadAllLines Получаете предпоследнюю строку в массиве и преобразуете ее в число (желательно, в UInt64).

Если хочется все-же как бинарный файл прочитать, то тогда нужно найти вторую с конца последовательность байт 0D 0A и прочитать последовательность символов, начиная со следующего символа после второго 0D 0A и заканчивая символом, предшествующим последнее OD OA. Из вашего примера:

... 0D 0A 32 38 34 30 0D 0A ...

Будет прочитано 4 символа. Либо прочитать их как массив байт (BinaryReader.ReadBytes) и затем преобразовать с помощью метода System.Text.Encoding.ASCII.GetString (либо UTF8.GetString, я точно не могу сказать, как правильней).

Answer 2

Считываем 30 последних байтов, получаем строку между последними двумя \n. Это и есть наше число.

static int readLastNumber(FileStream fstream)
{
    byte[] data = new byte[30];
    fstream.Seek(-data.Length, SeekOrigin.End);
    fstream.Read(data, 0, data.Length);
    // Индексы начала и конца числа
    int startPoint, endPoint;
    int i = data.Length - 1;
    if (data[i] == '\n')
        i--;
    while (data[i] != '\n')
        i--;
    endPoint = --i;
    while (data[i] != '\n')
        i--;
    startPoint = ++i;
    string number = Encoding.ASCII.GetString(data, startPoint, endPoint - startPoint);
    return int.Parse(number);
}
READ ALSO
Assembler в Pascal и в C#

Assembler в Pascal и в C#

Есть программа на паскале, написанная изначально не мнойПеревожу ее на C#

111
Как открыть cmd.exe от системного пути?

Как открыть cmd.exe от системного пути?

Всем привет! Нужно, чтобы cmdexe открывался по пути "{Системный диск}\Windows\System32"

104
MySql запрос (ругаеться на 2 Left Join)

MySql запрос (ругаеться на 2 Left Join)

Не выходит исправить ошибку 2 errors were found during analysis

119