Skip to content

Commit 8506854

Browse files
committed
Better Handling of legacyDrawing Xml
Fix issue #4105. Passing through form controls from load to save was part of the limited support added with PR #3130. However, the legacyDrawing vml, which is a crucial piece of that change, often contains a description only of notes (comments) with no form control contents. Since legacyDrawing is constructed by Xlsx Writer when it doesn't already exist, Xlsx Reader should not interfere with that ability - it should save the contents of legacyDrawing only when it appears to not consist merely of note descriptions. It should also be an option to delete the legacyDrawing vml before saving. This allows one to add comments, at the cost of losing form controls. A method `deleteLegacyDrawing` is added to Spreadsheet to permit this.
1 parent b406367 commit 8506854

File tree

3 files changed

+193
-1
lines changed

3 files changed

+193
-1
lines changed

src/PhpSpreadsheet/Reader/Xlsx.php

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1624,7 +1624,9 @@ protected function loadSpreadsheetFromFile(string $filename): Spreadsheet
16241624
foreach ($xmlSheet->legacyDrawing as $drawing) {
16251625
$drawingRelId = (string) self::getArrayItem(self::getAttributes($drawing, $xmlNamespaceBase), 'id');
16261626
if (isset($vmlDrawingContents[$drawingRelId])) {
1627-
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
1627+
if (self::onlyNoteVml($vmlDrawingContents[$drawingRelId]) === false) {
1628+
$unparsedLoadedData['sheets'][$docSheet->getCodeName()]['legacyDrawing'] = $vmlDrawingContents[$drawingRelId];
1629+
}
16281630
}
16291631
}
16301632
}
@@ -2357,4 +2359,36 @@ private function processIgnoredErrors(SimpleXMLElement $xml, Worksheet $sheet):
23572359
}
23582360
}
23592361
}
2362+
2363+
private static function onlyNoteVml(string $data): bool
2364+
{
2365+
$data = str_replace('<br>', '<br/>', $data);
2366+
2367+
try {
2368+
$sxml = @simplexml_load_string($data);
2369+
} catch (Throwable) {
2370+
$sxml = false;
2371+
}
2372+
2373+
if ($sxml === false) {
2374+
return false;
2375+
}
2376+
$shapes = $sxml->children(Namespaces::URN_VML);
2377+
foreach ($shapes->shape as $shape) {
2378+
$clientData = $shape->children(Namespaces::URN_EXCEL);
2379+
if (!isset($clientData->ClientData)) {
2380+
return false;
2381+
}
2382+
$attrs = $clientData->ClientData->attributes();
2383+
if (!isset($attrs['ObjectType'])) {
2384+
return false;
2385+
}
2386+
$objectType = (string) $attrs['ObjectType'];
2387+
if ($objectType !== 'Note') {
2388+
return false;
2389+
}
2390+
}
2391+
2392+
return true;
2393+
}
23602394
}

