Passed
Pull Request — master (#10)
by Tobias
02:25
created

DocxMustache::FetchReplaceableImages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 50
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
dl 0
loc 50
rs 9.3333
c 2
b 1
f 0
cc 3
eloc 26
nc 3
nop 2
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
    public 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
68
    public function cleanUpTmpDirs()
69
    {
70
        $now = time();
71
        $isExpired = ($now - (60 * 240));
72
        $disk = \Storage::disk($this->storageDisk);
73
        $all_dirs = $disk->directories($this->storagePathPrefix.'DocxMustache');
74
        foreach ($all_dirs as $dir) {
75
            //delete dirs older than 20min
76
            if ($disk->lastModified($dir) < $isExpired) {
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
88
        return $path;
89
    }
90
91
    public function copyTmplate()
92
    {
93
        $this->log('Get Copy of Template');
94
        $this->local_path = $this->getTmpDir();
95
        \Storage::disk($this->storageDisk)->copy($this->storagePathPrefix.$this->template_file, $this->local_path.$this->template_file_name);
96
    }
97
98
    protected function exctractOpenXmlFile($file)
99
    {
100
        $this->zipper->make($this->storagePath($this->local_path.$this->template_file_name))
101
            ->extractTo($this->storagePath($this->local_path), [$file], \Chumper\Zipper\Zipper::WHITELIST);
102
    }
103
104
    protected function ReadOpenXmlFile($file, $type = 'file')
105
    {
106
        $this->exctractOpenXmlFile($file);
107
108
        if ($type == 'file') {
109
            if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file)) {
110
                return $file_contents;
111
            } else {
112
                throw new Exception('Cannot not read file '.$file);
113
            }
114
        } else {
115
            if ($xml_object = simplexml_load_file($this->storagePath($this->local_path.$file))) {
116
                return $xml_object;
117
            } else {
118
                throw new Exception('Cannot load XML Object from file '.$file);
119
            }
120
        }
121
    }
122
123
    protected function saveOpenXmlFile($file, $folder, $content)
124
    {
125
        \Storage::disk($this->storageDisk)
126
            ->put($this->local_path.$file, $content);
127
        //add new content to word doc
128
        $this->zipper->folder($folder)
129
            ->add($this->storagePath($this->local_path.$file));
130
    }
131
132
    public function readTeamplate()
133
    {
134
        $this->log('Analyze Template');
135
        //get the main document out of the docx archive
136
        $this->word_doc = $this->ReadOpenXmlFile('word/document.xml', 'file');
137
138
        $this->log('Merge Data into Template');
139
140
        $this->word_doc = MustacheRender::render($this->items, $this->word_doc);
141
142
        $this->word_doc = HtmlConversion::convert($this->word_doc);
143
144
        $this->ImageReplacer();
145
146
        $this->log('Compact Template with Data');
147
148
        $this->saveOpenXmlFile('word/document.xml', 'word', $this->word_doc);
149
        $this->zipper->close();
150
    }
151
152
    protected function AddContentType($imageCt = 'jpeg')
153
    {
154
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml', 'object');
155
156
        if (!($ct_file instanceof \Traversable)) {
157
            throw new Exception('Cannot traverse through [Content_Types].xml.');
158
        } else {
159
            //check if content type for jpg has been set
160
            $i = 0;
161
            $ct_already_set = false;
162
            foreach ($ct_file as $ct) {
163
                if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
164
                    $ct_already_set = true;
165
                }
166
                $i++;
167
            }
168
169
            //if content type for jpg has not been set, add it to xml
170
            // and save xml to file and add it to the archive
171
            if (!$ct_already_set) {
172
                $sxe = $ct_file->addChild('Default');
173
                $sxe->addAttribute('Extension', $imageCt);
174
                $sxe->addAttribute('ContentType', 'image/'.$imageCt);
175
176
                if ($ct_file_xml = $ct_file->asXML()) {
177
                    $this->SaveOpenXmlFile('[Content_Types].xml', false, $ct_file_xml);
178
                } else {
179
                    throw new Exception('Cannot generate xml for [Content_Types].xml.');
180
                }
181
            }
182
        }
183
    }
184
185
    protected function FetchReplaceableImages(&$main_file, $ns)
186
    {
187
        //set up basic arrays to keep track of imgs
188
        $imgs = [];
189
        $imgs_replaced = []; // so they can later be removed from media and relation file.
190
        $newIdCounter = 1;
191
192
        //iterate through all drawing containers of the xml document
193
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
194
            $ueid = 'wrklstId'.$newIdCounter;
195
            $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'];
196
            $imgs_replaced[$wasId] = $wasId;
197
            $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;
198
199
            $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'];
200
            $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'];
201
202
            //figure out if there is a URL saved in the description field of the img
203
            $img_url = $this->analyseImgUrlString((string) $drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr']);
204
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr'] = $img_url['rest'];
205
206
            //check https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
207
            // for EMUs calculation
208
            /*
209
            295px @72 dpi = 1530350 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
210
            413px @72 dpi = 2142490 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
211
212
            */
213
214
            //if there is a url, save this img as a img to be replaced
215
            if (trim($img_url['url'])) {
216
                $imgs[] = [
217
                    'cx'     => $cx,
218
                    'cy'     => $cy,
219
                    'width'  => (int) ($cx / 5187.627118644067797),
220
                    'height' => (int) ($cy / 5187.627118644067797),
221
                    'wasId'  => $wasId,
222
                    'id'     => $ueid,
223
                    'url'    => $img_url['url'],
224
                ];
225
226
                $newIdCounter++;
227
            }
228
        }
229
230
        return [
231
            'imgs'          => $imgs,
232
            'imgs_replaced' => $imgs_replaced,
233
        ];
234
    }
