Add editAs Property for 2-cell Anchor Drawings (#2674)

* Add editAs Property for 2-cell Anchor Drawings

This change builds on PR #2532 (@naotake51 as PR #2532), using ideas from PR #2237 (@AdamGaskins), which has had changes requested for several months. It covers a lot of the same ground as 2532. In Excel, two-cell anchor drawings can be edited as "twocell", "onecell", or "absolute". This PR adds support for those options, with a sample file that demonstrates the difference in addition to unit tests. Several other tests are added to improve the spotty coverage for Drawings.

There have been several other tickets referencing two cell anchors, including issue #1159 and PR #1160 (@sgarwood, who also added support for editAs), PR #643, and issue #126, all now closed but not necessarily entirely resolved. I will try to ensure that those tickets are addressed with this one.

And, in trying to make sure 1160 is covered, I stumbled upon a bug. If you use the same image resource to create two+ memory drawings, the MemoryDrawing destructor for the first will cause the rest to generate a very long warning message. This is not a problem for Php8+, only for Php7-. I have suppressed the message in the MemoryDrawing constructor. 1160 went stale due to an unresolved test error, but I don't think this was the problem. At any rate, its test works now.

* Scrutinizer

It reported 1 minor issue (fixed normally), and 2 major. One is fixed with a kludge. The other is a case where Scrutinizer's analysis is just wrong, and I can't figure out a kludge. But I was able to add an annotation (the first time I've managed to get one past phpcs/php-cs-fixer). We'll see.
This commit is contained in:
oleibman 2022-03-16 16:12:38 -07:00 committed by GitHub
parent a8e179d5d9
commit 9428552d94
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 499 additions and 377 deletions

View File

@ -4220,26 +4220,6 @@ parameters:
count: 1
path: src/PhpSpreadsheet/Style/NumberFormat/PercentageFormatter.php
-
message: "#^Cannot call method getCell\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#"
count: 1
path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php
-
message: "#^Cannot call method getDrawingCollection\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#"
count: 1
path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php
-
message: "#^Cannot call method getHashCode\\(\\) on PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\|null\\.$#"
count: 1
path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php
-
message: "#^Property PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\BaseDrawing\\:\\:\\$shadow \\(PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\) does not accept PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Drawing\\\\Shadow\\|null\\.$#"
count: 1
path: src/PhpSpreadsheet/Worksheet/BaseDrawing.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\CellIterator\\:\\:adjustForExistingOnlyRange\\(\\) has no return type specified\\.$#"
count: 1
@ -5250,36 +5230,6 @@ parameters:
count: 2
path: src/PhpSpreadsheet/Writer/Xlsx/DocProps.php
-
message: "#^Parameter \\#1 \\$coordinates of static method PhpOffice\\\\PhpSpreadsheet\\\\Cell\\\\Coordinate\\:\\:indexesFromString\\(\\) expects string, string\\|null given\\.$#"
count: 1
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Parameter \\#1 \\$index of method PhpOffice\\\\PhpSpreadsheet\\\\Worksheet\\\\Worksheet\\:\\:getChartByIndex\\(\\) expects string, int\\<0, max\\> given\\.$#"
count: 1
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Parameter \\#2 \\$chart of method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Drawing\\:\\:writeChart\\(\\) expects PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart, PhpOffice\\\\PhpSpreadsheet\\\\Chart\\\\Chart\\|false given\\.$#"
count: 1
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Parameter \\#2 \\$content of method XMLWriter\\:\\:writeElement\\(\\) expects string\\|null, int given\\.$#"
count: 20
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int given\\.$#"
count: 10
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Parameter \\#2 \\$value of method XMLWriter\\:\\:writeAttribute\\(\\) expects string, int\\<0, max\\> given\\.$#"
count: 1
path: src/PhpSpreadsheet/Writer/Xlsx/Drawing.php
-
message: "#^Method PhpOffice\\\\PhpSpreadsheet\\\\Writer\\\\Xlsx\\\\Rels\\:\\:writeUnparsedRelationship\\(\\) has parameter \\$relationship with no type specified\\.$#"
count: 1

View File

@ -0,0 +1,78 @@
<?php
// Create new Spreadsheet object
use PhpOffice\PhpSpreadsheet\Helper\Dimension;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
require __DIR__ . '/../Header.php';
$helper->log('Create new Spreadsheet object');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->getCell('A1')->setValue('twocell');
$sheet->getCell('A2')->setValue('twocell');
$sheet->getCell('A3')->setValue('onecell');
$sheet->getCell('A6')->setValue('absolute');
// Add a drawing to the worksheet
$helper->log('Add a drawing to the worksheet two-cell anchor not resized');
$drawing = new Drawing();
$drawing->setName('PhpSpreadsheet');
$drawing->setDescription('PhpSpreadsheet');
$drawing->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png');
// anchor type will be two-cell because Coordinates2 is set
//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL);
$drawing->setCoordinates('B1');
$drawing->setCoordinates2('B1');
$drawing->setOffsetX2($drawing->getImageWidth());
$drawing->setOffsetY2($drawing->getImageHeight());
$drawing->setWorksheet($spreadsheet->getActiveSheet());
// Add a drawing to the worksheet
$helper->log('Add a drawing to the worksheet two-cell anchor resized');
$drawing2 = new Drawing();
$drawing2->setName('PhpSpreadsheet');
$drawing2->setDescription('PhpSpreadsheet');
$drawing2->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png');
// anchor type will be two-cell because Coordinates2 is set
//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL);
$drawing2->setCoordinates('C2');
$drawing2->setCoordinates2('C2');
$drawing2->setOffsetX2($drawing->getImageWidth());
$drawing2->setOffsetY2($drawing->getImageHeight());
$drawing2->setWorksheet($spreadsheet->getActiveSheet());
$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS);
$spreadsheet->getActiveSheet()->getRowDimension(2)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS);
// Add a drawing to the worksheet one cell anchor
$helper->log('Add a drawing to the worksheet one-cell anchor');
$drawing3 = new Drawing();
$drawing3->setName('PhpSpreadsheet');
$drawing3->setDescription('PhpSpreadsheet');
$drawing3->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png');
// anchor type will be one-cell because Coordinates2 is not set
//$drawing->setAnchorType(Drawing::ANCHORTYPE_ONECELL);
$drawing3->setCoordinates('D3');
$drawing3->setWorksheet($spreadsheet->getActiveSheet());
// Add a drawing to the worksheet
$helper->log('Add a drawing to the worksheet two-cell anchor resized absolute');
$drawing4 = new Drawing();
$drawing4->setName('PhpSpreadsheet');
$drawing4->setDescription('PhpSpreadsheet');
$drawing4->setPath(__DIR__ . '/../images/PhpSpreadsheet_logo.png');
// anchor type will be two-cell because Coordinates2 is set
//$drawing->setAnchorType(Drawing::ANCHORTYPE_TWOCELL);
$drawing4->setCoordinates('C6');
$drawing4->setCoordinates2('C6');
$drawing4->setOffsetX2($drawing->getImageWidth());
$drawing4->setOffsetY2($drawing->getImageHeight());
$drawing4->setWorksheet($spreadsheet->getActiveSheet());
$drawing4->setEditAs(Drawing::EDIT_AS_ABSOLUTE);
//$spreadsheet->getActiveSheet()->getColumnDimension('C')->setWidth($drawing->getImageWidth(), Dimension::UOM_PIXELS);
$spreadsheet->getActiveSheet()->getRowDimension(6)->setRowHeight($drawing->getImageHeight(), Dimension::UOM_PIXELS);
$helper->write($spreadsheet, __FILE__, ['Xlsx']);

