Skip to content

Instantly share code, notes, and snippets.

@IacopoMelani
Created July 9, 2020 14:44
Show Gist options
  • Save IacopoMelani/df8c2c3ecc748bd0c9f478bd270dccf2 to your computer and use it in GitHub Desktop.
Save IacopoMelani/df8c2c3ecc748bd0c9f478bd270dccf2 to your computer and use it in GitHub Desktop.
Fix class xlsxwriterplus.class.php from https://github.com/mk-j/PHP_XLSXWriter/issues/88
<?php
class XLSWriterPlus extends XLSXWriter
{
/**
* @var array
*/
private $images = [];
/**
* @var array
*/
private $imageOptions = [];
/**
* @var array
*/
private $ignoredErrorsCells = [];
/**
* @return array
*/
public function getIgnoredErrorsCells()
{
return $this->ignoredErrorsCells;
}
/**
* @param array $ignoredErrorsCells
*/
public function setIgnoredErrorsCells($ignoredErrorsCells)
{
$this->ignoredErrorsCells = $ignoredErrorsCells;
}
/**
* @return array
*/
public function getSheets()
{
return $this->sheets;
}
/**
* @param string $imagePath
* @param string $imageName
* @param array $imageOptions
* @param int $imageId
* @throws Exception
*/
public function addImage($imagePath, $imageName, $imageId, $imageOptions = [])
{
if(!file_exists($imagePath)){
throw new Exception(sprintf('File %s not found.', $imagePath));
}
$this->images[$imageId] = array(
'path' => $imagePath,
'name' => $imageName
);
$this->imageOptions[$imageId] = array_merge([
'startColNum' => 0,
'endColNum' => 0,
'startRowNum' => 0,
'endRowNum' => 0,
], $imageOptions);
}
public function writeToString()
{
$temp_file = $this->tempFilename();
$this->writeToFile($temp_file);
$string = file_get_contents($temp_file);
return $string;
}
/**
* @param $filename
* @throws Exception
*/
public function writeToFile($filename)
{
$i = 1;
foreach ($this->sheets as $sheet_name => $sheet) {
$this->finalizeSheet($sheet_name, $i);
$i++;
}
if (file_exists($filename)) {
if (is_writable($filename)) {
@unlink($filename);
} else {
throw new \Exception("Error in " . __CLASS__ . "::" . __FUNCTION__ . ", file is not writeable.");
}
}
$zip = new \ZipArchive();
if (empty($this->sheets)) {
throw new \Exception("Error in " . __CLASS__ . "::" . __FUNCTION__ . ", no worksheets defined.");
}
if (!$zip->open($filename, \ZipArchive::CREATE)) {
throw new \Exception("Error in " . __CLASS__ . "::" . __FUNCTION__ . ", unable to create zip.");
}
$zip->addEmptyDir("docProps/");
$zip->addFromString("docProps/app.xml" , $this->buildAppXML() );
$zip->addFromString("docProps/core.xml", $this->buildCoreXML());
$zip->addEmptyDir("_rels/");
$zip->addFromString("_rels/.rels", $this->buildRelationshipsXML());
if (count($this->images) > 0) {
$zip->addEmptyDir("xl/media/");
$zip->addEmptyDir("xl/drawings");
$zip->addEmptyDir("xl/drawings/_rels");
foreach ($this->images as $imageId => $image) {
$zip->addFile($image['path'], 'xl/media/' . $image['name']);
}
$i = 1;
foreach ($this->sheets as $sheet) {
$zip->addFromString("xl/drawings/drawing" . $i . ".xml", $this->buildDrawingXML());
$zip->addFromString("xl/drawings/_rels/drawing" . $i . ".xml.rels", $this->buildDrawingRelationshipXML());
$i++;
}
}
$zip->addEmptyDir("xl/worksheets/");
$zip->addEmptyDir("xl/worksheets/_rels/");
$i = 1;
foreach ($this->sheets as $sheet) {
$zip->addFile($sheet->filename, "xl/worksheets/" . $sheet->xmlname);
$zip->addFromString("xl/worksheets/_rels/" . $sheet->xmlname . '.rels', $this->buildSheetRelationshipXML($i++));
}
$zip->addFromString("xl/workbook.xml", $this->buildWorkbookXML());
$zip->addFile($this->writeStylesXML(), "xl/styles.xml");
$zip->addFromString("[Content_Types].xml", $this->buildContentTypesXML());
$zip->addEmptyDir("xl/_rels/");
$zip->addFromString("xl/_rels/workbook.xml.rels", $this->buildWorkbookRelsXML());
$zip->close();
}
/**
* @param string $imagePath
* @param int $imageId
* @return string
*/
public function buildDrawingXML()
{
$imageRelationshipXML = '';
if(count($this->images) > 0) {
$imageRelationshipXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<xdr:wsDr xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" xmlns:xdr="http://schemas.openxmlformats.org/drawingml/2006/spreadsheetDrawing">';
}
foreach($this->images as $imageId => $image) {
$imageOptions = $this->imageOptions[$imageId];
list($width, $height) = getimagesize($image['path']);
if($imageOptions['endColNum'] == 0 && $imageOptions['endRowNum'] == 0) {
$imageOptions['endColNum'] = round($height / 15); // MY EDIT
$imageOptions['endRowNum'] = round($width / 40); // MY EDIT
}
$endColOffset = round($width * 1);
$endRowOffset = round($height * 1);
$imageRelationshipXML .= '
<xdr:twoCellAnchor editAs="oneCell">
<xdr:from>
<xdr:col>' . $imageOptions['startColNum'] . '</xdr:col>
<xdr:colOff>0</xdr:colOff>
<xdr:row>' . $imageOptions['startRowNum'] . '</xdr:row>
<xdr:rowOff>0</xdr:rowOff>
</xdr:from>
<xdr:to>
<xdr:col>' . $imageOptions['endColNum'] . '</xdr:col>
<xdr:colOff>' . $endColOffset . '</xdr:colOff>
<xdr:row>' . $imageOptions['endRowNum'] . '</xdr:row>
<xdr:rowOff>' . $endRowOffset . '</xdr:rowOff>
</xdr:to>
<xdr:pic>
<xdr:nvPicPr>
<xdr:cNvPr id="' . $imageId . '" name="Picture ' . $imageId . '">
<a:extLst>
<a:ext uri="{FF2B5EF4-FFF2-40B4-BE49-F238E27FC236}">
<a16:creationId xmlns:a16="http://schemas.microsoft.com/office/drawing/2014/main" id="{D536D061-A3D2-4F2B-ACAF-CD70361876FA}" />
</a:ext>
</a:extLst>
</xdr:cNvPr>
<xdr:cNvPicPr>
<a:picLocks noChangeAspect="1" />
</xdr:cNvPicPr>
</xdr:nvPicPr>
<xdr:blipFill>
<a:blip xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="rId'.$imageId.'" cstate="print">
<a:extLst>
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0" />
</a:ext>
</a:extLst>
</a:blip>
<a:stretch>
<a:fillRect/>
</a:stretch>
</xdr:blipFill>
<xdr:spPr>
<a:prstGeom prst="rect">
<a:avLst/>
</a:prstGeom>
</xdr:spPr>
</xdr:pic>
<xdr:clientData />
</xdr:twoCellAnchor>';
}
if($imageRelationshipXML != '') {
$imageRelationshipXML .= '</xdr:wsDr>';
}
return $imageRelationshipXML;
}
/**
* @return string
*/
protected function buildContentTypesXML()
{
$content_types_xml = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' . "\n";
$content_types_xml .= '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">';
$content_types_xml .= '
<Default Extension="jpeg" ContentType="image/jpeg" />
<Default Extension="png" ContentType="image/png" />
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="xml" ContentType="application/xml" />
';
foreach ($this->sheets as $sheet_name => $sheet) {
$content_types_xml .= '<Override PartName="/xl/worksheets/' . ($sheet->xmlname) . '" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml"/>';
}
$content_types_xml .= '<Override PartName="/xl/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml"/>';
$content_types_xml .= '<Override PartName="/xl/workbook.xml" ContentType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml"/>';
if (count($this->images) > 0) {
$i = 1;
foreach ($this->sheets as $sheet_name => $sheet) {
$content_types_xml .= '<Override PartName="/xl/drawings/drawing' . ($i++) . '.xml" ContentType="application/vnd.openxmlformats-officedocument.drawing+xml" />';
}
}
$content_types_xml .= "\n";
$content_types_xml .= '</Types>';
return $content_types_xml;
}
/**
* @param $sheet_name
*/
protected function finalizeSheet($sheet_name, $i = 1)
{
if (empty($sheet_name) || $this->sheets[$sheet_name]->finalized)
return;
$sheet = &$this->sheets[$sheet_name];
$sheet->file_writer->write('</sheetData>');
if (!empty($sheet->merge_cells)) {
$sheet->file_writer->write('<mergeCells>');
foreach ($sheet->merge_cells as $range) {
$sheet->file_writer->write('<mergeCell ref="' . $range . '"/>');
}
$sheet->file_writer->write('</mergeCells>');
}
$sheet->file_writer->write('<printOptions headings="false" gridLines="false" gridLinesSet="true" horizontalCentered="false" verticalCentered="false"/>');
$sheet->file_writer->write('<pageMargins left="0.5" right="0.5" top="1.0" bottom="1.0" header="0.5" footer="0.5"/>');
$sheet->file_writer->write('<pageSetup blackAndWhite="false" cellComments="none" copies="1" draft="false" firstPageNumber="1" fitToHeight="1" fitToWidth="1" horizontalDpi="300" orientation="portrait" pageOrder="downThenOver" paperSize="1" scale="100" useFirstPageNumber="true" usePrinterDefaults="false" verticalDpi="300"/>');
$sheet->file_writer->write('<headerFooter differentFirst="false" differentOddEven="false">');
$sheet->file_writer->write('<oddHeader>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12&amp;A</oddHeader>');
$sheet->file_writer->write('<oddFooter>&amp;C&amp;&quot;Times New Roman,Regular&quot;&amp;12Page &amp;P</oddFooter>');
$sheet->file_writer->write('</headerFooter>');
if(count($this->getIgnoredErrorsCells()) > 0){
$sheet->file_writer->write('<ignoredErrors>');
foreach($this->getIgnoredErrorsCells() as $ignoredErrorsCell) {
$sheet->file_writer->write('<ignoredError sqref="' . $ignoredErrorsCell . '" numberStoredAsText="1"/>');
}
$sheet->file_writer->write('</ignoredErrors>');
}
if (count($this->images) > 0) {
$sheet->file_writer->write('<drawing r:id="rId' . $i . '" />');
}
$sheet->file_writer->write('</worksheet>');
$max_cell = $this->xlsCell($sheet->row_count - 1, count($sheet->columns) - 1);
$max_cell_tag = '<dimension ref="A1:' . $max_cell . '"/>';
$padding_length = $sheet->max_cell_tag_end - $sheet->max_cell_tag_start - strlen($max_cell_tag);
$sheet->file_writer->fseek($sheet->max_cell_tag_start);
$sheet->file_writer->write($max_cell_tag . str_repeat(" ", $padding_length));
$sheet->file_writer->close();
$sheet->finalized = true;
}
/**
* @return string
*/
public function buildDrawingRelationshipXML()
{
$drawingXML = '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' . "\n";
$drawingXML .= '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
foreach ($this->images as $imageId => $image) {
$drawingXML .= '<Relationship Id="rId' . $imageId . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" Target="../media/' . $image['name'] . '"/>';
}
$drawingXML .= "\n" . '</Relationships>';
return $drawingXML;
}
/**
* @param int $sheetId
* @return string
*/
public function buildSheetRelationshipXML($sheetId)
{
$lastRelationshipId = 0;
$rels_xml = "";
$rels_xml .= '<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>' . "\n";
$rels_xml .= '<Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">';
if (count($this->images) > 0) {
foreach ($this->images as $imageId => $image) {
$rels_xml .= '<Relationship Id="rId' . (++$lastRelationshipId) . '" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" Target="../drawings/drawing' . $sheetId . '.xml"/>';
}
}
$rels_xml .= "\n";
$rels_xml .= '</Relationships>';
return $rels_xml;
}
}
@NitemareReal
Copy link

