Passed
Push — master ( b04009...b8980d )
by Tobias
02:33
created

DocxMustache::ResampleImage()   B

Complexity

Conditions 2
Paths 2

Size

Total Lines 34
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 1 Features 0
Metric Value
c 2
b 1
f 0
dl 0
loc 34
rs 8.8571
cc 2
eloc 19
nc 2
nop 3
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
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
            {
78
                $disk->deleteDirectory($dir);
79
            }
80
        }
81
    }
82
83
    public function getTmpDir()
84
    {
85
        $this->cleanUpTmpDirs();
86
        $path = $this->storagePathPrefix.'DocxMustache/'.uniqid($this->template_file).'/';
87
        \File::makeDirectory($this->storagePath($path), 0775, true);
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), array($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
        {
110
            if ($file_contents = \Storage::disk($this->storageDisk)->get($this->local_path.$file))
111
            {
112
                return $file_contents;
113
            } else
114
            {
115
                throw new Exception('Cannot not read file '.$file);
116
            }
117
        } else
118
        {
119
            if ($xml_object = simplexml_load_file($this->storagePath($this->local_path.$file)))
120
            {
121
                return $xml_object;
122
            } else
123
            {
124
                throw new Exception('Cannot load XML Object from file '.$file);
125
            }
126
        }
127
    }
128
129
    protected function saveOpenXmlFile($file, $folder, $content)
130
    {
131
        \Storage::disk($this->storageDisk)
132
            ->put($this->local_path.$file, $content);
133
        //add new content to word doc
134
        $this->zipper->folder($folder)
135
            ->add($this->storagePath($this->local_path.$file));
136
    }
137
138
    public function readTeamplate()
139
    {
140
        $this->log('Analyze Template');
141
        //get the main document out of the docx archive
142
        $this->word_doc = ReadOpenXmlFile('word/document.xml','file');
143
144
        $this->log('Merge Data into Template');
145
146
        $this->word_doc = new MustacheRender($this->items, $this->word_doc);
147
148
        $this->word_doc = new HtmlConversion($this->word_doc);
149
150
        $this->ImageReplacer();
151
152
        $this->log('Compact Template with Data');
153
154
        $this->saveOpenXmlFile('word/document.xml', 'word', $this->word_doc);
155
        $this->zipper->close();
156
    }
157
158
    protected function AddContentType($imageCt = "jpeg")
159
    {
160
        $ct_file = $this->ReadOpenXmlFile('[Content_Types].xml','object');
161
162
        if (!($ct_file instanceof \Traversable)) {
163
            throw new Exception('Cannot traverse through [Content_Types].xml.');
164
        } else
165
        {
166
            //check if content type for jpg has been set
167
            $i = 0;
168
            $ct_already_set = false;
169
            foreach ($ct_file as $ct)
170
            {
171
                if ((string) $ct_file->Default[$i]['Extension'] == $imageCt) {
172
                                $ct_already_set = true;
173
                }
174
                $i++;
175
            }
176
177
            //if content type for jpg has not been set, add it to xml
178
            // and save xml to file and add it to the archive
179
            if (!$ct_already_set)
180
            {
181
                $sxe = $ct_file->addChild('Default');
182
                $sxe->addAttribute('Extension', $imageCt);
183
                $sxe->addAttribute('ContentType', 'image/'.$imageCt);
184
185
                if ($ct_file_xml = $ct_file->asXML())
186
                {
187
                    $this->SaveOpenXmlFile('[Content_Types].xml', false, $ct_file_xml);
188
                } else
189
                {
190
                    throw new Exception('Cannot generate xml for [Content_Types].xml.');
191
                }
192
            }
193
        }
194
    }
195
196
    protected function FetchReplaceableImages(&$main_file, $ns)
