Passed
Push — master ( 6464d5...03fcd4 )
by Tobias
02:17
created

DocxMustache::copyTmplate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 0
1
<?PHP
2
3
namespace WrkLst\DocxMustache;
4
5
use Exception;
6
use Illuminate\Support\Facades\Log;
7
8
//Custom DOCX template class to change content based on mustache templating engine.
9
class DocxMustache
10
{
11
    public $items;
12
    public $word_doc;
13
    public $template_file_name;
14
    public $template_file;
15
    public $local_path;
16
    public $storageDisk;
17
    public $storagePathPrefix;
18
    public $zipper;
19
    public $imageManipulation;
20
    public $verbose;
21
22
    public function __construct($items, $local_template_file)
23
    {
24
        $this->items = $items;
25
        $this->template_file_name = basename($local_template_file);
26
        $this->template_file = $local_template_file;
27
        $this->word_doc = false;
28
        $this->zipper = new \Chumper\Zipper\Zipper;
29
30
        //name of disk for storage
31
        $this->storageDisk = 'local';
32
33
        //prefix within your storage path
34
        $this->storagePathPrefix = 'app/';
35
36
        //if you use img urls that support manipulation via parameter
37
        $this->imageManipulation = ''; //'&w=1800';
38
39
        $this->verbose = false;
40
    }
41
42
    public function execute()
43
    {
44
        $this->copyTmplate();
45
        $this->readTeamplate();
46
    }
47
48
    /**
49
     * @param string $file
50
     */
51
    protected function storagePath($file)
52
    {
53
        return storage_path($file);
54
    }
55
56
    /**
57
     * @param string $msg
58
     */
59
    protected function log($msg)
60
    {
61
        //introduce logging method here to keep track of process
62
        // can be overwritten in extended class to log with custom preocess logger
63
        if($this->verbose)
64
            Log::error($msg);
65
    }
66
67
    public function cleanUpTmpDirs()
68
    {
69
        $now = time();
70
        $isExpired = ($now - (60 * 240));
71
        $disk = \Storage::disk($this->storageDisk);
72
        $all_dirs = $disk->directories($this->storagePathPrefix.'DocxMustache');
73
        foreach ($all_dirs as $dir) {
74
            //delete dirs older than 20min
75
            if ($disk->lastModified($dir) < $isExpired)
76
            {
77
                $disk->deleteDirectory($dir);
78
            }
79
        }
80
    }
81
82
    public function getTmpDir()
83
    {
84
        $this->cleanUpTmpDirs();
85
        $path = $this->storagePathPrefix.'DocxMustache/'.uniqid($this->template_file).'/';
86
        \File::makeDirectory($this->storagePath($path), 0775, true);
87
        return $path;
88
    }
89
90
    public function copyTmplate()
91
    {
92
        $this->log('Get Copy of Template');
93
        $this->local_path = $this->getTmpDir();
94
        \Storage::disk($this->storageDisk)->copy($this->storagePathPrefix.$this->template_file, $this->local_path.$this->template_file_name);
95
    }
96
97
    public function readTeamplate()
98
    {
99
        $this->log('Analyze Template');
100
        //get the main document out of the docx archive
101
        $this->zipper->make($this->storagePath($this->local_path.$this->template_file_name))
102
            ->extractTo($this->storagePath($this->local_path), array('word/document.xml'), \Chumper\Zipper\Zipper::WHITELIST);
103
104
        //if the main document exists
105
        if ($this->word_doc = \Storage::disk($this->storageDisk)->get($this->local_path.'word/document.xml'))
106
        {
107
            $this->log('Merge Data into Template');
108
            $this->word_doc = $this->MustacheTagCleaner($this->word_doc);
109
            $this->word_doc = $this->MustacheRender($this->items, $this->word_doc);
110
            $this->word_doc = $this->convertHtmlToOpenXML($this->word_doc);
111
112
            $this->ImageReplacer();
113
114
            $this->log('Compact Template with Data');
115
            //store new content
116
            \Storage::disk($this->storageDisk)
117
                ->put($this->local_path.'word/document.xml', $this->word_doc);
118
            //add new content to word doc
119
            $this->zipper->folder('word')
120
                ->add($this->storagePath($this->local_path.'word/document.xml'))
121
                ->close();
122
        } else
123
        {
124
            throw new Exception('docx has no main xml doc.');
125
        }
126
    }
127
128
    protected function MustacheTagCleaner($content)
129
    {
130
        //kills all xml tags within curly mustache brackets
131
        //this is necessary, as word might produce unnecesary xml tage inbetween curly backets.
132
        return preg_replace_callback(
133
            '/{{(.*?)}}/',
134
            function($treffer) {
135
                return strip_tags($treffer[0]);
136
            },
137
            $content
138
        );
139
    }
140
141
    protected function MustacheRender($items, $mustache_template)
142
    {
143
        $m = new \Mustache_Engine(array('escape' => function($value) {
144
            if (str_replace('*[[DONOTESCAPE]]*', '', $value) != $value)
145
                return str_replace('*[[DONOTESCAPE]]*', '', $value);
146
            return htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
147
        }));
148
        return $m->render($mustache_template, $items);
149
150
    }
151
152
    protected function AddContentType($imageCt="jpeg")
153
    {
154
        //get content type file from archive
155
        $this->zipper->make($this->storagePath($this->local_path.$this->template_file_name))
156
            ->extractTo($this->storagePath($this->local_path), array('[Content_Types].xml'), \Chumper\Zipper\Zipper::WHITELIST);
157
158
        // load content type file xml
159
        $ct_file = simplexml_load_file($this->storagePath($this->local_path.'[Content_Types].xml'));
160
161
        //check if content type for jpg has been set
162
        $i = 0;
163
        $ct_already_set = false;
164
        foreach ($ct_file as $ct)
165
        {
166
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt)
167
                $ct_already_set = true;
168
            $i++;
169
        }
170
171
        //if content type for jpg has not been set, add it to xml
172
        // and save xml to file and add it to the archive
173
        if (!$ct_already_set)
174
        {
175
            $sxe = $ct_file->addChild('Default');
176
            $sxe->addAttribute('Extension', $imageCt);
177
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
178
179 View Code Duplication
            if ($ct_file_xml = $ct_file->asXML())
180
            {
181
                \Storage::disk($this->storageDisk)->put($this->local_path.'[Content_Types].xml', $ct_file_xml);
182
                $this->zipper->add($this->storagePath($this->local_path.'[Content_Types].xml'));
183
            } else
184
            {
185
                throw new Exception('Cannot generate xml for [Content_Types].xml.');
186
            }
187
        }
188
    }
