From 795992835fba6144641b94673cc78812cb2bece5 Mon Sep 17 00:00:00 2001 From: jarrett jordaan Date: Thu, 24 Jun 2021 10:50:44 +0200 Subject: [PATCH] When image source is a URL, store the URL for use during extraction. (#2072) When image source is a link store the link. Add url mutator. Update section in documentation on image extraction. --- CHANGELOG.md | 1 + docs/topics/recipes.md | 37 +++- src/PhpSpreadsheet/Reader/Xlsx.php | 31 ++- src/PhpSpreadsheet/Worksheet/Drawing.php | 48 ++++- .../Reader/Utility/File.php | 199 ++++++++++++++++++ .../Reader/Xlsx/URLImageTest.php | 62 ++++++ tests/data/Reader/XLSX/urlImage.xlsx | Bin 0 -> 10097 bytes 7 files changed, 359 insertions(+), 19 deletions(-) create mode 100644 tests/PhpSpreadsheetTests/Reader/Utility/File.php create mode 100644 tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php create mode 100644 tests/data/Reader/XLSX/urlImage.xlsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ad529aac..040e1a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org). ## Unreleased - TBD ### Added +- Add ability to extract images if source is a URL. [Issue #1997](https://github.com/PHPOffice/PhpSpreadsheet/issues/1997) [PR #2072](https://github.com/PHPOffice/PhpSpreadsheet/pull/2072) - Support for passing flags in the Reader `load()` and Writer `save()`methods, and through the IOFactory, to set behaviours. [PR #2136](https://github.com/PHPOffice/PhpSpreadsheet/pull/2136) - See [documentation](https://phpspreadsheet.readthedocs.io/en/latest/topics/reading-and-writing-to-file/) for details diff --git a/docs/topics/recipes.md b/docs/topics/recipes.md index 471d1dda..ddf315be 100644 --- a/docs/topics/recipes.md +++ b/docs/topics/recipes.md @@ -1372,9 +1372,11 @@ The following code extracts images from the current active worksheet, and writes each as a separate file. ```php +use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing; $i = 0; + foreach ($spreadsheet->getActiveSheet()->getDrawingCollection() as $drawing) { - if ($drawing instanceof \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing) { + if ($drawing instanceof MemoryDrawing) { ob_start(); call_user_func( $drawing->getRenderingFunction(), @@ -1383,24 +1385,39 @@ foreach ($spreadsheet->getActiveSheet()->getDrawingCollection() as $drawing) { $imageContents = ob_get_contents(); ob_end_clean(); switch ($drawing->getMimeType()) { - case \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::MIMETYPE_PNG : + case MemoryDrawing::MIMETYPE_PNG : $extension = 'png'; break; - case \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::MIMETYPE_GIF: + case MemoryDrawing::MIMETYPE_GIF: $extension = 'gif'; break; - case \PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing::MIMETYPE_JPEG : + case MemoryDrawing::MIMETYPE_JPEG : $extension = 'jpg'; break; } } else { - $zipReader = fopen($drawing->getPath(),'r'); - $imageContents = ''; - while (!feof($zipReader)) { - $imageContents .= fread($zipReader,1024); + if ($drawing->getPath()) { + // Check if the source is a URL or a file path + if ($drawing->getIsURL()) { + $imageContents = file_get_contents($drawing->getPath()); + $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); + file_put_contents($filePath , $imageContents); + $mimeType = mime_content_type($filePath); + // You could use the below to find the extension from mime type. + // https://gist.github.com/alexcorvi/df8faecb59e86bee93411f6a7967df2c#gistcomment-2722664 + $extension = File::mime2ext($mimeType); + unlink($filePath); + } + else { + $zipReader = fopen($drawing->getPath(),'r'); + $imageContents = ''; + while (!feof($zipReader)) { + $imageContents .= fread($zipReader,1024); + } + fclose($zipReader); + $extension = $drawing->getExtension(); + } } - fclose($zipReader); - $extension = $drawing->getExtension(); } $myFileName = '00_Image_'.++$i.'.'.$extension; file_put_contents($myFileName,$imageContents); diff --git a/src/PhpSpreadsheet/Reader/Xlsx.php b/src/PhpSpreadsheet/Reader/Xlsx.php index da4a80d1..50b2a709 100644 --- a/src/PhpSpreadsheet/Reader/Xlsx.php +++ b/src/PhpSpreadsheet/Reader/Xlsx.php @@ -1149,17 +1149,25 @@ class Xlsx extends BaseReader $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')); - $imageKey = (string) self::getArrayItem( + $embedImageKey = (string) self::getArrayItem( $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'embed' ); - - if (isset($images[$imageKey])) { + if (isset($images[$embedImageKey])) { $objDrawing->setPath( 'zip://' . File::realpath($pFilename) . '#' . - $images[$imageKey], + $images[$embedImageKey], false ); + } else { + $linkImageKey = (string) self::getArrayItem( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'link' + ); + if (isset($images[$linkImageKey])) { + $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); + $objDrawing->setPath($url); + } } $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $oneCellAnchor->from->col) + 1) . ($oneCellAnchor->from->row + 1)); @@ -1220,16 +1228,25 @@ class Xlsx extends BaseReader $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')); - $imageKey = (string) self::getArrayItem( + $embedImageKey = (string) self::getArrayItem( $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), 'embed' ); - if (isset($images[$imageKey])) { + if (isset($images[$embedImageKey])) { $objDrawing->setPath( 'zip://' . File::realpath($pFilename) . '#' . - $images[$imageKey], + $images[$embedImageKey], false ); + } else { + $linkImageKey = (string) self::getArrayItem( + $blip->attributes('http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + 'link' + ); + if (isset($images[$linkImageKey])) { + $url = str_replace('xl/drawings/', '', $images[$linkImageKey]); + $objDrawing->setPath($url); + } } $objDrawing->setCoordinates(Coordinate::stringFromColumnIndex(((int) $twoCellAnchor->from->col) + 1) . ($twoCellAnchor->from->row + 1)); diff --git a/src/PhpSpreadsheet/Worksheet/Drawing.php b/src/PhpSpreadsheet/Worksheet/Drawing.php index 1f1dae93..a8cbb79d 100644 --- a/src/PhpSpreadsheet/Worksheet/Drawing.php +++ b/src/PhpSpreadsheet/Worksheet/Drawing.php @@ -13,6 +13,13 @@ class Drawing extends BaseDrawing */ private $path; + /** + * Whether or not we are dealing with a URL. + * + * @var bool + */ + private $isUrl; + /** * Create a new Drawing. */ @@ -20,6 +27,7 @@ class Drawing extends BaseDrawing { // Initialise values $this->path = ''; + $this->isUrl = false; // Initialize parent parent::__construct(); @@ -81,9 +89,25 @@ class Drawing extends BaseDrawing public function setPath($pValue, $pVerifyFile = true) { if ($pVerifyFile) { - if (file_exists($pValue)) { + // Check if a URL has been passed. https://stackoverflow.com/a/2058596/1252979 + if (filter_var($pValue, FILTER_VALIDATE_URL)) { + $this->path = $pValue; + // Implicit that it is a URL, rather store info than running check above on value in other places. + $this->isUrl = true; + $imageContents = file_get_contents($pValue); + $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); + if ($filePath) { + file_put_contents($filePath, $imageContents); + if (file_exists($filePath)) { + if ($this->width == 0 && $this->height == 0) { + // Get width/height + [$this->width, $this->height] = getimagesize($filePath); + } + unlink($filePath); + } + } + } elseif (file_exists($pValue)) { $this->path = $pValue; - if ($this->width == 0 && $this->height == 0) { // Get width/height [$this->width, $this->height] = getimagesize($pValue); @@ -98,6 +122,26 @@ class Drawing extends BaseDrawing return $this; } + /** + * Get isURL. + */ + public function getIsURL(): bool + { + return $this->isUrl; + } + + /** + * Set isURL. + * + * @return $this + */ + public function setIsURL(bool $isUrl): self + { + $this->isUrl = $isUrl; + + return $this; + } + /** * Get hash code. * diff --git a/tests/PhpSpreadsheetTests/Reader/Utility/File.php b/tests/PhpSpreadsheetTests/Reader/Utility/File.php new file mode 100644 index 00000000..f2283326 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Utility/File.php @@ -0,0 +1,199 @@ + '3g2', + 'video/3gp' => '3gp', + 'video/3gpp' => '3gp', + 'application/x-compressed' => '7zip', + 'audio/x-acc' => 'aac', + 'audio/ac3' => 'ac3', + 'application/postscript' => 'ai', + 'audio/x-aiff' => 'aif', + 'audio/aiff' => 'aif', + 'audio/x-au' => 'au', + 'video/x-msvideo' => 'avi', + 'video/msvideo' => 'avi', + 'video/avi' => 'avi', + 'application/x-troff-msvideo' => 'avi', + 'application/macbinary' => 'bin', + 'application/mac-binary' => 'bin', + 'application/x-binary' => 'bin', + 'application/x-macbinary' => 'bin', + 'image/bmp' => 'bmp', + 'image/x-bmp' => 'bmp', + 'image/x-bitmap' => 'bmp', + 'image/x-xbitmap' => 'bmp', + 'image/x-win-bitmap' => 'bmp', + 'image/x-windows-bmp' => 'bmp', + 'image/ms-bmp' => 'bmp', + 'image/x-ms-bmp' => 'bmp', + 'application/bmp' => 'bmp', + 'application/x-bmp' => 'bmp', + 'application/x-win-bitmap' => 'bmp', + 'application/cdr' => 'cdr', + 'application/coreldraw' => 'cdr', + 'application/x-cdr' => 'cdr', + 'application/x-coreldraw' => 'cdr', + 'image/cdr' => 'cdr', + 'image/x-cdr' => 'cdr', + 'zz-application/zz-winassoc-cdr' => 'cdr', + 'application/mac-compactpro' => 'cpt', + 'application/pkix-crl' => 'crl', + 'application/pkcs-crl' => 'crl', + 'application/x-x509-ca-cert' => 'crt', + 'application/pkix-cert' => 'crt', + 'text/css' => 'css', + 'text/x-comma-separated-values' => 'csv', + 'text/comma-separated-values' => 'csv', + 'application/vnd.msexcel' => 'csv', + 'application/x-director' => 'dcr', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/x-dvi' => 'dvi', + 'message/rfc822' => 'eml', + 'application/x-msdownload' => 'exe', + 'video/x-f4v' => 'f4v', + 'audio/x-flac' => 'flac', + 'video/x-flv' => 'flv', + 'image/gif' => 'gif', + 'application/gpg-keys' => 'gpg', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gzip', + 'application/mac-binhex40' => 'hqx', + 'application/mac-binhex' => 'hqx', + 'application/x-binhex40' => 'hqx', + 'application/x-mac-binhex40' => 'hqx', + 'text/html' => 'html', + 'image/x-icon' => 'ico', + 'image/x-ico' => 'ico', + 'image/vnd.microsoft.icon' => 'ico', + 'text/calendar' => 'ics', + 'application/java-archive' => 'jar', + 'application/x-java-application' => 'jar', + 'application/x-jar' => 'jar', + 'image/jp2' => 'jp2', + 'video/mj2' => 'jp2', + 'image/jpx' => 'jp2', + 'image/jpm' => 'jp2', + 'image/jpeg' => 'jpeg', + 'image/pjpeg' => 'jpeg', + 'application/x-javascript' => 'js', + 'application/json' => 'json', + 'text/json' => 'json', + 'application/vnd.google-earth.kml+xml' => 'kml', + 'application/vnd.google-earth.kmz' => 'kmz', + 'text/x-log' => 'log', + 'audio/x-m4a' => 'm4a', + 'audio/mp4' => 'm4a', + 'application/vnd.mpegurl' => 'm4u', + 'audio/midi' => 'mid', + 'application/vnd.mif' => 'mif', + 'video/quicktime' => 'mov', + 'video/x-sgi-movie' => 'movie', + 'audio/mpeg' => 'mp3', + 'audio/mpg' => 'mp3', + 'audio/mpeg3' => 'mp3', + 'audio/mp3' => 'mp3', + 'video/mp4' => 'mp4', + 'video/mpeg' => 'mpeg', + 'application/oda' => 'oda', + 'audio/ogg' => 'ogg', + 'video/ogg' => 'ogg', + 'application/ogg' => 'ogg', + 'font/otf' => 'otf', + 'application/x-pkcs10' => 'p10', + 'application/pkcs10' => 'p10', + 'application/x-pkcs12' => 'p12', + 'application/x-pkcs7-signature' => 'p7a', + 'application/pkcs7-mime' => 'p7c', + 'application/x-pkcs7-mime' => 'p7c', + 'application/x-pkcs7-certreqresp' => 'p7r', + 'application/pkcs7-signature' => 'p7s', + 'application/pdf' => 'pdf', + 'application/octet-stream' => 'pdf', + 'application/x-x509-user-cert' => 'pem', + 'application/x-pem-file' => 'pem', + 'application/pgp' => 'pgp', + 'application/x-httpd-php' => 'php', + 'application/php' => 'php', + 'application/x-php' => 'php', + 'text/php' => 'php', + 'text/x-php' => 'php', + 'application/x-httpd-php-source' => 'php', + 'image/png' => 'png', + 'image/x-png' => 'png', + 'application/powerpoint' => 'ppt', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.ms-office' => 'ppt', + 'application/msword' => 'doc', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/x-photoshop' => 'psd', + 'image/vnd.adobe.photoshop' => 'psd', + 'audio/x-realaudio' => 'ra', + 'audio/x-pn-realaudio' => 'ram', + 'application/x-rar' => 'rar', + 'application/rar' => 'rar', + 'application/x-rar-compressed' => 'rar', + 'audio/x-pn-realaudio-plugin' => 'rpm', + 'application/x-pkcs7' => 'rsa', + 'text/rtf' => 'rtf', + 'text/richtext' => 'rtx', + 'video/vnd.rn-realvideo' => 'rv', + 'application/x-stuffit' => 'sit', + 'application/smil' => 'smil', + 'text/srt' => 'srt', + 'image/svg+xml' => 'svg', + 'application/x-shockwave-flash' => 'swf', + 'application/x-tar' => 'tar', + 'application/x-gzip-compressed' => 'tgz', + 'image/tiff' => 'tiff', + 'font/ttf' => 'ttf', + 'text/plain' => 'txt', + 'text/x-vcard' => 'vcf', + 'application/videolan' => 'vlc', + 'text/vtt' => 'vtt', + 'audio/x-wav' => 'wav', + 'audio/wave' => 'wav', + 'audio/wav' => 'wav', + 'application/wbxml' => 'wbxml', + 'video/webm' => 'webm', + 'image/webp' => 'webp', + 'audio/x-ms-wma' => 'wma', + 'application/wmlc' => 'wmlc', + 'video/x-ms-wmv' => 'wmv', + 'video/x-ms-asf' => 'wmv', + 'font/woff' => 'woff', + 'font/woff2' => 'woff2', + 'application/xhtml+xml' => 'xhtml', + 'application/excel' => 'xl', + 'application/msexcel' => 'xls', + 'application/x-msexcel' => 'xls', + 'application/x-ms-excel' => 'xls', + 'application/x-excel' => 'xls', + 'application/x-dos_ms_excel' => 'xls', + 'application/xls' => 'xls', + 'application/x-xls' => 'xls', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.ms-excel' => 'xlsx', + 'application/xml' => 'xml', + 'text/xml' => 'xml', + 'text/xsl' => 'xsl', + 'application/xspf+xml' => 'xspf', + 'application/x-compress' => 'z', + 'application/x-zip' => 'zip', + 'application/zip' => 'zip', + 'application/x-zip-compressed' => 'zip', + 'application/s-compressed' => 'zip', + 'multipart/x-zip' => 'zip', + 'text/x-scriptzsh' => 'zsh', + ]; + + return $mime_map[$mime] ?? ''; + } +} diff --git a/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php new file mode 100644 index 00000000..b8e81501 --- /dev/null +++ b/tests/PhpSpreadsheetTests/Reader/Xlsx/URLImageTest.php @@ -0,0 +1,62 @@ +load($filename); + $worksheet = $spreadsheet->getActiveSheet(); + + foreach ($worksheet->getDrawingCollection() as $drawing) { + if ($drawing instanceof MemoryDrawing) { + // Skip memory drawings + } elseif ($drawing instanceof Drawing) { + // Check if the source is a URL or a file path + if ($drawing->getPath() && $drawing->getIsURL()) { + $imageContents = file_get_contents($drawing->getPath()); + $filePath = tempnam(sys_get_temp_dir(), 'Drawing'); + if ($filePath) { + file_put_contents($filePath, $imageContents); + if (file_exists($filePath)) { + $mimeType = mime_content_type($filePath); + // You could use the below to find the extension from mime type. + if ($mimeType) { + $extension = File::mime2ext($mimeType); + self::assertEquals('jpeg', $extension); + unlink($filePath); + } else { + self::fail('Could establish mime type.'); + } + } else { + self::fail('Could not write file to disk.'); + } + } else { + self::fail('Could not create fiel path.'); + } + } else { + self::fail('Could not assert that the file contains an image that is URL sourced.'); + } + } else { + self::fail('No image path found.'); + } + } + + if (empty($worksheet->getDrawingCollection())) { + self::fail('No image found in file.'); + } + } + } +} diff --git a/tests/data/Reader/XLSX/urlImage.xlsx b/tests/data/Reader/XLSX/urlImage.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..01e8e24de3254416230528e48b4deb637682661e GIT binary patch literal 10097 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 literal 0 HcmV?d00001