Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?phpdeclare(strict_types=1);namespace OpenSpout\Writer\ODS\Manager;use DateTimeImmutable;use DateTimeInterface;use OpenSpout\Common\Entity\Cell;use OpenSpout\Common\Entity\Row;use OpenSpout\Common\Entity\Style\Style;use OpenSpout\Common\Exception\InvalidArgumentException;use OpenSpout\Common\Exception\IOException;use OpenSpout\Common\Helper\Escaper\ODS as ODSEscaper;use OpenSpout\Writer\Common\Entity\Worksheet;use OpenSpout\Writer\Common\Helper\CellHelper;use OpenSpout\Writer\Common\Manager\RegisteredStyle;use OpenSpout\Writer\Common\Manager\Style\StyleMerger;use OpenSpout\Writer\Common\Manager\WorksheetManagerInterface;use OpenSpout\Writer\ODS\Manager\Style\StyleManager;/*** @internal*/final class WorksheetManager implements WorksheetManagerInterface{/** @var ODSEscaper Strings escaper */private readonly ODSEscaper $stringsEscaper;/** @var StyleManager Manages styles */private readonly StyleManager $styleManager;/** @var StyleMerger Helper to merge styles together */private readonly StyleMerger $styleMerger;/*** WorksheetManager constructor.*/public function __construct(StyleManager $styleManager,StyleMerger $styleMerger,ODSEscaper $stringsEscaper) {$this->styleManager = $styleManager;$this->styleMerger = $styleMerger;$this->stringsEscaper = $stringsEscaper;}/*** Prepares the worksheet to accept data.** @param Worksheet $worksheet The worksheet to start** @throws IOException If the sheet data file cannot be opened for writing*/public function startSheet(Worksheet $worksheet): void{$sheetFilePointer = fopen($worksheet->getFilePath(), 'w');\assert(false !== $sheetFilePointer);$worksheet->setFilePointer($sheetFilePointer);}/*** Returns the table XML root node as string.** @return string "<table>" node as string*/public function getTableElementStartAsString(Worksheet $worksheet): string{$externalSheet = $worksheet->getExternalSheet();$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());$tableStyleName = 'ta'.($externalSheet->getIndex() + 1);$tableElement = '<table:table table:style-name="'.$tableStyleName.'" table:name="'.$escapedSheetName.'">';$tableElement .= $this->styleManager->getStyledTableColumnXMLContent($worksheet->getMaxNumColumns());return $tableElement;}/*** Returns the table:database-range XML node for AutoFilter as string.*/public function getTableDatabaseRangeElementAsString(Worksheet $worksheet): string{$externalSheet = $worksheet->getExternalSheet();$escapedSheetName = $this->stringsEscaper->escape($externalSheet->getName());$databaseRange = '';if (null !== $autofilter = $externalSheet->getAutoFilter()) {$rangeAddress = sprintf('\'%s\'.%s%s:\'%s\'.%s%s',$escapedSheetName,CellHelper::getColumnLettersFromColumnIndex($autofilter->fromColumnIndex),$autofilter->fromRow,$escapedSheetName,CellHelper::getColumnLettersFromColumnIndex($autofilter->toColumnIndex),$autofilter->toRow);$databaseRange = '<table:database-range table:name="__Anonymous_Sheet_DB__'.$externalSheet->getIndex().'" table:target-range-address="'.$rangeAddress.'" table:display-filter-buttons="true"/>';}return $databaseRange;}/*** Adds a row to the given worksheet.** @param Worksheet $worksheet The worksheet to add the row to* @param Row $row The row to be added** @throws InvalidArgumentException If a cell value's type is not supported* @throws IOException If the data cannot be written*/public function addRow(Worksheet $worksheet, Row $row): void{$cells = $row->getCells();$rowStyle = $row->getStyle();$data = '<table:table-row table:style-name="ro1">';$currentCellIndex = 0;$nextCellIndex = 1;for ($i = 0; $i < $row->getNumCells(); ++$i) {/** @var Cell $cell */$cell = $cells[$currentCellIndex];/** @var null|Cell $nextCell */$nextCell = $cells[$nextCellIndex] ?? null;if (null === $nextCell || $cell->getValue() !== $nextCell->getValue()) {$registeredStyle = $this->applyStyleAndRegister($cell, $rowStyle);$cellStyle = $registeredStyle->getStyle();if ($registeredStyle->isMatchingRowStyle()) {$rowStyle = $cellStyle; // Replace actual rowStyle (possibly with null id) by registered style (with id)}$data .= $this->getCellXMLWithStyle($cell, $cellStyle, $currentCellIndex, $nextCellIndex);$currentCellIndex = $nextCellIndex;}++$nextCellIndex;}$data .= '</table:table-row>';$wasWriteSuccessful = fwrite($worksheet->getFilePointer(), $data);if (false === $wasWriteSuccessful) {throw new IOException("Unable to write data in {$worksheet->getFilePath()}");}// only update the count if the write worked$lastWrittenRowIndex = $worksheet->getLastWrittenRowIndex();$worksheet->setLastWrittenRowIndex($lastWrittenRowIndex + 1);}/*** Closes the worksheet.*/public function close(Worksheet $worksheet): void{fclose($worksheet->getFilePointer());}/*** Applies styles to the given style, merging the cell's style with its row's style.** @throws InvalidArgumentException If a cell value's type is not supported*/private function applyStyleAndRegister(Cell $cell, Style $rowStyle): RegisteredStyle{$isMatchingRowStyle = false;if ($cell->getStyle()->isEmpty()) {$cell->setStyle($rowStyle);$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);if ($possiblyUpdatedStyle->isUpdated()) {$registeredStyle = $this->styleManager->registerStyle($possiblyUpdatedStyle->getStyle());} else {$registeredStyle = $this->styleManager->registerStyle($rowStyle);$isMatchingRowStyle = true;}} else {$mergedCellAndRowStyle = $this->styleMerger->merge($cell->getStyle(), $rowStyle);$cell->setStyle($mergedCellAndRowStyle);$possiblyUpdatedStyle = $this->styleManager->applyExtraStylesIfNeeded($cell);if ($possiblyUpdatedStyle->isUpdated()) {$newCellStyle = $possiblyUpdatedStyle->getStyle();} else {$newCellStyle = $mergedCellAndRowStyle;}$registeredStyle = $this->styleManager->registerStyle($newCellStyle);}return new RegisteredStyle($registeredStyle, $isMatchingRowStyle);}private function getCellXMLWithStyle(Cell $cell, Style $style, int $currentCellIndex, int $nextCellIndex): string{$styleIndex = $style->getId() + 1; // 1-based$numTimesValueRepeated = ($nextCellIndex - $currentCellIndex);return $this->getCellXML($cell, $styleIndex, $numTimesValueRepeated);}/*** Returns the cell XML content, given its value.** @param Cell $cell The cell to be written* @param int $styleIndex Index of the used style* @param int $numTimesValueRepeated Number of times the value is consecutively repeated** @return string The cell XML content** @throws InvalidArgumentException If a cell value's type is not supported*/private function getCellXML(Cell $cell, int $styleIndex, int $numTimesValueRepeated): string{$data = '<table:table-cell table:style-name="ce'.$styleIndex.'"';if (1 !== $numTimesValueRepeated) {$data .= ' table:number-columns-repeated="'.$numTimesValueRepeated.'"';}if ($cell instanceof Cell\StringCell) {$data .= ' office:value-type="string" calcext:value-type="string">';$cellValueLines = explode("\n", $cell->getValue());foreach ($cellValueLines as $cellValueLine) {$data .= '<text:p>'.$this->stringsEscaper->escape($cellValueLine).'</text:p>';}$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\BooleanCell) {$value = $cell->getValue() ? 'true' : 'false'; // boolean-value spec: http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#datatype-boolean$data .= ' office:value-type="boolean" calcext:value-type="boolean" office:boolean-value="'.$value.'">';$data .= '<text:p>'.$cell->getValue().'</text:p>';$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\NumericCell) {$cellValue = $cell->getValue();$data .= ' office:value-type="float" calcext:value-type="float" office:value="'.$cellValue.'">';$data .= '<text:p>'.$cellValue.'</text:p>';$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\DateTimeCell) {$datevalue = substr((new DateTimeImmutable('@'.$cell->getValue()->getTimestamp()))->format(DateTimeInterface::W3C), 0, -6);$data .= ' office:value-type="date" calcext:value-type="date" office:date-value="'.$datevalue.'Z">';$data .= '<text:p>'.$datevalue.'Z</text:p>';$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\DateIntervalCell) {// workaround for missing DateInterval::format('c'), see https://stackoverflow.com/a/61088115/53538static $f = ['M0S', 'H0M', 'DT0H', 'M0D', 'Y0M', 'P0Y', 'Y0M', 'P0M'];static $r = ['M', 'H', 'DT', 'M', 'Y0M', 'P', 'Y', 'P'];$value = rtrim(str_replace($f, $r, $cell->getValue()->format('P%yY%mM%dDT%hH%iM%sS')), 'PT') ?: 'PT0S';$data .= ' office:value-type="time" office:time-value="'.$value.'">';$data .= '<text:p>'.$value.'</text:p>';$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\ErrorCell) {// only writes the error value if it's a string$data .= ' office:value-type="string" calcext:value-type="error" office:value="">';$data .= '<text:p>'.$cell->getRawValue().'</text:p>';$data .= '</table:table-cell>';} elseif ($cell instanceof Cell\EmptyCell) {$data .= '/>';}return $data;}}