From 17d4a54666edf898777046403b6930f964a5d346 Mon Sep 17 00:00:00 2001 From: yuzhakov Date: Mon, 7 May 2018 09:41:24 +0500 Subject: [PATCH] Read and write hyperlink for drawing image Fixes #490 --- CHANGELOG.md | 3 +- docs/references/features-cross-reference.md | 18 +++++++ .../20_Reader_worksheet_hyperlink_image.php | 54 +++++++++++++++++++ src/PhpSpreadsheet/Cell/Hyperlink.php | 8 +++ src/PhpSpreadsheet/Reader/Xlsx.php | 37 ++++++++++++- src/PhpSpreadsheet/Worksheet/BaseDrawing.php | 24 +++++++++ src/PhpSpreadsheet/Writer/Xlsx/Drawing.php | 30 ++++++++++- src/PhpSpreadsheet/Writer/Xlsx/Rels.php | 33 +++++++++++- .../Functional/DrawingImageHyperlinkTest.php | 51 ++++++++++++++++++ 9 files changed, 253 insertions(+), 5 deletions(-) create mode 100644 samples/Reader/20_Reader_worksheet_hyperlink_image.php create mode 100644 tests/PhpSpreadsheetTests/Functional/DrawingImageHyperlinkTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e252623..5bd4ae11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). ### Added -- Add excel function EXACT(value1, value2) support +- Add excel function EXACT(value1, value2) support - [595](https://github.com/PHPOffice/PhpSpreadsheet/pull/595) - Support workbook view attributes for Xlsx format - [#523](https://github.com/PHPOffice/PhpSpreadsheet/issues/523) +- Read and write hyperlink for drawing image - [#490](https://github.com/PHPOffice/PhpSpreadsheet/pull/490) ### Fixed diff --git a/docs/references/features-cross-reference.md b/docs/references/features-cross-reference.md index 2d28cd4d..716a3787 100644 --- a/docs/references/features-cross-reference.md +++ b/docs/references/features-cross-reference.md @@ -750,6 +750,24 @@ + + Drawing hyperlink + + ✔ + + + + + + + ✔ + + + + + $drawing->getHyperlink()->getUrl() + $drawing->setHyperlink()->setUrl($url) + Cell Formatting diff --git a/samples/Reader/20_Reader_worksheet_hyperlink_image.php b/samples/Reader/20_Reader_worksheet_hyperlink_image.php new file mode 100644 index 00000000..0636e467 --- /dev/null +++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php @@ -0,0 +1,54 @@ +log('Start'); + +$spreadsheet = new Spreadsheet(); + +$aSheet = $spreadsheet->getActiveSheet(); + +$gdImage = @imagecreatetruecolor(120, 20); +$textColor = imagecolorallocate($gdImage, 255, 255, 255); +imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); + +$baseUrl = 'https://phpspreadsheet.readthedocs.io/'; + +$drawing = new \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing(); +$drawing->setName('In-Memory image 1'); +$drawing->setDescription('In-Memory image 1'); +$drawing->setCoordinates('A1'); +$drawing->setImageResource($gdImage); +$drawing->setRenderingFunction( + \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::RENDERING_JPEG +); +$drawing->setMimeType(\PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::MIMETYPE_DEFAULT); +$drawing->setHeight(36); +$helper->log('Write image'); + +$hyperLink = new \PhpOffice\PhpSpreadsheet\Cell\Hyperlink($baseUrl, 'test image'); +$drawing->setHyperlink($hyperLink); +$helper->log('Write link: ' . $baseUrl); + +$drawing->setWorksheet($aSheet); + +$filename = tempnam(\PhpOffice\PhpSpreadsheet\Shared\File::sysGetTempDir(), 'phpspreadsheet-test'); + +$writer = \PhpOffice\PhpSpreadsheet\IOFactory::createWriter($spreadsheet, $inputFileType); +$writer->save($filename); + +$reader = \PhpOffice\PhpSpreadsheet\IOFactory::createReader($inputFileType); + +$reloadedSpreadsheet = $reader->load($filename); +unlink($filename); + +$helper->log('reloaded Spreadsheet'); + +foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $pDrawing) { + $helper->log('Read link: ' . $pDrawing->getHyperlink()->getUrl()); +} + +$helper->log('end'); diff --git a/src/PhpSpreadsheet/Cell/Hyperlink.php b/src/PhpSpreadsheet/Cell/Hyperlink.php index ad48cb6a..e17c20d9 100644 --- a/src/PhpSpreadsheet/Cell/Hyperlink.php +++ b/src/PhpSpreadsheet/Cell/Hyperlink.php @@ -89,6 +89,14 @@ class Hyperlink return strpos($this->url, 'sheet://') !== false; } + /** + * @return string + */ + public function getTypeHyperlink() + { + return $this->isInternal() ? '' : 'External'; + } + /** * Get hash code. * diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index 2b50bbd3..e36a5a57 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -3,6 +3,7 @@ namespace PhpOffice\PhpSpreadsheet\Reader; use PhpOffice\PhpSpreadsheet\Cell\Coordinate; +use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Document\Properties; use PhpOffice\PhpSpreadsheet\NamedRange; use PhpOffice\PhpSpreadsheet\Reader\Xlsx\Chart; @@ -1669,9 +1670,12 @@ class Xlsx extends BaseReader Settings::getLibXmlLoaderOptions() ); $images = []; - + $hyperlinks = []; if ($relsDrawing && $relsDrawing->Relationship) { foreach ($relsDrawing->Relationship as $ele) { + if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink') { + $hyperlinks[(string) $ele['Id']] = (string) $ele['Target']; + } if ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image') { $images[(string) $ele['Id']] = self::dirAdd($fileDrawing, $ele['Target']); } elseif ($ele['Type'] == 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart') { @@ -1699,6 +1703,9 @@ class Xlsx extends BaseReader $xfrm = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm; /** @var SimpleXMLElement $outerShdw */ $outerShdw = $oneCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw; + /** @var \SimpleXMLElement $hlinkClick */ + $hlinkClick = $oneCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; + $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $objDrawing->setName((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name')); $objDrawing->setDescription((string) self::getArrayItem($oneCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr')); @@ -1729,6 +1736,9 @@ class Xlsx extends BaseReader $shadow->getColor()->setRGB(self::getArrayItem($outerShdw->srgbClr->attributes(), 'val')); $shadow->setAlpha(self::getArrayItem($outerShdw->srgbClr->alpha->attributes(), 'val') / 1000); } + + $this->readHyperLinkDrawing($objDrawing, $oneCellAnchor, $hyperlinks); + $objDrawing->setWorksheet($docSheet); } else { // ? Can charts be positioned with a oneCellAnchor ? @@ -1746,6 +1756,7 @@ class Xlsx extends BaseReader $blip = $twoCellAnchor->pic->blipFill->children('http://schemas.openxmlformats.org/drawingml/2006/main')->blip; $xfrm = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->xfrm; $outerShdw = $twoCellAnchor->pic->spPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->effectLst->outerShdw; + $hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; $objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing(); $objDrawing->setName((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name')); $objDrawing->setDescription((string) self::getArrayItem($twoCellAnchor->pic->nvPicPr->cNvPr->attributes(), 'descr')); @@ -1777,6 +1788,9 @@ class Xlsx extends BaseReader $shadow->getColor()->setRGB(self::getArrayItem($outerShdw->srgbClr->attributes(), 'val')); $shadow->setAlpha(self::getArrayItem($outerShdw->srgbClr->alpha->attributes(), 'val') / 1000); } + + $this->readHyperLinkDrawing($objDrawing, $twoCellAnchor, $hyperlinks); + $objDrawing->setWorksheet($docSheet); } elseif (($this->includeCharts) && ($twoCellAnchor->graphicFrame)) { $fromCoordinate = Coordinate::stringFromColumnIndex(((string) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1); @@ -2427,6 +2441,27 @@ class Xlsx extends BaseReader return $value === 'true' || $value === 'TRUE'; } + /** + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $objDrawing + * @param \SimpleXMLElement $cellAnchor + * @param array $hyperlinks + */ + private function readHyperLinkDrawing($objDrawing, $cellAnchor, $hyperlinks) + { + $hlinkClick = $cellAnchor->pic->nvPicPr->cNvPr->children('http://schemas.openxmlformats.org/drawingml/2006/main')->hlinkClick; + + if ($hlinkClick->count() === 0) { + return; + } + + $hlinkId = (string) $hlinkClick->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships')['id']; + $hyperlink = new Hyperlink( + $hyperlinks[$hlinkId], + (string) self::getArrayItem($cellAnchor->pic->nvPicPr->cNvPr->attributes(), 'name') + ); + $objDrawing->setHyperlink($hyperlink); + } + private function readProtection(Spreadsheet $excel, SimpleXMLElement $xmlWorkbook) { if (!$xmlWorkbook->workbookProtection) { diff --git a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php index 6489f926..49ba1743 100644 --- a/src/PhpSpreadsheet/Worksheet/BaseDrawing.php +++ b/src/PhpSpreadsheet/Worksheet/BaseDrawing.php @@ -2,6 +2,7 @@ namespace PhpOffice\PhpSpreadsheet\Worksheet; +use PhpOffice\PhpSpreadsheet\Cell\Hyperlink; use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException; use PhpOffice\PhpSpreadsheet\IComparable; @@ -98,6 +99,13 @@ class BaseDrawing implements IComparable */ protected $shadow; + /** + * Image hyperlink. + * + * @var null|Hyperlink + */ + private $hyperlink; + /** * Create a new BaseDrawing. */ @@ -508,4 +516,20 @@ class BaseDrawing implements IComparable } } } + + /** + * @param null|Hyperlink $pHyperlink + */ + public function setHyperlink(Hyperlink $pHyperlink = null) + { + $this->hyperlink = $pHyperlink; + } + + /** + * @return null|Hyperlink + */ + public function getHyperlink() + { + return $this->hyperlink; + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php index 533c7b3b..08256a1d 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php @@ -43,7 +43,12 @@ class Drawing extends WriterPart $i = 1; $iterator = $pWorksheet->getDrawingCollection()->getIterator(); while ($iterator->valid()) { - $this->writeDrawing($objWriter, $iterator->current(), $i); + /** @var BaseDrawing $pDrawing */ + $pDrawing = $iterator->current(); + $pRelationId = $i; + $hlinkClickId = $pDrawing->getHyperlink() === null ? null : ++$i; + + $this->writeDrawing($objWriter, $pDrawing, $pRelationId, $hlinkClickId); $iterator->next(); ++$i; @@ -150,10 +155,11 @@ class Drawing extends WriterPart * @param XMLWriter $objWriter XML Writer * @param BaseDrawing $pDrawing * @param int $pRelationId + * @param null|int $hlinkClickId * * @throws WriterException */ - public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRelationId = -1) + public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRelationId = -1, $hlinkClickId = null) { if ($pRelationId >= 0) { // xdr:oneCellAnchor @@ -187,6 +193,10 @@ class Drawing extends WriterPart $objWriter->writeAttribute('id', $pRelationId); $objWriter->writeAttribute('name', $pDrawing->getName()); $objWriter->writeAttribute('descr', $pDrawing->getDescription()); + + //a:hlinkClick + $this->writeHyperLinkDrawing($objWriter, $hlinkClickId); + $objWriter->endElement(); // xdr:cNvPicPr @@ -490,4 +500,20 @@ class Drawing extends WriterPart return $aDrawings; } + + /** + * @param XMLWriter $objWriter + * @param null|int $hlinkClickId + */ + private function writeHyperLinkDrawing(XMLWriter $objWriter, $hlinkClickId) + { + if ($hlinkClickId === null) { + return; + } + + $objWriter->startElement('a:hlinkClick'); + $objWriter->writeAttribute('xmlns:r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId); + $objWriter->endElement(); + } } diff --git a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php index e60ce5e7..76c196b4 100644 --- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php +++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php @@ -329,12 +329,16 @@ class Rels extends WriterPart if ($iterator->current() instanceof \PhpOffice\PhpSpreadsheet\Worksheet\Drawing || $iterator->current() instanceof MemoryDrawing) { // Write relationship for image drawing + /** @var \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing */ + $drawing = $iterator->current(); $this->writeRelationship( $objWriter, $i, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', - '../media/' . str_replace(' ', '', $iterator->current()->getIndexedFilename()) + '../media/' . str_replace(' ', '', $drawing->getIndexedFilename()) ); + + $i = $this->writeDrawingHyperLink($objWriter, $drawing, $i); } $iterator->next(); @@ -432,4 +436,31 @@ class Rels extends WriterPart throw new WriterException('Invalid parameters passed.'); } } + + /** + * @param $objWriter + * @param \PhpOffice\PhpSpreadsheet\Worksheet\Drawing $drawing + * @param $i + * + * @throws WriterException + * + * @return int + */ + private function writeDrawingHyperLink($objWriter, $drawing, $i) + { + if ($drawing->getHyperlink() === null) { + return $i; + } + + ++$i; + $this->writeRelationship( + $objWriter, + $i, + 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', + $drawing->getHyperlink()->getUrl(), + $drawing->getHyperlink()->getTypeHyperlink() + ); + + return $i; + } } diff --git a/tests/PhpSpreadsheetTests/Functional/DrawingImageHyperlinkTest.php b/tests/PhpSpreadsheetTests/Functional/DrawingImageHyperlinkTest.php new file mode 100644 index 00000000..cb3f6823 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Functional/DrawingImageHyperlinkTest.php @@ -0,0 +1,51 @@ +getActiveSheet(); + + $gdImage = @imagecreatetruecolor(120, 20); + $textColor = imagecolorallocate($gdImage, 255, 255, 255); + imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor); + + $drawing = new MemoryDrawing(); + $drawing->setName('In-Memory image 1'); + $drawing->setDescription('In-Memory image 1'); + $drawing->setCoordinates('A1'); + $drawing->setImageResource($gdImage); + $drawing->setRenderingFunction( + MemoryDrawing::RENDERING_JPEG + ); + $drawing->setMimeType(MemoryDrawing::MIMETYPE_DEFAULT); + $drawing->setHeight(36); + $hyperLink = new Hyperlink($baseUrl, 'test image'); + $drawing->setHyperlink($hyperLink); + $drawing->setWorksheet($aSheet); + + $reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx'); + + foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $pDrawing) { + self::assertEquals('https://github.com/PHPOffice/PhpSpreadsheet', $pDrawing->getHyperlink()->getUrl(), 'functional test drawing hyperlink'); + } + } +}