Hello, good work! Can you select which sheet the image will be inserted on?

@IacopoMelani
Copy link
Author

@NitemareReal Hi thanks!
I think no, when I had done this I only had need one single sheet, I didn't test to build more sheets, but I think every image you add will repeat on every single sheet in same col/row position.
i'm sorry ๐Ÿ˜”

@NitemareReal
Copy link

NitemareReal commented Oct 8, 2020

@NitemareReal Hi thanks!
I think no, when I had done this I only had need one single sheet, I didn't test to build more sheets, but I think every image you add will repeat on every single sheet in same col/row position.
i'm sorry ๐Ÿ˜”

I have just tested it and image is just shown in first sheet...

@NitemareReal
Copy link

I finally fond out why image is just shown in first sheet while there are "drawingX.xml" and "_rels/drawingX.xml.rels" for every sheet... the problem is when you call "finalizeSheet". Second argument ($i) is incremental, for every sheet, but that's a mistake. This value is used in "finalizeSheet" to write the "drawing" tag, but its r:id should match the id created by "buildSheetRelationshipXML" (which starts from 1 for every sheet). And I think (not verified yet) that "buildSheetRelationshipXML" shouldn't create a "Relationship Id" for every image, it should create only one, and "buildDrawingRelationshipXML" should create all "Relations" to every image in one sheet (pointing to media folder). Fixed that, should be very easy modifing all methods, starting with "addImage", allowing to specify which "sheet" you want to add image to, as original XLSXWriter methods do. Maybe tomorrow i'll try to implement these changes...

@IacopoMelani
Copy link
Author

I finally fond out why image is just shown in first sheet while there are "drawingX.xml" and "_rels/drawingX.xml.rels" for every sheet... the problem is when you call "finalizeSheet". Second argument ($i) is incremental, for every sheet, but that's a mistake. This value is used in "finalizeSheet" to write the "drawing" tag, but its r:id should match the id created by "buildSheetRelationshipXML" (which starts from 1 for every sheet). And I think (not verified yet) that "buildSheetRelationshipXML" shouldn't create a "Relationship Id" for every image, it should create only one, and "buildDrawingRelationshipXML" should create all "Relations" to every image in one sheet (pointing to media folder). Fixed that, should be very easy modifing all methods, starting with "addImage", allowing to specify which "sheet" you want to add image to, as original XLSXWriter methods do. Maybe tomorrow i'll try to implement these changes...

It make sense, if you want to fork this I appreciate it or maybe directly on the original repo, but I think it is now archived or in any case no longer followed

@NitemareReal
Copy link

Done! I forked from your fork. Now you can add multiple images to a sheet and specify which sheet will image(s) insert on!
I have tested it and it works!

@NitemareReal
Copy link

New changes to show the picture with its original size if no end coordinates given

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment