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

PHPOffice / PHPPresentation / 13527819123

25 Feb 2025 06:01PM UTC coverage: 90.516% (-0.4%) from 90.953%
13527819123

push

github

web-flow
PDF/HTML Writer : Added support (#855)

129 of 193 new or added lines in 6 files covered. (66.84%)

9649 of 10660 relevant lines covered (90.52%)

43.67 hits per line

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

62.79
/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

92
        file_put_contents($pFilename, $this->getHtmlContent());
1✔
93
    }
94

95
    protected function getHtmlContent(): string
96
    {
97
        $presentation = $this->getPhpPresentation();
2✔
98
        $this->ratioX = $presentation->getLayout()->getCX(DocumentLayout::UNIT_PIXEL);
2✔
99
        $this->ratioY = $presentation->getLayout()->getCY(DocumentLayout::UNIT_PIXEL);
2✔
100

101
        // Style
102
        $this->writeCSS();
2✔
103

104
        // Slides
105
        $slides = $presentation->getAllSlides();
2✔
106
        $numSlides = count($slides);
2✔
107
        foreach ($slides as $key => $slide) {
2✔
108
            $this->writeSlide($slide, $key, $numSlides);
2✔
109
        }
110

111
        return  '<html>'
2✔
112
            . '<head><style>' . $this->style . '</style></head>'
2✔
113
            . '<body>' . $this->bodyList . $this->bodySlides . '</body>'
2✔
114
            . '</html>';
2✔
115
    }
116

117
    protected function writeCSS(): void
118
    {
119
        $this->style = '* {box-sizing: border-box;}
2✔
120
      body {width: 100%;height: 100%;margin: 0;}
121
      .slideItem {width: 100%;height: 100%;}
122
      .slideItem .content {width: 100%;height: 100%;position: relative;top: 0;left: 0;overflow: hidden;}
123

124
      .slideItem#slidePage0 .navigation {display: block;}
125
      .slideItem#slidePage0 .content {opacity: 1;}
126

127
      .slideList:target ~ .slideItem .navigation {display: none !important;}
128
      .slideList:target ~ .slideItem .content {opacity: 0 !important;}
129
      .slideList[id="slide0"]:target ~ .slideItem#slidePage0 .navigation,
130
      .slideList[id="slide1"]:target ~ .slideItem#slidePage1 .navigation,
131
      .slideList[id="slide2"]:target ~ .slideItem#slidePage2 .navigation,
132
      .slideList[id="slide3"]:target ~ .slideItem#slidePage3 .navigation,
133
      .slideList[id="slide4"]:target ~ .slideItem#slidePage4 .navigation {
134
        display: block !important;
135
      }';
2✔
136

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

171
    protected function writeSlide(Slide $slide, int $numSlideCurrent, int $numSlides): void
172
    {
173
        // PDF : No need navigation
174
        if (!$this->isPDF) {
2✔
175
            $this->bodyList .= '<div id="slide' . $numSlideCurrent . '" class="slideList"></div>';
1✔
176
        }
177
        $this->bodySlides .= '<div id="slidePage' . $numSlideCurrent . '" class="slideItem">';
2✔
178

179
        // PDF : No need navigation
180
        if (!$this->isPDF) {
2✔
181
            if ($numSlides >= 1) {
1✔
182
                $this->bodySlides .= '<div class="navigation">';
1✔
183
                if (($numSlideCurrent - 1) >= 0) {
1✔
NEW
184
                    $this->bodySlides .= '<a href="#slide' . ($numSlideCurrent - 1) . '" class="prev a' . $numSlideCurrent . '"></a>';
×
185
                }
186
                if (($numSlideCurrent + 1) < $numSlides) {
1✔
NEW
187
                    $this->bodySlides .= '<a href="#slide' . ($numSlideCurrent + 1) . '" class="next a' . $numSlideCurrent . '"></a>';
×
188
                }
189
                $this->bodySlides .= '</div>';
1✔
190
            }
191
        }
192

193
        $this->bodySlides .= '<div class="content">';
2✔
194
        foreach ($slide->getShapeCollection() as $shape) {
2✔
195
            if ($shape instanceof Media) {
2✔
NEW
196
                $this->writeVideo($shape);
×
197

NEW
198
                continue;
×
199
            }
200
            if ($shape instanceof Drawing\AbstractDrawingAdapter) {
2✔
201
                $this->writeImage($shape);
2✔
202

203
                continue;
2✔
204
            }
205
            if ($shape instanceof RichText) {
2✔
206
                $this->writeRichText($shape);
2✔
207

208
                continue;
2✔
209
            }
210
            if ($shape instanceof Table) {
2✔
211
                $this->writeTable($shape);
2✔
212

213
                continue;
2✔
214
            }
215
        }
216
        $this->bodySlides .= '</div></div>';
2✔
217
    }
