diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php
index e5cb5b25f0..f5b4b84bb7 100644
--- a/src/PhpWord/Shared/Converter.php
+++ b/src/PhpWord/Shared/Converter.php
@@ -17,6 +17,8 @@
namespace PhpOffice\PhpWord\Shared;
+use PhpOffice\PhpWord\Exception\InvalidStyleException;
+
/**
* Common converter functions
*/
@@ -227,6 +229,121 @@ public static function emuToPixel($emu = 1)
return round($emu / self::PIXEL_TO_EMU);
}
+ /**
+ * Convert an absolute CSS measurement to pixels
+ *
+ * Units for absolute CSS measurements are cm, mm, in, px, pt and pc
+ *
+ * Note that the result will be rounded to the nearest pixel
+ *
+ * @param string $cssMeasurement If no measurement unit is included then cm
+ * is assumed
+ *
+ * @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
+ *
+ * @return float
+ */
+ public static function cssToPixel($cssMeasurement = '1cm')
+ {
+ $units = trim(preg_replace('/^-?(?:\\d+\\.\\d+|\\.?\\d+)/', '', trim($cssMeasurement)));
+ $value = preg_replace('/\\D+$/', '', trim($cssMeasurement));
+ if ((strlen($value) > 0) && ($value[0] == '.')) {
+ $value = '0' . $value;
+ }
+ switch (strtolower($units)) {
+ case 'in':
+ $pixel = $value * static::INCH_TO_PIXEL;
+ break;
+ case 'cm':
+ case '':
+ $pixel = ($value / static::INCH_TO_CM) * static::INCH_TO_PIXEL;
+ break;
+ case 'mm':
+ $pixel = ($value / (10 * static::INCH_TO_CM)) * static::INCH_TO_PIXEL;
+ break;
+ case 'pt':
+ $pixel = ($value / static::INCH_TO_POINT) * static::INCH_TO_PIXEL;
+ break;
+ case 'pc':
+ $pixel = ($value / (12 * static::INCH_TO_POINT)) * static::INCH_TO_PIXEL;
+ break;
+ case 'px':
+ $pixel = floatval($value);
+ break;
+ default:
+ throw new InvalidStyleException($cssMeasurement . ' is an unsupported CSS measurement');
+ }
+ return $pixel;
+ }
+
+ /**
+ * Convert an absolute CSS measurement to EMU
+ *
+ * Units for absolute CSS measurements are cm, mm, in, px, pt and pc
+ *
+ * @param string $cssMeasurement If no measurement unit is included then cm
+ * is assumed
+ *
+ * @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
+ *
+ * @return float
+ */
+ public static function cssToEmu($cssMeasurement = '1cm')
+ {
+ return static::cssToPixel($cssMeasurement) * static::PIXEL_TO_EMU;
+ }
+
+ /**
+ * Convert an absolute CSS measurement to point
+ *
+ * Units for absolute CSS measurements are cm, mm, in, px, pt and pc
+ *
+ * @param string $cssMeasurement If no measurement unit is included then cm
+ * is assumed
+ *
+ * @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
+ *
+ * @return float
+ */
+ public static function cssToPoint($cssMeasurement = '1cm')
+ {
+ return static::cssToPixel($cssMeasurement) * static::INCH_TO_POINT / static::INCH_TO_PIXEL;
+ }
+
+ /**
+ * Convert an absolute CSS measurement to twip
+ *
+ * Units for absolute CSS measurements are cm, mm, in, px, pt and pc
+ *
+ * @param string $cssMeasurement If no measurement unit is included then cm
+ * is assumed
+ *
+ * @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
+ *
+ * @return float
+ */
+ public static function cssToTwip($cssMeasurement = '1cm')
+ {
+ return static::cssToPixel($cssMeasurement) * static::INCH_TO_TWIP / static::INCH_TO_PIXEL;
+ }
+
+ /**
+ * Convert an absolute CSS measurement to centimeter
+ *
+ * Units for absolute CSS measurements are cm, mm, in, px, pt and pc
+ *
+ * @param string $cssMeasurement If no measurement unit is included then cm
+ * is assumed
+ *
+ * @throws \PHPOffice\PhpWord\Exception\InvalidStyleException
+ *
+ * @return float
+ */
+ public static function cssToCm($cssMeasurement = '1cm')
+ {
+ return static::cssToPixel($cssMeasurement) * static::INCH_TO_CM / static::INCH_TO_PIXEL;
+ }
+
/**
* Convert degree to angle
*
diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php
index e7a8d039c4..cbc238ceb3 100644
--- a/src/PhpWord/TemplateProcessor.php
+++ b/src/PhpWord/TemplateProcessor.php
@@ -22,6 +22,7 @@
use PhpOffice\PhpWord\Exception\CopyFileException;
use PhpOffice\PhpWord\Exception\CreateTemporaryFileException;
use PhpOffice\PhpWord\Exception\Exception;
+use PhpOffice\PhpWord\Shared\Converter;
use PhpOffice\PhpWord\Shared\ZipArchive;
use Zend\Stdlib\StringUtils;
@@ -29,6 +30,32 @@ class TemplateProcessor
{
const MAXIMUM_REPLACEMENTS_DEFAULT = -1;
+ /**
+ * sprintf Template for a table
+ *
+ * sprintf arguments:
+ * 1: s, gridCol definition elements
+ * 2: s, table cell elements
+ */
+ const TABLE_TEMPLATE = '%s%s';
+
+ /**
+ * sprintf Template for a gridCol element in a table definition
+ *
+ * sprintf arguments:
+ * 1: d, width of the column in Twips
+ */
+ const TABLEGRIDCOL_TEMPLATE = '';
+
+ /**
+ * sprintf Template for a table cell element in a table definition
+ *
+ * sprintf arguments:
+ * 1: d, width of the cell in Twips
+ * 2: s, cell text content
+ */
+ const TABLECELL_TEMPLATE = '%s';
+
/**
* ZipArchive object.
*
@@ -147,7 +174,7 @@ protected function transformXml($xml, $xsltProcessor)
/**
* Applies XSL style sheet to template's parts.
- *
+ *
* Note: since the method doesn't make any guess on logic of the provided XSL style sheet,
* make sure that output is correctly escaped. Otherwise you may get broken document.
*
@@ -256,6 +283,44 @@ public function getVariables()
return array_unique($variables);
}
+ /**
+ * Insert a table at the place marked by the block template
+ * @param string $name Name of block template
+ * @param array $columns Array keyed on column (template) name, containing
+ * column width in CSS units (string) or twips (integer)
+ *
+ * @return \PhpOffice\PhpWord\TemplateProcessor
+ */
+ public function insertTable($name, array $columns)
+ {
+ $gridCols = '';
+ $cells = '';
+
+ foreach ($columns as $variable => $width) {
+ $twipWidth = is_string($width) ? Converter::cssToTwip($width) : $width;
+ $gridCols .= sprintf(static::TABLEGRIDCOL_TEMPLATE, $twipWidth);
+ $cells .= sprintf(static::TABLECELL_TEMPLATE, $twipWidth, static::ensureMacroCompleted($variable));
+ }
+
+ return $this->replaceXmlBlock($name, sprintf(static::TABLE_TEMPLATE, $gridCols, $cells));
+ }
+
+ /**
+ * Delete a table containing the given variable
+ *
+ * @param string $search
+ *
+ * @return \PhpOffice\PhpWord\TemplateProcessor
+ */
+ public function deleteTable($search)
+ {
+ $block = $this->findContainingXmlBlockForMacro($search, 'w:tbl');
+
+ $this->tempDocumentMainPart = $this->getSlice(0, $block['start']) . $this->getSlice($block['end']);
+
+ return $this;
+ }
+
/**
* Clone a table row in a template document.
*
@@ -315,6 +380,22 @@ public function cloneRow($search, $numberOfClones)
$this->tempDocumentMainPart = $result;
}
+ /**
+ * Delete a row containing the given variable
+ *
+ * @param string $search
+ *
+ * @return \PhpOffice\PhpWord\Template
+ */
+ public function deleteRow($search)
+ {
+ $block = $this->findContainingXmlBlockForMacro($search, 'w:tr');
+
+ $this->tempDocumentMainPart= $this->getSlice(0, $block['start']) . $this->getSlice($block['end']);
+
+ return $this;
+ }
+
/**
* Clone a block.
*
@@ -584,4 +665,138 @@ protected function getSlice($startPosition, $endPosition = 0)
return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition));
}
+
+ /**
+ * Replace an XML block surrounding a macro with a new block
+ *
+ * @param string $macro Name of macro
+ * @param string $block New block content
+ * @param string $blockType XML tag type of block
+ * @return \PhpOffice\PhpWord\TemplateProcessor Fluent interface
+ */
+ protected function replaceXmlBlock($macro, $block, $blockType = 'w:p')
+ {
+ $where = $this->findContainingXmlBlockForMacro($macro, $blockType);
+ if (false !== $where) {
+ $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']);
+ }
+ return $this;
+ }
+
+ /**
+ * Find start and end of XML block containing the given macro
+ * e.g. ...${macro}...
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $macro Name of macro
+ * @param string $blockType XML tag for block
+ * @return boolean|integer[] FALSE if not found, otherwise array with start and end
+ */
+ protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p')
+ {
+ $macroPos = $this->findMacro($macro);
+ if (false === $macroPos) {
+ return false;
+ }
+
+ $start = $this->findXmlBlockStart($macroPos, $blockType);
+ if (0 > $start) {
+ return false;
+ }
+
+ $end = $this->findXmlBlockEnd($macroPos, $blockType);
+ if (0 > $end) {
+ return false;
+ }
+
+ return array('start' => $start, 'end' => $end);
+ }
+
+ /**
+ * Find start and end of XML block containing the given block macro
+ * e.g. ...${macro}...${/macro}...
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $macro Name of macro
+ * @param string $blockType XML tag for block
+ * @return boolean|integer[] FALSE if not found, otherwise array with start and end
+ */
+ protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p')
+ {
+ $macroStartPos = $this->findMacro($macro);
+ if (0 > $macroStartPos) {
+ return false;
+ }
+
+ $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos);
+ if (0 > $macroEndPos) {
+ return false;
+ }
+
+ $start = $this->findXmlBlockStart($macroStartPos, $blockType);
+ if (0 > $start) {
+ return false;
+ }
+
+ $end = $this->findXmlBlockEnd($macroEndPos, $blockType);
+ if (0 > $end) {
+ return false;
+ }
+
+ return array('start' => $start, 'end' => $end);
+ }
+
+ /**
+ * Find the position of (the start of) a macro
+ *
+ * Returns -1 if not found, otherwise position of opening $
+ *
+ * Note that only the first instance of the macro will be found
+ *
+ * @param string $search Macro name
+ * @param string $offset Offset from which to start searching
+ * @return integer -1 if macro not found
+ */
+ protected function findMacro($search, $offset = 0)
+ {
+ $search = static::ensureMacroCompleted($search);
+
+ $pos = strpos($this->tempDocumentMainPart, $search, $offset);
+
+ return ($pos === false) ? -1 : $pos;
+ }
+
+ /**
+ * Find the start position of the nearest XML block start before $offset
+ *
+ * @param integer $offset Search position
+ * @param string $blockType XML Block tag
+ * @return integer -1 if block start not found
+ */
+ protected function findXmlBlockStart($offset, $blockType)
+ {
+ // first try XML tag with attributes
+ $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
+ if (false === $blockStart) {
+ // also try XML tag without attributes
+ $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1));
+ }
+ return ($blockStart === false) ? -1 : $blockStart;
+ }
+
+ /**
+ * Find the nearest block end position after $offset
+ *
+ * @param integer $offset Search position
+ * @param string $blockType XML Block tag
+ * @return integer -1 if block end not found
+ */
+ protected function findXmlBlockEnd($offset, $blockType)
+ {
+ $blockEndStart = strpos($this->tempDocumentMainPart, '' . $blockType . '>', $offset);
+ // return position of end of tag if found, otherwise -1
+ return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType);
+ }
}
diff --git a/tests/PhpWord/Shared/ConverterTest.php b/tests/PhpWord/Shared/ConverterTest.php
index e307f09b1e..8ed251d666 100644
--- a/tests/PhpWord/Shared/ConverterTest.php
+++ b/tests/PhpWord/Shared/ConverterTest.php
@@ -112,4 +112,84 @@ public function testHtmlToRGB()
$this->assertEquals($value[1], $result);
}
}
+
+ /**
+ * @covers ::cssToPixel
+ * @test
+ */
+ public function testCssToPixel()
+ {
+ // Prepare test values [ original, expected ]
+ $values[] = array('1pt', (1 / 72) * 96);
+ $values[] = array('10.5pc', (10.5 / (72 * 12)) * 96);
+ $values[] = array('4cm', (4 / 2.54) * 96);
+ $values[] = array('1', (1 / 2.54) * 96);
+ $values[] = array('2mm', (0.2 / 2.54) * 96);
+ $values[] = array('.1', (0.1 / 2.54) * 96);
+ $values[] = array('2in', 2 * 96);
+ foreach ($values as $value) {
+ $result = Converter::cssToPixel($value[0]);
+ $this->assertEquals($value[1], $result);
+ }
+ }
+
+ /**
+ * @covers ::cssToEmu
+ * @test
+ */
+ public function testCssToEmu()
+ {
+ // Prepare test values [ original, expected ]
+ $values[] = array('1pt', (1 / 72) * 96 * 9525);
+ $values[] = array('10.5pc', (10.5 / (72 * 12)) * 96 * 9525);
+ $values[] = array('4cm', (4 / 2.54) * 96 * 9525);
+ $values[] = array('1', (1 / 2.54) * 96 * 9525);
+ $values[] = array('2mm', (0.2 / 2.54) * 96 * 9525);
+ $values[] = array('.1', (0.1 / 2.54) * 96 * 9525);
+ $values[] = array('2in', 2 * 96 * 9525);
+ foreach ($values as $value) {
+ $result = Converter::cssToEmu($value[0]);
+ $this->assertEquals($value[1], $result);
+ }
+ }
+
+ /**
+ * @covers ::cssToCm
+ * @test
+ */
+ public function testCssToCm()
+ {
+ // Prepare test values [ original, expected ]
+ $values[] = array('1pt', (1 / 72) * 2.54);
+ $values[] = array('10.5pc', (10.5 / (72 * 12)) * 2.54);
+ $values[] = array('4cm', 4);
+ $values[] = array('1', 1);
+ $values[] = array('2mm', 2 / 10);
+ $values[] = array('.1', 1 / 10);
+ $values[] = array('2in', 2 * 2.54);
+ foreach ($values as $value) {
+ $result = Converter::cssToCm($value[0]);
+ $this->assertEquals($value[1], $result);
+ }
+ }
+
+ /**
+ * @covers ::cssToTwip
+ * @test
+ */
+ public function testCssToTwip()
+ {
+ // Prepare test values [ original, expected ]
+ $values[] = array('1pt', (1 / 72) * 1440);
+ $values[] = array('10.5pc', (10.5 / (72 * 12)) * 1440);
+ $values[] = array('4cm', (4 / 2.54) * 1440);
+ $values[] = array('1', (1 / 2.54) * 1440);
+ $values[] = array('2mm', (0.2 / 2.54) * 1440);
+ $values[] = array('.1', (0.1 / 2.54) * 1440);
+ $values[] = array('2in', 2 * 1440);
+ foreach ($values as $value) {
+ $result = Converter::cssToTwip($value[0]);
+ $this->assertEquals($value[1], $result);
+ }
+ }
}
diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php
index 11b43cf454..ac81573d2f 100644
--- a/tests/PhpWord/TemplateProcessorTest.php
+++ b/tests/PhpWord/TemplateProcessorTest.php
@@ -17,6 +17,8 @@
namespace PhpOffice\PhpWord;
+use PhpOffice\PhpWord\Shared\ZipArchive;
+
/**
* @covers \PhpOffice\PhpWord\TemplateProcessor
* @coversDefaultClass \PhpOffice\PhpWord\TemplateProcessor
@@ -223,4 +225,106 @@ public function testCloneDeleteBlock()
unlink($docName);
$this->assertTrue($docFound);
}
+
+ /**
+ * @covers ::insertTable
+ * @covers ::saveAs
+ * @test
+ */
+ public function testInsertTable()
+ {
+ $docName = 'table-insert.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('myTable', 'otherContent'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->insertTable('myTable', array('myCol1#' => 6000, 'myCol2#' => '1in', 'myCol3#' => '72pt'));
+ $this->assertEquals(
+ array('myCol1#', 'myCol2#', 'myCol3#', 'otherContent'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ $this->assertTrue(false !== strpos($mainDoc, ''));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol1#}'));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol2#}'));
+ $this->assertTrue(false !== strpos($mainDoc, '${myCol3#}'));
+ }
+
+ /**
+ * @covers ::deleteTable
+ * @covers ::saveAs
+ * @test
+ */
+ public function testDeleteTable()
+ {
+ $docName = 'table-delete.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('KEEP_THIS_TABLE', 'DELETE_THIS_TABLE'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->deleteTable('DELETE_THIS_TABLE');
+ $this->assertEquals(
+ array('KEEP_THIS_TABLE'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ // preg_match_all requires >= 3 parameters in PHP5.3
+ $dummy = null;
+ $numMatches = preg_match_all('//u', $mainDoc, $dummy);
+ $this->assertTrue($numMatches === 1);
+ }
+
+ /**
+ * @covers ::deleteRow
+ * @covers ::saveAs
+ * @test
+ */
+ public function testDeleteRow()
+ {
+ $docName = 'table-delete-row.docx';
+ $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/' . $docName);
+
+ $this->assertEquals(
+ array('KEEP_THE_HEADER', 'DELETE_THIS_ROW', 'RETAIN_THIS_ROW'),
+ $templateProcessor->getVariables()
+ );
+
+ $templateProcessor->deleteRow('DELETE_THIS_ROW');
+ $this->assertEquals(
+ array('KEEP_THE_HEADER', 'RETAIN_THIS_ROW'),
+ $templateProcessor->getVariables()
+ );
+ $templateProcessor->saveAs($docName);
+ $docFound = file_exists($docName);
+ $zip = new ZipArchive();
+ $zip->open($docName);
+ $mainDoc = $zip->getFromName('word/document.xml');
+ $zip->close();
+ unlink($docName);
+ $this->assertTrue($docFound);
+ // preg_match_all requires >= 3 parameters in PHP5.3
+ $dummy = null;
+ $numMatches = preg_match_all('/assertTrue($numMatches === 2);
+ }
}
diff --git a/tests/PhpWord/_files/templates/table-delete-row.docx b/tests/PhpWord/_files/templates/table-delete-row.docx
new file mode 100644
index 0000000000..738d044d67
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-delete-row.docx differ
diff --git a/tests/PhpWord/_files/templates/table-delete.docx b/tests/PhpWord/_files/templates/table-delete.docx
new file mode 100644
index 0000000000..203a214e53
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-delete.docx differ
diff --git a/tests/PhpWord/_files/templates/table-insert.docx b/tests/PhpWord/_files/templates/table-insert.docx
new file mode 100644
index 0000000000..82fd58c885
Binary files /dev/null and b/tests/PhpWord/_files/templates/table-insert.docx differ