Здравствуйте, уважаемое читатели!
Продолжаем историю про генерацию 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' ) );
/
/******************************/
$w->assign('image.jpg');
$w->assign('Кто узнал эту женщину - тот настоящий знаток женской красоты.');
$w->create();
Вот в принципе и всё.
В планах: генерация таблиц.
Ссылки:
PHPDocx на гитхабе.
Домашняя страница PHPDocx.
Скачать PHPDocx (все исходники из статьи и пример).
Автор: alexios