PHP / [Из песочницы] Честная генерация DOCX на PHP. Часть 1

в 10:33, , рубрики: docx, php, метки: ,

image Здравствуйте, уважаемое читатели! Как-то раз был на хабре интересный материал про генерацию doc-файлов средствами PHP. К сожалению, больше на хабре ничего на эту тему я не нашел. На тот момент я разработал собственное решение.
Оно состояло в том, чтобы генерировать .docx файлы. Аргументы были следующие:

  • На дворе 2012 год, а этот формат появился аж в 2007-м
  • Генерить .docx несомненно проще, чем .doc, поскольку .docx = .zip, а .doc — бинарный файл
  • Костыль с генерацией HTML и переименованием в doc не подойдет для более-менее уважающих себя проектов
  • С помощью приведенного ниже метода мы с легкостью сгенерируем Excel, и вообще всё что угодно.

Подробности под катом.

Структура файла

image image Возьмите ваш любой файл .docx и переименуйте его в .zip, а затем откройте. И вы увидите структуру docx-файла. Да, да! Это обычный zip-архив. Кратко скажу, что самое интересное для нас лежит в папке word. Здесь-же в корне находятся общие настройки документа.
Самое же интересное для нас в папке word — файл document.xml, который представляет из себя файл с содержимым Office Open XML. Именно он содержит в себе непосредственно содержимое документа. Подробнее об этом формате можно почитать на английской Википедии. В папке _rels находится файл document.xml.rels. Он нам пригодится в будущем, чтобы описывать связи прикрепленных файлов внутри документа. Может еще существовать папка media, если в вашем документе присутствуют изображения. Имена остальных файлов вроде-бы говорят за себя.

Учимся генерить .docx

Итак, как мы уже определились, .docx это просто обычный zip-архив, поэтому решение напрашивается само собой: класс-генератор документов должен быть наследником класса ZipArchive, который доступен «из коробки». А остальное — дело техники. Ниже приведен класс для создания пустого .docx-файла (не забываем включить zlib и использовать кодировку UTF-8).
class Word extends ZipArchive{

// Файлы для включения в архив
private $files;

// Путь к шаблону
public $path;

public function __construct($filename, $template_path = '/template/' ){

// Путь к шаблону
$this->path = dirname(__FILE__) . $template_path;

// Если не получилось открыть файл, то жизнь бессмысленна.
if ($this->open($filename, ZIPARCHIVE::CREATE) !== TRUE) {
die("Unable to open <$filename>n");
}

// Структура документа
$this->files = array(
"word/_rels/document.xml.rels",
"word/theme/theme1.xml",
"word/fontTable.xml",
"word/settings.xml",
"word/styles.xml",
"word/document.xml",
"word/stylesWithEffects.xml",
"word/webSettings.xml",
"_rels/.rels",
"docProps/app.xml",
"docProps/core.xml",
"[Content_Types].xml" );

// Добавляем каждый файл в цикле
foreach( $this->files as $f )
$this->addFile($this->path . $f , $f );
}

// Упаковываем архив
public function create(){

$this->close();
}
}

$w = new Word( "Example.docx" );

$w->create();

Возле скрипта должен появиться файл Example.docx При этом не забываем создать саму структуру файлов. Для её получения пользуемся пресловутым MS Office и Winrar'ом. После сборки пробуем открыть в через MS Office. В случае незначительных ошибок в XML ворд выдаст предупреждение, что в документе содержатся ошибки, но и предложит их исправить. Если же документ собран совсем неправильно, ворд лишь ругнется и откажется открывать.

Вставляем текст

Для получения требуемого XML текста я использовал тот же подход ламера: печатал текст в ворде, извлекал внутренности и изучал. Вот какой XML у меня получился для обычного абзаца:
<w:p w:rsidR="00BB20FC" w:rsidRPr="00357A74" w:rsidRDefault="00357A74" w:rsidP="00BB20FC">
<w:pPr>
<w:jc w:val="left"/>
<w:rPr>
<w:sz w:val="28"/>
<w:lang w:val="en-US"/>
</w:rPr>
</w:pPr>
<w:r w:rsidRPr="00357A74">
<w:rPr>
<w:sz w:val="28"/>
<w:lang w:val="en-US"/>
</w:rPr>
<w:t>{TEXT}</w:t>
</w:r>
</w:p>

Нетрудно понять, что нужно изменить, чтобы получить требуемое выравнивание и размер текста. В тег w:t вставляем наш текст, но без переноса строк!
Вводим в наш класс метод assign, и генератор становится таким:
class Word extends ZipArchive{

// Файлы для включения в архив
private $files;

// Путь к шаблону
public $path;

// Содержимое документа
protected $content;

public function __construct($filename, $template_path = '/template/' ){

// Путь к шаблону
$this->path = dirname(__FILE__) . $template_path;

// Если не получилось открыть файл, то жизнь бессмысленна.
if ($this->open($filename, ZIPARCHIVE::CREATE) !== TRUE) {
die("Unable to open <$filename>n");
}

// Структура документа
$this->files = array(
"word/_rels/document.xml.rels",
"word/theme/theme1.xml",
"word/fontTable.xml",
"word/settings.xml",
"word/styles.xml",
"word/stylesWithEffects.xml",
"word/webSettings.xml",
"_rels/.rels",
"docProps/app.xml",
"docProps/core.xml",
"[Content_Types].xml" );

// Добавляем каждый файл в цикле
foreach( $this->files as $f )
$this->addFile($this->path . $f , $f );
}

// Регистрируем текст
public function assign( $text = '' ){

// Берем шаблон абзаца
$p = file_get_contents( $this->path . 'p.xml' );

// Нам нужно разбить текст по строкам
$text_array = explode( "n", $text );

foreach( $text_array as $str )
$this->content .= str_replace( '{TEXT}', $str, $p );
}

// Упаковываем архив
public function create(){

// Добавляем содержимое
$this->addFromString("word/document.xml", str_replace( '{CONTENT}', $this->content, file_get_contents( $this->path . "word/document.xml" ) ) );

$this->close();
}
}

$w = new Word( "Пример.docx" );

$w->assign('Пример текста.
Будущее не предопределено.');

$w->create();

Вот в принципе и всё. В следующий раз мы научимся вставлять изображения.
Просто, не правда ли? Весь код с примером доступен на гитхабе.

Автор: alexios

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js