Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
127 / 127 |
|
100.00% |
10 / 10 |
CRAP | |
100.00% |
1 / 1 |
| Word2007 | |
100.00% |
127 / 127 |
|
100.00% |
10 / 10 |
34 | |
100.00% |
1 / 1 |
| __construct | |
100.00% |
30 / 30 |
|
100.00% |
1 / 1 |
3 | |||
| save | |
100.00% |
29 / 29 |
|
100.00% |
1 / 1 |
6 | |||
| getContentTypes | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| getRelationships | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| addHeaderFooterMedia | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| addHeaderFooterContent | |
100.00% |
11 / 11 |
|
100.00% |
1 / 1 |
3 | |||
| addNotes | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
4 | |||
| addComments | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
| addChart | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
3 | |||
| registerContentTypes | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
6 | |||
| 1 | <?php |
| 2 | |
| 3 | /** |
| 4 | * This file is part of PHPWord - A pure PHP library for reading and writing |
| 5 | * word processing documents. |
| 6 | * |
| 7 | * PHPWord is free software distributed under the terms of the GNU Lesser |
| 8 | * General Public License version 3 as published by the Free Software Foundation. |
| 9 | * |
| 10 | * For the full copyright and license information, please read the LICENSE |
| 11 | * file that was distributed with this source code. For the full list of |
| 12 | * contributors, visit https://github.com/PHPOffice/PHPWord/contributors. |
| 13 | * |
| 14 | * @see https://github.com/PHPOffice/PHPWord |
| 15 | * |
| 16 | * @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3 |
| 17 | */ |
| 18 | |
| 19 | namespace PhpOffice\PhpWord\Writer; |
| 20 | |
| 21 | use PhpOffice\PhpWord\Element\Section; |
| 22 | use PhpOffice\PhpWord\Media; |
| 23 | use PhpOffice\PhpWord\PhpWord; |
| 24 | use PhpOffice\PhpWord\Shared\ZipArchive; |
| 25 | |
| 26 | /** |
| 27 | * Word2007 writer. |
| 28 | */ |
| 29 | class Word2007 extends AbstractWriter implements WriterInterface |
| 30 | { |
| 31 | /** |
| 32 | * Content types values. |
| 33 | * |
| 34 | * @var array |
| 35 | */ |
| 36 | private $contentTypes = ['default' => [], 'override' => []]; |
| 37 | |
| 38 | /** |
| 39 | * Document relationship. |
| 40 | * |
| 41 | * @var array |
| 42 | */ |
| 43 | private $relationships = []; |
| 44 | |
| 45 | /** |
| 46 | * Create new Word2007 writer. |
| 47 | */ |
| 48 | public function __construct(?PhpWord $phpWord = null) |
| 49 | { |
| 50 | // Assign PhpWord |
| 51 | $this->setPhpWord($phpWord); |
| 52 | |
| 53 | // Create parts |
| 54 | // The first four files need to be in this order for Mimetype detection to work |
| 55 | $this->parts = [ |
| 56 | 'ContentTypes' => '[Content_Types].xml', |
| 57 | 'Rels' => '_rels/.rels', |
| 58 | 'RelsDocument' => 'word/_rels/document.xml.rels', |
| 59 | 'Document' => 'word/document.xml', |
| 60 | 'DocPropsApp' => 'docProps/app.xml', |
| 61 | 'DocPropsCore' => 'docProps/core.xml', |
| 62 | 'DocPropsCustom' => 'docProps/custom.xml', |
| 63 | 'Comments' => 'word/comments.xml', |
| 64 | 'Styles' => 'word/styles.xml', |
| 65 | 'Numbering' => 'word/numbering.xml', |
| 66 | 'Settings' => 'word/settings.xml', |
| 67 | 'WebSettings' => 'word/webSettings.xml', |
| 68 | 'FontTable' => 'word/fontTable.xml', |
| 69 | 'Theme' => 'word/theme/theme1.xml', |
| 70 | 'RelsPart' => '', |
| 71 | 'Header' => '', |
| 72 | 'Footer' => '', |
| 73 | 'Footnotes' => '', |
| 74 | 'Endnotes' => '', |
| 75 | 'Chart' => '', |
| 76 | ]; |
| 77 | foreach (array_keys($this->parts) as $partName) { |
| 78 | $partClass = static::class . '\\Part\\' . $partName; |
| 79 | if (class_exists($partClass)) { |
| 80 | /** @var Word2007\Part\AbstractPart $part Type hint */ |
| 81 | $part = new $partClass(); |
| 82 | $part->setParentWriter($this); |
| 83 | $this->writerParts[strtolower($partName)] = $part; |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | // Set package paths |
| 88 | $this->mediaPaths = ['image' => 'word/media/', 'object' => 'word/embeddings/']; |
| 89 | } |
| 90 | |
| 91 | /** |
| 92 | * Save document by name. |
| 93 | */ |
| 94 | public function save(string $filename): void |
| 95 | { |
| 96 | $filename = $this->getTempFile($filename); |
| 97 | $zip = $this->getZipArchive($filename); |
| 98 | $phpWord = $this->getPhpWord(); |
| 99 | |
| 100 | // Content types |
| 101 | $this->contentTypes['default'] = [ |
| 102 | 'rels' => 'application/vnd.openxmlformats-package.relationships+xml', |
| 103 | 'xml' => 'application/xml', |
| 104 | ]; |
| 105 | |
| 106 | // Add section media files |
| 107 | $sectionMedia = Media::getElements('section'); |
| 108 | if (!empty($sectionMedia)) { |
| 109 | $this->addFilesToPackage($zip, $sectionMedia); |
| 110 | $this->registerContentTypes($sectionMedia); |
| 111 | foreach ($sectionMedia as $element) { |
| 112 | $this->relationships[] = $element; |
| 113 | } |
| 114 | } |
| 115 | |
| 116 | // Add header/footer media files & relations |
| 117 | $this->addHeaderFooterMedia($zip, 'header'); |
| 118 | $this->addHeaderFooterMedia($zip, 'footer'); |
| 119 | |
| 120 | // Add header/footer contents |
| 121 | $rId = Media::countElements('section') + 6; //@see Rels::writeDocRels for 6 first elements |
| 122 | $sections = $phpWord->getSections(); |
| 123 | foreach ($sections as $section) { |
| 124 | $this->addHeaderFooterContent($section, $zip, 'header', $rId); |
| 125 | $this->addHeaderFooterContent($section, $zip, 'footer', $rId); |
| 126 | } |
| 127 | |
| 128 | $this->addNotes($zip, $rId, 'footnote'); |
| 129 | $this->addNotes($zip, $rId, 'endnote'); |
| 130 | $this->addComments($zip, $rId); |
| 131 | $this->addChart($zip, $rId); |
| 132 | |
| 133 | // Write parts |
| 134 | foreach ($this->parts as $partName => $fileName) { |
| 135 | if ($fileName != '') { |
| 136 | $zip->addFromString($fileName, $this->getWriterPart($partName)->write()); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | // Close zip archive and cleanup temp file |
| 141 | $zip->close(); |
| 142 | $this->cleanupTempFile(); |
| 143 | } |
| 144 | |
| 145 | /** |
| 146 | * Get content types. |
| 147 | * |
| 148 | * @return array |
| 149 | */ |
| 150 | public function getContentTypes() |
| 151 | { |
| 152 | return $this->contentTypes; |
| 153 | } |
| 154 | |
| 155 | /** |
| 156 | * Get content types. |
| 157 | * |
| 158 | * @return array |
| 159 | */ |
| 160 | public function getRelationships() |
| 161 | { |
| 162 | return $this->relationships; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * Add header/footer media files, e.g. footer1.xml.rels. |
| 167 | * |
| 168 | * @param string $docPart |
| 169 | */ |
| 170 | private function addHeaderFooterMedia(ZipArchive $zip, $docPart): void |
| 171 | { |
| 172 | $elements = Media::getElements($docPart); |
| 173 | if (!empty($elements)) { |
| 174 | foreach ($elements as $file => $media) { |
| 175 | if (count($media) > 0) { |
| 176 | if (!empty($media)) { |
| 177 | $this->addFilesToPackage($zip, $media); |
| 178 | $this->registerContentTypes($media); |
| 179 | } |
| 180 | |
| 181 | /** @var Word2007\Part\AbstractPart $writerPart Type hint */ |
| 182 | $writerPart = $this->getWriterPart('relspart')->setMedia($media); |
| 183 | $zip->addFromString("word/_rels/{$file}.xml.rels", $writerPart->write()); |
| 184 | } |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
| 189 | /** |
| 190 | * Add header/footer content. |
| 191 | * |
| 192 | * @param string $elmType header|footer |
| 193 | * @param int &$rId |
| 194 | */ |
| 195 | private function addHeaderFooterContent(Section &$section, ZipArchive $zip, $elmType, &$rId): void |
| 196 | { |
| 197 | $getFunction = $elmType == 'header' ? 'getHeaders' : 'getFooters'; |
| 198 | $elmCount = ($section->getSectionId() - 1) * 3; |
| 199 | $elements = $section->$getFunction(); |
| 200 | /** @var \PhpOffice\PhpWord\Element\AbstractElement $element Type hint */ |
| 201 | foreach ($elements as &$element) { |
| 202 | ++$elmCount; |
| 203 | $element->setRelationId(++$rId); |
| 204 | $elmFile = "{$elmType}{$elmCount}.xml"; // e.g. footer1.xml |
| 205 | $this->contentTypes['override']["/word/$elmFile"] = $elmType; |
| 206 | $this->relationships[] = ['target' => $elmFile, 'type' => $elmType, 'rID' => $rId]; |
| 207 | |
| 208 | /** @var Word2007\Part\AbstractPart $writerPart Type hint */ |
| 209 | $writerPart = $this->getWriterPart($elmType)->setElement($element); |
| 210 | $zip->addFromString("word/$elmFile", $writerPart->write()); |
| 211 | } |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Add footnotes/endnotes. |
| 216 | * |
| 217 | * @param int &$rId |
| 218 | * @param string $noteType |
| 219 | */ |
| 220 | private function addNotes(ZipArchive $zip, &$rId, $noteType = 'footnote'): void |
| 221 | { |
| 222 | $phpWord = $this->getPhpWord(); |
| 223 | $noteType = ($noteType == 'endnote') ? 'endnote' : 'footnote'; |
| 224 | $partName = "{$noteType}s"; |
| 225 | $method = 'get' . ucfirst($partName); |
| 226 | $collection = $phpWord->$method(); |
| 227 | |
| 228 | // Add footnotes media files, relations, and contents |
| 229 | if ($collection->countItems() > 0) { |
| 230 | $media = Media::getElements($noteType); |
| 231 | $this->addFilesToPackage($zip, $media); |
| 232 | $this->registerContentTypes($media); |
| 233 | $this->contentTypes['override']["/word/{$partName}.xml"] = $partName; |
| 234 | $this->relationships[] = ['target' => "{$partName}.xml", 'type' => $partName, 'rID' => ++$rId]; |
| 235 | |
| 236 | // Write relationships file, e.g. word/_rels/footnotes.xml |
| 237 | if (!empty($media)) { |
| 238 | /** @var Word2007\Part\AbstractPart $writerPart Type hint */ |
| 239 | $writerPart = $this->getWriterPart('relspart')->setMedia($media); |
| 240 | $zip->addFromString("word/_rels/{$partName}.xml.rels", $writerPart->write()); |
| 241 | } |
| 242 | |
| 243 | // Write content file, e.g. word/footnotes.xml |
| 244 | $writerPart = $this->getWriterPart($partName)->setElements($collection->getItems()); |
| 245 | $zip->addFromString("word/{$partName}.xml", $writerPart->write()); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | /** |
| 250 | * Add comments. |
| 251 | * |
| 252 | * @param int &$rId |
| 253 | */ |
| 254 | private function addComments(ZipArchive $zip, &$rId): void |
| 255 | { |
| 256 | $phpWord = $this->getPhpWord(); |
| 257 | $collection = $phpWord->getComments(); |
| 258 | $partName = 'comments'; |
| 259 | |
| 260 | // Add comment relations and contents |
| 261 | if ($collection->countItems() > 0) { |
| 262 | $this->relationships[] = ['target' => "{$partName}.xml", 'type' => $partName, 'rID' => ++$rId]; |
| 263 | |
| 264 | // Write content file, e.g. word/comments.xml |
| 265 | $writerPart = $this->getWriterPart($partName)->setElements($collection->getItems()); |
| 266 | $zip->addFromString("word/{$partName}.xml", $writerPart->write()); |
| 267 | } |
| 268 | } |
| 269 | |
| 270 | /** |
| 271 | * Add chart. |
| 272 | * |
| 273 | * @param int &$rId |
| 274 | */ |
| 275 | private function addChart(ZipArchive $zip, &$rId): void |
| 276 | { |
| 277 | $phpWord = $this->getPhpWord(); |
| 278 | |
| 279 | $collection = $phpWord->getCharts(); |
| 280 | $index = 0; |
| 281 | if ($collection->countItems() > 0) { |
| 282 | /** @var \PhpOffice\PhpWord\Element\Chart $chart */ |
| 283 | foreach ($collection->getItems() as $chart) { |
| 284 | ++$index; |
| 285 | ++$rId; |
| 286 | $filename = "charts/chart{$index}.xml"; |
| 287 | |
| 288 | // ContentTypes.xml |
| 289 | $this->contentTypes['override']["/word/{$filename}"] = 'chart'; |
| 290 | |
| 291 | // word/_rels/document.xml.rel |
| 292 | $this->relationships[] = ['target' => $filename, 'type' => 'chart', 'rID' => $rId]; |
| 293 | |
| 294 | // word/charts/chartN.xml |
| 295 | $chart->setRelationId($rId); |
| 296 | $writerPart = $this->getWriterPart('Chart'); |
| 297 | $writerPart->setElement($chart); |
| 298 | $zip->addFromString("word/{$filename}", $writerPart->write()); |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | /** |
| 304 | * Register content types for each media. |
| 305 | * |
| 306 | * @param array $media |
| 307 | */ |
| 308 | private function registerContentTypes($media): void |
| 309 | { |
| 310 | foreach ($media as $medium) { |
| 311 | $mediumType = $medium['type']; |
| 312 | if ($mediumType == 'image') { |
| 313 | $extension = $medium['imageExtension']; |
| 314 | if (!isset($this->contentTypes['default'][$extension])) { |
| 315 | $this->contentTypes['default'][$extension] = $medium['imageType']; |
| 316 | } |
| 317 | } elseif ($mediumType == 'object') { |
| 318 | if (!isset($this->contentTypes['default']['bin'])) { |
| 319 | $this->contentTypes['default']['bin'] = 'application/vnd.openxmlformats-officedocument.oleObject'; |
| 320 | } |
| 321 | } |
| 322 | } |
| 323 | } |
| 324 | } |