• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PHPOffice / PHPPresentation / 13526002009

25 Feb 2025 04:24PM UTC coverage: 90.469% (-0.5%) from 90.953%
13526002009

Pull #855

github

web-flow
Merge 7cd74700e into 70cf1918a
Pull Request #855: PDF/HTML Writer : Added support

134 of 204 new or added lines in 6 files covered. (65.69%)

9654 of 10671 relevant lines covered (90.47%)

43.63 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

62.36
/src/PhpPresentation/Writer/HTML.php
1
<?php
2

3
/**
4
 * This file is part of PHPPresentation - A pure PHP library for reading and writing
5
 * presentations documents.
6
 *
7
 * PHPPresentation is free software distributed under the terms of the GNU Lesser
8
 * General Public License version 3 as published by the Free Software Foundation.
9
 *
10
 * For the full copyright and license information, please read the LICENSE
11
 * file that was distributed with this source code. For the full list of
12
 * contributors, visit https://github.com/PHPOffice/PHPPresentation/contributors.
13
 *
14
 * @see        https://github.com/PHPOffice/PHPPresentation
15
 *
16
 * @license     http://www.gnu.org/licenses/lgpl.txt LGPL version 3
17
 */
18

19
declare(strict_types=1);
20

21
namespace PhpOffice\PhpPresentation\Writer;
22

23
use PhpOffice\PhpPresentation\DocumentLayout;
24
use PhpOffice\PhpPresentation\Exception\InvalidParameterException;
25
use PhpOffice\PhpPresentation\HashTable;
26
use PhpOffice\PhpPresentation\PhpPresentation;
27
use PhpOffice\PhpPresentation\Shape\Drawing;
28
use PhpOffice\PhpPresentation\Shape\Media;
29
use PhpOffice\PhpPresentation\Shape\RichText;
30
use PhpOffice\PhpPresentation\Shape\Table;
31
use PhpOffice\PhpPresentation\Slide;
32
use PhpOffice\PhpPresentation\Style\Alignment;
33
use PhpOffice\PhpPresentation\Style\Font;
34
use PhpOffice\PhpPresentation\Style\Shadow;
35

36
/**
37
 * HTML writer.
38
 */