View File

@ -1315,6 +1315,11 @@ class Xlsx extends BaseReader
$outerShdw = $twoCellAnchor->pic->spPr->children(Namespaces::DRAWINGML)->effectLst->outerShdw;
$hlinkClick = $twoCellAnchor->pic->nvPicPr->cNvPr->children(Namespaces::DRAWINGML)->hlinkClick;
$objDrawing = new \PhpOffice\PhpSpreadsheet\Worksheet\Drawing();
/** @scrutinizer ignore-call */
$editAs = $twoCellAnchor->attributes();
if (isset($editAs, $editAs['editAs'])) {
$objDrawing->setEditAs($editAs['editAs']);
}
$objDrawing->setName((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'name'));
$objDrawing->setDescription((string) self::getArrayItem(self::getAttributes($twoCellAnchor->pic->nvPicPr->cNvPr), 'descr'));
$embedImageKey = (string) self::getArrayItem(

View File

@ -8,6 +8,22 @@ use PhpOffice\PhpSpreadsheet\IComparable;
class BaseDrawing implements IComparable
{
const EDIT_AS_ABSOLUTE = 'absolute';
const EDIT_AS_ONECELL = 'onecell';
const EDIT_AS_TWOCELL = 'twocell';
private const VALID_EDIT_AS = [
self::EDIT_AS_ABSOLUTE,
self::EDIT_AS_ONECELL,
self::EDIT_AS_TWOCELL,
];
/**
* The editAs attribute, used only with two cell anchor.
*
* @var string
*/
protected $editAs = '';
/**
* Image counter.
*
@ -27,14 +43,14 @@ class BaseDrawing implements IComparable
*
* @var string
*/
protected $name;
protected $name = '';
/**
* Description.
*
* @var string
*/
protected $description;
protected $description = '';
/**
* Worksheet.
@ -48,70 +64,84 @@ class BaseDrawing implements IComparable
*
* @var string
*/
protected $coordinates;
protected $coordinates = 'A1';
/**
* Offset X.
*
* @var int
*/
protected $offsetX;
protected $offsetX = 0;
/**
* Offset Y.
*
* @var int
*/
protected $offsetY;
protected $offsetY = 0;
/**
* Coordinates2.
*
* @var null|string
* @var string
*/
protected $coordinates2;
protected $coordinates2 = '';
/**
* Offset X2.
*
* @var int
*/
protected $offsetX2;
protected $offsetX2 = 0;
/**
* Offset Y2.
*
* @var int
*/
protected $offsetY2;
protected $offsetY2 = 0;
/**
* Width.
*
* @var int
*/
protected $width;
protected $width = 0;
/**
* Height.
*
* @var int
*/
protected $height;
protected $height = 0;
/**
* Pixel width of image. See $width for the size the Drawing will be in the sheet.
*
* @var int
*/
protected $imageWidth = 0;
/**
* Pixel width of image. See $height for the size the Drawing will be in the sheet.
*
* @var int
*/
protected $imageHeight = 0;
/**
* Proportional resize.
*
* @var bool
*/
protected $resizeProportional;
protected $resizeProportional = true;
/**
* Rotation.
*
* @var int
*/
protected $rotation;
protected $rotation = 0;
/**
* Shadow.
@ -132,7 +162,7 @@ class BaseDrawing implements IComparable
*
* @var int
*/
protected $type;
protected $type = IMAGETYPE_UNKNOWN;
/**
* Create a new BaseDrawing.
@ -140,91 +170,43 @@ class BaseDrawing implements IComparable
public function __construct()
{
// Initialise values
$this->name = '';
$this->description = '';
$this->worksheet = null;
$this->coordinates = 'A1';
$this->offsetX = 0;
$this->offsetY = 0;
$this->coordinates2 = null;
$this->offsetX2 = 0;
$this->offsetY2 = 0;
$this->width = 0;
$this->height = 0;
$this->resizeProportional = true;
$this->rotation = 0;
$this->shadow = new Drawing\Shadow();
$this->type = IMAGETYPE_UNKNOWN;
$this->setShadow();
// Set image index
++self::$imageCounter;
$this->imageIndex = self::$imageCounter;
}
/**
* Get image index.
*
* @return int
*/
public function getImageIndex()
public function getImageIndex(): int
{
return $this->imageIndex;
}
/**
* Get Name.
*
* @return string
*/
public function getName()
public function getName(): string
{
return $this->name;
}
/**
* Set Name.
*
* @param string $name
*
* @return $this
*/
public function setName($name)
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
/**
* Get Description.
*
* @return string
*/
public function getDescription()
public function getDescription(): string
{
return $this->description;
}
/**
* Set Description.
*
* @param string $description
*
* @return $this
*/
public function setDescription($description)
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
/**
* Get Worksheet.
*
* @return null|Worksheet
*/
public function getWorksheet()
public function getWorksheet(): ?Worksheet
{
return $this->worksheet;
}
@ -233,16 +215,16 @@ class BaseDrawing implements IComparable
* Set Worksheet.
*
* @param bool $overrideOld If a Worksheet has already been assigned, overwrite it and remove image from old Worksheet?
*
* @return $this
*/
public function setWorksheet(?Worksheet $worksheet = null, $overrideOld = false)
public function setWorksheet(?Worksheet $worksheet = null, bool $overrideOld = false): self
{
if ($this->worksheet === null) {
// Add drawing to \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
if ($worksheet !== null) {
$this->worksheet = $worksheet;
$this->worksheet->getCell($this->coordinates);
$this->worksheet->getDrawingCollection()->append($this);
}
} else {
if ($overrideOld) {
// Remove drawing from old \PhpOffice\PhpSpreadsheet\Worksheet\Worksheet
@ -267,168 +249,84 @@ class BaseDrawing implements IComparable
return $this;
}
/**
* Get Coordinates.
*
* @return string
*/
public function getCoordinates()
public function getCoordinates(): string
{
return $this->coordinates;
}
/**
* Set Coordinates.
*
* @param string $coordinates eg: 'A1'
*
* @return $this
*/
public function setCoordinates($coordinates)
public function setCoordinates(string $coordinates): self
{
$this->coordinates = $coordinates;
return $this;
}
/**
* Get OffsetX.
*
* @return int
*/
public function getOffsetX()
public function getOffsetX(): int
{
return $this->offsetX;
}
/**
* Set OffsetX.
*
* @param int $offsetX
*
* @return $this
*/
public function setOffsetX($offsetX)
public function setOffsetX(int $offsetX): self
{
$this->offsetX = $offsetX;
return $this;
}
/**
* Get OffsetY.
*
* @return int
*/
public function getOffsetY()
public function getOffsetY(): int
{
return $this->offsetY;
}
/**
* Get Coordinates2.
*
* @return null|string
*/
public function getCoordinates2()
{
return $this->coordinates2;
}
/**
* Set Coordinates2.
*
* @param null|string $coordinates2 eg: 'A1'
*
* @return $this
*/
public function setCoordinates2($coordinates2)
{
$this->coordinates2 = $coordinates2;
return $this;
}
/**
* Get OffsetX2.
*
* @return int
*/
public function getOffsetX2()
{
return $this->offsetX2;
}
/**
* Set OffsetX2.
*
* @param int $offsetX2
*
* @return $this
*/
public function setOffsetX2($offsetX2)
{
$this->offsetX2 = $offsetX2;
return $this;
}
/**
* Get OffsetY2.
*
* @return int
*/
public function getOffsetY2()
{
return $this->offsetY2;
}
/**
* Set OffsetY2.
*
* @param int $offsetY2
*
* @return $this
*/
public function setOffsetY2($offsetY2)
{
$this->offsetY2 = $offsetY2;
return $this;
}
/**
* Set OffsetY.
*
* @param int $offsetY
*
* @return $this
*/
public function setOffsetY($offsetY)
public function setOffsetY(int $offsetY): self
{
$this->offsetY = $offsetY;
return $this;
}
/**
* Get Width.
*
* @return int
*/
public function getWidth()
public function getCoordinates2(): string
{
return $this->coordinates2;
}
public function setCoordinates2(string $coordinates2): self
{
$this->coordinates2 = $coordinates2;
return $this;
}
public function getOffsetX2(): int
{
return $this->offsetX2;
}
public function setOffsetX2(int $offsetX2): self
{
$this->offsetX2 = $offsetX2;
return $this;
}
public function getOffsetY2(): int
{
return $this->offsetY2;
}
public function setOffsetY2(int $offsetY2): self
{
$this->offsetY2 = $offsetY2;
return $this;
}
public function getWidth(): int
{
return $this->width;
}
/**
* Set Width.
*
* @param int $width
*
* @return $this
*/
public function setWidth($width)
public function setWidth(int $width): self
{
// Resize proportional?
if ($this->resizeProportional && $width != 0) {
@ -442,24 +340,12 @@ class BaseDrawing implements IComparable
return $this;
}
/**
* Get Height.
*
* @return int
*/
public function getHeight()
public function getHeight(): int
{
return $this->height;
}
/**
* Set Height.
*
* @param int $height
*
* @return $this
*/
public function setHeight($height)
public function setHeight(int $height): self
{
// Resize proportional?
if ($this->resizeProportional && $height != 0) {
@ -482,14 +368,9 @@ class BaseDrawing implements IComparable
* $objDrawing->setWidthAndHeight(160,120);
* </code>
*
* @param int $width
* @param int $height
*
* @return $this
*
* @author Vincent@luo MSN:kele_100@hotmail.com
*/
public function setWidthAndHeight($width, $height)
public function setWidthAndHeight(int $width, int $height): self
{
$xratio = $width / ($this->width != 0 ? $this->width : 1);
$yratio = $height / ($this->height != 0 ? $this->height : 1);
@ -509,72 +390,38 @@ class BaseDrawing implements IComparable
return $this;
}
/**
* Get ResizeProportional.
*
* @return bool
*/
public function getResizeProportional()
public function getResizeProportional(): bool
{
return $this->resizeProportional;
}
/**
* Set ResizeProportional.
*
* @param bool $resizeProportional
*
* @return $this
*/
public function setResizeProportional($resizeProportional)
public function setResizeProportional(bool $resizeProportional): self
{
$this->resizeProportional = $resizeProportional;
return $this;
}
/**
* Get Rotation.
*
* @return int
*/
public function getRotation()
public function getRotation(): int
{
return $this->rotation;
}
/**
* Set Rotation.
*
* @param int $rotation
*
* @return $this
*/
public function setRotation($rotation)
public function setRotation(int $rotation): self
{
$this->rotation = $rotation;
return $this;
}
/**
* Get Shadow.
*
* @return Drawing\Shadow
*/
public function getShadow()
public function getShadow(): Drawing\Shadow
{
return $this->shadow;
}
/**
* Set Shadow.
*
* @return $this
*/
public function setShadow(?Drawing\Shadow $shadow = null)
public function setShadow(?Drawing\Shadow $shadow = null): self
{
$this->shadow = $shadow;
$this->shadow = $shadow ?? new Drawing\Shadow();
return $this;
}
@ -589,7 +436,7 @@ class BaseDrawing implements IComparable
return md5(
$this->name .
$this->description .
$this->worksheet->getHashCode() .
(($this->worksheet === null) ? '' : $this->worksheet->getHashCode()) .
$this->coordinates .
$this->offsetX .
$this->offsetY .
@ -626,10 +473,7 @@ class BaseDrawing implements IComparable
$this->hyperlink = $hyperlink;
}
/**
* @return null|Hyperlink
*/
public function getHyperlink()
public function getHyperlink(): ?Hyperlink
{
return $this->hyperlink;
}
@ -639,15 +483,19 @@ class BaseDrawing implements IComparable
*/
protected function setSizesAndType(string $path): void
{
if ($this->width == 0 && $this->height == 0 && $this->type == IMAGETYPE_UNKNOWN) {
if ($this->imageWidth === 0 && $this->imageHeight === 0 && $this->type === IMAGETYPE_UNKNOWN) {
$imageData = getimagesize($path);
if (is_array($imageData)) {
$this->width = $imageData[0];
$this->height = $imageData[1];
$this->imageWidth = $imageData[0];
$this->imageHeight = $imageData[1];
$this->type = $imageData[2];
}
}
if ($this->width === 0 && $this->height === 0) {
$this->width = $this->imageWidth;
$this->height = $this->imageHeight;
}
}
/**
@ -657,4 +505,31 @@ class BaseDrawing implements IComparable
{
return $this->type;
}
public function getImageWidth(): int
{
return $this->imageWidth;
}
public function getImageHeight(): int
{
return $this->imageHeight;
}
public function getEditAs(): string
{
return $this->editAs;
}
public function setEditAs(string $editAs): self
{
$this->editAs = $editAs;
return $this;
}
public function validEditAs(): bool
{
return in_array($this->editAs, self::VALID_EDIT_AS);
}
}

View File

@ -47,6 +47,9 @@ class MemoryDrawing extends BaseDrawing
*/
private $uniqueName;
/** @var null|resource */
private $alwaysNull;
/**
* Create a new MemoryDrawing.
*/
@ -56,6 +59,7 @@ class MemoryDrawing extends BaseDrawing
$this->renderingFunction = self::RENDERING_DEFAULT;
$this->mimeType = self::MIMETYPE_DEFAULT;
$this->uniqueName = md5(mt_rand(0, 9999) . time() . mt_rand(0, 9999));
$this->alwaysNull = null;
// Initialize parent
parent::__construct();
@ -64,8 +68,9 @@ class MemoryDrawing extends BaseDrawing
public function __destruct()
{
if ($this->imageResource) {
imagedestroy($this->imageResource);
$this->imageResource = null;
$rslt = @imagedestroy($this->imageResource);
// "Fix" for Scrutinizer
$this->imageResource = $rslt ? null : $this->alwaysNull;
}
}

View File

@ -3,6 +3,7 @@
namespace PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use PhpOffice\PhpSpreadsheet\Cell\Coordinate;
use PhpOffice\PhpSpreadsheet\Shared\Drawing as SharedDrawing;
use PhpOffice\PhpSpreadsheet\Shared\XMLWriter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
@ -56,7 +57,10 @@ class Drawing extends WriterPart
// Loop through charts and write the chart position
if ($chartCount > 0) {
for ($c = 0; $c < $chartCount; ++$c) {
$this->writeChart($objWriter, $worksheet->getChartByIndex($c), $c + $i);
$chart = $worksheet->getChartByIndex((string) $c);
if ($chart !== false) {
$this->writeChart($objWriter, $chart, $c + $i);
}
}
}
}
@ -90,16 +94,16 @@ class Drawing extends WriterPart
$objWriter->startElement('xdr:twoCellAnchor');
$objWriter->startElement('xdr:from');
$objWriter->writeElement('xdr:col', $tlColRow[0] - 1);
$objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['xOffset']));
$objWriter->writeElement('xdr:row', $tlColRow[1] - 1);
$objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($tl['yOffset']));
$objWriter->writeElement('xdr:col', (string) ($tlColRow[0] - 1));
$objWriter->writeElement('xdr:colOff', self::stringEmu($tl['xOffset']));
$objWriter->writeElement('xdr:row', (string) ($tlColRow[1] - 1));
$objWriter->writeElement('xdr:rowOff', self::stringEmu($tl['yOffset']));
$objWriter->endElement();
$objWriter->startElement('xdr:to');
$objWriter->writeElement('xdr:col', $brColRow[0] - 1);
$objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['xOffset']));
$objWriter->writeElement('xdr:row', $brColRow[1] - 1);
$objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($br['yOffset']));
$objWriter->writeElement('xdr:col', (string) ($brColRow[0] - 1));
$objWriter->writeElement('xdr:colOff', self::stringEmu($br['xOffset']));
$objWriter->writeElement('xdr:row', (string) ($brColRow[1] - 1));
$objWriter->writeElement('xdr:rowOff', self::stringEmu($br['yOffset']));
$objWriter->endElement();
$objWriter->startElement('xdr:graphicFrame');
@ -107,7 +111,7 @@ class Drawing extends WriterPart
$objWriter->startElement('xdr:nvGraphicFramePr');
$objWriter->startElement('xdr:cNvPr');
$objWriter->writeAttribute('name', 'Chart ' . $relationId);
$objWriter->writeAttribute('id', 1025 * $relationId);
$objWriter->writeAttribute('id', (string) (1025 * $relationId));
$objWriter->endElement();
$objWriter->startElement('xdr:cNvGraphicFramePr');
$objWriter->startElement('a:graphicFrameLocks');
@ -153,28 +157,31 @@ class Drawing extends WriterPart
public function writeDrawing(XMLWriter $objWriter, BaseDrawing $drawing, $relationId = -1, $hlinkClickId = null): void
{
if ($relationId >= 0) {
$isTwoCellAnchor = $drawing->getCoordinates2() !== null;
$isTwoCellAnchor = $drawing->getCoordinates2() !== '';
if ($isTwoCellAnchor) {
// xdr:twoCellAnchor
$objWriter->startElement('xdr:twoCellAnchor');
if ($drawing->validEditAs()) {
$objWriter->writeAttribute('editAs', $drawing->getEditAs());
}
// Image location
$aCoordinates = Coordinate::indexesFromString($drawing->getCoordinates());
$aCoordinates2 = Coordinate::indexesFromString($drawing->getCoordinates2());
// xdr:from
$objWriter->startElement('xdr:from');
$objWriter->writeElement('xdr:col', $aCoordinates[0] - 1);
$objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX()));
$objWriter->writeElement('xdr:row', $aCoordinates[1] - 1);
$objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY()));
$objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1));
$objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX()));
$objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1));
$objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY()));
$objWriter->endElement();
// xdr:to
$objWriter->startElement('xdr:to');
$objWriter->writeElement('xdr:col', $aCoordinates2[0] - 1);
$objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX2()));
$objWriter->writeElement('xdr:row', $aCoordinates2[1] - 1);
$objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY2()));
$objWriter->writeElement('xdr:col', (string) ($aCoordinates2[0] - 1));
$objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX2()));
$objWriter->writeElement('xdr:row', (string) ($aCoordinates2[1] - 1));
$objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY2()));
$objWriter->endElement();
} else {
// xdr:oneCellAnchor
@ -184,16 +191,16 @@ class Drawing extends WriterPart
// xdr:from
$objWriter->startElement('xdr:from');
$objWriter->writeElement('xdr:col', $aCoordinates[0] - 1);
$objWriter->writeElement('xdr:colOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetX()));
$objWriter->writeElement('xdr:row', $aCoordinates[1] - 1);
$objWriter->writeElement('xdr:rowOff', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getOffsetY()));
$objWriter->writeElement('xdr:col', (string) ($aCoordinates[0] - 1));
$objWriter->writeElement('xdr:colOff', self::stringEmu($drawing->getOffsetX()));
$objWriter->writeElement('xdr:row', (string) ($aCoordinates[1] - 1));
$objWriter->writeElement('xdr:rowOff', self::stringEmu($drawing->getOffsetY()));
$objWriter->endElement();
// xdr:ext
$objWriter->startElement('xdr:ext');
$objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth()));
$objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight()));
$objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth()));
$objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight()));
$objWriter->endElement();
}
@ -205,7 +212,7 @@ class Drawing extends WriterPart
// xdr:cNvPr
$objWriter->startElement('xdr:cNvPr');
$objWriter->writeAttribute('id', $relationId);
$objWriter->writeAttribute('id', (string) $relationId);
$objWriter->writeAttribute('name', $drawing->getName());
$objWriter->writeAttribute('descr', $drawing->getDescription());
@ -247,11 +254,11 @@ class Drawing extends WriterPart
// a:xfrm
$objWriter->startElement('a:xfrm');
$objWriter->writeAttribute('rot', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getRotation()));
$objWriter->writeAttribute('rot', (string) SharedDrawing::degreesToAngle($drawing->getRotation()));
if ($isTwoCellAnchor) {
$objWriter->startElement('a:ext');
$objWriter->writeAttribute('cx', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getWidth()));
$objWriter->writeAttribute('cy', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getHeight()));
$objWriter->writeAttribute('cx', self::stringEmu($drawing->getWidth()));
$objWriter->writeAttribute('cy', self::stringEmu($drawing->getHeight()));
$objWriter->endElement();
}
$objWriter->endElement();
@ -271,9 +278,9 @@ class Drawing extends WriterPart
// a:outerShdw
$objWriter->startElement('a:outerShdw');
$objWriter->writeAttribute('blurRad', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getBlurRadius()));
$objWriter->writeAttribute('dist', \PhpOffice\PhpSpreadsheet\Shared\Drawing::pixelsToEMU($drawing->getShadow()->getDistance()));
$objWriter->writeAttribute('dir', \PhpOffice\PhpSpreadsheet\Shared\Drawing::degreesToAngle($drawing->getShadow()->getDirection()));
$objWriter->writeAttribute('blurRad', self::stringEmu($drawing->getShadow()->getBlurRadius()));
$objWriter->writeAttribute('dist', self::stringEmu($drawing->getShadow()->getDistance()));
$objWriter->writeAttribute('dir', (string) SharedDrawing::degreesToAngle($drawing->getShadow()->getDirection()));
$objWriter->writeAttribute('algn', $drawing->getShadow()->getAlignment());
$objWriter->writeAttribute('rotWithShape', '0');
@ -283,7 +290,7 @@ class Drawing extends WriterPart
// a:alpha
$objWriter->startElement('a:alpha');
$objWriter->writeAttribute('val', $drawing->getShadow()->getAlpha() * 1000);
$objWriter->writeAttribute('val', (string) ($drawing->getShadow()->getAlpha() * 1000));
$objWriter->endElement();
$objWriter->endElement();
@ -528,4 +535,9 @@ class Drawing extends WriterPart
$objWriter->writeAttribute('r:id', 'rId' . $hlinkClickId);
$objWriter->endElement();
}
private static function stringEmu(int $pixelValue): string
{
return (string) SharedDrawing::pixelsToEMU($pixelValue);
}
}

View File

@ -2,7 +2,10 @@
namespace PhpOffice\PhpSpreadsheetTests\Worksheet;
use PhpOffice\PhpSpreadsheet\Exception as PhpSpreadsheetException;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\HeaderFooterDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use PHPUnit\Framework\TestCase;
@ -41,4 +44,76 @@ class DrawingTest extends TestCase
$spreadsheet->disconnectWorksheets();
}
}
public function testChangeWorksheet(): void
{
$spreadsheet = new Spreadsheet();
$sheet1 = $spreadsheet->getActiveSheet();
$sheet2 = $spreadsheet->createSheet();
$drawing = new Drawing();
$drawing->setName('Green Square');
$drawing->setPath('tests/data/Writer/XLSX/green_square.gif');
self::assertEquals($drawing->getWidth(), 150);
self::assertEquals($drawing->getHeight(), 150);
$drawing->setCoordinates('A1');
$drawing->setOffsetX(30);
$drawing->setOffsetY(10);
$drawing->setWorksheet($sheet1);
try {
$drawing->setWorksheet($sheet2);
self::fail('Should throw exception when attempting set worksheet without specifying override');
} catch (PhpSpreadsheetException $e) {
self::assertStringContainsString('A Worksheet has already been assigned.', $e->getMessage());
}
self::assertSame($sheet1, $drawing->getWorksheet());
self::assertCount(1, $sheet1->getDrawingCollection());
self::assertCount(0, $sheet2->getDrawingCollection());
$drawing->setWorksheet($sheet2, true);
self::assertSame($sheet2, $drawing->getWorksheet());
self::assertCount(0, $sheet1->getDrawingCollection());
self::assertCount(1, $sheet2->getDrawingCollection());
}
public function testHeaderFooter(): void
{
$drawing1 = new HeaderFooterDrawing();
$drawing1->setName('Blue Square');
$drawing1->setPath('tests/data/Writer/XLSX/blue_square.png');
self::assertEquals($drawing1->getWidth(), 100);
self::assertEquals($drawing1->getHeight(), 100);
$drawing2 = new HeaderFooterDrawing();
$drawing2->setName('Blue Square');
$drawing2->setPath('tests/data/Writer/XLSX/blue_square.png');
self::assertSame($drawing1->getHashCode(), $drawing2->getHashCode());
$drawing2->setOffsetX(100);
self::assertNotEquals($drawing1->getHashCode(), $drawing2->getHashCode());
}
public function testSetWidthAndHeight(): void
{
$drawing = new Drawing();
$drawing->setName('Blue Square');
$drawing->setPath('tests/data/Writer/XLSX/blue_square.png');
self::assertSame(100, $drawing->getWidth());
self::assertSame(100, $drawing->getHeight());
self::assertTrue($drawing->getResizeProportional());
$drawing->setResizeProportional(false);
$drawing->setWidthAndHeight(150, 200);
self::assertSame(150, $drawing->getWidth());
self::assertSame(200, $drawing->getHeight());
$drawing->setResizeProportional(true);
$drawing->setWidthAndHeight(300, 250);
// width increase% more than height, so scale width
self::assertSame(188, $drawing->getWidth());
self::assertSame(250, $drawing->getHeight());
$drawing->setResizeProportional(false);
$drawing->setWidthAndHeight(150, 200);
$drawing->setResizeProportional(true);
// height increase% more than width, so scale height
$drawing->setWidthAndHeight(175, 350);
self::assertSame(175, $drawing->getWidth());
self::assertSame(234, $drawing->getHeight());
}
}

View File

@ -8,7 +8,9 @@ use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Reader\Xlsx;
use PhpOffice\PhpSpreadsheet\Shared\File;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Worksheet\BaseDrawing;
use PhpOffice\PhpSpreadsheet\Worksheet\Drawing;
use PhpOffice\PhpSpreadsheet\Worksheet\MemoryDrawing;
use PhpOffice\PhpSpreadsheetTests\Functional\AbstractFunctional;
class DrawingsTest extends AbstractFunctional
@ -28,6 +30,8 @@ class DrawingsTest extends AbstractFunctional
// Fake assert. The only thing we need is to ensure the file is loaded without exception
self::assertNotNull($reloadedSpreadsheet);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
@ -59,6 +63,8 @@ class DrawingsTest extends AbstractFunctional
// Fake assert. The only thing we need is to ensure the file is loaded without exception
self::assertNotNull($reloadedSpreadsheet);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
@ -96,6 +102,8 @@ class DrawingsTest extends AbstractFunctional
unlink($tempFileName);
self::assertNotNull($reloadedSpreadsheet);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
@ -173,7 +181,8 @@ class DrawingsTest extends AbstractFunctional
self::assertEquals($comment->getBackgroundImage()->getType(), IMAGETYPE_PNG);
unlink($tempFileName);
self::assertNotNull($reloadedSpreadsheet);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
@ -287,6 +296,7 @@ class DrawingsTest extends AbstractFunctional
$drawing->setPath('tests/data/Writer/XLSX/orange_square_24_bit.bmp');
self::assertEquals($drawing->getWidth(), 70);
self::assertEquals($drawing->getHeight(), 70);
self::assertSame(IMAGETYPE_PNG, $drawing->getImageTypeForSave());
$comment = $sheet->getComment('A6');
$comment->setBackgroundImage($drawing);
$comment->setSizeAsBackgroundImage();
@ -304,6 +314,8 @@ class DrawingsTest extends AbstractFunctional
$drawing = new Drawing();
$drawing->setName('Purple Square');
$drawing->setPath('tests/data/Writer/XLSX/purple_square.tiff');
self::assertStringContainsString('purple_square.tiff', $drawing->getFilename());
self::assertFalse($drawing->getIsUrl());
$comment = $sheet->getComment('A7');
self::assertTrue($comment instanceof Comment);
self::assertFalse($comment->hasBackgroundImage());
@ -326,6 +338,14 @@ class DrawingsTest extends AbstractFunctional
self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
}
try {
$drawing->getMediaFilename();
self::fail('Should throw exception when attempting to get media file name for tiff');
} catch (PhpSpreadsheetException $e) {
self::assertTrue($e instanceof PhpSpreadsheetException);
self::assertEquals($e->getMessage(), 'Unsupported image type in comment background. Supported types: PNG, JPEG, BMP, GIF.');
}
try {
$drawing->getImageFileExtensionForSave();
self::fail('Should throw exception when attempting to get image file extention for tiff');
@ -428,7 +448,8 @@ class DrawingsTest extends AbstractFunctional
unlink($tempFileName);
self::assertNotNull($reloadedSpreadsheet);
$spreadsheet->disconnectWorksheets();
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
@ -436,7 +457,6 @@ class DrawingsTest extends AbstractFunctional
*/
public function testTwoCellAnchorDrawing(): void
{
$reader = new Xlsx();
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
@ -455,31 +475,133 @@ class DrawingsTest extends AbstractFunctional
$drawing->setWorksheet($sheet);
// Write file
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
$tempFileName = File::sysGetTempDir() . '/drawings_image_that_two_cell_anchor.xlsx';
$writer->save($tempFileName);
// Read new file
$reloadedSpreadsheet = $reader->load($tempFileName);
$sheet = $reloadedSpreadsheet->getActiveSheet();
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
// Check image coordinates.
$drawingCollection = $sheet->getDrawingCollection();
$drawingCollection = $rsheet->getDrawingCollection();
self::assertCount(1, $drawingCollection);
$drawing = $drawingCollection[0];
self::assertNotNull($drawing);
self::assertSame(150, $drawing->getWidth());
self::assertSame(150, $drawing->getHeight());
self::assertSame('A1', $drawing->getCoordinates());
self::assertSame(30, $drawing->getOffsetX());
self::assertSame(10, $drawing->getOffsetY());
self::assertSame('E8', $drawing->getCoordinates2());
self::assertSame(-50, $drawing->getOffsetX2());
self::assertSame(-20, $drawing->getOffsetY2());
self::assertSame($rsheet, $drawing->getWorksheet());
$reloadedSpreadsheet->disconnectWorksheets();
}
/**
* Test editAs attribute for two-cell anchors.
*
* @dataProvider providerEditAs
*/
public function testTwoCellEditAs(string $editAs, ?string $expectedResult = null): void
{
if ($expectedResult === null) {
$expectedResult = $editAs;
}
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
// Add gif image that coordinates is two cell anchor.
$drawing = new Drawing();
$drawing->setName('Green Square');
$drawing->setPath('tests/data/Writer/XLSX/green_square.gif');
self::assertEquals($drawing->getWidth(), 150);
self::assertEquals($drawing->getHeight(), 150);
self::assertEquals($drawing->getCoordinates(), 'A1');
self::assertEquals($drawing->getOffsetX(), 30);
self::assertEquals($drawing->getOffsetY(), 10);
self::assertEquals($drawing->getCoordinates2(), 'E8');
self::assertEquals($drawing->getOffsetX2(), -50);
self::assertEquals($drawing->getOffsetY2(), -20);
self::assertEquals($drawing->getWorksheet(), $sheet);
$drawing->setCoordinates('A1');
$drawing->setOffsetX(30);
$drawing->setOffsetY(10);
$drawing->setCoordinates2('E8');
$drawing->setOffsetX2(-50);
$drawing->setOffsetY2(-20);
if ($editAs !== '') {
$drawing->setEditAs($editAs);
}
$drawing->setWorksheet($sheet);
unlink($tempFileName);
// Write file
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
$rsheet = $reloadedSpreadsheet->getActiveSheet();
self::assertNotNull($reloadedSpreadsheet);
// Check image coordinates.
$drawingCollection = $rsheet->getDrawingCollection();
$drawing = $drawingCollection[0];
self::assertNotNull($drawing);
self::assertSame(150, $drawing->getWidth());
self::assertSame(150, $drawing->getHeight());
self::assertSame('A1', $drawing->getCoordinates());
self::assertSame(30, $drawing->getOffsetX());
self::assertSame(10, $drawing->getOffsetY());
self::assertSame('E8', $drawing->getCoordinates2());
self::assertSame(-50, $drawing->getOffsetX2());
self::assertSame(-20, $drawing->getOffsetY2());
self::assertSame($rsheet, $drawing->getWorksheet());
self::assertSame($expectedResult, $drawing->getEditAs());
$reloadedSpreadsheet->disconnectWorksheets();
}
public function providerEditAs(): array
{
return [
'absolute' => ['absolute'],
'onecell' => ['onecell'],
'twocell' => ['twocell'],
'unset (will be treated as twocell)' => [''],
'unknown (will be treated as twocell)' => ['unknown', ''],
];
}
public function testMemoryDrawingDuplicateResource(): void
{
$gdImage = imagecreatetruecolor(120, 20);
$textColor = ($gdImage === false) ? false : imagecolorallocate($gdImage, 255, 255, 255);
if ($gdImage === false || $textColor === false) {
self::fail('imagecreatetruecolor or imagecolorallocate failed');
} else {
$spreadsheet = new Spreadsheet();
$aSheet = $spreadsheet->getActiveSheet();
imagestring($gdImage, 1, 5, 5, 'Created with PhpSpreadsheet', $textColor);
$listOfModes = [
BaseDrawing::EDIT_AS_TWOCELL,
BaseDrawing::EDIT_AS_ABSOLUTE,
BaseDrawing::EDIT_AS_ONECELL,
];
foreach ($listOfModes as $i => $mode) {
$drawing = new MemoryDrawing();
$drawing->setName('In-Memory image ' . $i);
$drawing->setDescription('In-Memory image ' . $i);
$drawing->setCoordinates('A' . ((4 * $i) + 1));
$drawing->setCoordinates2('D' . ((4 * $i) + 4));
$drawing->setEditAs($mode);
$drawing->setImageResource($gdImage);
$drawing->setRenderingFunction(
MemoryDrawing::RENDERING_JPEG
);
$drawing->setMimeType(MemoryDrawing::MIMETYPE_DEFAULT);
$drawing->setWorksheet($aSheet);
}
$reloadedSpreadsheet = $this->writeAndReload($spreadsheet, 'Xlsx');
$spreadsheet->disconnectWorksheets();
foreach ($reloadedSpreadsheet->getActiveSheet()->getDrawingCollection() as $index => $pDrawing) {
self::assertEquals($listOfModes[$index], $pDrawing->getEditAs(), 'functional test drawing twoCellAnchor');
}
$reloadedSpreadsheet->disconnectWorksheets();
}
}
}