first commit

This commit is contained in:
2024-11-05 12:22:50 +01:00
commit e5682a3912
19641 changed files with 2948548 additions and 0 deletions

View File

@@ -0,0 +1,68 @@
<?php
namespace Box\Spout\Writer\ODS\Helper;
use Box\Spout\Writer\Style\BorderPart;
use Box\Spout\Writer\Style\Border;
/**
* Class BorderHelper
*
* The fo:border, fo:border-top, fo:border-bottom, fo:border-left and fo:border-right attributes
* specify border properties
* http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__1419780_253892949
*
* Example table-cell-properties
*
* <style:table-cell-properties
* fo:border-bottom="0.74pt solid #ffc000" style:diagonal-bl-tr="none"
* style:diagonal-tl-br="none" fo:border-left="none" fo:border-right="none"
* style:rotation-align="none" fo:border-top="none"/>
*/
class BorderHelper
{
/**
* Width mappings
*
* @var array
*/
protected static $widthMap = [
Border::WIDTH_THIN => '0.75pt',
Border::WIDTH_MEDIUM => '1.75pt',
Border::WIDTH_THICK => '2.5pt',
];
/**
* Style mapping
*
* @var array
*/
protected static $styleMap = [
Border::STYLE_SOLID => 'solid',
Border::STYLE_DASHED => 'dashed',
Border::STYLE_DOTTED => 'dotted',
Border::STYLE_DOUBLE => 'double',
];
/**
* @param BorderPart $borderPart
* @return string
*/
public static function serializeBorderPart(BorderPart $borderPart)
{
$definition = 'fo:border-%s="%s"';
if ($borderPart->getStyle() === Border::STYLE_NONE) {
$borderPartDefinition = sprintf($definition, $borderPart->getName(), 'none');
} else {
$attributes = [
self::$widthMap[$borderPart->getWidth()],
self::$styleMap[$borderPart->getStyle()],
'#' . $borderPart->getColor(),
];
$borderPartDefinition = sprintf($definition, $borderPart->getName(), implode(' ', $attributes));
}
return $borderPartDefinition;
}
}

View File

