Некоторое время назад мы с вами обсуждали получение чистого текста из различных форматов данных: будь то PDF или DOC. В одном из обсуждений был высказано предположение, что при парсинге презентаций PowerPoint я заработаю геморрой или другую страшную болезнь мягкой точки. Что ж, волей судеб мне пришлось доставать текст и из этого «сладенького» формата. Скажу честно, геморрой заработать не удалось, а вот класс для парсинга презентаций вышел.
Как и DOC, PPT является надстройкой над форматом WCBFF (структурированного бинарного файлового формата), о котором вы можете прочесть в статье о MS Word, написанной мною ранее. Отмечу лишь то, что за время тестирования я нашёл несколько ошибок (а кроме того, повидал кучу «битых» файлов, из которых приходилось получать текст) в старых реализация WCBFF и DOC, поэтому советую обновить свои исходники тем, кто использует мои наработки.
Так, мы отвлеклись. Продолжим беседу о PowerPoint. В отличие от DOC, мы будем работать не с «файлами»
Итак, чтобы начать получать данные из PPT стоит прочесть небольшой «файл»-запись (или «файл», состоящий из одной записи
Сейчас же я расскажу, как нужно читать записи в PPT. Любая запись в презентации содержит специальный заголовок
Что дальше? Вернёмся к записи
После получения всех смещений
Теперь вернёмся к последнему прочитанному
Нас будут интересовать всего три типа записей, что хранятся в этом главном блоке:
Когда я в первый раз узнал об этом факте, скажу честно, я расстроился. Ещё 600 с гаком страниц документации. Но ODRAW, как оказалось, построен на тех же
Код с комментариями вы можете получить на GitHub'е. Пока ещё немножко сыро, но я думаю, что в ближайшее время найду все подводные камни. Основные ошибки выскакивают именно из-за не совсем правильно (?) прочтения
НеМного о формате PPT
Как и DOC, PPT является надстройкой над форматом WCBFF (структурированного бинарного файлового формата), о котором вы можете прочесть в статье о MS Word, написанной мною ранее. Отмечу лишь то, что за время тестирования я нашёл несколько ошибок (а кроме того, повидал кучу «битых» файлов, из которых приходилось получать текст) в старых реализация WCBFF и DOC, поэтому советую обновить свои исходники тем, кто использует мои наработки.
Так, мы отвлеклись. Продолжим беседу о PowerPoint. В отличие от DOC, мы будем работать не с «файлами»
WordDocument
и 1Table
, как раньше, а со специфичными для презентаций: Current User
и PowerPoint Document
. Наличие обоих «файлов» в «файловой системе» CBF-файла обязательно для презентаций. По ним же можно определить, что перед нами презентация, в случае ошибочного расширения.Итак, чтобы начать получать данные из PPT стоит прочесть небольшой «файл»-запись (или «файл», состоящий из одной записи
CurrentUserAtom
) Current User
. Эта запись содержит техническую информацию о том, кто редактировал этот файл в последний раз, но это не самое важное. В этом блоке есть информация о смещении к первой записи UserEditAtom
, речь о которой пойдёт чуть ниже.Сейчас же я расскажу, как нужно читать записи в PPT. Любая запись в презентации содержит специальный заголовок
rh
, который содержит техническую информацию о ней. Для этого достаточно прочесть первые 8 байт любой записи. Первое слово обычно не содержит нужной информации, а вот следующие 6 байт нам потребуются. WORD по смещению 2 (rh.recType
) идентифицирует тип записи, по которому можно узнать, что делать с записью дальше. Long по смещению 4 (recLen
) — длина записи без учёта заголовка в восемь байт. Такой способ записи достаточно удобен и позволяет избежать многих ошибок при разборе файла презентацииЧто дальше? Вернёмся к записи
UserEditAtom
. Эта запись находится уже в PowerPoint Document
. В последствии работать мы будем только с этим «файлом». С помощью прочтения этой и связанных с ней записей мы должны построить такую дивную штуку, как массив смещений PersistDirectory
, с помощью которого мы будем искать главную структуру документа PowerPoint — DocumentContainer
. Для этого мы должны прочитать текущую запись UserEditAtom
, найти в ней смещение offsetPersistDirectory
к текущей «live» версии PersistDirectory
и смещение offsetLastEdit
к следующей записи UserEditAtom
. Так продолжим получать смещения, пока не наткнёмся на нули в DWORD'е offsetLastEdit
.После получения всех смещений
offsetPersistDirectory
мы должны создать эту самую PersistDirectory
. Идём по смещения в обратном порядке и читаем записи PersistDirectoryAtom
. Они содержат в себе массив записей PersistDirectoryEntry
. Каждая из коих содержит номер persistId
первого entry и их количество cPersist
в текущей записи. После этой информации идёт массив смещений к объектам PersistDirectory
. Это самый важный массив, по которому мы будем находить ссылки на все объекты презентации.Теперь вернёмся к последнему прочитанному
UserEditAtom
и найдём там поле docPersistIdRef
. Это номер самого важного объекта DocumentContainer
в PersistDirectory
. Прочитаем его. В нём хранится вагон и маленькая тележка информации о текущей презентации: колонтитулы, заметки к слайдам и главное — запись SlideListWithTextContainer
, содержащая всякое разное о слайдах.Нас будут интересовать всего три типа записей, что хранятся в этом главном блоке:
TextCharsAtom
, TextBytesAtom
и SlidePersistAtom
. С первыми два всё несложно: это unicode-текст на слайде и обычный ANSI, соответственно. Другое дело, когда вместо текста получаем ссылку на слайд SlidePersistAtom
. По ней мы должны прочитать объект Drawing
, который (sic!) не является объектом PPT. Да-да, внутри слайда в этом случае внедрён объект MS Drawing, с достаточно неприятной структуру вложенных записей.Когда я в первый раз узнал об этом факте, скажу честно, я расстроился. Ещё 600 с гаком страниц документации. Но ODRAW, как оказалось, построен на тех же
rh
-заголовках с теми же recType
'ами, что и PPT. Это позволило облегчить себе задачу и чуть-чуть смухлевать с помощью поиска в Drawing
-объекте всё тех же атомов TextCharsAtom
и TextBytesAtom
по их recType
'ам. Реализация
Код с комментариями вы можете получить на GitHub'е. Пока ещё немножко сыро, но я думаю, что в ближайшее время найду все подводные камни. Основные ошибки выскакивают именно из-за не совсем правильно (?) прочтения
PersistDirectory
. Если у кого есть уточнения, то я с удовольствием их выслушаю.