Passed
Push — master ( 87b6ec...820633 )
by Tobias
02:26
created

DocxMustache::analyseImgUrlString()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 25
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 25
rs 8.8571
cc 2
eloc 18
nc 2
nop 1
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
    /**
98
     * @param string $value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
99
     */
100
    protected function exctractOpenXmlFile($file)
101
    {
102
        $this->zipper->make($this->storagePath($this->local_path.$this->template_file_name))
103
            ->extractTo($this->storagePath($this->local_path), array($file), \Chumper\Zipper\Zipper::WHITELIST);
104
    }
105
106
    /**
107
     * @param string $value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
108
     */
109
    protected function readOpenXmlFile($file)
110
    {
111
        $this->exctractOpenXmlFile($file);
112
113
        if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file))
114
        {
115
            return $file_contents;
116
        } else
117
        {
118
            throw new Exception('could not read');
119
        }
120
    }
121
122
    /**
123
     * @param string $value
0 ignored issues
show
Bug introduced by
There is no parameter named $value. Was it maybe removed?

This check looks for PHPDoc comments describing methods or function parameters that do not exist on the corresponding method or function.

Consider the following example. The parameter $italy is not defined by the method finale(...).

/**
 * @param array $germany
 * @param array $island
 * @param array $italy
 */
function finale($germany, $island) {
    return "2:1";
}

The most likely cause is that the parameter was removed, but the annotation was not.

Loading history...
124
     */
125
    protected function readOpenXmlObject($file)
126
    {
127
        $this->exctractOpenXmlFile($file);
128
129
        return simplexml_load_file($this->storagePath($this->local_path.$file));
130
    }
131
132
    protected function saveOpenXmlFile($file,$folder,$content)
133
    {
134
        \Storage::disk($this->storageDisk)
135
            ->put($this->local_path.$file, $content);
136
        //add new content to word doc
137
        $this->zipper->folder($folder)
138
            ->add($this->storagePath($this->local_path.$file));
139
    }
140
141
    public function readTeamplate()
142
    {
143
        $this->log('Analyze Template');
144
        //get the main document out of the docx archive
145
        $this->word_doc = readOpenXmlFile('word/document.xml');
146
147
        $this->log('Merge Data into Template');
148
        $this->word_doc = $this->MustacheRender($this->items, $this->word_doc);
149
        $this->word_doc = $this->convertHtmlToOpenXML($this->word_doc);
150
151
        $this->ImageReplacer();
152
153
        $this->log('Compact Template with Data');
154
155
        $this->saveOpenXmlFile('word/document.xml','word',$this->word_doc);
156
        $this->zipper->close();
157
    }
158
159
    protected function MustacheTagCleaner($content)
160
    {
161
        //kills all xml tags within curly mustache brackets
162
        //this is necessary, as word might produce unnecesary xml tage inbetween curly backets.
163
        return preg_replace_callback(
164
            '/{{(.*?)}}/',
165
            function($match) {
166
                return strip_tags($match[0]);
167
            },
168
            $content
169
        );
170
    }
171
172
    protected function MustacheRender($items, $mustache_template, $clean_tags = true)
173
    {
174
        if($clean_tags)
175
            $mustache_template = $this->MustacheTagCleaner($mustache_template);
176
177
        $m = new \Mustache_Engine(array('escape' => function($value) {
178
            if (str_replace('*[[DONOTESCAPE]]*', '', $value) != $value) {
179
                            return str_replace('*[[DONOTESCAPE]]*', '', $value);
180
            }
181
            return htmlspecialchars($value, ENT_COMPAT, 'UTF-8');
182
        }));
183
        return $m->render($mustache_template, $items);
184
    }
185
186
    protected function AddContentType($imageCt = "jpeg")
187
    {
188
        $ct_file = $this->readOpenXmlObject('[Content_Types].xml');
189
190
        //check if content type for jpg has been set
191
        $i = 0;
192
        $ct_already_set = false;
193
        foreach ($ct_file as $ct)
194
        {
195
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
196
                            $ct_already_set = true;
197
            }
198
            $i++;
199
        }
200
201
        //if content type for jpg has not been set, add it to xml
202
        // and save xml to file and add it to the archive
203
        if (!$ct_already_set)
204
        {
205
            $sxe = $ct_file->addChild('Default');
206
            $sxe->addAttribute('Extension', $imageCt);
207
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
208
209
            if ($ct_file_xml = $ct_file->asXML())
210
            {
211
                $this->saveOpenXmlFile('[Content_Types].xml',false,$ct_file_xml);
212
            } else
213
            {
214
                throw new Exception('Cannot generate xml for [Content_Types].xml.');
215
            }
216
        }
217
    }
218
219
    protected function FetchReplaceableImages(&$main_file, $ns)
220
    {
221
        //set up basic arrays to keep track of imgs
222
        $imgs = array();
223
        $imgs_replaced = array(); // so they can later be removed from media and relation file.
224
        $newIdCounter = 1;
225
226
        //iterate through all drawing containers of the xml document
227
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
228
        {
229
            $ueid = "wrklstId".$newIdCounter;
230
            $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"];
231
            $imgs_replaced[$wasId] = $wasId;
232
            $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;
233
234
            $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"];
235
            $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"];
236
237
            //figure out if there is a URL saved in the description field of the img
238
            $img_url = $this->analyseImgUrlString((string) $drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"]);
239
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"] = $img_url["rest"];
240
241
            //check https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
242
            // for EMUs calculation
243
            /*
244
            295px @72 dpi = 1530350 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
245
            413px @72 dpi = 2142490 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
246
247
            */
248
249
            //if there is a url, save this img as a img to be replaced
250
            if (trim($img_url["url"]))
251
            {
252
                $imgs[] = array(
253
                    "cx" => $cx,
254
                    "cy" => $cy,
255
                    "width" => (int) ($cx / 5187.627118644067797),
256
                    "height" => (int) ($cy / 5187.627118644067797),
257
                    "wasId" => $wasId,
258
                    "id" => $ueid,
259
                    "url" => $img_url["url"],
260
                );
261
262
                $newIdCounter++;
263
            }
264
        }
265
        return array(
266
            'imgs' => $imgs,
267
            'imgs_replaced' => $imgs_replaced
268
        );
269
    }
270
271
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
272
    {
273
        //iterate through replaced images and clean rels files from them
274
        foreach ($imgs_replaced as $img_replaced)
275
        {
276
            $i = 0;
277
            foreach ($rels_file as $rel)
278
            {
279
                if ((string) $rel->attributes()['Id'] == $img_replaced)
280
                {
281
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
282
                    unset($rels_file->Relationship[$i]);
283
                }
284
                $i++;
285
            }
286
        }
287
    }
288
289
    protected function AllowedContentTypeImages()
290
    {
291
        return array(
292
            'image/gif' => '.gif',
293
            'image/jpeg' => '.jpeg',
294
            'image/png' => '.png',
295
            'image/bmp' => '.bmp',
296
        );
297
    }
298
299
    protected function GetImageFromUrl($url)
300
    {
301
        $allowed_imgs = $this->AllowedContentTypeImages();
302
303
        if ($img_file_handle = fopen($url.$this->imageManipulation, "rb"))
304
        {
305
            $img_data = stream_get_contents($img_file_handle);
306
            fclose($img_file_handle);
307
            $fi = new \finfo(FILEINFO_MIME);
308
309
            $image_mime = strstr($fi->buffer($img_data), ';', true);
310
            //dd($image_mime);
311
            if (isset($allowed_imgs[$image_mime]))
312
            {
313
                return array(
314
                    'data' => $img_data,
315
                    'mime' => $image_mime,
316
                );
317
            }
318
        }
319
        return false;
320
    }
321
322
    protected function ResampleImage($imgs, $k, $data)
323
    {
324
        \Storage::disk($this->storageDisk)->put($this->local_path.'word/media/'.$imgs[$k]['img_file_src'], $data);
325
326
        //rework img to new size and jpg format
327
        $img_rework = \Image::make($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_src']));
328
        $w = $imgs[$k]['width'];
329
        $h = $imgs[$k]['height'];
330
        if ($w > $h) {
331
            $h = null;
332
        } else {
333
            $w = null;
334
        }
335
        $img_rework->resize($w, $h, function($constraint) {
336
            $constraint->aspectRatio();
337
            $constraint->upsize();
338
        });
339
        $new_height = $img_rework->height();
340
        $new_width = $img_rework->width();
341
        $img_rework->save($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_dest']));
342
343
        $this->zipper->folder('word/media')->add($this->storagePath($this->local_path.'word/media/'.$imgs[$k]['img_file_dest']));
344
345
        return array(
346
            'height' => $new_height,
347
            'width' => $new_width,
348
        );
349
    }
350
351
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file)
352
    {
353
        //define what images are allowed
354
        $allowed_imgs = $this->AllowedContentTypeImages();
355
356
        //iterate through replacable images
357
        foreach ($imgs as $k=>$img)
358
        {
359
            //get file type of img and test it against supported imgs
360
            if($imgageData = $this->GetImageFromUrl($img['url']))
361
            {
362
                $imgs[$k]['img_file_src'] = str_replace("wrklstId", "wrklst_image", $img['id']).$allowed_imgs[$imgageData['mime']];
363
                $imgs[$k]['img_file_dest'] = str_replace("wrklstId", "wrklst_image", $img['id']).'.jpeg';
364
365
                $resampled_img = $this->ResampleImage($imgs, $k, $imgageData['data']);
366
367
                $sxe = $rels_file->addChild('Relationship');
368
                $sxe->addAttribute('Id', $img['id']);
369
                $sxe->addAttribute('Type', "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image");
370
                $sxe->addAttribute('Target', "media/".$imgs[$k]['img_file_dest']);
371
372
                //update height and width of image in document.xml
373
                $new_height_emus = (int) ($resampled_img['height'] * 5187.627118644067797);
374
                $new_width_emus = (int) ($resampled_img['width'] * 5187.627118644067797);
375
376
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
377
                {
378
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
379
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
380
                        ->blip->attributes($ns['r'])["embed"])
381
                    {
382
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
383
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
384
                            ->xfrm->ext->attributes()["cx"] = $new_width_emus;
385
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
386
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
387
                            ->xfrm->ext->attributes()["cy"] = $new_height_emus;
388
389
                        //the following also changes the contraints of the container for the img.
390
                        // probably not wanted, as this will make images larger than the constraints of the placeholder
391
                        /*
392
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cx"] = $new_width_emus;
393
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cy"] = $new_height_emus;
394
                        */
395
                        break;
396
                    }
397
                }
398
            }
399
        }
400
    }
401
402
    protected function ImageReplacer()
403
    {
404
        $this->log('Merge Images into Template');
405
406
        //load main doc xml
407
        $main_file = simplexml_load_string($this->word_doc);
408
409
        //get all namespaces of the document
410
        $ns = $main_file->getNamespaces(true);
411
412
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
413
        $imgs = $replaceableImage['imgs'];
414
        $imgs_replaced = $replaceableImage['imgs_replaced'];
415
416
        $rels_file = $this->readOpenXmlObject('word/_rels/document.xml.rels');
417
418
        $this->RemoveReplaceImages($imgs_replaced, $rels_file);
419
420
        //add jpg content type if not set
421
        $this->AddContentType('jpeg');
422
423
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
424
425
        if ($rels_file_xml = $rels_file->asXML())
426
        {
427
            $this->saveOpenXmlFile('word/_rels/document.xml.rels','word/_rels',$rels_file_xml);
428
        } else
429
        {
430
            throw new Exception('Cannot generate xml for word/_rels/document.xml.rels.');
431
        }
432
433
        if ($main_file_xml = $main_file->asXML())
434
        {
435
            $this->word_doc = $main_file_xml;
436
        } else
437
        {
438
            throw new Exception('Cannot generate xml for word/document.xml.');
439
        }
440
    }
441
442
    /**
443
     * @param string $string
444
     */
445
    protected function analyseImgUrlString($string)
446
    {
447
        $start = "[IMG-REPLACE]";
448
        $end = "[/IMG-REPLACE]";
449
        $string = ' '.$string;
450
        $ini = strpos($string, $start);
451
        if ($ini == 0)
452
        {
453
            $url = '';
454
            $rest = $string;
455
        } else
456
        {
457
            $ini += strlen($start);
458
            $len = ((strpos($string, $end, $ini)) - $ini);
459
            $url = substr($string, $ini, $len);
460
461
            $ini = strpos($string, $start);
462
            $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
463
            $rest = substr($string, 0, $ini).substr($string, $len);
464
        }
465
        return array(
466
            "url" => $url,
467
            "rest" => $rest,
468
        );
469
    }
470
471
    protected function convertHtmlToOpenXMLTag($value, $tag = "b")
472
    {
473
        $value_array = array();
474
        $run_again = false;
475
        //this could be used instead if html was already escaped
476
        /*
477
        $bo = "&lt;";
478
        $bc = "&gt;";
479
        */
480
        $bo = "<";
481
        $bc = ">";
482
483
        //get first BOLD
484
        $tag_open_values = explode($bo.$tag.$bc, $value, 2);
485
486
        if (count($tag_open_values) > 1)
487
        {
488
            //save everything before the bold and close it
489
            $value_array[] = $tag_open_values[0];
490
            $value_array[] = '</w:t></w:r>';
491
492
            //define styling parameters
493
            $wrPr_open = strrpos($tag_open_values[0], '<w:rPr>');
494
            $wrPr_close = strrpos($tag_open_values[0], '</w:rPr>', $wrPr_open);
495
            $neutral_style = '<w:r><w:rPr>'.substr($tag_open_values[0], ($wrPr_open + 7), ($wrPr_close - ($wrPr_open + 7))).'</w:rPr><w:t>';
496
            $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>';
497
498
            //open new text run and make it bold, include previous styling
499
            $value_array[] = $tagged_style;
500
            //get everything before bold close and after
501
            $tag_close_values = explode($bo.'/'.$tag.$bc, $tag_open_values[1], 2);
502
            //add bold text
503
            $value_array[] = $tag_close_values[0];
504
            //close bold run
505
            $value_array[] = '</w:t></w:r>';
506
            //open run for after bold
507
            $value_array[] = $neutral_style;
508
            $value_array[] = $tag_close_values[1];
509
510
            $run_again = true;
511
        } else {
512
            $value_array[] = $tag_open_values[0];
513
        }
514
515
        $value = implode('', $value_array);
516
517
        if ($run_again) {
518
                    $value = $this->convertHtmlToOpenXMLTag($value, $tag);
519
        }
520
521
        return $value;
522
    }
523
524
    /**
525
     * @param string $value
526
     */
527
    protected function convertHtmlToOpenXML($value)
528
    {
529
        $line_breaks = array("&lt;br /&gt;", "&lt;br/&gt;", "&lt;br&gt;", "<br />", "<br/>", "<br>");
530
        $value = str_replace($line_breaks, '<w:br/>', $value);
531
532
        $value = $this->convertHtmlToOpenXMLTag($value, "b");
533
        $value = $this->convertHtmlToOpenXMLTag($value, "i");
534
        $value = $this->convertHtmlToOpenXMLTag($value, "u");
535
536
        return $value;
537
    }
538
539
    public function saveAsPdf()
540
    {
541
        $this->log('Converting DOCX to PDF');
542
        //convert to pdf with libre office
543
        $command = "soffice --headless --convert-to pdf ".$this->storagePath($this->local_path.$this->template_file_name).' --outdir '.$this->storagePath($this->local_path);
544
        $process = new \Symfony\Component\Process\Process($command);
545
        $process->start();
546
        while ($process->isRunning()) {
547
            //wait until process is ready
548
        }
549
        // executes after the command finishes
550
        if (!$process->isSuccessful()) {
551
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
552
        } else
553
        {
554
            $path_parts = pathinfo($this->storagePath($this->local_path.$this->template_file_name));
555
            return $this->storagePath($this->local_path.$path_parts['filename'].'pdf');
556
        }
557
    }
558
}
559