4058 читателей, 404 поста
Администрация
Модераторы
Блог для обмена опытом

(First line \ First line \n Second line with brackets \(\))
First line First line Second line with brackets ()
\053), с помощью отдельного двухбайтового hex'а (<2B>) или даже их последовательности (<54776F20>). Например, следующие строки эквивалентны:(Two + two = four.) (Two \053 two \075 four.) (Two <2B> two <3D> four.) (<54776F202B2074776F203D20> four.)
[(Hello,)10(world!)]. Массивы подчас содержат текстовые строки.<< /Length 681 /Filter /FlateDecode >>
$dictionary = array(Потоки
"Length" => "681",
"Filter" => true,
"FlateDecode" => true,
);
stream и endstream. Любые бинарные данные, будь-то сжатый текст, изображение или внедрённый шрифт, будут представлены в виде потока. Поток всегда находится внутри объекта (чуть ниже) и характеризуется, как минимум, своей длиной (опция /Length N в словаре) и очень часто методом сжатия (например, /Filter /FlateDecode). PDF поддерживает достаточное количество форматов сжатия (в том числе и формат шифрования /CryptDecode), нас же будут интересовать лишь три: наиболее часто используемый Flate (gzip-сжатие) и более редкие ASCII Hex (представление данных в виде шестнадцатеричной строки с конечным символом >) и ASCII 85-based (сжатие, когда подряд идущие 4 символа исходного текста кодируются 5 символами от ! до y в ASCII таблице).obj и endobj. Объект имеет свой ID внутри документа, по которому можно на него ссылаться. Нам в первую очередь интересны объекты с потоками внутри себя (не забываем об основной подзадаче), которые почти всегда содержат ещё и набор дополнительных опций в виде словаря. Вот обычный пример объекта внутри PDF-файла (с несжатым содержимым потока):2 0 obj << /Length 9 2 R >> stream BT /F1 12 Tf 72 712 Td (A short text stream.) Tj ET endstream endobj
/Filter /FlateDecode /ASCIIHexDecode). Что ж нам нужен какой-нибудь действительный пример. Пожалуйста, стихотворение Михаила Юрьевича Лермонтова «Парус» в PDF-формате (документ создан на Acrobat.com из odt-файла из прошлой статьи).
/Length 681), что поток сжат (/Filter) в gzip (/FlateDecode). Уже достаточно информации, чтобы разжать поток данных — подойдёт gzuncompress:0.1 w q 0 -0.1 612.1 792.1 re W* n q 0 0 0 RG 0 0 0 rg BT 2 Tr 0.59999 w 56.8 716.6 Td /F1 18 Tf[<01>17<02>10<03>10<04>17<05>]TJ ET Q q 0 0 0 rg BT 56.8 682.5 Td /F1 11 Tf[<06>9<07>11<08>6<07>11<07>11<09>13<0A>4<0B>14<0C>11<0D>11<0E>9 <0F>9<0A>4<10>11<11>10<12>23<13>6<10>11<14>10<10>11<15>]TJ ET ... много текста ...
BT (beginning of text) и конца ET (end of text).Tj (отобразить текст) или маркера TJ (отобразить текст с учётом индивидуального символьного позиционирования). Данные маркеры стоят после строки текста или массива строк, как в данном случае ([<01>17<02>10<03>10<04>17<05>]TJ).1. <01>17<02>10<03>10<04>17<05> 2. <06>9<07>11<08>6<07>11<07>11<09>13<0A>4<0B>14<0C>11<0D>11<0E>9 <0F>9<0A>4<10>11<11>10<12>23<13>6<10>11<14>10<10>11<15>
ПАРУС кодируется, как 01 02 03 04 05Белеет — как 06 07 08 07 07 09...
/CIDInit/ProcSet findresource begin 12 dict begin begincmap /CIDSystemInfo<< /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def /CMapName/Adobe-Identity-UCS def /CMapType 2 def 1 begincodespacerange <00> endcodespacerange 45 beginbfchar <01> <041F> <02> <0410> <03> <0420> <04> <0423> <05> <0421> <06> <0411> <07> <0435> <08> <043B> <09> <0442> ... много строчек преобразований ... endbfchar endcmap CMapName currentdict /CMap defineresource pop end end
<01>, <02> и так далее? Ещё бы — мы их видели чуть раньше в текстовых строках. Предположим, что мы должны заменить 01 на 041F, взглянем, что скрывает за собой это число. Ура! #x041F = П! Мы нашли трансформацию одного символа в другой, теперь обратимся к документации и узнаем чуть больше.beginbfchar и endbfchar, самое простое. Оно ставит в соответствие первому коду другой. Например, в примере выше мы узнали, что 01 скрывает за собой код символа П. Но это лишь частный случай работы данного преобразования — есть возможность ставить в соответствие одному коду целую строку до 512 символов длины (т.е. до 128 символов в Unicode).beginbfrange и endbfrange. Оно работает уже не с отдельными символами, а уже с их диапазонами. Преобразование поддерживает два вариант своей работы:<0000> <005E> <0020> — мы работает с диапазоном от 0000 до 005E, каждое значение из которого преобразуется в значения из промежутка 0020 и 007E. Заметили принцип? 0000 преобразуется в 0020, 0001 в 0021, 0002 в 0022 и так далее;<005F> <0061> [<00660066> <00660069> <00660066006C>] — каждое значение из промежутка между 005F и 0061 (т.е. ещё 0060) заменяется на соответствующую последовательность из массива в квадратных скобках: 005F будет заменён на 0066 00 66 (т.е. на ff), 0060 на fi, а 0061 на ffl.Полный исходник можете найти по ссылке.
- function pdf2text($filename) {
- // Читаем данные из pdf-файла в строку, учитываем, что файл может содержать
- // бинарные потоки.
- $infile = @file_get_contents($filename, FILE_BINARY);
- if (empty($infile))
- return "";
- // Проход первый. Нам требуется получить все текстовые данные из файла.
- // В 1ом проходе мы получаем лишь "грязные" данные, с позиционированием,
- // с вставками hex и так далее.
- $transformations = array();
- $texts = array();
- // Для начала получим список всех объектов из pdf-файла.
- preg_match_all("#obj(.*)endobj#ismU", $infile, $objects);
- $objects = @$objects[1];
- // Начнём обходить, то что нашли - помимо текста, нам может попасться
- // много всего интересного и не всегда "вкусного", например, те же шрифты.
- for ($i = ; $i < count($objects); $i++) {
- $currentObject = $objects[$i];
- // Проверяем, есть ли в текущем объекте поток данных, почти всегда он
- // сжат с помощью gzip.
- if (preg_match("#stream(.*)endstream#ismU", $currentObject, $stream)) {
- $stream = ltrim($stream[1]);
- // Читаем параметры данного объекта, нас интересует только текстовые
- // данные, поэтому делаем минимальные отсечения, чтобы ускорить
- // выполнения
- $options = getObjectOptions($currentObject);
- if (!(empty($options["Length1"]) && empty($options["Type"]) && empty($options["Subtype"])))
- continue;
- // Итак, перед нами "возможно" текст, расшифровываем его из бинарного
- // представления. После этого действия мы имеем дело только с plain text.
- $data = getDecodedStream($stream, $options);
- if (strlen($data)) {
- // Итак, нам нужно найти контейнер текста в текущем потоке.
- // В случае успеха найденный "грязный" текст отправится к остальным
- // найденным до этого
- if (preg_match_all("#BT(.*)ET#ismU", $data, $textContainers)) {
- $textContainers = @$textContainers[1];
- getDirtyTexts($texts, $textContainers);
- // В противном случае, пытаемся найти символьные трансформации,
- // которые будем использовать во втором шаге.
- } else
- getCharTransformations($transformations, $data);
- }
- }
- }
- // По окончанию первичного парсинга pdf-документа, начинаем разбор полученных
- // текстовых блоков с учётом символьных трансформаций. По окончанию, возвращаем
- // полученный результат.
- return getTextUsingTransformations($texts, $transformations);
- }
$content = shell_exec('/usr/local/bin/pdftotext '.$filename.' -');. Но в данном случае стояла задача чтения PDF под любой платформой и на любой площадке.
комментарии (19)
Внесу свои пять копеек: когда мне приходилось решать эту задачу, я залез на nl3.php.net/manual/en/ref.pdf.php (выделение текста тоже только средствами PHP).
Там был код, который иногда работал, иногда нет. Потом выяснилось, что проблема была в том, что в стандарте так и не смогли договориться, как кодировать переносы строк — \n, \r или \r\n. В общем, я эту проблему поправил, и все заработало довольно прилично. Мой код — это последний комментарий на вышеуказанной странице.
нет ни красоты ни стиля
Примеры вы хорошие подбираете :)
А статья супер: просто, четко и красиво изложено.
а возможно ли сделать так, чтобы текст вытаскивался с форматированием, т.е. курсив / жирный и т.д.?
Начертание текста (полужирное, курсив или ещё воз и маленькая эффектов, что поддерживает PDF) — это к шрифтам. Выше по комментариям, я обещал поковырять PDF на предмет их чтения.
Ну и от себя — не ищите готовых решений. Тут уже придётся сесть за редактор кода и PDF с описанием формата. :)
поправьте