235
236
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
237
    {
238
        //iterate through replaced images and clean rels files from them
239
        foreach ($imgs_replaced as $img_replaced) {
240
            $i = 0;
241
            foreach ($rels_file as $rel) {
242
                if ((string) $rel->attributes()['Id'] == $img_replaced) {
243
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
244
                    unset($rels_file->Relationship[$i]);
245
                }
246
                $i++;
247
            }
248
        }
249
    }
250
251
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file)
252
    {
253
        $docimage = new DocImage();
254
255
        //define what images are allowed
256
        $allowed_imgs = $docimage->AllowedContentTypeImages();
257
258
        //iterate through replacable images
259
        foreach ($imgs as $k=>$img) {
260
            //get file type of img and test it against supported imgs
261
            if ($imgageData = $docimage->GetImageFromUrl($img['url'], $this->imageManipulation)) {
262
                $imgs[$k]['img_file_src'] = str_replace('wrklstId', 'wrklst_image', $img['id']).$allowed_imgs[$imgageData['mime']];
263
                $imgs[$k]['img_file_dest'] = str_replace('wrklstId', 'wrklst_image', $img['id']).'.jpeg';
264
265
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data']);
266
267
                $sxe = $rels_file->addChild('Relationship');
268
                $sxe->addAttribute('Id', $img['id']);
269
                $sxe->addAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
270
                $sxe->addAttribute('Target', 'media/'.$imgs[$k]['img_file_dest']);
271
272
                //update height and width of image in document.xml
273
                $new_height_emus = (int) ($resampled_img['height'] * 5187.627118644067797);
274
                $new_width_emus = (int) ($resampled_img['width'] * 5187.627118644067797);
275
276
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
277
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
278
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
279
                        ->blip->attributes($ns['r'])['embed']) {
280
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
281
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
282
                            ->xfrm->ext->attributes()['cx'] = $new_width_emus;
283
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
284
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
285
                            ->xfrm->ext->attributes()['cy'] = $new_height_emus;
286
287
                        //the following also changes the contraints of the container for the img.
288
                        // probably not wanted, as this will make images larger than the constraints of the placeholder
289
                        /*
290
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cx"] = $new_width_emus;
291
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cy"] = $new_height_emus;
292
                        */
293
                        break;
294
                    }
295
                }
296
            }
297
        }
298
    }
299
300
    protected function ImageReplacer()
301
    {
302
        $this->log('Merge Images into Template');
303
304
        //load main doc xml
305
        $main_file = simplexml_load_string($this->word_doc);
306
307
        //get all namespaces of the document
308
        $ns = $main_file->getNamespaces(true);
309
310
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
311
        $imgs = $replaceableImage['imgs'];
312
        $imgs_replaced = $replaceableImage['imgs_replaced'];
313
314
        $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels', 'object');
315
316
        $this->RemoveReplaceImages($imgs_replaced, $rels_file);
317
318
        //add jpg content type if not set
319
        $this->AddContentType('jpeg');
320
321
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
322
323
        if ($rels_file_xml = $rels_file->asXML()) {
324
            $this->SaveOpenXmlFile('word/_rels/document.xml.rels', 'word/_rels', $rels_file_xml);
325
        } else {
326
            throw new Exception('Cannot generate xml for word/_rels/document.xml.rels.');
327
        }
328
329
        if ($main_file_xml = $main_file->asXML()) {
330
            $this->word_doc = $main_file_xml;
331
        } else {
332
            throw new Exception('Cannot generate xml for word/document.xml.');
333
        }
334
    }
335
336
    /**
337
     * @param string $string
338
     */
339
    protected function analyseImgUrlString($string)
340
    {
341
        $start = '[IMG-REPLACE]';
342
        $end = '[/IMG-REPLACE]';
343
        $string = ' '.$string;
344
        $ini = strpos($string, $start);
345
        if ($ini == 0) {
346
            $url = '';
347
            $rest = $string;
348
        } else {
349
            $ini += strlen($start);
350
            $len = ((strpos($string, $end, $ini)) - $ini);
351
            $url = substr($string, $ini, $len);
352
353
            $ini = strpos($string, $start);
354
            $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
355
            $rest = substr($string, 0, $ini).substr($string, $len);
356
        }
357
358
        return [
359
            'url'  => $url,
360
            'rest' => $rest,
361
        ];
362
    }
363
364
    public function saveAsPdf()
365
    {
366
        $this->log('Converting DOCX to PDF');
367
        //convert to pdf with libre office
368
        $command = 'soffice --headless --convert-to pdf '.$this->storagePath($this->local_path.$this->template_file_name).' --outdir '.$this->storagePath($this->local_path);
369
        $process = new \Symfony\Component\Process\Process($command);
370
        $process->start();
371
        while ($process->isRunning()) {
372
            //wait until process is ready
373
        }
374
        // executes after the command finishes
375
        if (!$process->isSuccessful()) {
376
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
377
        } else {
378
            $path_parts = pathinfo($this->storagePath($this->local_path.$this->template_file_name));
379
380
            return $this->storagePath($this->local_path.$path_parts['filename'].'pdf');
381
        }
382
    }
383
}
384