39
class HTML extends AbstractWriter implements WriterInterface
40
{
41
    /**
42
     * @var string
43
     */
44
    protected $style = '';
45

46
    /**
47
     * @var string
48
     */
49
    protected $bodyList = '';
50

51
    /**
52
     * @var string
53
     */
54
    protected $bodySlides = '';
55

56
    /**
57
     * @var float
58
     */
59
    protected $ratioX;
60

61
    /**
62
     * @var float
63
     */
64
    protected $ratioY;
65

66
    /**
67
     * @var bool
68
     */
69
    protected $isPDF = false;
70

71
    /**
72
     * Create a new \PhpOffice\PhpPresentation\Writer\ODPresentation.
73
     */
74
    public function __construct(?PhpPresentation $pPhpPresentation = null)
75
    {
76
        // Assign PhpPresentation
77
        $this->setPhpPresentation($pPhpPresentation ?? new PhpPresentation());
4✔
78

79
        // Set HashTable variables
80
        $this->oDrawingHashTable = new HashTable();
4✔
81
    }
82

83
    /**
84
     * Save PhpPresentation to file.
85
     */
86
    public function save(string $pFilename): void
87
    {
88
        if (empty($pFilename)) {
2✔
89
            throw new InvalidParameterException('pFilename', '');
1✔
90
        }
91
        // If $pFilename is php://output or php://stdout, make it a temporary file...
92
        $originalFilename = $pFilename;
1✔
93
        if ('php://output' == strtolower($pFilename) || 'php://stdout' == strtolower($pFilename)) {
1✔
NEW
94
            $pFilename = @tempnam('./', 'phppttmp');
×
NEW
95
            if ('' == $pFilename) {
×
NEW
96
                $pFilename = $originalFilename;
×
97
            }
98
        }
99

100
        $html = $this->getHtmlContent();
1✔
101

102
        file_put_contents($pFilename, $html);
1✔
103
    }
104

105
    protected function getHtmlContent(): string
106
    {
107
        $presentation = $this->getPhpPresentation();
2✔
108
        $this->ratioX = $presentation->getLayout()->getCX(DocumentLayout::UNIT_PIXEL);
2✔
109
        $this->ratioY = $presentation->getLayout()->getCY(DocumentLayout::UNIT_PIXEL);
2✔
110

111
        // Style
112
        $this->writeCSS();
2✔
113

114
        // Slides
115
        $slides = $presentation->getAllSlides();
2✔
116
        $numSlides = count($slides);
2✔
117
        foreach ($slides as $key => $slide) {
2✔
118
            $this->writeSlide($slide, $key, $numSlides);
2✔
119
        }
120

121
        return  '<html>'
2✔
122
            . '<head><style>' . $this->style . '</style></head>'
2✔
123
            . '<body>' . $this->bodyList . $this->bodySlides . '</body>'
2✔
124
            . '</html>';
2✔
125
    }
126

127
    protected function writeCSS(): void
128
    {
129
        $this->style = '* {box-sizing: border-box;}
2✔
130
      body {width: 100%;height: 100%;margin: 0;}
131
      .slideItem {width: 100%;height: 100%;}
132
      .slideItem .content {width: 100%;height: 100%;position: relative;top: 0;left: 0;overflow: hidden;}
133

134
      .slideItem#slidePage0 .navigation {display: block;}
135
      .slideItem#slidePage0 .content {opacity: 1;}
136

137
      .slideList:target ~ .slideItem .navigation {display: none !important;}
138
      .slideList:target ~ .slideItem .content {opacity: 0 !important;}
139
      .slideList[id="slide0"]:target ~ .slideItem#slidePage0 .navigation,
140
      .slideList[id="slide1"]:target ~ .slideItem#slidePage1 .navigation,
141
      .slideList[id="slide2"]:target ~ .slideItem#slidePage2 .navigation,
142
      .slideList[id="slide3"]:target ~ .slideItem#slidePage3 .navigation,
143
      .slideList[id="slide4"]:target ~ .slideItem#slidePage4 .navigation {
144
        display: block !important;
145
      }';
2✔
146

147
        if ($this->isPDF) {
2✔
148
            // Style PDF Output
149
            $this->style .= PHP_EOL
1✔
150
            . '.slideItem { page-break-after: always; position: relative;}' . PHP_EOL
1✔
151
            . '.slideItem .content { opacity: 1; }' . PHP_EOL;
1✔
152
        } else {
153
            // Style HTML Output
154
            $this->style .= PHP_EOL
1✔
155
            . 'body { overflow: hidden; }' . PHP_EOL
1✔
156
            . '.slideItem { position: absolute;}' . PHP_EOL
1✔
157
            . '.slideItem .content { opacity: 0; }' . PHP_EOL
1✔
158
            // Navigation
1✔
159
            . '.slideItem .navigation {display: none;}
1✔
160
                .slideItem .navigation a {text-decoration: none}
161
                .navigation {position: absolute;z-index: 5;bottom: 30px;right: 30px;font-size: 60px;}
162
                .navigation .next, .navigation .prev {color: #7dbeeb;}
163
                .navigation .disabled {color: #0a629f;}
164
                .navigation .next::after {content: "⮞";}
165
                .navigation .prev {margin-right: 0.3em;}
166
                .navigation .prev::after {content: "⮜";}' . PHP_EOL
1✔
167
            // Animations
1✔
168
            . '.slideList[id="slide0"]:target ~ .slideItem#slidePage0 .content,
1✔
169
                .slideList[id="slide1"]:target ~ .slideItem#slidePage1 .content,
170
                .slideList[id="slide2"]:target ~ .slideItem#slidePage2 .content,
171
                .slideList[id="slide3"]:target ~ .slideItem#slidePage3 .content,
172
                .slideList[id="slide4"]:target ~ .slideItem#slidePage4 .content {
173
                  animation-name: fade_in;
174
                  animation-duration: 0.5s;
175
                  opacity: 1 !important;
176
                }' . PHP_EOL
1✔
177
            . '@keyframes fade_in {from {opacity: 0;} to {opacity: 1;}}' . PHP_EOL;
1✔
178
        }
179
    }
180

181
    protected function writeSlide(Slide $slide, int $numSlideCurrent, int $numSlides): void
182
    {
183
        // PDF : No need navigation
184
        if (!$this->isPDF) {
2✔
185
            $this->bodyList .= '<div id="slide' . $numSlideCurrent . '" class="slideList"></div>';
1✔
186
        }
187
        $this->bodySlides .= '<div id="slidePage' . $numSlideCurrent . '" class="slideItem">';
2✔
188

189
        // PDF : No need navigation
190
        if (!$this->isPDF) {
2✔
191
            if ($numSlides >= 1) {
1✔
192
                $this->bodySlides .= '<div class="navigation">';
1✔
193
                if (($numSlideCurrent - 1) >= 0) {
1✔
NEW
194
                    $this->bodySlides .= '<a href="#slide' . ($numSlideCurrent - 1) . '" class="prev a' . $numSlideCurrent . '"></a>';
×
195
                }
196
                if (($numSlideCurrent + 1) < $numSlides) {
1✔
NEW
197
                    $this->bodySlides .= '<a href="#slide' . ($numSlideCurrent + 1) . '" class="next a' . $numSlideCurrent . '"></a>';
×
198
                }
199
                $this->bodySlides .= '</div>';
1✔
200
            }
201
        }
202

203
        $this->bodySlides .= '<div class="content">';
2✔
204
        foreach ($slide->getShapeCollection() as $shape) {
2✔
205
            if ($shape instanceof Media) {
2✔
NEW
206
                $this->writeVideo($shape);
×
207

NEW
208
                continue;
×
209
            }
210
            if ($shape instanceof Drawing\AbstractDrawingAdapter) {
2✔
211
                $this->writeImage($shape);
2✔
212

213
                continue;
2✔
214
            }
215
            if ($shape instanceof RichText) {
2✔
216
                $this->writeRichText($shape);
2✔
217

218
                continue;
2✔
219
            }
220
            if ($shape instanceof Table) {
2✔
221
                $this->writeTable($shape);
2✔
222

223
                continue;
2✔
224
            }
225
        }
226
        $this->bodySlides .= '</div></div>';
2✔
227
    }
228

229
    protected function writeImage(Drawing\AbstractDrawingAdapter $shape): void
230
    {
231
        $imageData = 'data:' . $shape->getMimeType() . ';base64,' . base64_encode($shape->getContents());
2✔
232

233
        $styles = [];
2✔
234
        $styles[] = 'position: absolute';
2✔
235
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
236
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
237
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
238
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
239
        $styles = array_merge($styles, $this->getStyleShadow($shape->getShadow()));
2✔
240

241
        $this->bodySlides .= '<img src="' . $imageData . '" style="' . implode(';', $styles) . '" alt="' . $shape->getDescription() . '" title="' . $shape->getDescription() . '" />';
2✔
242
    }
243

244
    protected function writeRichText(RichText $shape): void
245
    {
246
        $styles = [];
2✔
247
        $styles[] = 'position: absolute';
2✔
248
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
249
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
250
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
251
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
252

253
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
254
        foreach ($shape->getParagraphs() as $paragraph) {
2✔
255
            if ($paragraph instanceof RichText\Paragraph) {
2✔
256
                $this->writeRichTextParagraph($paragraph);
2✔
257

258
                continue;
2✔
259
            }
260
        }
261
        $this->bodySlides .= '</div>';
2✔
262
    }
263

264
    protected function writeTable(Table $shape): void
265
    {
266
        $styles = [];
2✔
267
        $styles[] = 'position: absolute';
2✔
268
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
269
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
270
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
271
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
272

273
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
274
        $this->bodySlides .= '<table style="width:100%"><tbody>';
2✔
275
        foreach ($shape->getRows() as $row) {
2✔
276
            $this->bodySlides .= '<tr>';
2✔
277
            foreach ($row->getCells() as $cell) {
2✔
278
                $this->bodySlides .= '<td';
2✔
279
                if ($cell->getColspan() > 1) {
2✔
NEW
280
                    $this->bodySlides .= ' colspan="' . $cell->getColspan() . '"';
×
281
                }
282
                $this->bodySlides .= '>';
2✔
283
                foreach ($cell->getParagraphs() as $paragraph) {
2✔
284
                    if ($paragraph instanceof RichText\Paragraph) {
2✔
285
                        $this->writeRichTextParagraph($paragraph);
2✔
286

287
                        continue;
2✔
288
                    }
289
                }
290
                $this->bodySlides .= '</td>';
2✔
291
            }
292
            $this->bodySlides .= '</tr>';
2✔
293
        }
294
        $this->bodySlides .= '</tbody></table>';
2✔
295
        $this->bodySlides .= '</div>';
2✔
296
    }
297

298
    protected function writeVideo(Media $shape): void
299
    {
NEW
300
        $imageData = 'data:' . $shape->getMimeType() . ';base64,' . base64_encode($shape->getContents());
×
301

NEW
302
        $styles = [];
×
NEW
303
        $styles[] = 'position: absolute';
×
NEW
304
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
×
NEW
305
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
×
NEW
306
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
×
NEW
307
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
×
NEW
308
        $styles = array_merge($styles, $this->getStyleShadow($shape->getShadow()));
×
309

NEW
310
        $this->bodySlides .= '<video controls style="' . implode(';', $styles) . '">'
×
NEW
311
          . '<source type ="' . $shape->getMimeType() . '" src="' . $imageData . '" />'
×
NEW
312
          . '</video>';
×
313
    }
314

315
    protected function writeRichTextParagraph(RichText\Paragraph $paragraph): void
316
    {
317
        $styles = array_merge([], $this->getStyleAlignment($paragraph->getAlignment()));
2✔
318
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
319
        foreach ($paragraph->getRichTextElements() as $richTextElement) {
2✔
NEW
320
            if ($richTextElement instanceof RichText\Run) {
×
NEW
321
                if ($richTextElement->hasHyperlink() && '' != $richTextElement->getHyperlink()->getUrl()) {
×
NEW
322
                    $this->bodySlides .= '<a href="' . $richTextElement->getHyperlink()->getUrl() . '">';
×
323
                }
NEW
324
                $styles = array_merge([], $this->getStyleFont($richTextElement->getFont()));
×
NEW
325
                $this->bodySlides .= '<span style="' . implode(';', $styles) . '">';
×
NEW
326
                $this->bodySlides .= $richTextElement->getText();
×
NEW
327
                $this->bodySlides .= '</span>';
×
NEW
328
                if ($richTextElement->hasHyperlink() && '' != $richTextElement->getHyperlink()->getUrl()) {
×
NEW
329
                    $this->bodySlides .= '</a>';
×
330
                }
331

NEW
332
                continue;
×
333
            }
NEW
334
            if ($richTextElement instanceof RichText\BreakElement) {
×
NEW
335
                $this->bodySlides .= '<br />';
×
336

NEW
337
                continue;
×
338
            }
339
        }
340
        $this->bodySlides .= '</div>';
2✔
341
    }
342

343
    /**
344
     * @return array<string>
345
     */
346
    protected function getStyleAlignment(Alignment $style): array
347
    {
348
        $styles = [];
2✔
349
        $styles[] = 'text-align: ' . ($style->getHorizontal() === Alignment::HORIZONTAL_CENTER ? 'center' : ($style->getHorizontal() === Alignment::HORIZONTAL_RIGHT ? 'right' : 'left'));
2✔
350

351
        return $styles;
2✔
352
    }
353

354
    /**
355
     * @return array<string>
356
     */
357
    protected function getStyleFont(Font $style): array
358
    {
NEW
359
        $styles = [];
×
NEW
360
        if ($style->isBold()) {
×
NEW
361
            $styles[] = 'font-weight: bold';
×
362
        }
NEW
363
        $styles[] = 'font-family: ' . $style->getName();
×
NEW
364
        $styles[] = 'font-size: ' . $style->getSize() . 'px';
×
NEW
365
        $styles[] = 'color: #' . $style->getColor()->getRGB();
×
366

NEW
367
        return $styles;
×
368
    }
369

370
    /**
371
     * @return array<string>
372
     */
373
    protected function getStyleShadow(Shadow $style): array
374
    {
375
        if (!$style->isVisible()) {
2✔
376
            return [];
2✔
377
        }
NEW
378
        $boxShadow = '';
×
NEW
379
        switch($style->getDirection()) {
×
NEW
380
            case 45:
×
NEW
381
                $boxShadow = $style->getDistance() . 'px ' . $style->getDistance() . 'px';
×
382

NEW
383
                break;
×
NEW
384
            case 90:
×
NEW
385
                $boxShadow = $style->getDistance() . 'px 0px';
×
386

NEW
387
                break;
×
NEW
388
            case 135:
×
NEW
389
                $boxShadow = '-' . $style->getDistance() . 'px ' . $style->getDistance() . 'px';
×
390

NEW
391
                break;
×
NEW
392
            case 180:
×
NEW
393
                $boxShadow = '-' . $style->getDistance() . 'px 0px';
×
394

NEW
395
                break;
×
NEW
396
            case 225:
×
NEW
397
                $boxShadow = '-' . $style->getDistance() . 'px -' . $style->getDistance() . 'px';
×
398

NEW
399
                break;
×
NEW
400
            case 270:
×
NEW
401
                $boxShadow = '-' . $style->getDistance() . 'px 0px';
×
402

NEW
403
                break;
×
NEW
404
            case 315:
×
NEW
405
                $boxShadow = $style->getDistance() . 'px -' . $style->getDistance() . 'px';
×
406

NEW
407
                break;
×
408
        }
409

NEW
410
        $styles = [];
×
NEW
411
        if ($boxShadow) {
×
NEW
412
            $styles[] = 'box-shadow: ' . $boxShadow . ' ' . $style->getBlurRadius() . 'px  #' . $style->getColor()->getRGB();
×
413
        }
NEW
414
        $styles[] = 'opacity: ' . ($style->getColor()->getAlpha() / 100);
×
415

NEW
416
        return $styles;
×
417
    }
418
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc