Passed
Pull Request — master (#15)
by Tobias
04:49 queued 02:21
created

DocxMustache::InsertImages()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 50
Code Lines 31

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 2 Features 0
Metric Value
c 6
b 2
f 0
dl 0
loc 50
rs 6.7272
cc 7
eloc 31
nc 7
nop 4
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
        if ($folder) {
129
            $this->zipper->folder($folder)
130
                ->add($this->StoragePath($this->local_path.$file));
131
        } else {
132
            $this->zipper
133
                ->add($this->StoragePath($this->local_path.$file));
134
        }
135
    }
136
137
    protected function SaveOpenXmlObjectToFile($xmlObject, $file, $folder)
138
    {
139
        if ($xmlString = $xmlObject->asXML()) {
140
            $this->SaveOpenXmlFile($file, $folder, $xmlString);
141
        } else {
142
            throw new Exception('Cannot generate xml for '.$file);
143
        }
144
    }
145
146
    public function ReadTeamplate()
147
    {
148
        $this->Log('Analyze Template');
149
        //get the main document out of the docx archive
150
        $this->word_doc = $this->ReadOpenXmlFile('word/document.xml', 'file');
151
152
        $this->Log('Merge Data into Template');
153
154
        $this->word_doc = MustacheRender::render($this->items, $this->word_doc);
155
156
        $this->word_doc = HtmlConversion::convert($this->word_doc);
157
158
        $this->ImageReplacer();
159
160
        $this->Log('Compact Template with Data');
161
162
        $this->SaveOpenXmlFile('word/document.xml', 'word', $this->word_doc);
163
        $this->zipper->close();
164
    }
165
166
    protected function AddContentType($imageCt = 'jpeg')
167
    {
168
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml', 'object');
169
170
        if (! ($ct_file instanceof \Traversable)) {
171
            throw new Exception('Cannot traverse through [Content_Types].xml.');
172
        }
173
174
        //check if content type for jpg has been set
175
        $i = 0;
176
        $ct_already_set = false;
177
        foreach ($ct_file as $ct) {
178
            if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
179
                $ct_already_set = true;
180
            }
181
            $i++;
182
        }
183
184
        //if content type for jpg has not been set, add it to xml
185
        // and save xml to file and add it to the archive
186
        if (! $ct_already_set) {
187
            $sxe = $ct_file->addChild('Default');
188
            $sxe->addAttribute('Extension', $imageCt);
189
            $sxe->addAttribute('ContentType', 'image/'.$imageCt);
190
            $this->SaveOpenXmlObjectToFile($ct_file, '[Content_Types].xml', false);
191
        }
192
    }
193
194
    protected function FetchReplaceableImages(&$main_file, $ns)
195
    {
196
        //set up basic arrays to keep track of imgs
197
        $imgs = [];
198
        $imgs_replaced = []; // so they can later be removed from media and relation file.
199
        $newIdCounter = 1;
200
201
        //iterate through all drawing containers of the xml document
202
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
203
            $ueid = 'wrklstId'.$newIdCounter;
204
            $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'];
205
            $imgs_replaced[$wasId] = $wasId;
206
            $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;
207
208
            $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'];
209
            $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'];
210
211
            //figure out if there is a URL saved in the description field of the img
212
            $img_url = $this->AnalyseImgUrlString((string) $drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr']);
213
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()['descr'] = $img_url['rest'];
214
215
            //check https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
216
            // for EMUs calculation
217
            /*
218
            295px @72 dpi = 1530350 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
219
            413px @72 dpi = 2142490 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
220
221
            */
222
223
            //if there is a url, save this img as a img to be replaced
224
            if (trim($img_url['url'])) {
225
                $imgs[] = [
226
                    'cx'     => (int) $cx,
227
                    'cy'     => (int) $cy,
228
                    'width'  => (int) ($cx / 5187.627118644067797),
229
                    'height' => (int) ($cy / 5187.627118644067797),
230
                    'wasId'  => $wasId,
231
                    'id'     => $ueid,
232
                    'url'    => $img_url['url'],
233
                ];
234
235
                $newIdCounter++;
236
            }
237
        }
238
239
        return [
240
            'imgs'          => $imgs,
241
            'imgs_replaced' => $imgs_replaced,
242
        ];
243
    }