189
190
    protected function ImageReplacerFetchReplaceableImages(&$main_file, $ns)
191
    {
192
        //set up basic arrays to keep track of imgs
193
        $imgs = array();
194
        $imgs_replaced = array(); // so they can later be removed from media and relation file.
195
        $newIdCounter = 1;
196
197
        //iterate through all drawing containers of the xml document
198
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
199
        {
200
            $ueid = "wrklstId".$newIdCounter;
201
            $wasId = (string) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])["embed"];
202
            $imgs_replaced[$wasId] = $wasId;
203
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])["embed"] = $ueid;
204
205
            $cx = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()["cx"];
206
            $cy = (int) $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()["cy"];
207
208
            //figure out if there is a URL saved in the description field of the img
209
            $img_url = $this->analyseImgUrlString((string) $drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"]);
210
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"] = $img_url["rest"];
211
212
            //check https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
213
            // for EMUs calculation
214
            /*
215
            295px @72 dpi = 1530350 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
216
            413px @72 dpi = 2142490 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
217
218
            */
219
220
            //if there is a url, save this img as a img to be replaced
221
            if (trim($img_url["url"]))
222
            {
223
                $imgs[] = array(
224
                    "cx" => $cx,
225
                    "cy" => $cy,
226
                    "width" => (int) ($cx / 5187.627118644067797),
227
                    "height" => (int) ($cy / 5187.627118644067797),
228
                    "wasId" => $wasId,
229
                    "id" => $ueid,
230
                    "url" => $img_url["url"],
231
                );
232
233
                $newIdCounter++;
234
            }
235
        }
236
        return array(
237
            'imgs' => $imgs,
238
            'imgs_replaced' => $imgs_replaced
239
        );
240
    }
241
242
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
243
    {
244
        //iterate through replaced images and clean rels files from them
245
        foreach ($imgs_replaced as $img_replaced)
246
        {
247
            $i = 0;
248
            foreach ($rels_file as $rel)
249
            {
250
                if ((string) $rel->attributes()['Id'] == $img_replaced)
251
                {
252
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
253
                    unset($rels_file->Relationship[$i]);
254
                }
255
                $i++;
256
            }
257
        }
258
    }
259
260
    protected function InsertImages(&$imgs, &$rels_file, &$main_file)
261
    {
262
        //define what images are allowed
263
        $allowed_imgs = array(
264
            'image/gif' => '.gif',
265
            'image/jpeg' => '.jpeg',
266
            'image/png' => '.png',
267
            'image/bmp' => '.bmp',
268
        );
269
270
        //iterate through replacable images
271
        foreach ($imgs as $k=>$img)
272
        {
273
            //get file type of img and test it against supported imgs
274
            if ($img_file_handle = fopen($img['url'].$this->imageManipulation, "rb"))
275
            {
276
                $img_data = stream_get_contents($img_file_handle);
277
                fclose($img_file_handle);
278
                $fi = new \finfo(FILEINFO_MIME);
279
280
                $image_mime = strstr($fi->buffer($img_data), ';', true);
281
                //dd($image_mime);
282
                if (isset($allowed_imgs[$image_mime]))
283
                {
284
                    $imgs[$k]['img_file_src'] = str_replace("wrklstId", "wrklst_image", $img['id']).$allowed_imgs[$image_mime];
285
                    $imgs[$k]['img_file_dest'] = str_replace("wrklstId", "wrklst_image", $img['id']).'.jpeg';
286
287
                    \Storage::disk($this->storageDisk)->put($this->local_path.'word/media/'.$imgs[$k]['img_file_src'], $img_data);
288
289
                    //rework img to new size and jpg format
290
                    $img_rework = \Image::make($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_src']));
291
                    $w = $img['width'];
292
                    $h = $img['height'];
293
                    if ($w > $h)
294
                        $h = null;
295
                    else
296
                        $w = null;
297
                    $img_rework->resize($w, $h, function($constraint) {
298
                        $constraint->aspectRatio();
299
                        $constraint->upsize();
300
                    });
301
                    $new_height = $img_rework->height();
302
                    $new_width = $img_rework->width();
303
                    $img_rework->save($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_dest']));
304
305
                    $this->zipper->folder('word/media')->add($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_dest']));
306
307
                    $sxe = $rels_file->addChild('Relationship');
308
                    $sxe->addAttribute('Id', $img['id']);
309
                    $sxe->addAttribute('Type', "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image");
310
                    $sxe->addAttribute('Target', "media/".$imgs[$k]['img_file_dest']);
311
312
                    //update height and width of image in document.xml
313
                    $new_height_emus = (int) ($new_height * 5187.627118644067797);
314
                    $new_width_emus = (int) ($new_width * 5187.627118644067797);
315
                    foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
316
                    {
317
                        if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])->blip->attributes($ns['r'])["embed"])
318
                        {
319
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()["cx"] = $new_width_emus;
0 ignored issues
show
Bug introduced by
The variable $ns does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
320
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])->xfrm->ext->attributes()["cy"] = $new_height_emus;
321
322
                            //the following also changes the contraints of the container for the img.
323
                            // probably not wanted, as this will make images larger than the constraints of the placeholder
324
                            /*
325
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cx"] = $new_width_emus;
326
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cy"] = $new_height_emus;
327
                            */
328
                            break;
329
                        }
330
                    }
331
                }
332
            }
333
        }
334
    }
335
336
    protected function ImageReplacer()
337
    {
338
        $this->log('Merge Images into Template');
339
340
        //load main doc xml
341
        $main_file = simplexml_load_string($this->word_doc);
342
343
        //get all namespaces of the document
344
        $ns = $main_file->getNamespaces(true);
345
346
        $replaceableImage = $this->ImageReplacerFetchReplaceableImages($main_file, $ns);
347
        $imgs = $replaceableImage['imgs'];
348
        $imgs_replaced = $replaceableImage['imgs_replaced'];
349
350
351
        //get relation xml file for img relations
352
        $this->zipper->make($this->storagePath($this->local_path.$this->template_file_name))
353
            ->extractTo($this->storagePath($this->local_path), array('word/_rels/document.xml.rels'), \Chumper\Zipper\Zipper::WHITELIST);
354
355
        //load img relations into xml
356
        $rels_file = simplexml_load_file($this->storagePath($this->local_path.'word/_rels/document.xml.rels'));
357
358
        $this->RemoveReplaceImages($imgs_replaced, $rels_file);
359
360
        //add jpg content type if not set
361
        $this->AddContentType('jpeg');
362
363
        $this->InsertImages($imgs, $rels_file, $main_file);
364
365 View Code Duplication
        if ($rels_file_xml = $rels_file->asXML())
366
        {
367
            \Storage::disk($this->storageDisk)->put($this->local_path.'word/_rels/document.xml.rels', $rels_file_xml);
368
            $this->zipper->folder('word/_rels')->add($this->storagePath($this->local_path.'word/_rels/document.xml.rels'));
369
        } else
370
        {
371
            throw new Exception('Cannot generate xml for word/_rels/document.xml.rels.');
372
        }
373
374
        if ($main_file_xml = $main_file->asXML())
375
        {
376
            $this->word_doc = $main_file_xml;
377
        } else
378
        {
379
            throw new Exception('Cannot generate xml for word/document.xml.');
380
        }
381
    }
382
383
    /**
384
     * @param string $string
385
     */
386
    protected function analyseImgUrlString($string)
387
    {
388
        $start = "[IMG-REPLACE]";
389
        $end = "[/IMG-REPLACE]";
390
        $string = ' '.$string;
391
        $ini = strpos($string, $start);
392
        if ($ini == 0)
393
        {
394
            $url = '';
395
            $rest = $string;
396
        } else
397
        {
398
            $ini += strlen($start);
399
            $len = ((strpos($string, $end, $ini)) - $ini);
400
            $url = substr($string, $ini, $len);
401
402
            $ini = strpos($string, $start);
403
            $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
404
            $rest = substr($string, 0, $ini).substr($string, $len);
405
        }
406
        return array(
407
            "url" => $url,
408
            "rest" => $rest,
409
        );
410
    }
411
412
    protected function convertHtmlToOpenXMLTag($value, $tag = "b")
413
    {
414
        $value_array = array();
415
        $run_again = false;
416
        //this could be used instead if html was already escaped
417
        /*
418
        $bo = "&lt;";
419
        $bc = "&gt;";
420
        */
421
        $bo = "<";
422
        $bc = ">";
423
424
        //get first BOLD
425
        $tag_open_values = explode($bo.$tag.$bc, $value, 2);
426
427
        if (count($tag_open_values) > 1)
428
        {
429
            //save everything before the bold and close it
430
            $value_array[] = $tag_open_values[0];
431
            $value_array[] = '</w:t></w:r>';
432
433
            //define styling parameters
434
            $wrPr_open = strrpos($tag_open_values[0], '<w:rPr>');
435
            $wrPr_close = strrpos($tag_open_values[0], '</w:rPr>', $wrPr_open);
436
            $neutral_style = '<w:r><w:rPr>'.substr($tag_open_values[0], ($wrPr_open + 7), ($wrPr_close - ($wrPr_open + 7))).'</w:rPr><w:t>';
437
            $tagged_style = '<w:r><w:rPr><w:'.$tag.'/>'.substr($tag_open_values[0], ($wrPr_open + 7), ($wrPr_close - ($wrPr_open + 7))).'</w:rPr><w:t>';
438
439
            //open new text run and make it bold, include previous styling
440
            $value_array[] = $tagged_style;
441
            //get everything before bold close and after
442
            $tag_close_values = explode($bo.'/'.$tag.$bc, $tag_open_values[1], 2);
443
            //add bold text
444
            $value_array[] = $tag_close_values[0];
445
            //close bold run
446
            $value_array[] = '</w:t></w:r>';
447
            //open run for after bold
448
            $value_array[] = $neutral_style;
449
            $value_array[] = $tag_close_values[1];
450
451
            $run_again = true;
452
        } else {
453
            $value_array[] = $tag_open_values[0];
454
        }
455
456
        $value = implode('', $value_array);
457
458
        if ($run_again) {
459
                    $value = $this->convertHtmlToOpenXMLTag($value, $tag);
460
        }
461
462
        return $value;
463
    }
464
465
    /**
466
     * @param string $value
467
     */
468
    protected function convertHtmlToOpenXML($value)
469
    {
470
        $line_breaks = array("&lt;br /&gt;", "&lt;br/&gt;", "&lt;br&gt;", "<br />", "<br/>", "<br>");
471
        $value = str_replace($line_breaks, '<w:br/>', $value);
472
473
        $value = $this->convertHtmlToOpenXMLTag($value, "b");
474
        $value = $this->convertHtmlToOpenXMLTag($value, "i");
475
        $value = $this->convertHtmlToOpenXMLTag($value, "u");
476
477
        return $value;
478
    }
479
480
    public function saveAsPdf()
481
    {
482
        $this->log('Converting DOCX to PDF');
483
        //convert to pdf with libre office
484
        $command = "soffice --headless --convert-to pdf ".$this->storagePath($this->local_path.$this->template_file_name).' --outdir '.$this->storagePath($this->local_path);
485
        $process = new \Symfony\Component\Process\Process($command);
486
        $process->start();
487
        while ($process->isRunning()) {
488
            //wait until process is ready
489
        }
490
        // executes after the command finishes
491
        if (!$process->isSuccessful()) {
492
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
493
        } else
494
        {
495
            $path_parts = pathinfo($this->storagePath($this->local_path.$this->template_file_name));
496
            return $this->storagePath($this->local_path.$path_parts['filename'].'pdf');
497
        }
498
    }
499
}
500