Passed
Push — master ( 9ab622...fdd895 )
by Tobias
02:03
created

DocxMustache::FetchReplaceableImages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 51
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 51
rs 9.4109
cc 3
eloc 26
nc 3
nop 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 FetchReplaceableImages(&$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($ns, &$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;
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->FetchReplaceableImages($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($ns, $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