Здравствуйте, уважаемое хабрасообщество!Продолжаем историю про генерацию DOCX средствами PHP.
Что нас ждет сегодня:
- Мы узнаем, как вставлять изображения в документ;
- Просветимся на счет English Metric Units;
- Сделаем задел на будущую генерацию Exel.
Тем, кто не в курсе, рекомендуется прочитать первую часть. Ну а кто в теме – прошу под кат.
Ещё раз
Но сначала обо всем по порядку. С момента публикации прошлой статьи было написано достаточное количество комментариев: эмоциональных и по делу; у проекта PHPDocx на гитхабе появилось несколько форков. Всё это говорит о том, что эта тема достаточно актуальна. Но некоторые разработчики не понимают самой сути моего подхода. А подход этот заключается в использовании наследования: класс генератор должен быть наследником ZipArchive. Послушайте, ну если не хотите Вы использовать наследование, установите PHP 5.4 и используйте traits, в конце концов! Этот подход несравненно лучше, чем работать постоянно через одно свойство:
$this->archive->open( … ); $this->archive->addFile( … ); $this->archive->close( .. );
Для чего вообще нужно генерить DOCX на PHP? Некоторые разработчики не понимают, зачем вообще это нужно. Я ориентировался на то, чтобы сделать возможность сохранить web-страницу в формате Word. Лично я использую свой класс для сохранения отчетов Яндекс.Метрики в формате DOCX. Пользователь seriyPS спросил, зачем я разбивал текст на строки? Я это делал, предполагая, что текст является полем из БД, а перенос строки — новый абзац. В общем, не будем этого делать для ясности. Сделайте сами разбивку на абзацы.
Кроме того, наш генератор должен иметь максимально удобный API. Я думаю, мне удалось его реализовать. API состоит всего из трех методов: конструктора, assign, create.
Ну что ж, поговорили, и хватит. Приступим.
Что нового
Во-первых, я существенно изменил код, используемый в той статье, и оформил это всё в полноценную OpenSource библиотеку. Ссылки в конце. А сейчас по пунктам:
1. Класс OfficeDocument и WordDocument
Как мы уже поняли, в корне архива хранятся файлы, необходимые документу MS Office в целом. В папке word/ хранятся документы, необходимые документу MS Office Word непосредственно. Решение напрашивается само собой: сделать класс общий для документов MS Office, и класс-наследник для Word-документов непосредственно.
Сразу опишу структуру:
// Общий класс для создания генераторов MS Office документов class OfficeDocument extends ZipArchive{ __construct($filename, $template_path = '/template/' ); protected function add_rels( $filename, $rels, $path = '' ); protected function pparse( $replace, $content ); } // Класс для создания документов MS Word class WordDocument extends OfficeDocument{ public function __construct( $filename, $template_path = '/template/' ) // Обращаю внимание, это метод API public function assign( $content = '', $return = false ); public function create(); }
Зачем я это сделал. Это задел на будущее, в котором мы будем генерить файлы MS Excel классом XlsxDocument.
Давайте разберем внутренности.
2. Динамическое создание связей
Внутри docx-файла существуют файлы _rels/.xml и word/_rels/document.xml.rels. Они подключают файлы в документ. Если не описать какой-либо файл в этих структурах, то он просто окажется лишним весом в docx-документе. Таким образом можно просто прятать инфу внутри docx. Мы же в конструкторах создадим массивы внутренних связей между XML-документами. Вот, например, связи для документа MS Office:
// Описываем связи для документа MS Office $this->rels = array_merge( $this->rels, array( 'rId3' => array( 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties', 'docProps/app.xml' ), 'rId2' => array( 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', 'docProps/core.xml' ), ) );
Идентификатором подключаемого файла является запись «rIdN». Файлы app.xml и core.xml являются статичными. Мы их просто будем упаковывать в архив методом add_rels, параллельно создавая XML-файл описания связей _rels.xml:
// Генерация зависимостей protected function add_rels( $filename, $rels, $path = '' ){ // Шапка XML $xmlstring = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">'; // Добавляем документы по описанным связям foreach( $rels as $rId => $params ){ // Если указан путь к файлу, берем. Если нет, то берем из репозитория $pathfile = empty( $params[2] ) ? $this->path . $path . $params[1] : $params[2]; // Добавляем документ в архив if( $this->addFile( $pathfile , $path . $params[1] ) === false ) die('Не удалось добавить в архив ' . $path . $params[1] ); // Прописываем в связях $xmlstring .= '<Relationship Id="' . $rId . '" Type="' . $params[0] . '" Target="' . $params[1] . '"/>'; } $xmlstring .= '</Relationships>'; // Добавляем в архив $this->addFromString( $path . $filename, $xmlstring ); }
Обращаю внимание, что add_rels описан в OfficeDocument, а используется в обоих классах: OfficeDocument и WordDocument, поскольку внутри docx файла существует два документа _rels.xml, описывающих зависимости. Это выйгрыш ООП подхода, который я предложил, и здесь методология, предложенная VolCh, точно не подойдет.
В результате получаем такой типовой _rels:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"> <Relationship Id="rId3" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" Target="docProps/app.xml"/> <Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/> <Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/> </Relationships>
Файл word/document.xml мы сгенерим и подключим динамически. Надеюсь, с динамическим созданием связей понятно. Теперь со вставкой изображения.
Учимся вставлять изображения
Сначала приведу XML-фрагмент, полученный экспериментальным методом, для вставки в document.xml, чтобы получить изображение в Word-документе:
<w:p w:rsidR="000E3348" w:rsidRDefault="00CD6FED"> <w:r> <w:rPr> <w:noProof/> <w:lang w:eastAsia="ru-RU"/> </w:rPr> <w:drawing> <wp:inline distT="0" distB="0" distL="0" distR="0"> <wp:extent cx="{WIDTH}" cy="{HEIGHT}"/> <wp:effectExtent l="19050" t="0" r="0" b="0"/> <wp:docPr id="2" name="Рисунок 2"/> <wp:cNvGraphicFramePr> <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/> </wp:cNvGraphicFramePr> <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"> <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"> <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture"> <pic:nvPicPr> <pic:cNvPr id="0" name="image.jpg"/> <pic:cNvPicPr/> </pic:nvPicPr> <pic:blipFill> <a:blip r:embed="{RID}"> <a:extLst> <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}"> <a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/> </a:ext> </a:extLst> </a:blip> <a:stretch> <a:fillRect/> </a:stretch> </pic:blipFill> <pic:spPr> <a:xfrm> <a:off x="0" y="0"/> <a:ext cx="{WIDTH}" cy="{HEIGHT}"/> </a:xfrm> <a:prstGeom prst="rect"> <a:avLst/> </a:prstGeom> <a:noFill/> <a:ln> <a:noFill/> </a:ln> </pic:spPr> </pic:pic> </a:graphicData> </a:graphic> </wp:inline> </w:drawing> </w:r> </w:p>
Нам нужно будет заменить {RID} на идентификатор подключенного изображения, а также прописать {WIDTH} и {HEIGHT}.
За вставку изображения, как и за вставку текста отвечает один метод API — assign:
public function assign( $content = '', $return = false ){ // Проверяем, является ли $text файлом. Если да, то подключаем изображение if( is_file( $content ) ){ // Берем шаблон абзаца $block = file_get_contents( $this->path . 'image.xml' ); list( $width, $height ) = getimagesize( $content ); $rid = "rId" . count( $this->word_rels ) . 'i'; $this->word_rels[$rid] = array( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", "media/" . $content, // Указываем непосредственно путь к файлу $content ); $xml = $this->pparse( array( '{WIDTH}' => $width * $this->px_emu, '{HEIGHT}' => $height * $this->px_emu, '{RID}' => $rid, ), $block ); } else{ // Берем шаблон абзаца $block = file_get_contents( $this->path . 'p.xml' ); $xml = $this->pparse( array( '{TEXT}' => $content, ), $block ); } // Если нам указали, что нужно возвратить XML, возвращаем if( $return ) return $xml; else $this->content .= $xml; }
Кто умеет читать код, заметит, что в методе используется хитрая метрическая система. Называется она English Metric Units (EMU). Почитать об этом можно на английской википедии. Кратко: можно получить EMU из px умножением на число. Только вот на википедии написано, что это число равно 12700. Я же экспериментально выяснил, что оно равно 8625. При этом множителе картинка отображалась пиксель в пиксель.
Ну и конечно, подключаем непосредственно файл изображения в структуру связей:
$rid = "rId" . count( $this->word_rels ) . 'i'; $this->word_rels[$rid] = array( "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image", "media/" . $content, // Указываем непосредственно путь к файлу $content );
В результате
В результате мы получили полноценную библиотеку. Теперь мы можем использовать её вот так:
// Подключаем класс include 'PHPDocx_0.9.2.php'; // Создаем и пишем в файл. Деструктор закрывает $w = new WordDocument( "Пример.docx" ); // Использование метода assign /****************************** / / $w->assign( 'text' ); / $w->assign( 'image.png' ); / $xml = $w->assign( 'image.png', true ); / $w->assign( $w->assign( 'image.png', true ) ); / /******************************/ $w->assign('image.jpg'); $w->assign('Кто узнал эту женщину - тот настоящий знаток женской красоты.'); $w->create();
Вот в принципе и всё.
В планах: генерация таблиц.
Ссылки:
PHPDocx на гитхабе.
Страница проекта PHPDocx.
Скачать исходники.