197
    {
198
        //set up basic arrays to keep track of imgs
199
        $imgs = array();
200
        $imgs_replaced = array(); // so they can later be removed from media and relation file.
201
        $newIdCounter = 1;
202
203
        //iterate through all drawing containers of the xml document
204
        foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
205
        {
206
            $ueid = "wrklstId".$newIdCounter;
207
            $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"];
208
            $imgs_replaced[$wasId] = $wasId;
209
            $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;
210
211
            $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"];
212
            $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"];
213
214
            //figure out if there is a URL saved in the description field of the img
215
            $img_url = $this->analyseImgUrlString((string) $drawing->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"]);
216
            $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->xpath('wp:docPr')[0]->attributes()["descr"] = $img_url["rest"];
217
218
            //check https://startbigthinksmall.wordpress.com/2010/01/04/points-inches-and-emus-measuring-units-in-office-open-xml/
219
            // for EMUs calculation
220
            /*
221
            295px @72 dpi = 1530350 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
222
            413px @72 dpi = 2142490 EMUs = Multiplier for 72dpi pixels 5187.627118644067797
223
224
            */
225
226
            //if there is a url, save this img as a img to be replaced
227
            if (trim($img_url["url"]))
228
            {
229
                $imgs[] = array(
230
                    "cx" => $cx,
231
                    "cy" => $cy,
232
                    "width" => (int) ($cx / 5187.627118644067797),
233
                    "height" => (int) ($cy / 5187.627118644067797),
234
                    "wasId" => $wasId,
235
                    "id" => $ueid,
236
                    "url" => $img_url["url"],
237
                );
238
239
                $newIdCounter++;
240
            }
241
        }
242
        return array(
243
            'imgs' => $imgs,
244
            'imgs_replaced' => $imgs_replaced
245
        );
246
    }
247
248
    protected function RemoveReplaceImages($imgs_replaced, &$rels_file)
249
    {
250
        //iterate through replaced images and clean rels files from them
251
        foreach ($imgs_replaced as $img_replaced)
252
        {
253
            $i = 0;
254
            foreach ($rels_file as $rel)
255
            {
256
                if ((string) $rel->attributes()['Id'] == $img_replaced)
257
                {
258
                    $this->zipper->remove('word/'.(string) $rel->attributes()['Target']);
259
                    unset($rels_file->Relationship[$i]);
260
                }
261
                $i++;
262
            }
263
        }
264
    }
265
266
    protected function InsertImages($ns, &$imgs, &$rels_file, &$main_file)
267
    {
268
        $docimage = new DocImage();
269
270
        //define what images are allowed
271
        $allowed_imgs = $docimage->AllowedContentTypeImages();
0 ignored issues
show
Bug introduced by
The method AllowedContentTypeImages() cannot be called from this context as it is declared protected in class WrkLst\DocxMustache\DocImage.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
272
273
        //iterate through replacable images
274
        foreach ($imgs as $k=>$img)
275
        {
276
            //get file type of img and test it against supported imgs
277
            if ($imgageData = $docimage->GetImageFromUrl($img['url']))
278
            {
279
                $imgs[$k]['img_file_src'] = str_replace("wrklstId", "wrklst_image", $img['id']).$allowed_imgs[$imgageData['mime']];
280
                $imgs[$k]['img_file_dest'] = str_replace("wrklstId", "wrklst_image", $img['id']).'.jpeg';
281
282
                $resampled_img = $docimage->ResampleImage($this, $imgs, $k, $imgageData['data']);
0 ignored issues
show
Bug introduced by
The method ResampleImage() cannot be called from this context as it is declared protected in class WrkLst\DocxMustache\DocImage.

This check looks for access to methods that are not accessible from the current context.

If you need to make a method accessible to another context you can raise its visibility level in the defining class.

Loading history...
283
284
                $sxe = $rels_file->addChild('Relationship');
285
                $sxe->addAttribute('Id', $img['id']);
286
                $sxe->addAttribute('Type', "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image");
287
                $sxe->addAttribute('Target', "media/".$imgs[$k]['img_file_dest']);
288
289
                //update height and width of image in document.xml
290
                $new_height_emus = (int) ($resampled_img['height'] * 5187.627118644067797);
291
                $new_width_emus = (int) ($resampled_img['width'] * 5187.627118644067797);
292
293
                foreach ($main_file->xpath('//w:drawing') as $k=>$drawing)
294
                {
295
                    if ($img['id'] == $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
296
                        ->graphic->graphicData->children($ns['pic'])->pic->blipFill->children($ns['a'])
297
                        ->blip->attributes($ns['r'])["embed"])
298
                    {
299
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
300
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
301
                            ->xfrm->ext->attributes()["cx"] = $new_width_emus;
302
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->children($ns['a'])
303
                            ->graphic->graphicData->children($ns['pic'])->pic->spPr->children($ns['a'])
304
                            ->xfrm->ext->attributes()["cy"] = $new_height_emus;
305
306
                        //the following also changes the contraints of the container for the img.
307
                        // probably not wanted, as this will make images larger than the constraints of the placeholder
308
                        /*
309
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cx"] = $new_width_emus;
310
                        $main_file->xpath('//w:drawing')[$k]->children($ns['wp'])->inline->extent->attributes()["cy"] = $new_height_emus;
311
                        */
312
                        break;
313
                    }
314
                }
315
            }
316
        }
317
    }
318
319
    protected function ImageReplacer()
320
    {
321
        $this->log('Merge Images into Template');
322
323
        //load main doc xml
324
        $main_file = simplexml_load_string($this->word_doc);
325
326
        //get all namespaces of the document
327
        $ns = $main_file->getNamespaces(true);
328
329
        $replaceableImage = $this->FetchReplaceableImages($main_file, $ns);
330
        $imgs = $replaceableImage['imgs'];
331
        $imgs_replaced = $replaceableImage['imgs_replaced'];
332
333
        $rels_file = $this->ReadOpenXmlFile('word/_rels/document.xml.rels','object');
334
335
        $this->RemoveReplaceImages($imgs_replaced, $rels_file);
336
337
        //add jpg content type if not set
338
        $this->AddContentType('jpeg');
339
340
        $this->InsertImages($ns, $imgs, $rels_file, $main_file);
341
342
        if ($rels_file_xml = $rels_file->asXML())
343
        {
344
            $this->SaveOpenXmlFile('word/_rels/document.xml.rels', 'word/_rels', $rels_file_xml);
345
        } else
346
        {
347
            throw new Exception('Cannot generate xml for word/_rels/document.xml.rels.');
348
        }
349
350
        if ($main_file_xml = $main_file->asXML())
351
        {
352
            $this->word_doc = $main_file_xml;
353
        } else
354
        {
355
            throw new Exception('Cannot generate xml for word/document.xml.');
356
        }
357
    }
358
359
    /**
360
     * @param string $string
361
     */
362
    protected function analyseImgUrlString($string)
363
    {
364
        $start = "[IMG-REPLACE]";
365
        $end = "[/IMG-REPLACE]";
366
        $string = ' '.$string;
367
        $ini = strpos($string, $start);
368
        if ($ini == 0)
369
        {
370
            $url = '';
371
            $rest = $string;
372
        } else
373
        {
374
            $ini += strlen($start);
375
            $len = ((strpos($string, $end, $ini)) - $ini);
376
            $url = substr($string, $ini, $len);
377
378
            $ini = strpos($string, $start);
379
            $len = strpos($string, $end, $ini + strlen($start)) + strlen($end);
380
            $rest = substr($string, 0, $ini).substr($string, $len);
381
        }
382
        return array(
383
            "url" => $url,
384
            "rest" => $rest,
385
        );
386
    }
387
388
    public function saveAsPdf()
389
    {
390
        $this->log('Converting DOCX to PDF');
391
        //convert to pdf with libre office
392
        $command = "soffice --headless --convert-to pdf ".$this->storagePath($this->local_path.$this->template_file_name).' --outdir '.$this->storagePath($this->local_path);
393
        $process = new \Symfony\Component\Process\Process($command);
394
        $process->start();
395
        while ($process->isRunning()) {
396
            //wait until process is ready
397
        }
398
        // executes after the command finishes
399
        if (!$process->isSuccessful()) {
400
            throw new \Symfony\Component\Process\Exception\ProcessFailedException($process);
401
        } else
402
        {
403
            $path_parts = pathinfo($this->storagePath($this->local_path.$this->template_file_name));
404
            return $this->storagePath($this->local_path.$path_parts['filename'].'pdf');
405
        }
406
    }
407
}
408