218

219
    protected function writeImage(Drawing\AbstractDrawingAdapter $shape): void
220
    {
221
        $imageData = 'data:' . $shape->getMimeType() . ';base64,' . base64_encode($shape->getContents());
2✔
222

223
        $styles = [];
2✔
224
        $styles[] = 'position: absolute';
2✔
225
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
226
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
227
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
228
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
229
        $styles = array_merge($styles, $this->getStyleShadow($shape->getShadow()));
2✔
230

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

234
    protected function writeRichText(RichText $shape): void
235
    {
236
        $styles = [];
2✔
237
        $styles[] = 'position: absolute';
2✔
238
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
239
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
240
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
241
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
242

243
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
244
        foreach ($shape->getParagraphs() as $paragraph) {
2✔
245
            if ($paragraph instanceof RichText\Paragraph) {
2✔
246
                $this->writeRichTextParagraph($paragraph);
2✔
247

248
                continue;
2✔
249
            }
250
        }
251
        $this->bodySlides .= '</div>';
2✔
252
    }
253

254
    protected function writeTable(Table $shape): void
255
    {
256
        $styles = [];
2✔
257
        $styles[] = 'position: absolute';
2✔
258
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
2✔
259
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
2✔
260
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
2✔
261
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
2✔
262

263
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
264
        $this->bodySlides .= '<table style="width:100%"><tbody>';
2✔
265
        foreach ($shape->getRows() as $row) {
2✔
266
            $this->bodySlides .= '<tr>';
2✔
267
            foreach ($row->getCells() as $cell) {
2✔
268
                $this->bodySlides .= '<td';
2✔
269
                if ($cell->getColspan() > 1) {
2✔
NEW
270
                    $this->bodySlides .= ' colspan="' . $cell->getColspan() . '"';
×
271
                }
272
                $this->bodySlides .= '>';
2✔
273
                foreach ($cell->getParagraphs() as $paragraph) {
2✔
274
                    if ($paragraph instanceof RichText\Paragraph) {
2✔
275
                        $this->writeRichTextParagraph($paragraph);
2✔
276

277
                        continue;
2✔
278
                    }
279
                }
280
                $this->bodySlides .= '</td>';
2✔
281
            }
282
            $this->bodySlides .= '</tr>';
2✔
283
        }
284
        $this->bodySlides .= '</tbody></table>';
2✔
285
        $this->bodySlides .= '</div>';
2✔
286
    }
287

288
    protected function writeVideo(Media $shape): void
289
    {
NEW
290
        $imageData = 'data:' . $shape->getMimeType() . ';base64,' . base64_encode($shape->getContents());
×
291

NEW
292
        $styles = [];
×
NEW
293
        $styles[] = 'position: absolute';
×
NEW
294
        $styles[] = 'width: ' . $shape->getWidth() . 'px';
×
NEW
295
        $styles[] = 'height: ' . $shape->getHeight() . 'px';
×
NEW
296
        $styles[] = 'top: ' . $shape->getOffsetY() . 'px';
×
NEW
297
        $styles[] = 'left: ' . $shape->getOffsetX() . 'px';
×
NEW
298
        $styles = array_merge($styles, $this->getStyleShadow($shape->getShadow()));
×
299

NEW
300
        $this->bodySlides .= '<video controls style="' . implode(';', $styles) . '">'
×
NEW
301
          . '<source type ="' . $shape->getMimeType() . '" src="' . $imageData . '" />'
×
NEW
302
          . '</video>';
×
303
    }
304

305
    protected function writeRichTextParagraph(RichText\Paragraph $paragraph): void
306
    {
307
        $styles = array_merge([], $this->getStyleAlignment($paragraph->getAlignment()));
2✔
308
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
2✔
309
        foreach ($paragraph->getRichTextElements() as $richTextElement) {
2✔
NEW
310
            if ($richTextElement instanceof RichText\Run) {
×
NEW
311
                if ($richTextElement->hasHyperlink() && '' != $richTextElement->getHyperlink()->getUrl()) {
×
NEW
312
                    $this->bodySlides .= '<a href="' . $richTextElement->getHyperlink()->getUrl() . '">';
×
313
                }
NEW
314
                $styles = array_merge([], $this->getStyleFont($richTextElement->getFont()));
×
NEW
315
                $this->bodySlides .= '<span style="' . implode(';', $styles) . '">';
×
NEW
316
                $this->bodySlides .= $richTextElement->getText();
×
NEW
317
                $this->bodySlides .= '</span>';
×
NEW
318
                if ($richTextElement->hasHyperlink() && '' != $richTextElement->getHyperlink()->getUrl()) {
×
NEW
319
                    $this->bodySlides .= '</a>';
×
320
                }
321

NEW
322
                continue;
×
323
            }
NEW
324
            if ($richTextElement instanceof RichText\BreakElement) {
×
NEW
325
                $this->bodySlides .= '<br />';
×
326

NEW
327
                continue;
×
328
            }
329
        }
330
        $this->bodySlides .= '</div>';
2✔
331
    }
332

333
    /**
334
     * @return array<string>
335
     */
336
    protected function getStyleAlignment(Alignment $style): array
337
    {
338
        $styles = [];
2✔
339
        $styles[] = 'text-align: ' . ($style->getHorizontal() === Alignment::HORIZONTAL_CENTER ? 'center' : ($style->getHorizontal() === Alignment::HORIZONTAL_RIGHT ? 'right' : 'left'));
2✔
340

341
        return $styles;
2✔
342
    }
343

344
    /**
345
     * @return array<string>
346
     */
347
    protected function getStyleFont(Font $style): array
348
    {
NEW
349
        $styles = [];
×
NEW
350
        if ($style->isBold()) {
×
NEW
351
            $styles[] = 'font-weight: bold';
×
352
        }
NEW
353
        $styles[] = 'font-family: ' . $style->getName();
×
NEW
354
        $styles[] = 'font-size: ' . $style->getSize() . 'px';
×
NEW
355
        $styles[] = 'color: #' . $style->getColor()->getRGB();
×
356

NEW
357
        return $styles;
×
358
    }
359

360
    /**
361
     * @return array<string>
362
     */
363
    protected function getStyleShadow(Shadow $style): array
364
    {
365
        if (!$style->isVisible()) {
2✔
366
            return [];
2✔
367
        }
NEW
368
        $boxShadow = '';
×
NEW
369
        switch($style->getDirection()) {
×
NEW
370
            case 45:
×
NEW
371
                $boxShadow = $style->getDistance() . 'px ' . $style->getDistance() . 'px';
×
372

NEW
373
                break;
×
NEW
374
            case 90:
×
NEW
375
                $boxShadow = $style->getDistance() . 'px 0px';
×
376

NEW
377
                break;
×
NEW
378
            case 135:
×
NEW
379
                $boxShadow = '-' . $style->getDistance() . 'px ' . $style->getDistance() . 'px';
×
380

NEW
381
                break;
×
NEW
382
            case 180:
×
NEW
383
                $boxShadow = '-' . $style->getDistance() . 'px 0px';
×
384

NEW
385
                break;
×
NEW
386
            case 225:
×
NEW
387
                $boxShadow = '-' . $style->getDistance() . 'px -' . $style->getDistance() . 'px';
×
388

NEW
389
                break;
×
NEW
390
            case 270:
×
NEW
391
                $boxShadow = '-' . $style->getDistance() . 'px 0px';
×
392

NEW
393
                break;
×
NEW
394
            case 315:
×
NEW
395
                $boxShadow = $style->getDistance() . 'px -' . $style->getDistance() . 'px';
×
396

NEW
397
                break;
×
398
        }
399

NEW
400
        $styles = [];
×
NEW
401
        if ($boxShadow) {
×
NEW
402
            $styles[] = 'box-shadow: ' . $boxShadow . ' ' . $style->getBlurRadius() . 'px  #' . $style->getColor()->getRGB();
×
403
        }
NEW
404
        $styles[] = 'opacity: ' . ($style->getColor()->getAlpha() / 100);
×
405

NEW
406
        return $styles;
×
407
    }
408
}
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