@@ -0,0 +1,279 @@
<?php
namespace Box\Spout\Writer\ODS\Helper;
use Box\Spout\Writer\Common\Helper\ZipHelper;
use Box\Spout\Writer\ODS\Internal\Worksheet;
/**
* Class FileSystemHelper
* This class provides helper functions to help with the file system operations
* like files/folders creation & deletion for ODS files
*
* @package Box\Spout\Writer\ODS\Helper
*/
class FileSystemHelper extends \Box\Spout\Common\Helper\FileSystemHelper
{
const APP_NAME = 'Spout';
const MIMETYPE = 'application/vnd.oasis.opendocument.spreadsheet';
const META_INF_FOLDER_NAME = 'META-INF';
const SHEETS_CONTENT_TEMP_FOLDER_NAME = 'worksheets-temp';
const MANIFEST_XML_FILE_NAME = 'manifest.xml';
const CONTENT_XML_FILE_NAME = 'content.xml';
const META_XML_FILE_NAME = 'meta.xml';
const MIMETYPE_FILE_NAME = 'mimetype';
const STYLES_XML_FILE_NAME = 'styles.xml';
/** @var string Path to the root folder inside the temp folder where the files to create the ODS will be stored */
protected $rootFolder;
/** @var string Path to the "META-INF" folder inside the root folder */
protected $metaInfFolder;
/** @var string Path to the temp folder, inside the root folder, where specific sheets content will be written to */
protected $sheetsContentTempFolder;
/**
* @return string
*/
public function getRootFolder()
{
return $this->rootFolder;
}
/**
* @return string
*/
public function getSheetsContentTempFolder()
{
return $this->sheetsContentTempFolder;
}
/**
* Creates all the folders needed to create a ODS file, as well as the files that won't change.
*
* @return void
* @throws \Box\Spout\Common\Exception\IOException If unable to create at least one of the base folders
*/
public function createBaseFilesAndFolders()
{
$this
->createRootFolder()
->createMetaInfoFolderAndFile()
->createSheetsContentTempFolder()
->createMetaFile()
->createMimetypeFile();
}
/**
* Creates the folder that will be used as root
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
*/
protected function createRootFolder()
{
$this->rootFolder = $this->createFolder($this->baseFolderRealPath, uniqid('ods'));
return $this;
}
/**
* Creates the "META-INF" folder under the root folder as well as the "manifest.xml" file in it
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder or the "manifest.xml" file
*/
protected function createMetaInfoFolderAndFile()
{
$this->metaInfFolder = $this->createFolder($this->rootFolder, self::META_INF_FOLDER_NAME);
$this->createManifestFile();
return $this;
}
/**
* Creates the "manifest.xml" file under the "META-INF" folder (under root)
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createManifestFile()
{
$manifestXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8"?>
<manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0" manifest:version="1.2">
<manifest:file-entry manifest:full-path="/" manifest:media-type="application/vnd.oasis.opendocument.spreadsheet"/>
<manifest:file-entry manifest:full-path="styles.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="content.xml" manifest:media-type="text/xml"/>
<manifest:file-entry manifest:full-path="meta.xml" manifest:media-type="text/xml"/>
</manifest:manifest>
EOD;
$this->createFileWithContents($this->metaInfFolder, self::MANIFEST_XML_FILE_NAME, $manifestXmlFileContents);
return $this;
}
/**
* Creates the temp folder where specific sheets content will be written to.
* This folder is not part of the final ODS file and is only used to be able to jump between sheets.
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the folder
*/
protected function createSheetsContentTempFolder()
{
$this->sheetsContentTempFolder = $this->createFolder($this->rootFolder, self::SHEETS_CONTENT_TEMP_FOLDER_NAME);
return $this;
}
/**
* Creates the "meta.xml" file under the root folder
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createMetaFile()
{
$appName = self::APP_NAME;
$createdDate = (new \DateTime())->format(\DateTime::W3C);
$metaXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-meta office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:meta="urn:oasis:names:tc:opendocument:xmlns:meta:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
<office:meta>
<dc:creator>$appName</dc:creator>
<meta:creation-date>$createdDate</meta:creation-date>
<dc:date>$createdDate</dc:date>
</office:meta>
</office:document-meta>
EOD;
$this->createFileWithContents($this->rootFolder, self::META_XML_FILE_NAME, $metaXmlFileContents);
return $this;
}
/**
* Creates the "mimetype" file under the root folder
*
* @return FileSystemHelper
* @throws \Box\Spout\Common\Exception\IOException If unable to create the file
*/
protected function createMimetypeFile()
{
$this->createFileWithContents($this->rootFolder, self::MIMETYPE_FILE_NAME, self::MIMETYPE);
return $this;
}
/**
* Creates the "content.xml" file under the root folder
*
* @param Worksheet[] $worksheets
* @param StyleHelper $styleHelper
* @return FileSystemHelper
*/
public function createContentFile($worksheets, $styleHelper)
{
$contentXmlFileContents = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-content office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:calcext="urn:org:documentfoundation:names:experimental:calc:xmlns:calcext:1.0" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
EOD;
$contentXmlFileContents .= $styleHelper->getContentXmlFontFaceSectionContent();
$contentXmlFileContents .= $styleHelper->getContentXmlAutomaticStylesSectionContent(count($worksheets));
$contentXmlFileContents .= '<office:body><office:spreadsheet>';
$this->createFileWithContents($this->rootFolder, self::CONTENT_XML_FILE_NAME, $contentXmlFileContents);
// Append sheets content to "content.xml"
$contentXmlFilePath = $this->rootFolder . '/' . self::CONTENT_XML_FILE_NAME;
$contentXmlHandle = fopen($contentXmlFilePath, 'a');
foreach ($worksheets as $worksheet) {
// write the "<table:table>" node, with the final sheet's name
fwrite($contentXmlHandle, $worksheet->getTableElementStartAsString());
$worksheetFilePath = $worksheet->getWorksheetFilePath();
$this->copyFileContentsToTarget($worksheetFilePath, $contentXmlHandle);
fwrite($contentXmlHandle, '</table:table>');
}
$contentXmlFileContents = '</office:spreadsheet></office:body></office:document-content>';
fwrite($contentXmlHandle, $contentXmlFileContents);
fclose($contentXmlHandle);
return $this;
}
/**
* Streams the content of the file at the given path into the target resource.
* Depending on which mode the target resource was created with, it will truncate then copy
* or append the content to the target file.
*
* @param string $sourceFilePath Path of the file whose content will be copied
* @param resource $targetResource Target resource that will receive the content
* @return void
*/
protected function copyFileContentsToTarget($sourceFilePath, $targetResource)
{
$sourceHandle = fopen($sourceFilePath, 'r');
stream_copy_to_stream($sourceHandle, $targetResource);
fclose($sourceHandle);
}
/**
* Deletes the temporary folder where sheets content was stored.
*
* @return FileSystemHelper
*/
public function deleteWorksheetTempFolder()
{
$this->deleteFolderRecursively($this->sheetsContentTempFolder);
return $this;
}
/**
* Creates the "styles.xml" file under the root folder
*
* @param StyleHelper $styleHelper
* @param int $numWorksheets Number of created worksheets
* @return FileSystemHelper
*/
public function createStylesFile($styleHelper, $numWorksheets)
{
$stylesXmlFileContents = $styleHelper->getStylesXMLFileContent($numWorksheets);
$this->createFileWithContents($this->rootFolder, self::STYLES_XML_FILE_NAME, $stylesXmlFileContents);
return $this;
}
/**
* Zips the root folder and streams the contents of the zip into the given stream
*
* @param resource $streamPointer Pointer to the stream to copy the zip
* @return void
*/
public function zipRootFolderAndCopyToStream($streamPointer)
{
$zipHelper = new ZipHelper($this->rootFolder);
// In order to have the file's mime type detected properly, files need to be added
// to the zip file in a particular order.
// @see http://www.jejik.com/articles/2010/03/how_to_correctly_create_odf_documents_using_zip/
$zipHelper->addUncompressedFileToArchive($this->rootFolder, self::MIMETYPE_FILE_NAME);
$zipHelper->addFolderToArchive($this->rootFolder, ZipHelper::EXISTING_FILES_SKIP);
$zipHelper->closeArchiveAndCopyToStream($streamPointer);
// once the zip is copied, remove it
$this->deleteFile($zipHelper->getZipFilePath());
}
}

View File

@@ -0,0 +1,356 @@
<?php
namespace Box\Spout\Writer\ODS\Helper;
use Box\Spout\Writer\Common\Helper\AbstractStyleHelper;
use Box\Spout\Writer\Style\BorderPart;
/**
* Class StyleHelper
* This class provides helper functions to manage styles
*
* @package Box\Spout\Writer\ODS\Helper
*/
class StyleHelper extends AbstractStyleHelper
{
/** @var string[] [FONT_NAME] => [] Map whose keys contain all the fonts used */
protected $usedFontsSet = [];
/**
* Registers the given style as a used style.
* Duplicate styles won't be registered more than once.
*
* @param \Box\Spout\Writer\Style\Style $style The style to be registered
* @return \Box\Spout\Writer\Style\Style The registered style, updated with an internal ID.
*/
public function registerStyle($style)
{
$this->usedFontsSet[$style->getFontName()] = true;
return parent::registerStyle($style);
}
/**
* @return string[] List of used fonts name
*/
protected function getUsedFonts()
{
return array_keys($this->usedFontsSet);
}
/**
* Returns the content of the "styles.xml" file, given a list of styles.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
public function getStylesXMLFileContent($numWorksheets)
{
$content = <<<EOD
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<office:document-styles office:version="1.2" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" xmlns:msoxl="http://schemas.microsoft.com/office/excel/formula" xmlns:number="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0" xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:xlink="http://www.w3.org/1999/xlink">
EOD;
$content .= $this->getFontFaceSectionContent();
$content .= $this->getStylesSectionContent();
$content .= $this->getAutomaticStylesSectionContent($numWorksheets);
$content .= $this->getMasterStylesSectionContent($numWorksheets);
$content .= <<<EOD
</office:document-styles>
EOD;
return $content;
}
/**
* Returns the content of the "<office:font-face-decls>" section, inside "styles.xml" file.
*
* @return string
*/
protected function getFontFaceSectionContent()
{
$content = '<office:font-face-decls>';
foreach ($this->getUsedFonts() as $fontName) {
$content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>';
}
$content .= '</office:font-face-decls>';
return $content;
}
/**
* Returns the content of the "<office:styles>" section, inside "styles.xml" file.
*
* @return string
*/
protected function getStylesSectionContent()
{
$defaultStyle = $this->getDefaultStyle();
return <<<EOD
<office:styles>
<number:number-style style:name="N0">
<number:number number:min-integer-digits="1"/>
</number:number-style>
<style:style style:data-style-name="N0" style:family="table-cell" style:name="Default">
<style:table-cell-properties fo:background-color="transparent" style:vertical-align="automatic"/>
<style:text-properties fo:color="#{$defaultStyle->getFontColor()}"
fo:font-size="{$defaultStyle->getFontSize()}pt" style:font-size-asian="{$defaultStyle->getFontSize()}pt" style:font-size-complex="{$defaultStyle->getFontSize()}pt"
style:font-name="{$defaultStyle->getFontName()}" style:font-name-asian="{$defaultStyle->getFontName()}" style:font-name-complex="{$defaultStyle->getFontName()}"/>
</style:style>
</office:styles>
EOD;
}
/**
* Returns the content of the "<office:automatic-styles>" section, inside "styles.xml" file.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
protected function getAutomaticStylesSectionContent($numWorksheets)
{
$content = '<office:automatic-styles>';
for ($i = 1; $i <= $numWorksheets; $i++) {
$content .= <<<EOD
<style:page-layout style:name="pm$i">
<style:page-layout-properties style:first-page-number="continue" style:print="objects charts drawings" style:table-centering="none"/>
<style:header-style/>
<style:footer-style/>
</style:page-layout>
EOD;
}
$content .= '</office:automatic-styles>';
return $content;
}
/**
* Returns the content of the "<office:master-styles>" section, inside "styles.xml" file.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
protected function getMasterStylesSectionContent($numWorksheets)
{
$content = '<office:master-styles>';
for ($i = 1; $i <= $numWorksheets; $i++) {
$content .= <<<EOD
<style:master-page style:name="mp$i" style:page-layout-name="pm$i">
<style:header/>
<style:header-left style:display="false"/>
<style:footer/>
<style:footer-left style:display="false"/>
</style:master-page>
EOD;
}
$content .= '</office:master-styles>';
return $content;
}
/**
* Returns the contents of the "<office:font-face-decls>" section, inside "content.xml" file.
*
* @return string
*/
public function getContentXmlFontFaceSectionContent()
{
$content = '<office:font-face-decls>';
foreach ($this->getUsedFonts() as $fontName) {
$content .= '<style:font-face style:name="' . $fontName . '" svg:font-family="' . $fontName . '"/>';
}
$content .= '</office:font-face-decls>';
return $content;
}
/**
* Returns the contents of the "<office:automatic-styles>" section, inside "content.xml" file.
*
* @param int $numWorksheets Number of worksheets created
* @return string
*/
public function getContentXmlAutomaticStylesSectionContent($numWorksheets)
{
$content = '<office:automatic-styles>';
foreach ($this->getRegisteredStyles() as $style) {
$content .= $this->getStyleSectionContent($style);
}
$content .= <<<EOD
<style:style style:family="table-column" style:name="co1">
<style:table-column-properties fo:break-before="auto"/>
</style:style>
<style:style style:family="table-row" style:name="ro1">
<style:table-row-properties fo:break-before="auto" style:row-height="15pt" style:use-optimal-row-height="true"/>
</style:style>
EOD;
for ($i = 1; $i <= $numWorksheets; $i++) {
$content .= <<<EOD
<style:style style:family="table" style:master-page-name="mp$i" style:name="ta$i">
<style:table-properties style:writing-mode="lr-tb" table:display="true"/>
</style:style>
EOD;
}
$content .= '</office:automatic-styles>';
return $content;
}
/**
* Returns the contents of the "<style:style>" section, inside "<office:automatic-styles>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
protected function getStyleSectionContent($style)
{
$styleIndex = $style->getId() + 1; // 1-based
$content = '<style:style style:data-style-name="N0" style:family="table-cell" style:name="ce' . $styleIndex . '" style:parent-style-name="Default">';
$content .= $this->getTextPropertiesSectionContent($style);
$content .= $this->getTableCellPropertiesSectionContent($style);
$content .= '</style:style>';
return $content;
}
/**
* Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
private function getTextPropertiesSectionContent($style)
{
$content = '';
if ($style->shouldApplyFont()) {
$content .= $this->getFontSectionContent($style);
}
return $content;
}
/**
* Returns the contents of the "<style:text-properties>" section, inside "<style:style>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
private function getFontSectionContent($style)
{
$defaultStyle = $this->getDefaultStyle();
$content = '<style:text-properties';
$fontColor = $style->getFontColor();
if ($fontColor !== $defaultStyle->getFontColor()) {
$content .= ' fo:color="#' . $fontColor . '"';
}
$fontName = $style->getFontName();
if ($fontName !== $defaultStyle->getFontName()) {
$content .= ' style:font-name="' . $fontName . '" style:font-name-asian="' . $fontName . '" style:font-name-complex="' . $fontName . '"';
}
$fontSize = $style->getFontSize();
if ($fontSize !== $defaultStyle->getFontSize()) {
$content .= ' fo:font-size="' . $fontSize . 'pt" style:font-size-asian="' . $fontSize . 'pt" style:font-size-complex="' . $fontSize . 'pt"';
}
if ($style->isFontBold()) {
$content .= ' fo:font-weight="bold" style:font-weight-asian="bold" style:font-weight-complex="bold"';
}
if ($style->isFontItalic()) {
$content .= ' fo:font-style="italic" style:font-style-asian="italic" style:font-style-complex="italic"';
}
if ($style->isFontUnderline()) {
$content .= ' style:text-underline-style="solid" style:text-underline-type="single"';
}
if ($style->isFontStrikethrough()) {
$content .= ' style:text-line-through-style="solid"';
}
$content .= '/>';
return $content;
}
/**
* Returns the contents of the "<style:table-cell-properties>" section, inside "<style:style>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
private function getTableCellPropertiesSectionContent($style)
{
$content = '';
if ($style->shouldWrapText()) {
$content .= $this->getWrapTextXMLContent();
}
if ($style->shouldApplyBorder()) {
$content .= $this->getBorderXMLContent($style);
}
if ($style->shouldApplyBackgroundColor()) {
$content .= $this->getBackgroundColorXMLContent($style);
}
return $content;
}
/**
* Returns the contents of the wrap text definition for the "<style:table-cell-properties>" section
*
* @return string
*/
private function getWrapTextXMLContent()
{
return '<style:table-cell-properties fo:wrap-option="wrap" style:vertical-align="automatic"/>';
}
/**
* Returns the contents of the borders definition for the "<style:table-cell-properties>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
private function getBorderXMLContent($style)
{
$borderProperty = '<style:table-cell-properties %s />';
$borders = array_map(function (BorderPart $borderPart) {
return BorderHelper::serializeBorderPart($borderPart);
}, $style->getBorder()->getParts());
return sprintf($borderProperty, implode(' ', $borders));
}
/**
* Returns the contents of the background color definition for the "<style:table-cell-properties>" section
*
* @param \Box\Spout\Writer\Style\Style $style
* @return string
*/
private function getBackgroundColorXMLContent($style)
{
return sprintf(
'<style:table-cell-properties fo:background-color="#%s"/>',
$style->getBackgroundColor()
);
}
}