1 | <?php |
||||
2 | |||||
3 | namespace PhpDocxTemplate; |
||||
4 | |||||
5 | use DOMDocument; |
||||
6 | use Exception; |
||||
7 | use RecursiveIteratorIterator; |
||||
8 | use RecursiveDirectoryIterator; |
||||
9 | use PhpDocxTemplate\Escaper\RegExp; |
||||
10 | use ZipArchive; |
||||
11 | |||||
12 | /** |
||||
13 | * Class DocxDocument |
||||
14 | * |
||||
15 | * @package PhpDocxTemplate |
||||
16 | */ |
||||
17 | class DocxDocument |
||||
18 | { |
||||
19 | private $path; |
||||
20 | private $tmpDir; |
||||
21 | private $document; |
||||
22 | private $zipClass; |
||||
23 | private $tempDocumentMainPart; |
||||
24 | private $tempDocumentRelations = []; |
||||
25 | private $tempDocumentContentTypes = ''; |
||||
26 | private $tempDocumentNewImages = []; |
||||
27 | private $tempDocumentHeaders = []; |
||||
28 | private $tempDocumentFooters = []; |
||||
29 | |||||
30 | /** |
||||
31 | * Construct an instance of Document |
||||
32 | * |
||||
33 | * @param string $path - path to the document |
||||
34 | * |
||||
35 | * @throws Exception |
||||
36 | */ |
||||
37 | 12 | public function __construct(string $path) |
|||
38 | { |
||||
39 | 12 | if (file_exists($path)) { |
|||
40 | 12 | $this->path = $path; |
|||
41 | 12 | $this->tmpDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid("", true) . date("His"); |
|||
42 | 12 | $this->zipClass = new ZipArchive(); |
|||
43 | 12 | $this->extract(); |
|||
44 | } else { |
||||
45 | throw new Exception("The template " . $path . " was not found!"); |
||||
46 | } |
||||
47 | 12 | } |
|||
48 | |||||
49 | /** |
||||
50 | * Extract (unzip) document contents |
||||
51 | */ |
||||
52 | 12 | private function extract(): void |
|||
53 | { |
||||
54 | 12 | if (file_exists($this->tmpDir) && is_dir($this->tmpDir)) { |
|||
55 | $this->rrmdir($this->tmpDir); |
||||
56 | } |
||||
57 | |||||
58 | 12 | mkdir($this->tmpDir); |
|||
59 | |||||
60 | 12 | $this->zipClass->open($this->path); |
|||
61 | 12 | $this->zipClass->extractTo($this->tmpDir); |
|||
62 | |||||
63 | 12 | $index = 1; |
|||
64 | 12 | while (false !== $this->zipClass->locateName($this->getHeaderName($index))) { |
|||
65 | 1 | $this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index)); |
|||
66 | 1 | $index++; |
|||
67 | } |
||||
68 | 12 | $index = 1; |
|||
69 | 12 | while (false !== $this->zipClass->locateName($this->getFooterName($index))) { |
|||
70 | 1 | $this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index)); |
|||
71 | 1 | $index++; |
|||
72 | } |
||||
73 | |||||
74 | 12 | $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName()); |
|||
75 | |||||
76 | 12 | $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName()); |
|||
77 | |||||
78 | 12 | $this->document = file_get_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR)); |
|||
79 | 12 | } |
|||
80 | |||||
81 | /** |
||||
82 | * Get document main part |
||||
83 | * |
||||
84 | * @return string |
||||
85 | */ |
||||
86 | 1 | public function getDocumentMainPart(): string |
|||
87 | { |
||||
88 | 1 | return $this->tempDocumentMainPart; |
|||
89 | } |
||||
90 | |||||
91 | /** |
||||
92 | * @return array |
||||
93 | */ |
||||
94 | 7 | public function getHeaders(): array |
|||
95 | { |
||||
96 | 7 | return $this->tempDocumentHeaders; |
|||
97 | } |
||||
98 | |||||
99 | /** |
||||
100 | * @return array |
||||
101 | */ |
||||
102 | 7 | public function getFooters(): array |
|||
103 | { |
||||
104 | 7 | return $this->tempDocumentFooters; |
|||
105 | } |
||||
106 | |||||
107 | 7 | public function setHeaders(array $headers): void |
|||
108 | { |
||||
109 | 7 | $this->tempDocumentHeaders = $headers; |
|||
110 | 7 | } |
|||
111 | |||||
112 | 7 | public function setFooters(array $footers): void |
|||
113 | { |
||||
114 | 7 | $this->tempDocumentFooters = $footers; |
|||
115 | 7 | } |
|||
116 | |||||
117 | /** |
||||
118 | * Get the name of main part document (method from PhpOffice\PhpWord) |
||||
119 | * |
||||
120 | * @return string |
||||
121 | */ |
||||
122 | 12 | public function getMainPartName(): string |
|||
123 | { |
||||
124 | 12 | $contentTypes = $this->zipClass->getFromName('[Content_Types].xml'); |
|||
125 | |||||
126 | $pattern = '~PartName="\/(word\/document.*?\.xml)" ' . |
||||
127 | 'ContentType="application\/vnd\.openxmlformats-officedocument' . |
||||
128 | 12 | '\.wordprocessingml\.document\.main\+xml"~'; |
|||
129 | |||||
130 | 12 | $matches = []; |
|||
131 | 12 | preg_match($pattern, $contentTypes, $matches); |
|||
132 | |||||
133 | 12 | return array_key_exists(1, $matches) ? $matches[1] : sprintf('word%sdocument.xml', DIRECTORY_SEPARATOR); |
|||
134 | } |
||||
135 | |||||
136 | /** |
||||
137 | * @return string |
||||
138 | */ |
||||
139 | 12 | private function getDocumentContentTypesName(): string |
|||
140 | { |
||||
141 | 12 | return '[Content_Types].xml'; |
|||
142 | } |
||||
143 | |||||
144 | /** |
||||
145 | * Read document part (method from PhpOffice\PhpWord) |
||||
146 | * |
||||
147 | * @param string $fileName |
||||
148 | * |
||||
149 | * @return string |
||||
150 | */ |
||||
151 | 12 | private function readPartWithRels(string $fileName): string |
|||
152 | { |
||||
153 | 12 | $relsFileName = $this->getRelationsName($fileName); |
|||
154 | 12 | $partRelations = $this->zipClass->getFromName($relsFileName); |
|||
155 | 12 | if ($partRelations !== false) { |
|||
156 | 12 | $this->tempDocumentRelations[$fileName] = $partRelations; |
|||
157 | } |
||||
158 | |||||
159 | 12 | return $this->fixBrokenMacros($this->zipClass->getFromName($fileName)); |
|||
160 | } |
||||
161 | |||||
162 | /** |
||||
163 | * Get the name of the relations file for document part (method from PhpOffice\PhpWord) |
||||
164 | * |
||||
165 | * @param string $documentPartName |
||||
166 | * |
||||
167 | * @return string |
||||
168 | */ |
||||
169 | 12 | private function getRelationsName(string $documentPartName): string |
|||
170 | { |
||||
171 | 12 | return sprintf('word%s_rels%s%s.rels', DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR, pathinfo($documentPartName, PATHINFO_BASENAME)); |
|||
0 ignored issues
–
show
Bug
introduced
by
![]() |
|||||
172 | } |
||||
173 | |||||
174 | 1 | public function getNextRelationsIndex(string $documentPartName): int |
|||
175 | { |
||||
176 | 1 | if (isset($this->tempDocumentRelations[$documentPartName])) { |
|||
177 | 1 | $candidate = substr_count($this->tempDocumentRelations[$documentPartName], '<Relationship'); |
|||
178 | 1 | while (strpos($this->tempDocumentRelations[$documentPartName], 'Id="rId' . $candidate . '"') !== false) { |
|||
179 | $candidate++; |
||||
180 | } |
||||
181 | |||||
182 | 1 | return $candidate; |
|||
183 | } |
||||
184 | |||||
185 | return 1; |
||||
186 | } |
||||
187 | |||||
188 | /** |
||||
189 | * Finds parts of broken macros and sticks them together (method from PhpOffice\PhpWord) |
||||
190 | * |
||||
191 | * @param string $documentPart |
||||
192 | * |
||||
193 | * @return string |
||||
194 | */ |
||||
195 | 12 | private function fixBrokenMacros(string $documentPart): string |
|||
196 | { |
||||
197 | 12 | return preg_replace_callback( |
|||
198 | 12 | '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U', |
|||
199 | function ($match) { |
||||
200 | return strip_tags($match[0]); |
||||
201 | 12 | }, |
|||
202 | $documentPart |
||||
203 | ); |
||||
204 | } |
||||
205 | |||||
206 | /** |
||||
207 | * @param string $macro |
||||
208 | * |
||||
209 | * @return string |
||||
210 | */ |
||||
211 | protected static function ensureMacroCompleted(string $macro): string |
||||
212 | { |
||||
213 | if (substr($macro, 0, 2) !== '{{' && substr($macro, -1) !== '}}') { |
||||
214 | $macro = '{{' . $macro . '}}'; |
||||
215 | } |
||||
216 | return $macro; |
||||
217 | } |
||||
218 | |||||
219 | /** |
||||
220 | * Get the name of the header file for $index. |
||||
221 | * |
||||
222 | * @param int $index |
||||
223 | * |
||||
224 | * @return string |
||||
225 | */ |
||||
226 | 12 | private function getHeaderName(int $index): string |
|||
227 | { |
||||
228 | 12 | return sprintf('word%sheader%d.xml', DIRECTORY_SEPARATOR, $index); |
|||
229 | } |
||||
230 | |||||
231 | /** |
||||
232 | * Get the name of the footer file for $index. |
||||
233 | * |
||||
234 | * @param int $index |
||||
235 | * |
||||
236 | * @return string |
||||
237 | */ |
||||
238 | 12 | private function getFooterName(int $index): string |
|||
239 | { |
||||
240 | 12 | return sprintf('word%sfooter%d.xml', DIRECTORY_SEPARATOR, $index); |
|||
241 | } |
||||
242 | |||||
243 | /** |
||||
244 | * Find all variables in $documentPartXML. |
||||
245 | * |
||||
246 | * @param string $documentPartXML |
||||
247 | * |
||||
248 | * @return string[] |
||||
249 | */ |
||||
250 | private function getVariablesForPart(string $documentPartXML): array |
||||
251 | { |
||||
252 | $matches = array(); |
||||
253 | preg_match_all('/\{\{(.*?)\}\}/i', $documentPartXML, $matches); |
||||
254 | return $matches[1]; |
||||
255 | } |
||||
256 | |||||
257 | private function getImageArgs(string $varNameWithArgs): array |
||||
258 | { |
||||
259 | $varElements = explode(':', $varNameWithArgs); |
||||
260 | array_shift($varElements); // first element is name of variable => remove it |
||||
261 | |||||
262 | $varInlineArgs = array(); |
||||
263 | // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396 |
||||
264 | foreach ($varElements as $argIdx => $varArg) { |
||||
265 | if (strpos($varArg, '=')) { // arg=value |
||||
266 | list($argName, $argValue) = explode('=', $varArg, 2); |
||||
267 | $argName = strtolower($argName); |
||||
268 | if ($argName == 'size') { |
||||
269 | list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2); |
||||
270 | } else { |
||||
271 | $varInlineArgs[strtolower($argName)] = $argValue; |
||||
272 | } |
||||
273 | } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40 |
||||
274 | list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2); |
||||
275 | } else { // :60:40:f |
||||
276 | switch ($argIdx) { |
||||
277 | case 0: |
||||
278 | $varInlineArgs['width'] = $varArg; |
||||
279 | break; |
||||
280 | case 1: |
||||
281 | $varInlineArgs['height'] = $varArg; |
||||
282 | break; |
||||
283 | case 2: |
||||
284 | $varInlineArgs['ratio'] = $varArg; |
||||
285 | break; |
||||
286 | } |
||||
287 | } |
||||
288 | } |
||||
289 | |||||
290 | return $varInlineArgs; |
||||
291 | } |
||||
292 | |||||
293 | /** |
||||
294 | * @param mixed $replaceImage |
||||
295 | * @param array $varInlineArgs |
||||
296 | * |
||||
297 | * @return array |
||||
298 | */ |
||||
299 | 1 | public function prepareImageAttrs($replaceImage, array $varInlineArgs = []): array |
|||
300 | { |
||||
301 | // get image path and size |
||||
302 | 1 | $width = null; |
|||
303 | 1 | $height = null; |
|||
304 | 1 | $unit = null; |
|||
305 | 1 | $ratio = null; |
|||
306 | |||||
307 | // a closure can be passed as replacement value which after resolving, can contain the replacement info for the image |
||||
308 | // use case: only when a image if found, the replacement tags can be generated |
||||
309 | 1 | if (is_callable($replaceImage)) { |
|||
310 | $replaceImage = $replaceImage(); |
||||
311 | } |
||||
312 | |||||
313 | 1 | if (is_array($replaceImage) && isset($replaceImage['path'])) { |
|||
314 | 1 | $imgPath = $replaceImage['path']; |
|||
315 | 1 | if (isset($replaceImage['width'])) { |
|||
316 | 1 | $width = $replaceImage['width']; |
|||
317 | } |
||||
318 | 1 | if (isset($replaceImage['height'])) { |
|||
319 | 1 | $height = $replaceImage['height']; |
|||
320 | } |
||||
321 | 1 | if (isset($replaceImage['unit'])) { |
|||
322 | 1 | $unit = $replaceImage['unit']; |
|||
323 | } |
||||
324 | 1 | if (isset($replaceImage['ratio'])) { |
|||
325 | 1 | $ratio = $replaceImage['ratio']; |
|||
326 | } |
||||
327 | } else { |
||||
328 | $imgPath = $replaceImage; |
||||
329 | } |
||||
330 | |||||
331 | 1 | $width = $this->chooseImageDimension($width, $unit ? $unit : 'px', isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115); |
|||
332 | 1 | $height = $this->chooseImageDimension($height, $unit ? $unit : 'px', isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70); |
|||
333 | |||||
334 | 1 | $imageData = @getimagesize($imgPath); |
|||
335 | 1 | if (!is_array($imageData)) { |
|||
336 | throw new Exception(sprintf('Invalid image: %s', $imgPath)); |
||||
337 | } |
||||
338 | 1 | list($actualWidth, $actualHeight, $imageType) = $imageData; |
|||
339 | |||||
340 | // fix aspect ratio (by default) |
||||
341 | 1 | if (is_null($ratio) && isset($varInlineArgs['ratio'])) { |
|||
342 | $ratio = $varInlineArgs['ratio']; |
||||
343 | } |
||||
344 | 1 | if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) { |
|||
345 | 1 | $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight); |
|||
346 | } |
||||
347 | |||||
348 | $imageAttrs = array( |
||||
349 | 1 | 'src' => $imgPath, |
|||
350 | 1 | 'mime' => image_type_to_mime_type($imageType), |
|||
351 | 1 | 'width' => $width * 9525, |
|||
352 | 1 | 'height' => $height * 9525, |
|||
353 | ); |
||||
354 | |||||
355 | 1 | return $imageAttrs; |
|||
356 | } |
||||
357 | |||||
358 | /** |
||||
359 | * @param mixed $width |
||||
360 | * @param mixed $height |
||||
361 | * @param int $actualWidth |
||||
362 | * @param int $actualHeight |
||||
363 | */ |
||||
364 | 1 | private function fixImageWidthHeightRatio(&$width, &$height, int $actualWidth, int $actualHeight): void |
|||
365 | { |
||||
366 | 1 | $imageRatio = $actualWidth / $actualHeight; |
|||
367 | |||||
368 | 1 | if (($width === '') && ($height === '')) { // defined size are empty |
|||
369 | $width = $actualWidth; |
||||
370 | $height = $actualHeight; |
||||
371 | 1 | } elseif ($width === '') { // defined width is empty |
|||
372 | $heightFloat = (float)$height; |
||||
373 | $width = $heightFloat * $imageRatio; |
||||
374 | 1 | } elseif ($height === '') { // defined height is empty |
|||
375 | $widthFloat = (float)$width; |
||||
376 | $height = $widthFloat / $imageRatio; |
||||
377 | } |
||||
378 | 1 | } |
|||
379 | |||||
380 | /** |
||||
381 | * @param mixed $baseValue |
||||
382 | * @param string $unit |
||||
383 | * @param int|null $inlineValue |
||||
384 | * @param int $defaultValue |
||||
385 | */ |
||||
386 | 1 | private function chooseImageDimension($baseValue, string $unit, ?int $inlineValue, int $defaultValue): string |
|||
387 | { |
||||
388 | 1 | $value = $baseValue; |
|||
389 | 1 | if (is_null($value) && isset($inlineValue)) { |
|||
390 | $value = $inlineValue; |
||||
391 | } |
||||
392 | 1 | if (is_null($value)) { |
|||
393 | $value = $defaultValue; |
||||
394 | } |
||||
395 | 1 | switch ($unit) { |
|||
396 | 1 | case 'mm': |
|||
397 | $value = $value * 3.8; // 1mm = 3.8px |
||||
398 | break; |
||||
399 | 1 | case 'pt': |
|||
400 | $value = $value / 3 * 4; // 1pt = 4/3 px |
||||
401 | break; |
||||
402 | 1 | case 'pc': |
|||
403 | $value = $value * 16; // 1px = 16px |
||||
404 | break; |
||||
405 | } |
||||
406 | 1 | return $value; |
|||
407 | } |
||||
408 | |||||
409 | 1 | public function addImageToRelations(string $partFileName, string $rid, string $imgPath, string $imageMimeType): void |
|||
410 | { |
||||
411 | // define templates |
||||
412 | 1 | $typeTpl = '<Override PartName="/word/media/{IMG}" ContentType="image/{EXT}"/>'; |
|||
413 | 1 | $relationTpl = '<Relationship Id="{RID}" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="media/{IMG}"/>'; |
|||
414 | 1 | $newRelationsTpl = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>' . "\n" . '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"></Relationships>'; |
|||
415 | 1 | $newRelationsTypeTpl = '<Override PartName="/{RELS}" ContentType="application/vnd.openxmlformats-package.relationships+xml"/>'; |
|||
416 | $extTransform = array( |
||||
417 | 1 | 'image/jpeg' => 'jpeg', |
|||
418 | 'image/png' => 'png', |
||||
419 | 'image/bmp' => 'bmp', |
||||
420 | 'image/gif' => 'gif', |
||||
421 | ); |
||||
422 | //tempDocumentRelations |
||||
423 | |||||
424 | // get image embed name |
||||
425 | 1 | if (isset($this->tempDocumentNewImages[$imgPath])) { |
|||
426 | $imgName = $this->tempDocumentNewImages[$imgPath]; |
||||
427 | } else { |
||||
428 | // transform extension |
||||
429 | 1 | if (isset($extTransform[$imageMimeType])) { |
|||
430 | 1 | $imgExt = $extTransform[$imageMimeType]; |
|||
431 | } else { |
||||
432 | throw new Exception("Unsupported image type $imageMimeType"); |
||||
433 | } |
||||
434 | |||||
435 | // add image to document |
||||
436 | 1 | $imgName = 'image_' . $rid . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt; |
|||
0 ignored issues
–
show
Are you sure
pathinfo($partFileName, ...late\PATHINFO_FILENAME) of type array|string can be used in concatenation ?
(
Ignorable by Annotation
)
If this is a false-positive, you can also ignore this issue in your code via the
![]() |
|||||
437 | 1 | $this->tempDocumentNewImages[$imgPath] = $imgName; |
|||
438 | |||||
439 | 1 | $targetDir = sprintf('%s%sword%smedia', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR); |
|||
440 | 1 | if (!file_exists($targetDir)) { |
|||
441 | mkdir($targetDir, 0777, true); |
||||
442 | } |
||||
443 | 1 | copy($imgPath, sprintf('%s%s%s', $targetDir, DIRECTORY_SEPARATOR, $imgName)); |
|||
444 | |||||
445 | // setup type for image |
||||
446 | 1 | $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl); |
|||
447 | 1 | $this->tempDocumentContentTypes = str_replace('</Types>', $xmlImageType, $this->tempDocumentContentTypes) . '</Types>'; |
|||
448 | } |
||||
449 | |||||
450 | 1 | $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl); |
|||
451 | |||||
452 | 1 | if (!isset($this->tempDocumentRelations[$partFileName])) { |
|||
453 | // create new relations file |
||||
454 | $this->tempDocumentRelations[$partFileName] = $newRelationsTpl; |
||||
455 | // and add it to content types |
||||
456 | $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl); |
||||
457 | $this->tempDocumentContentTypes = str_replace('</Types>', $xmlRelationsType, $this->tempDocumentContentTypes) . '</Types>'; |
||||
458 | } |
||||
459 | |||||
460 | // add image to relations |
||||
461 | 1 | $this->tempDocumentRelations[$partFileName] = str_replace('</Relationships>', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . '</Relationships>'; |
|||
462 | 1 | } |
|||
463 | |||||
464 | 1 | public function getImageTemplate(): string |
|||
465 | { |
||||
466 | 1 | return '</w:t></w:r><w:r><w:drawing><wp:inline xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture" xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"> <wp:extent cx="{WIDTH}" cy="{HEIGHT}"/> <wp:docPr id="{IMAGEID}" name=""/> <wp:cNvGraphicFramePr> <a:graphicFrameLocks noChangeAspect="1"/> </wp:cNvGraphicFramePr> <a:graphic> <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture"> <pic:pic> <pic:nvPicPr> <pic:cNvPr id="{IMAGEID}" name=""/> <pic:cNvPicPr/> </pic:nvPicPr> <pic:blipFill> <a:blip r:embed="rId{IMAGEID}"/> <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> </pic:spPr> </pic:pic> </a:graphicData> </a:graphic> </wp:inline> </w:drawing></w:r><w:r><w:t xml:space="preserve">'; |
|||
467 | } |
||||
468 | |||||
469 | /** |
||||
470 | * Find and replace macros in the given XML section. |
||||
471 | * |
||||
472 | * @param mixed $search |
||||
473 | * @param mixed $replace |
||||
474 | * @param string $documentPartXML |
||||
475 | * |
||||
476 | * @return string |
||||
477 | */ |
||||
478 | protected function setValueForPart($search, $replace, string $documentPartXML): string |
||||
479 | { |
||||
480 | // Note: we can't use the same function for both cases here, because of performance considerations. |
||||
481 | $regExpEscaper = new RegExp(); |
||||
482 | |||||
483 | return preg_replace($regExpEscaper->escape($search), $replace, $documentPartXML); |
||||
484 | } |
||||
485 | |||||
486 | /** |
||||
487 | * Get document.xml contents as DOMDocument |
||||
488 | * |
||||
489 | * @return DOMDocument |
||||
490 | */ |
||||
491 | 9 | public function getDOMDocument(): DOMDocument |
|||
492 | { |
||||
493 | 9 | $dom = new DOMDocument(); |
|||
494 | |||||
495 | 9 | $dom->loadXML($this->document); |
|||
496 | 9 | return $dom; |
|||
497 | } |
||||
498 | |||||
499 | /** |
||||
500 | * Update document.xml contents |
||||
501 | * |
||||
502 | * @param DOMDocument $dom - new contents |
||||
503 | */ |
||||
504 | 7 | public function updateDOMDocument(DOMDocument $dom): void |
|||
505 | { |
||||
506 | 7 | $this->document = $dom->saveXml(); |
|||
507 | 7 | file_put_contents(sprintf('%s%sword%sdocument.xml', $this->tmpDir, DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR), $this->document); |
|||
508 | 7 | } |
|||
509 | |||||
510 | /** |
||||
511 | * Fix table corruption |
||||
512 | * |
||||
513 | * @param string $xml - xml to fix |
||||
514 | * |
||||
515 | * @return DOMDocument |
||||
516 | */ |
||||
517 | 7 | public function fixTables(string $xml): DOMDocument |
|||
518 | { |
||||
519 | 7 | $dom = new DOMDocument(); |
|||
520 | 7 | $dom->loadXML($xml); |
|||
521 | 7 | $tables = $dom->getElementsByTagName('tbl'); |
|||
522 | 7 | foreach ($tables as $table) { |
|||
523 | 3 | $columns = []; |
|||
524 | 3 | $columnsLen = 0; |
|||
525 | 3 | $toAdd = 0; |
|||
526 | 3 | $tableGrid = null; |
|||
527 | 3 | foreach ($table->childNodes as $el) { |
|||
528 | 3 | if ($el->nodeName == 'w:tblGrid') { |
|||
529 | 3 | $tableGrid = $el; |
|||
530 | 3 | foreach ($el->childNodes as $col) { |
|||
531 | 3 | if ($col->nodeName == 'w:gridCol') { |
|||
532 | 3 | $columns[] = $col; |
|||
533 | 3 | $columnsLen += 1; |
|||
534 | } |
||||
535 | } |
||||
536 | 3 | } elseif ($el->nodeName == 'w:tr') { |
|||
537 | 3 | $cellsLen = 0; |
|||
538 | 3 | foreach ($el->childNodes as $col) { |
|||
539 | 3 | if ($col->nodeName == 'w:tc') { |
|||
540 | 3 | $cellsLen += 1; |
|||
541 | } |
||||
542 | } |
||||
543 | 3 | if (($columnsLen + $toAdd) < $cellsLen) { |
|||
544 | $toAdd = $cellsLen - $columnsLen; |
||||
545 | } |
||||
546 | } |
||||
547 | } |
||||
548 | |||||
549 | // add columns, if necessary |
||||
550 | 3 | if (!is_null($tableGrid) && $toAdd > 0) { |
|||
551 | $width = 0; |
||||
552 | foreach ($columns as $col) { |
||||
553 | if (!is_null($col->getAttribute('w:w'))) { |
||||
554 | $width += $col->getAttribute('w:w'); |
||||
555 | } |
||||
556 | } |
||||
557 | if ($width > 0) { |
||||
558 | $oldAverage = $width / $columnsLen; |
||||
559 | $newAverage = round($width / ($columnsLen + $toAdd)); |
||||
560 | foreach ($columns as $col) { |
||||
561 | $col->setAttribute('w:w', round($col->getAttribute('w:w') * $newAverage / $oldAverage)); |
||||
562 | } |
||||
563 | while ($toAdd > 0) { |
||||
564 | $newCol = $dom->createElement("w:gridCol"); |
||||
565 | $newCol->setAttribute('w:w', $newAverage); |
||||
566 | $tableGrid->appendChild($newCol); |
||||
567 | $toAdd -= 1; |
||||
568 | } |
||||
569 | } |
||||
570 | } |
||||
571 | |||||
572 | // remove columns, if necessary |
||||
573 | 3 | $columns = []; |
|||
574 | 3 | foreach ($tableGrid->childNodes as $col) { |
|||
575 | 3 | if ($col->nodeName == 'w:gridCol') { |
|||
576 | 3 | $columns[] = $col; |
|||
577 | } |
||||
578 | } |
||||
579 | 3 | $columnsLen = count($columns); |
|||
580 | |||||
581 | 3 | $cellsLen = 0; |
|||
582 | 3 | $cellsLenMax = 0; |
|||
583 | 3 | foreach ($table->childNodes as $el) { |
|||
584 | 3 | if ($el->nodeName == 'w:tr') { |
|||
585 | 3 | $cells = []; |
|||
586 | 3 | foreach ($el->childNodes as $col) { |
|||
587 | 3 | if ($col->nodeName == 'w:tc') { |
|||
588 | 3 | $cells[] = $col; |
|||
589 | } |
||||
590 | } |
||||
591 | 3 | $cellsLen = $this->getCellLen($cells); |
|||
592 | 3 | $cellsLenMax = max($cellsLenMax, $cellsLen); |
|||
593 | } |
||||
594 | } |
||||
595 | 3 | $toRemove = $cellsLen - $cellsLenMax; |
|||
596 | 3 | if ($toRemove > 0) { |
|||
597 | $removedWidth = 0.0; |
||||
598 | for ($i = $columnsLen - 1; ($i + 1) >= $toRemove; $i -= 1) { |
||||
599 | $extraCol = $columns[$i]; |
||||
600 | $removedWidth += $extraCol->getAttribute('w:w'); |
||||
601 | $tableGrid->removeChild($extraCol); |
||||
602 | } |
||||
603 | |||||
604 | $columnsLeft = []; |
||||
605 | foreach ($tableGrid->childNodes as $col) { |
||||
606 | if ($col->nodeName == 'w:gridCol') { |
||||
607 | $columnsLeft[] = $col; |
||||
608 | } |
||||
609 | } |
||||
610 | $extraSpace = 0; |
||||
611 | if (count($columnsLeft) > 0) { |
||||
612 | $extraSpace = $removedWidth / count($columnsLeft); |
||||
613 | } |
||||
614 | foreach ($columnsLeft as $col) { |
||||
615 | $col->setAttribute('w:w', round($col->getAttribute('w:w') + $extraSpace)); |
||||
616 | } |
||||
617 | } |
||||
618 | } |
||||
619 | 7 | return $dom; |
|||
620 | } |
||||
621 | |||||
622 | /** |
||||
623 | * Get total cells length |
||||
624 | * |
||||
625 | * @param array $cells - cells |
||||
626 | * |
||||
627 | * @return int |
||||
628 | */ |
||||
629 | 3 | private function getCellLen(array $cells): int |
|||
630 | { |
||||
631 | 3 | $total = 0; |
|||
632 | 3 | foreach ($cells as $cell) { |
|||
633 | 3 | foreach ($cell->childNodes as $tc) { |
|||
634 | 3 | if ($tc->nodeName == 'w:tcPr') { |
|||
635 | 3 | foreach ($tc->childNodes as $span) { |
|||
636 | 3 | if ($span->nodeName == 'w:gridSpan') { |
|||
637 | 1 | $total += intval($span->getAttribute('w:val')); |
|||
638 | 1 | break; |
|||
639 | } |
||||
640 | } |
||||
641 | 3 | break; |
|||
642 | } |
||||
643 | } |
||||
644 | } |
||||
645 | 3 | return $total + 1; |
|||
646 | } |
||||
647 | |||||
648 | /** |
||||
649 | * @param string $fileName |
||||
650 | */ |
||||
651 | 3 | protected function savePartWithRels(string $fileName): void |
|||
652 | { |
||||
653 | 3 | if (isset($this->tempDocumentRelations[$fileName])) { |
|||
654 | 3 | $relsFileName = $this->getRelationsName($fileName); |
|||
655 | 3 | $targetDir = dirname($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName); |
|||
656 | 3 | if (!file_exists($targetDir)) { |
|||
657 | mkdir($targetDir, 0777, true); |
||||
658 | } |
||||
659 | 3 | file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $relsFileName, $this->tempDocumentRelations[$fileName]); |
|||
660 | } |
||||
661 | 3 | } |
|||
662 | |||||
663 | /** |
||||
664 | * Save the document to the target path |
||||
665 | * |
||||
666 | * @param string $path - target path |
||||
667 | */ |
||||
668 | 3 | public function save(string $path): void |
|||
669 | { |
||||
670 | 3 | $rootPath = realpath($this->tmpDir); |
|||
671 | |||||
672 | 3 | $zip = new ZipArchive(); |
|||
673 | 3 | $zip->open($path, ZipArchive::CREATE | ZipArchive::OVERWRITE); |
|||
674 | |||||
675 | 3 | $this->savePartWithRels($this->getMainPartName()); |
|||
676 | 3 | file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getDocumentContentTypesName(), $this->tempDocumentContentTypes); |
|||
677 | |||||
678 | 3 | foreach ($this->tempDocumentHeaders as $index => $xml) { |
|||
679 | 1 | file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getHeaderName($index), $xml); |
|||
680 | } |
||||
681 | 3 | foreach ($this->tempDocumentFooters as $index => $xml) { |
|||
682 | 1 | file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $this->getFooterName($index), $xml); |
|||
683 | } |
||||
684 | |||||
685 | 3 | $files = new RecursiveIteratorIterator( |
|||
686 | 3 | new RecursiveDirectoryIterator($rootPath), |
|||
687 | 3 | RecursiveIteratorIterator::LEAVES_ONLY |
|||
688 | ); |
||||
689 | |||||
690 | 3 | foreach ($files as $name => $file) { |
|||
691 | 3 | if (!$file->isDir()) { |
|||
692 | 3 | $filePath = $file->getRealPath(); |
|||
693 | 3 | $relativePath = substr($filePath, strlen($rootPath) + 1); |
|||
694 | 3 | $zip->addFile($filePath, $relativePath); |
|||
695 | } |
||||
696 | } |
||||
697 | |||||
698 | 3 | $zip->close(); |
|||
699 | |||||
700 | 3 | if (isset($this->zipClass)) { |
|||
701 | 3 | $this->zipClass->close(); |
|||
702 | } |
||||
703 | |||||
704 | 3 | $this->rrmdir($this->tmpDir); |
|||
705 | 3 | } |
|||
706 | |||||
707 | /** |
||||
708 | * Remove recursively directory |
||||
709 | * |
||||
710 | * @param string $dir - target directory |
||||
711 | */ |
||||
712 | 8 | private function rrmdir(string $dir): void |
|||
713 | { |
||||
714 | 8 | $objects = scandir($dir); |
|||
715 | 8 | if (is_array($objects)) { |
|||
0 ignored issues
–
show
|
|||||
716 | 8 | foreach ($objects as $object) { |
|||
717 | 8 | if ($object != "." && $object != "..") { |
|||
718 | 8 | if (filetype($dir . DIRECTORY_SEPARATOR . $object) == "dir") { |
|||
719 | 8 | $this->rrmdir($dir . DIRECTORY_SEPARATOR . $object); |
|||
720 | } else { |
||||
721 | 8 | unlink($dir . DIRECTORY_SEPARATOR . $object); |
|||
722 | } |
||||
723 | } |
||||
724 | } |
||||
725 | 8 | reset($objects); |
|||
726 | 8 | rmdir($dir); |
|||
727 | } |
||||
728 | 8 | } |
|||
729 | |||||
730 | /** |
||||
731 | * Close document |
||||
732 | */ |
||||
733 | 5 | public function close(): void |
|||
734 | { |
||||
735 | 5 | if (isset($this->zipClass)) { |
|||
736 | 5 | $this->zipClass->close(); |
|||
737 | } |
||||
738 | 5 | $this->rrmdir($this->tmpDir); |
|||
739 | 5 | } |
|||
740 | } |
||||
741 |