Работа с OpenOffice.org в PHP при помощи PUNO

Вступление

Предположим, что вам необходимо на сервере конвертировать HTML в RTF. Причём, документы имеют нетривиальное оформление и довольно внушительный размер, на котором PHPRtfLite надолго зависает (делает он это потому, что обрабатывает входной текст посимвольно).

В итоге, вы скорее всего придёте к мысли, что неплохо бы использовать OpenOffice.org в headless режиме.

Авторы OpenOffice.org, как настоящие адепты open source, решили не брать что-нибудь готовое, а изобрести велосипед. Вот так и родилось нечто под названием UNO.

Официальная wiki рассказывает, что это Universal Network Objects. Ещё нам сообщается, что UNO позволит нам взаимодействовать между различными языками программирования, объектными моделями, аппаратными архитектурами, в рамках одного процесса, так и межу процессами, например, через интернет.

UNO

Короче говоря, они сделали свой COM.

Позволять то взаимодействовать, может, оно и позволяет, но через адские муки. Боль и страдание. В PHP для этого предназначен модуль PUNO.

Если в ваши головы закралась идея поработать с UNO, то приготовьтесь к практически полному отсутствию внятной документации (а в PUNO к её абсолютному отсутствию).

Я вас предупреждал.

Не буду расписывать настройку, установку OO.o и PUNO. Это отдельный долгий и мучительный процесс. О нём, во всех подробностях, со здоровым FAQ и большим количеством исключительных ситуаций, можно почитать здесь.

От себя добавлю только:

  • Старайтесь брать версии посвежее. Память течёт в жутких объёмах везде, но если сравнивать, например, 3.1 и 3.2, то разница заметна на глаз;
  • Включите своп. Сколько бы у вас не было оперативки — её не хватит. Занять 400 мегабайт ОЗУ при конвертировании файла в сотню килобайт и зависнуть — нормальная практика;
  • Исправьте в исходниках PUNO кодировку с ISO_8859_15 на UTF8. Казалось бы, XXI век.

Поехали

Чтобы открыть документ, нам надо создать экземпляр Writer и загрузить в него файл. В этом нам поможет следующая функция:

1
2
3
4
5
function LoadDocument($strDocumentPath) {
    $xComponentLoader = get_remote_xcomponent("uno:socket,host=localhost,port=8100;urp;StarOffice.ServiceManager","com.sun.star.frame.Desktop");
    $xComponent = $xComponentLoader->loadComponentFromURL($strDocumentPath, "_blank", 0, array());
    return $xComponent;
}

А вот так её используем:

1
$obWriter = LoadDocument('file://' . pathToHtml);

Использовать временные файлы для конвертирования, конечно, не лучший вариант, но код с созданием контейнера в памяти на PHP перенести не удалось. Если у вас получится, то обязательно похвалитесь.

Да и не очень то и хотелось, честно говоря. Дело в том, что OO.o в headless режиме не поддерживает обработку данных в несколько потоков. Другими словами, если к нему будет два одновременных вызова, то работать будет только один. Соответственно, нужна синхронизация. А временный файл можно использовать как mutex.

Вариант при котором в памяти находится несколько процессов OO.o я даже не рассматриваю. У вас памяти не хватит.

Так, это было лирическое отступление, теперь, вернёмся к процессу.

Сделаем такую вот магию:

1
2
$obTextDocument = $obWriter;
$obWriterFactory = $obWriter;

Зачем это надо не очень понятно, но в скудных ошмётках документации (вернее, в примерах использования) авторы PUNO рекомендуют так делать, чтобы было похоже на Java код. Только там надо руками запрашивать интерфейсы, а здесь автоматически.

Вообще, разработчики PUNO, вместо того чтобы сделать хоть какую нибудь документацию, советуют смотреть Developers Guide (в котором найти нужную информацию сложнее чем иголку в стоге сена) и General Programming Guidelines (в котором её в общем-то почти и нет) и писать PHP взяв за образец Java.

Получается, скажем так, не очень.

Далее. OO.o по умолчанию при сохранении не внедряет картинки в документ, а ставит на них ссылки, поэтому сгенерированный RTF, который скачает пользователь, будет зиять пустотами. Для устранения этого недостатка нам необходимо найти в документе все изображения и внедрить их.

Получаем количество графических объектов:

1
2
$obGraphicObjects = $obTextDocument->getGraphicObjects();
$graphicObjectsCount = $obGraphicObjects->getCount();

Запомним имена двух сервисов, которые отвечают за изображения:

1
2
$s1 = "com.sun.star.drawing.GraphicObjectShape";
$s2 = "com.sun.star.text.TextGraphicObject";

Вот где, как вы думаете я их нашёл? В документации? Нет, конечно же, на форуме.

1
2
3
4
5
6
7
8
9
10
11
12
for($i = 0; $i < $graphicObjectsCount; $i++) {
  $obImage = $obGraphicObjects->GetByIndex(0);

  $s1 = "com.sun.star.drawing.GraphicObjectShape";
  $s2 = "com.sun.star.text.TextGraphicObject";

  if ($obImage->supportsService($s1)){
  }
  else if ($obImage->supportsService($s2)) {
    EmbedImage2($obImage);
  }
}

Как вы можете заметить обработчик первого условия пуст. Я не стал писать код для него. Во-первых, изображения внедрялись и так, а во вторых у меня сходу не получилось портировать код с форума. Оставляю это в качестве домашнего упражнения пытливому читателю.

EmbedImage2 это моя функция и вот её содержимое:

1
2
3
4
5
6
7
8
function EmbedImage2($obImage) {
  $strName = $obImage->getPropertyValue("LinkDisplayName");
  $strGraphicURL = $obImage->getPropertyValue("GraphicURL");
  $obBitmaps = $obWriter->createInstance("com.sun.star.drawing.BitmapTable");
  $obBitmaps->insertByName($strName, $strGraphicURL);
  $srNewURL = $obBitmaps->getByName($strName);
  $obImage->setPropertyValue("GraphicURL", $strNewURL);
}

Не спрашивайте меня что здесь происходит. Лучше не знать, я думаю. Иногда он падает. Тоже с форума, кстати.

Осталось сохранить полученный документ:

1
2
3
4
5
6
$arrStoreProperties = array();
$arrStoreProperties[0] = create_struct("com.sun.star.beans.PropertyValue");
$arrStoreProperties[0]->Name = "FilterName";
$arrStoreProperties[0]->Value = "Rich Text Format";

$obTextDocument->storeToURL('file://' . rtfFileName, $arrStoreProperties);

И в конце, обязательно, не забыть вызвать:

1
$obWriter->dispose();

Это спасёт вас от совсем фатальных утечек памяти, но, я думаю, что всё равно рано или поздно придётся написать скрипт, который периодически убивает и запускает процесс OO.o. Или делать это каждый раз, что выглядит не так уж странно.

Собственно, вот и всё чем я хотел поделиться. Код не идеален. Возможно, на некоторых файлах он не будет работать вовсе, но это лучше чем ничего, как было в моём случае. По крайней мере будет от чего оттолкнуться.

Однако, основной посыл статьи — если вы можете не использовать OO.o, то не используйте. Избегайте всеми возможными способами.