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

PHPOffice / PhpSpreadsheet / 23549816303

25 Mar 2026 03:40PM UTC coverage: 96.96% (+0.06%) from 96.904%
23549816303

Pull #4834

github

web-flow
Merge 707a20c8e into eb391d1f2
Pull Request #4834: Add parallel Xlsx writing via pcntl_fork

187 of 187 new or added lines in 6 files covered. (100.0%)

111 existing lines in 4 files now uncovered.

47966 of 49470 relevant lines covered (96.96%)

383.46 hits per line

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

94.04
/src/PhpSpreadsheet/Writer/Xlsx/ContentTypes.php
1
<?php
2

3
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
4

5
use Composer\Pcre\Preg;
6
use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Namespaces;
7
use PhpOffice\PhpSpreadsheet\Shared\File;
8
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
9
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing as WorksheetDrawing;
11
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
12
use PhpOffice\PhpSpreadsheet\Writer\Exception as WriterException;
13

14
class ContentTypes extends WriterPart
15
{
16
    /**
17
     * Write content types to XML format.
18
     *
19
     * @param bool $includeCharts Flag indicating if we should include drawing details for charts
20
     *
21
     * @return string XML Output
22
     */
23
    public function writeContentTypes(Spreadsheet $spreadsheet, bool $includeCharts = false): string
443✔
24
    {
25
        // Create XML writer
26
        $objWriter = null;
443✔
27
        if ($this->getParentWriter()->getUseDiskCaching()) {
443✔
28
            $objWriter = new XMLWriter(XMLWriter::STORAGE_DISK, $this->getParentWriter()->getDiskCachingDirectory());
10✔
29
        } else {
30
            $objWriter = new XMLWriter(XMLWriter::STORAGE_MEMORY);
435✔
31
        }
32

33
        // XML header
34
        $objWriter->startDocument('1.0', 'UTF-8', 'yes');
443✔
35

36
        // Types
37
        $objWriter->startElement('Types');
443✔
38
        $objWriter->writeAttribute('xmlns', Namespaces::CONTENT_TYPES);
443✔
39

40
        // Theme
41
        $this->writeOverrideContentType($objWriter, '/xl/theme/theme1.xml', 'application/vnd.openxmlformats-officedocument.theme+xml');
443✔
42

43
        // Styles
44
        $this->writeOverrideContentType($objWriter, '/xl/styles.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml');
443✔
45

46
        // Rels
47
        $this->writeDefaultContentType($objWriter, 'rels', 'application/vnd.openxmlformats-package.relationships+xml');
443✔
48

49
        // XML
50
        $this->writeDefaultContentType($objWriter, 'xml', 'application/xml');
443✔
51

52
        // VML
53
        $this->writeDefaultContentType($objWriter, 'vml', 'application/vnd.openxmlformats-officedocument.vmlDrawing');
443✔
54

55
        // Workbook
56
        if ($spreadsheet->hasMacros()) { //Macros in workbook ?
443✔
57
            // Yes : not standard content but "macroEnabled"
58
            $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.ms-excel.sheet.macroEnabled.main+xml');
2✔
59
            //... and define a new type for the VBA project
60
            // Better use Override, because we can use 'bin' also for xl\printerSettings\printerSettings1.bin
61
            $this->writeOverrideContentType($objWriter, '/xl/vbaProject.bin', 'application/vnd.ms-office.vbaProject');
2✔
62
            if ($spreadsheet->hasMacrosCertificate()) {
2✔
63
                // signed macros ?
64
                // Yes : add needed information
65
                $this->writeOverrideContentType($objWriter, '/xl/vbaProjectSignature.bin', 'application/vnd.ms-office.vbaProjectSignature');
2✔
66
            }
67
        } else {
68
            // no macros in workbook, so standard type
69
            $this->writeOverrideContentType($objWriter, '/xl/workbook.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml');
441✔
70
        }
71

72
        // DocProps
73
        $this->writeOverrideContentType($objWriter, '/docProps/app.xml', 'application/vnd.openxmlformats-officedocument.extended-properties+xml');
443✔
74

75
        $this->writeOverrideContentType($objWriter, '/docProps/core.xml', 'application/vnd.openxmlformats-package.core-properties+xml');
443✔
76

77
        $customPropertyList = $spreadsheet->getProperties()->getCustomProperties();
443✔
78
        if (!empty($customPropertyList)) {
443✔
79
            $this->writeOverrideContentType($objWriter, '/docProps/custom.xml', 'application/vnd.openxmlformats-officedocument.custom-properties+xml');
33✔
80
        }
81

82
        // Worksheets
83
        $sheetCount = $spreadsheet->getSheetCount();
443✔
84
        for ($i = 0; $i < $sheetCount; ++$i) {
443✔
85
            $this->writeOverrideContentType($objWriter, '/xl/worksheets/sheet' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml');
443✔
86
        }
87

88
        // Shared strings
89
        $this->writeOverrideContentType($objWriter, '/xl/sharedStrings.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml');
443✔
90

91
        // Table
92
        $table = 1;
443✔
93
        for ($i = 0; $i < $sheetCount; ++$i) {
443✔
94
            $tableCount = $spreadsheet->getSheet($i)->getTableCollection()->count();
443✔
95

96
            for ($t = 1; $t <= $tableCount; ++$t) {
443✔
97
                $this->writeOverrideContentType($objWriter, '/xl/tables/table' . $table++ . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml');
8✔
98
            }
99
        }
100

101
        // Add worksheet relationship content types
102
        /** @var mixed[][][][] */
103
        $unparsedLoadedData = $spreadsheet->getUnparsedLoadedData();
443✔
104
        $chart = 1;
443✔
105
        for ($i = 0; $i < $sheetCount; ++$i) {
443✔
106
            $drawings = $spreadsheet->getSheet($i)->getDrawingCollection();
443✔
107
            $drawingCount = count($drawings);
443✔
108
            $chartCount = ($includeCharts) ? $spreadsheet->getSheet($i)->getChartCount() : 0;
443✔
109
            $hasUnparsedDrawing = isset($unparsedLoadedData['sheets'][$spreadsheet->getSheet($i)->getCodeName()]['drawingOriginalIds']);
443✔
110

111
            //    We need a drawing relationship for the worksheet if we have either drawings or charts
112
            if (($drawingCount > 0) || ($chartCount > 0) || $hasUnparsedDrawing) {
443✔
113
                $this->writeOverrideContentType($objWriter, '/xl/drawings/drawing' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.drawing+xml');
145✔
114
            }
115

116
            //    If we have charts, then we need a chart relationship for every individual chart
117
            if ($chartCount > 0) {
443✔
118
                for ($c = 0; $c < $chartCount; ++$c) {
79✔
119
                    $this->writeOverrideContentType($objWriter, '/xl/charts/chart' . $chart++ . '.xml', 'application/vnd.openxmlformats-officedocument.drawingml.chart+xml');
79✔
120
                }
121
            }
122
        }
123

124
        // Comments
125
        for ($i = 0; $i < $sheetCount; ++$i) {
443✔
126
            if (count($spreadsheet->getSheet($i)->getComments()) > 0) {
443✔
127
                $this->writeOverrideContentType($objWriter, '/xl/comments' . ($i + 1) . '.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml');
31✔
128
            }
129
        }
130

131
        // Add media content-types
132
        $aMediaContentTypes = [];
443✔
133
        $mediaCount = $this->getParentWriter()->getDrawingHashTable()->count();
443✔
134
        for ($i = 0; $i < $mediaCount; ++$i) {
443✔
135
            $extension = '';
74✔
136
            $mimeType = '';
74✔
137

138
            $drawing = $this->getParentWriter()->getDrawingHashTable()->getByIndex($i);
74✔
139
            if ($drawing instanceof WorksheetDrawing && $drawing->getPath() !== '') {
74✔
140
                $extension = strtolower($drawing->getExtension());
63✔
141
                if ($drawing->getIsUrl()) {
63✔
UNCOV
142
                    $mimeType = image_type_to_mime_type($drawing->getType());
×
143
                } else {
144
                    $mimeType = $this->getImageMimeType($drawing->getPath());
63✔
145
                }
146
            } elseif ($drawing instanceof MemoryDrawing) {
11✔
147
                $extension = strtolower($drawing->getMimeType());
11✔
148
                $extension = explode('/', $extension);
11✔
149
                $extension = $extension[1];
11✔
150

151
                $mimeType = $drawing->getMimeType();
11✔
152
            }
153

154
            if ($mimeType !== '' && !isset($aMediaContentTypes[$extension])) {
74✔
155
                $aMediaContentTypes[$extension] = $mimeType;
74✔
156

157
                $this->writeDefaultContentType($objWriter, $extension, $mimeType);
74✔
158
            }
159
        }
160

161
        if ($spreadsheet->hasInCellDrawings()) {
443✔
162
            $this->writeOverrideContentType($objWriter, '/xl/richData/richValueRel.xml', 'application/vnd.ms-excel.richvaluerel+xml');
8✔
163
            $this->writeOverrideContentType($objWriter, '/xl/richData/rdrichvalue.xml', 'application/vnd.ms-excel.rdrichvalue+xml');
8✔
164
            $this->writeOverrideContentType($objWriter, '/xl/richData/rdrichvaluestructure.xml', 'application/vnd.ms-excel.rdrichvaluestructure+xml');
8✔
165
            $this->writeOverrideContentType($objWriter, '/xl/richData/rdRichValueTypes.xml', 'application/vnd.ms-excel.rdrichvaluetypes+xml');
8✔
166
        }
167

168
        // Add pass-through media content types
169
        /** @var array<string, array<string, mixed>> $sheets */
170
        $sheets = $unparsedLoadedData['sheets'] ?? [];
443✔
171
        foreach ($sheets as $sheetData) {
443✔
172
            if (($sheetData['drawingPassThroughEnabled'] ?? false) !== true) {
82✔
173
                continue;
73✔
174
            }
175
            /** @var string[] $mediaFiles */
176
            $mediaFiles = $sheetData['drawingMediaFiles'] ?? [];
9✔
177
            foreach ($mediaFiles as $mediaPath) {
9✔
178
                $extension = strtolower(pathinfo($mediaPath, PATHINFO_EXTENSION));
9✔
179
                if ($extension !== '' && !isset($aMediaContentTypes[$extension])) {
9✔
180
                    $mimeType = match ($extension) { // @phpstan-ignore match.unhandled
3✔
181
                        'png' => 'image/png',
1✔
UNCOV
182
                        'jpg', 'jpeg' => 'image/jpeg',
×
183
                        'gif' => 'image/gif',
1✔
184
                        'bmp' => 'image/bmp',
1✔
185
                        'tif', 'tiff' => 'image/tiff',
1✔
186
                        'svg' => 'image/svg+xml',
3✔
187
                    };
3✔
188
                    $aMediaContentTypes[$extension] = $mimeType;
3✔
189
                    $this->writeDefaultContentType($objWriter, $extension, $mimeType);
3✔
190
                }
191
            }
192
        }
193

194
        if ($spreadsheet->hasRibbonBinObjects()) {
443✔
195
            // Some additional objects in the ribbon ?
196
            // we need to write "Extension" but not already write for media content
197
            /** @var string[] */
198
            $tabRibbonTypes = array_diff($spreadsheet->getRibbonBinObjects('types') ?? [], array_keys($aMediaContentTypes));
×
199
            foreach ($tabRibbonTypes as $aRibbonType) {
×
200
                $mimeType = 'image/.' . $aRibbonType; //we wrote $mimeType like customUI Editor
×
UNCOV
201
                $this->writeDefaultContentType($objWriter, $aRibbonType, $mimeType);
×
202
            }
203
        }
204
        $sheetCount = $spreadsheet->getSheetCount();
443✔
205
        for ($i = 0; $i < $sheetCount; ++$i) {
443✔
206
            if (count($spreadsheet->getSheet($i)->getHeaderFooter()->getImages()) > 0) {
443✔
207
                foreach ($spreadsheet->getSheet($i)->getHeaderFooter()->getImages() as $image) {
4✔
208
                    if ($image->getPath() !== '' && !isset($aMediaContentTypes[strtolower($image->getExtension())])) {
4✔
209
                        $aMediaContentTypes[strtolower($image->getExtension())] = $this->getImageMimeType($image->getPath());
4✔
210

211
                        $this->writeDefaultContentType($objWriter, strtolower($image->getExtension()), $aMediaContentTypes[strtolower($image->getExtension())]);
4✔
212
                    }
213
                }
214
            }
215

216
            if (count($spreadsheet->getSheet($i)->getComments()) > 0) {
443✔
217
                foreach ($spreadsheet->getSheet($i)->getComments() as $comment) {
31✔
218
                    if (!$comment->hasBackgroundImage()) {
31✔
219
                        continue;
29✔
220
                    }
221

222
                    $bgImage = $comment->getBackgroundImage();
3✔
223
                    $bgImageExtentionKey = strtolower($bgImage->getImageFileExtensionForSave(false));
3✔
224

225
                    if (!isset($aMediaContentTypes[$bgImageExtentionKey])) {
3✔
226
                        $aMediaContentTypes[$bgImageExtentionKey] = $bgImage->getImageMimeType();
3✔
227

228
                        $this->writeDefaultContentType($objWriter, $bgImageExtentionKey, $aMediaContentTypes[$bgImageExtentionKey]);
3✔
229
                    }
230
                }
231
            }
232

233
            $bgImage = $spreadsheet->getSheet($i)->getBackgroundImage();
443✔
234
            $mimeType = $spreadsheet->getSheet($i)->getBackgroundMime();
443✔
235
            $extension = $spreadsheet->getSheet($i)->getBackgroundExtension();
443✔
236
            if ($bgImage !== '' && !isset($aMediaContentTypes[$extension])) {
443✔
237
                $this->writeDefaultContentType($objWriter, $extension, $mimeType);
1✔
238
            }
239
        }
240

241
        // unparsed defaults
242
        if (isset($unparsedLoadedData['default_content_types'])) {
443✔
243
            /** @var array<string, string> */
244
            $unparsedDefault = $unparsedLoadedData['default_content_types'];
41✔
245
            foreach ($unparsedDefault as $extName => $contentType) {
41✔
246
                $this->writeDefaultContentType($objWriter, $extName, $contentType);
41✔
247
            }
248
        }
249

250
        // unparsed overrides
251
        if (isset($unparsedLoadedData['override_content_types'])) {
443✔
252
            /** @var array<string, string> */
253
            $unparsedOverride = $unparsedLoadedData['override_content_types'];
4✔
254
            foreach ($unparsedOverride as $partName => $overrideType) {
4✔
255
                $this->writeOverrideContentType($objWriter, $partName, $overrideType);
4✔
256
            }
257
        }
258

259
        // Metadata needed for Dynamic Arrays
260
        if ($this->getParentWriter()->useDynamicArrays() || $spreadsheet->hasInCellDrawings()) {
443✔
261
            $this->writeOverrideContentType($objWriter, '/xl/metadata.xml', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml');
20✔
262
        }
263

264
        if ($spreadsheet->getUsesCheckboxStyle()) {
443✔
265
            $this->writeOverrideContentType($objWriter, '/xl/featurePropertyBag/featurePropertyBag.xml', 'application/vnd.ms-excel.featurepropertybag+xml');
2✔
266
        }
267

268
        $objWriter->endElement();
443✔
269

270
        // Return
271
        return $objWriter->getData();
443✔
272
    }
273

274
    private static int $three = 3; // phpstan silliness
275

276
    /**
277
     * Get image mime type.
278
     *
279
     * @param string $filename Filename
280
     *
281
     * @return string Mime Type
282
     */
283
    private function getImageMimeType(string $filename): string
67✔
284
    {
285
        if (Preg::isMatch('~^data:(image/[^;]+);base64,~', $filename, $matches)) {
67✔
286
            return $matches[1];
1✔
287
        }
288
        if (File::fileExists($filename)) {
66✔
289
            $image = getimagesize($filename);
66✔
290

291
            return image_type_to_mime_type((is_array($image) && count($image) >= self::$three) ? $image[2] : 0);
66✔
292
        }
293

UNCOV
294
        throw new WriterException("File $filename does not exist");
×
295
    }
296

297
    /**
298
     * Write Default content type.
299
     *
300
     * @param string $partName Part name
301
     * @param string $contentType Content type
302
     */
303
    private function writeDefaultContentType(XMLWriter $objWriter, string $partName, string $contentType): void
443✔
304
    {
305
        if ($partName != '' && $contentType != '') {
443✔
306
            // Write content type
307
            $objWriter->startElement('Default');
443✔
308
            $objWriter->writeAttribute('Extension', $partName);
443✔
309
            $objWriter->writeAttribute('ContentType', $contentType);
443✔
310
            $objWriter->endElement();
443✔
311
        } else {
UNCOV
312
            throw new WriterException('Invalid parameters passed.');
×
313
        }
314
    }
315

316
    /**
317
     * Write Override content type.
318
     *
319
     * @param string $partName Part name
320
     * @param string $contentType Content type
321
     */
322
    private function writeOverrideContentType(XMLWriter $objWriter, string $partName, string $contentType): void
443✔
323
    {
324
        if ($partName != '' && $contentType != '') {
443✔
325
            // Write content type
326
            $objWriter->startElement('Override');
443✔
327
            $objWriter->writeAttribute('PartName', $partName);
443✔
328
            $objWriter->writeAttribute('ContentType', $contentType);
443✔
329
            $objWriter->endElement();
443✔
330
        } else {
UNCOV
331
            throw new WriterException('Invalid parameters passed.');
×
332
        }
333
    }
334
}
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