244
245
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
246
    {
247
        //iterate through replaced images and clean rels files from them
248
        foreach ($imgs_replaced as $img_replaced) {
249
            $i = 0;
250
            foreach ($rels_file as $rel) {
251
                if ((string) $rel->attributes()['Id'] == $img_replaced) {
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
        $docimage = new DocImage();
263
        $allowed_imgs = $docimage->AllowedContentTypeImages();
264
265
        //iterate through replacable images
266
        foreach ($imgs as $k=>$img) {
267
            //get file type of img and test it against supported imgs
268
            if ($imgageData = $docimage->GetImageFromUrl($img['url'], $this->imageManipulation)) {
269
                $imgs[$k]['img_file_src'] = str_replace('wrklstId', 'wrklst_image', $img['id']).$allowed_imgs[$imgageData['mime']];
270
                $imgs[$k]['img_file_dest'] = str_replace('wrklstId', 'wrklst_image', $img['id']).'.jpeg';
271
272
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data']);
273
274
                $sxe = $rels_file->addChild('Relationship');
275
                $sxe->addAttribute('Id', $img['id']);
276
                $sxe->addAttribute('Type', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image');
277
                $sxe->addAttribute('Target', 'media/'.$imgs[$k]['img_file_dest']);
278
279
                //update height and width of image in document.xml
280
                $new_height_emus = (int) ($resampled_img['height'] * 5187.627118644067797);
281
                $new_width_emus = (int) ($resampled_img['width'] * 5187.627118644067797);
282
283
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing) {
284
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
285
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
286
                        ->blip->attributes($ns['r'])['embed']) {
287
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
288
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
289
                            ->xfrm->ext->attributes()['cx'] = $new_width_emus;
290
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
291
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
292
                            ->xfrm->ext->attributes()['cy'] = $new_height_emus;
293
                        //anchored images
294
                        if ($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor) {
295
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cx'] = $new_width_emus;
296
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->anchor->extent->attributes()['cy'] = $new_height_emus;
297
                        }
298
                        //inlined images
299
                        elseif ($main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline) {
300
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cx'] = $new_width_emus;
301
                            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()['cy'] = $new_height_emus;
302
                        }
303
304
                        break;
305
                    }
306
                }
307
            }
308
        }
309
    }
310
311
    protected function ImageReplacer()
312
    {
313
        $this->Log('Merge Images into Template');
314
315
        //load main doc xml
316
        $main_file = simplexml_load_string($this->word_doc);
317
318
        //get all namespaces of the document
319
        $ns = $main_file->getNamespaces(true);
320
321
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
322
        $imgs = $replaceableImage['imgs'];
323
        $imgs_replaced = $replaceableImage['imgs_replaced'];
324
325
        $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels', 'object');
326
327
        $this->RemoveReplaceImages($imgs_replaced, $rels_file);
328
329
        //add jpg content type if not set
330
        $this->AddContentType('jpeg');
331
332
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
333
334
        $this->SaveOpenXmlObjectToFile($rels_file, 'word/_rels/document.xml.rels', 'word/_rels');
335
336
        if ($main_file_xml = $main_file->asXML()) {
337
            $this->word_doc = $main_file_xml;
338
        } else {
339
            throw new Exception('Cannot generate xml for word/document.xml.');
340
        }
341
    }
342
343
    /**
344
     * @param string $string
345
     */
346
    protected function AnalyseImgUrlString($string)
347
    {
348
        $start = '[IMG-REPLACE]';
349
        $end = '[/IMG-REPLACE]';
350
        $string = ' '.$string;
351
        $ini = strpos($string, $start);
352
        if ($ini == 0) {
353
            $url = '';
354
            $rest = $string;
355
        } else {
356
            $ini += strlen($start);
357
            $len = ((strpos($string, $end, $ini)) - $ini);
358
            $url = substr($string, $ini, $len);
359
360
            $ini = strpos($string, $start);
361
            $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
362
            $rest = substr($string, 0, $ini).substr($string, $len);
363
        }
364
365
        return [
366
            'url'  => trim($url),
367
            'rest' => trim($rest),
368
        ];
369
    }
370
371
    public function SaveAsPdf()
372
    {
373
        $this->Log('Converting DOCX to PDF');
374
        //convert to pdf with libre office
375
        $command = 'soffice --headless --convert-to pdf '.$this->StoragePath($this->local_path.$this->template_file_name).' --outdir '.$this->StoragePath($this->local_path);
376
        $process = new \Symfony\Component\Process\Process($command);
377
        $process->start();
378
        while ($process->isRunning()) {
379
            //wait until process is ready
380
        }
381
        // executes after the command finishes
382
        if (! $process->isSuccessful()) {
383
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
384
        } else {
385
            $path_parts = pathinfo($this->StoragePath($this->local_path.$this->template_file_name));
386
387
            return $this->StoragePath($this->local_path.$path_parts['filename'].'pdf');
388
        }
389
    }
390
}
391