From 17d4a54666edf898777046403b6930f964a5d346 Mon Sep 17 00:00:00 2001
From: yuzhakov <georgiokot@gmail.com>
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 9e252623ac..5bd4ae11ac 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 2d28cd4dac..716a3787b9 100644
--- a/docs/references/features-cross-reference.md
+++ b/docs/references/features-cross-reference.md
@@ -750,6 +750,24 @@
         <td></td>
         <td></td>
     </tr>
+    <tr>
+        <td style="padding-left: 1em;">Drawing hyperlink</td>
+        <td></td>
+        <td style="text-align: center; color: green;">✔</td> 
+        <td></td>
+        <td></td>
+        <td></td>
+        <td></td>
+        <td></td>
+        <td></td>
+        <td style="text-align: center; color: green;">✔</td> 
+        <td></td>
+        <td></td>
+        <td></td>
+        <td></td>
+        <td>$drawing->getHyperlink()->getUrl()</td>
+        <td>$drawing->setHyperlink()->setUrl($url)</td>
+    </tr>
     <tr>
         <td><strong>Cell Formatting</strong></td>
         <td></td>
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 0000000000..0636e46717
--- /dev/null
+++ b/samples/Reader/20_Reader_worksheet_hyperlink_image.php
@@ -0,0 +1,54 @@
+<?php
+
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+
+require __DIR__ . '/../Header.php';
+$inputFileType = 'Xlsx';
+
+$helper->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 ad48cb6a50..e17c20d9be 100644
--- a/src/PhpSpreadsheet/Cell/Hyperlink.php
+++ b/src/PhpSpreadsheet/Cell/Hyperlink.php
@@ -89,6 +89,14 @@ public function isInternal()
         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 2b50bbd364..e36a5a57e7 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 @@ public function load($pFilename)
                                             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 @@ public function load($pFilename)
                                                     $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 @@ public function load($pFilename)
                                                         $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 @@ public function load($pFilename)
                                                     $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 @@ public function load($pFilename)
                                                         $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 @@ private static function boolean($value)
         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 6489f92605..49ba174341 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 @@ public function __clone()
             }
         }
     }
+
+    /**
+     * @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 533c7b3b9d..08256a1d5b 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
@@ -43,7 +43,12 @@ public function writeDrawings(\PhpOffice\PhpSpreadsheet\Worksheet\Worksheet $pWo
         $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 @@ public function writeChart(XMLWriter $objWriter, \PhpOffice\PhpSpreadsheet\Chart
      * @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 @@ public function writeDrawing(XMLWriter $objWriter, BaseDrawing $pDrawing, $pRela
             $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 @@ public function allDrawings(Spreadsheet $spreadsheet)
 
         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 e60ce5e752..76c196b449 100644
--- a/src/PhpSpreadsheet/Writer/Xlsx/Rels.php
+++ b/src/PhpSpreadsheet/Writer/Xlsx/Rels.php
@@ -329,12 +329,16 @@ public function writeDrawingRelationships(\PhpOffice\PhpSpreadsheet\Worksheet\Wo
             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 @@ private function writeRelationship(XMLWriter $objWriter, $pId, $pType, $pTarget,
             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 0000000000..cb3f6823b2
--- /dev/null
+++ b/tests/PhpSpreadsheetTests/Functional/DrawingImageHyperlinkTest.php
@@ -0,0 +1,51 @@
+<?php
+/**
+ * Created by PhpStorm.
+ * User: yuzhakov
+ * Date: 08.05.18
+ * Time: 12:00.
+ */
+
+namespace PhpOffice\PhpSpreadsheetTests\Functional;
+
+use PhpOffice\PhpSpreadsheet\Cell\Hyperlink;
+use PhpOffice\PhpSpreadsheet\Spreadsheet;
+use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
+
+class DrawingImageHyperlinkTest extends AbstractFunctional
+{
+    /**
+     * @throws \PhpOffice\PhpSpreadsheet\Exception
+     */
+    public function testDrawingImageHyperlinkTest()
+    {
+        $baseUrl = 'https://github.com/PHPOffice/PhpSpreadsheet';
+        $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);
+
+        $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');
+        }
+    }
+}