src/PhpSpreadsheet/Spreadsheet.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1578,4 +1578,14 @@ public function getExcelCalendar(): int
15781578
{
15791579
return $this->excelCalendar;
15801580
}
1581+
1582+
public function deleteLegacyDrawing(Worksheet $worksheet): void
1583+
{
1584+
unset($this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing']);
1585+
}
1586+
1587+
public function getLegacyDrawing(Worksheet $worksheet): ?string
1588+
{
1589+
return $this->unparsedLoadedData['sheets'][$worksheet->getCodeName()]['legacyDrawing'] ?? null;
1590+
}
15811591
}
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpOffice\PhpSpreadsheetTests\Reader\Xlsx;
6+
7+
use PhpOffice\PhpSpreadsheet\Reader\Xlsx as XlsxReader;
8+
use PhpOffice\PhpSpreadsheet\Shared\File;
9+
use PhpOffice\PhpSpreadsheet\Spreadsheet;
10+
use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
11+
use PHPUnit\Framework\TestCase;
12+
13+
class VmlTest extends TestCase
14+
{
15+
private string $outfile1 = '';
16+
17+
private string $outfile2 = '';
18+
19+
protected function tearDown(): void
20+
{
21+
if ($this->outfile1 !== '') {
22+
unlink($this->outfile1);
23+
$this->outfile1 = '';
24+
}
25+
if ($this->outfile2 !== '') {
26+
unlink($this->outfile2);
27+
$this->outfile2 = '';
28+
}
29+
}
30+
31+
public function testAddComments(): void
32+
{
33+
$spreadsheet = new Spreadsheet();
34+
$sheet = $spreadsheet->getActiveSheet();
35+
$sheet->getComment('A1')->getText()->createText('top left cell');
36+
$writer = new XlsxWriter($spreadsheet);
37+
$this->outfile1 = File::temporaryFileName();
38+
$writer->save($this->outfile1);
39+
$spreadsheet->disconnectWorksheets();
40+
41+
$reader = new XlsxReader();
42+
$file = 'zip://' . $this->outfile1 . '#xl/worksheets/sheet1.xml';
43+
$sheetContents = file_get_contents($file) ?: '';
44+
self::assertStringContainsString('<legacyDrawing ', $sheetContents);
45+
$file = 'zip://' . $this->outfile1 . '#xl/drawings/vmlDrawing1.vml';
46+
$vmlContents = file_get_contents($file) ?: '';
47+
$count = substr_count($vmlContents, '<v:shape ');
48+
self::assertSame(1, $count);
49+
$count = substr_count($vmlContents, '<x:ClientData ObjectType="Note">');
50+
self::assertSame(1, $count);
51+
52+
$spreadsheet2 = $reader->load($this->outfile1);
53+
$sheet2 = $spreadsheet2->getActiveSheet();
54+
self::assertSame('top left cell', $sheet2->getComment('A1')->getText()->getPlainText());
55+
self::assertNull($spreadsheet2->getLegacyDrawing($sheet2));
56+
$sheet2->getComment('H1')->getText()->createText('Show me');
57+
$sheet2->getComment('H2')->getText()->createText('Hide me');
58+
$sheet2->getComment('H1')->setVisible(true);
59+
$writer = new XlsxWriter($spreadsheet2);
60+
$this->outfile2 = File::temporaryFileName();
61+
$writer->save($this->outfile2);
62+
$spreadsheet2->disconnectWorksheets();
63+
64+
$file = 'zip://' . $this->outfile2 . '#xl/worksheets/sheet1.xml';
65+
$sheetContents = file_get_contents($file) ?: '';
66+
self::assertStringContainsString('<legacyDrawing ', $sheetContents);
67+
$file = 'zip://' . $this->outfile2 . '#xl/drawings/vmlDrawing1.vml';
68+
$vmlContents = file_get_contents($file) ?: '';
69+
$count = substr_count($vmlContents, '<v:shape ');
70+
self::assertSame(3, $count);
71+
$count = substr_count($vmlContents, '<x:ClientData ObjectType="Note">');
72+
self::assertSame(3, $count);
73+
74+
$reader = new XlsxReader();
75+
$spreadsheet3 = $reader->load($this->outfile2);
76+
$sheet3 = $spreadsheet3->getActiveSheet();
77+
self::assertSame('top left cell', $sheet3->getComment('A1')->getText()->getPlainText());
78+
self::assertSame('Show me', $sheet3->getComment('H1')->getText()->getPlainText());
79+
self::assertSame('Hide me', $sheet3->getComment('H2')->getText()->getPlainText());
80+
self::assertNull($spreadsheet3->getLegacyDrawing($sheet3));
81+
self::assertTrue($sheet3->getComment('H1')->getVisible());
82+
self::assertFalse($sheet3->getComment('H2')->getVisible());
83+
$spreadsheet3->disconnectWorksheets();
84+
}
85+
86+
public function testDeleteNullLegacy(): void
87+
{
88+
$spreadsheet = new Spreadsheet();
89+
$sheet = $spreadsheet->getActiveSheet();
90+
$sheet->getComment('A1')->getText()->createText('top left cell');
91+
self::assertNull($spreadsheet->getLegacyDrawing($sheet));
92+
$spreadsheet->deleteLegacyDrawing($sheet);
93+
$writer = new XlsxWriter($spreadsheet);
94+
$this->outfile1 = File::temporaryFileName();
95+
$writer->save($this->outfile1);
96+
$spreadsheet->disconnectWorksheets();
97+
98+
$reader = new XlsxReader();
99+
$file = 'zip://' . $this->outfile1 . '#xl/worksheets/sheet1.xml';
100+
$sheetContents = file_get_contents($file) ?: '';
101+
self::assertStringContainsString('<legacyDrawing ', $sheetContents);
102+
$file = 'zip://' . $this->outfile1 . '#xl/drawings/vmlDrawing1.vml';
103+
$vmlContents = file_get_contents($file) ?: '';
104+
$count = substr_count($vmlContents, '<v:shape ');
105+
self::assertSame(1, $count);
106+
$count = substr_count($vmlContents, '<x:ClientData ObjectType="Note">');
107+
self::assertSame(1, $count);
108+
109+
$spreadsheet2 = $reader->load($this->outfile1);
110+
$sheet2 = $spreadsheet2->getActiveSheet();
111+
self::assertSame('top left cell', $sheet2->getComment('A1')->getText()->getPlainText());
112+
$spreadsheet2->disconnectWorksheets();
113+
}
114+
115+
public function testAddCommentDeleteFormControls(): void
116+
{
117+
$infile = 'samples/Reader2/sampleData/formscomments.xlsx';
118+
$reader = new XlsxReader();
119+
$reader->setLoadSheetsOnly('FormsComments');
120+
$spreadsheet = $reader->load($infile);
121+
self::assertTrue(true);
122+
$sheet = $spreadsheet->getActiveSheet();
123+
self::assertSame('row1', $sheet->getCell('H1')->getValue());
124+
self::assertStringContainsString('Hello', $sheet->getComment('F1')->getText()->getPlainText());
125+
$vmlContents = $spreadsheet->getLegacyDrawing($sheet) ?? '';
126+
$count = substr_count($vmlContents, '<v:shape ');
127+
self::assertSame(4, $count);
128+
$count = substr_count($vmlContents, '<x:ClientData ');
129+
self::assertSame(4, $count);
130+
$count = substr_count($vmlContents, '<x:ClientData ObjectType="Note"');
131+
self::assertSame(1, $count);
132+
$spreadsheet->deleteLegacyDrawing($sheet);
133+
$sheet->getComment('F2')->getText()->createText('Goodbye');
134+
$writer = new XlsxWriter($spreadsheet);
135+
$this->outfile1 = File::temporaryFileName();
136+
$writer->save($this->outfile1);
137+
$spreadsheet->disconnectWorksheets();
138+
139+
$reader2 = new XlsxReader();
140+
$spreadsheet2 = $reader2->load($this->outfile1);
141+
$sheet2 = $spreadsheet2->getActiveSheet();
142+
self::assertNull($spreadsheet2->getLegacyDrawing($sheet2));
143+
self::assertSame('row1', $sheet2->getCell('H1')->getValue());
144+
self::assertStringContainsString('Hello', $sheet2->getComment('F1')->getText()->getPlainText());
145+
self::assertStringContainsString('Goodbye', $sheet2->getComment('F2')->getText()->getPlainText());
146+
$spreadsheet2->disconnectWorksheets();
147+
}
148+
}

0 commit comments

Comments
 (0)