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

PHPOffice / PHPPresentation / 13520067745

25 Feb 2025 11:23AM UTC coverage: 89.215% (-1.7%) from 90.953%
13520067745

Pull #855

github

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

1 of 205 new or added lines in 6 files covered. (0.49%)

9521 of 10672 relevant lines covered (89.21%)

43.47 hits per line

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

0.0
/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
NEW
77
        $this->setPhpPresentation($pPhpPresentation ?? new PhpPresentation());
×
78

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

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

NEW
100
        $html = $this->getHtmlContent();
×
101

NEW
102
        file_put_contents($pFilename, $html);
×
103
    }
104

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

111
        // Style
NEW
112
        $this->writeCSS();
×
113

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

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

127
    protected function writeCSS(): void
128
    {
NEW
129
        $this->style = '* {box-sizing: border-box;}
×
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;
NEW
145
      }';
×
146

NEW
147
        if ($this->isPDF) {
×
148
            // Style PDF Output
NEW
149
            $this->style .= PHP_EOL
×
NEW
150
            . '.slideItem { page-break-after: always; position: relative;}' . PHP_EOL
×
NEW
151
            . '.slideItem .content { opacity: 1; }' . PHP_EOL;
×
152
        } else {
153
            // Style HTML Output
NEW
154
            $this->style .= PHP_EOL
×
NEW
155
            . 'body { overflow: hidden; }' . PHP_EOL
×
NEW
156
            . '.slideItem { position: absolute;}' . PHP_EOL
×
NEW
157
            . '.slideItem .content { opacity: 0; }' . PHP_EOL
×
NEW
158
            // Navigation
×
NEW
159
            . '.slideItem .navigation {display: none;}
×
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;}
NEW
166
                .navigation .prev::after {content: "⮜";}' . PHP_EOL
×
NEW
167
            // Animations
×
NEW
168
            . '.slideList[id="slide0"]:target ~ .slideItem#slidePage0 .content,
×
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;
NEW
176
                }' . PHP_EOL
×
NEW
177
            . '@keyframes fade_in {from {opacity: 0;} to {opacity: 1;}}' . PHP_EOL;
×
178
        }
179
    }
180

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

NEW
287
                        continue;
×
288
                    }
289
                }
NEW
290
                $this->bodySlides .= '</td>';
×
291
            }
NEW
292
            $this->bodySlides .= '</tr>';
×
293
        }
NEW
294
        $this->bodySlides .= '</tbody></table>';
×
NEW
295
        $this->bodySlides .= '</div>';
×
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
    {
NEW
317
        $styles = array_merge([], $this->getStyleAlignment($paragraph->getAlignment()));
×
NEW
318
        $this->bodySlides .= '<div style="' . implode(';', $styles) . '">';
×
NEW
319
        foreach ($paragraph->getRichTextElements() as $richTextElement) {
×
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
        }
NEW
340
        $this->bodySlides .= '</div>';
×
341
    }
342

343
    protected function getStyleAlignment(Alignment $style): array
344
    {
NEW
345
        $styles = [];
×
NEW
346
        $styles[] = 'text-align: ' . ($style->getHorizontal() === Alignment::HORIZONTAL_CENTER ? 'center' : ($style->getHorizontal() === Alignment::HORIZONTAL_RIGHT ? 'right' : 'left'));
×
347

NEW
348
        return $styles;
×
349
    }
350

351
    protected function getStyleFont(Font $style): array
352
    {
NEW
353
        $styles = [];
×
NEW
354
        if ($style->isBold()) {
×
NEW
355
            $styles[] = 'font-weight: bold';
×
356
        }
NEW
357
        $styles[] = 'font-family: ' . $style->getName();
×
NEW
358
        $styles[] = 'font-size: ' . $style->getSize() . 'px';
×
NEW
359
        $styles[] = 'color: #' . $style->getColor()->getRGB();
×
360

NEW
361
        return $styles;
×
362
    }
363

364
    protected function getStyleShadow(Shadow $style): array
365
    {
NEW
366
        if (!$style->isVisible()) {
×
NEW
367
            return [];
×
368
        }
NEW
369
        $boxShadow = '';
×
NEW
370
        switch($style->getDirection()) {
×
NEW
371
            case 45:
×
NEW
372
                $boxShadow = $style->getDistance() . 'px ' . $style->getDistance() . 'px';
×
373

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

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

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

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

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

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

NEW
398
                break;
×
399
        }
400

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

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