diff --git a/CHANGELOG.md b/CHANGELOG.md index 4584c4a4..0ccba8cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ This is the changelog between releases of PHPWord. Releases are listed in revers ## 0.11.0 - Not yet released -This release marked the change of PHPWord license from LGPL 2.1 to LGPL 3. Three new elements were added: TextBox, ListItemRun, and Field. Relative and absolute positioning for images and textboxes were added. Writer classes were refactored into parts, elements, and styles. ODT and RTF features were enhanced. Ability to add elements to PHPWord object via HTML were implemeted. +This release marked the change of PHPWord license from LGPL 2.1 to LGPL 3. Three new elements were added: TextBox, ListItemRun, and Field. Relative and absolute positioning for images and textboxes were added. Writer classes were refactored into parts, elements, and styles. ODT and RTF features were enhanced. Ability to add elements to PHPWord object via HTML were implemeted. RTF reader were initiated. ### Features @@ -30,6 +30,7 @@ This release marked the change of PHPWord license from LGPL 2.1 to LGPL 3. Three - RTF Writer: Ability to write document properties - @ivanlanin - RTF Writer: Ability to write image - @ivanlanin - Element: New `Field` element - @basjan GH-251 +- RTF Reader: Basic RTF reader - @ivanlanin GH-72 ### Bugfixes diff --git a/docs/elements.rst b/docs/elements.rst index 7e0c33f6..901832eb 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -88,7 +88,7 @@ Inline style examples: $textrun = $section->addTextRun(); $textrun->addText('I am bold', array('bold' => true)); $textrun->addText('I am italic', array('italic' => true)); - $textrun->addText('I am colored, array('color' => 'AACC00')); + $textrun->addText('I am colored', array('color' => 'AACC00')); Defined style examples: diff --git a/docs/intro.rst b/docs/intro.rst index a64fb2ad..3da729e8 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -124,7 +124,7 @@ Readers +---------------------------+----------------------+--------+-------+-------+ | | Custom | ✓ | | | +---------------------------+----------------------+--------+-------+-------+ -| **Element Type** | Text | ✓ | ✓ | | +| **Element Type** | Text | ✓ | ✓ | ✓ | +---------------------------+----------------------+--------+-------+-------+ | | Text Run | ✓ | | | +---------------------------+----------------------+--------+-------+-------+ diff --git a/docs/src/documentation.md b/docs/src/documentation.md index 889842f9..0259dd8b 100644 --- a/docs/src/documentation.md +++ b/docs/src/documentation.md @@ -114,7 +114,7 @@ Below are the supported features for each file formats. |-------------------------|--------------------|------|-----|-----| | **Document Properties** | Standard | ✓ | | | | | Custom | ✓ | | | -| **Element Type** | Text | ✓ | ✓ | | +| **Element Type** | Text | ✓ | ✓ | ✓ | | | Text Run | ✓ | | | | | Title | ✓ | ✓ | | | | Link | ✓ | | | @@ -495,7 +495,7 @@ $section->addText('I am simple paragraph', $fontStyle, $paragraphStyle); $textrun = $section->addTextRun(); $textrun->addText('I am bold', array('bold' => true)); $textrun->addText('I am italic', array('italic' => true)); -$textrun->addText('I am colored, array('color' => 'AACC00')); +$textrun->addText('I am colored', array('color' => 'AACC00')); ``` Defined style examples: diff --git a/samples/Sample_11_ReadWord2007.php b/samples/Sample_11_ReadWord2007.php index 09d9cab0..c0b54c7a 100644 --- a/samples/Sample_11_ReadWord2007.php +++ b/samples/Sample_11_ReadWord2007.php @@ -3,7 +3,8 @@ include_once 'Sample_Header.php'; // Read contents $name = basename(__FILE__, '.php'); -$source = "resources/{$name}.docx"; +$source = __DIR__ . "/resources/{$name}.docx"; + echo date('H:i:s'), " Reading contents from `{$source}`", EOL; $phpWord = \PhpOffice\PhpWord\IOFactory::load($source); diff --git a/samples/Sample_24_ReadODText.php b/samples/Sample_24_ReadODText.php index bb5332e6..42df23ae 100644 --- a/samples/Sample_24_ReadODText.php +++ b/samples/Sample_24_ReadODText.php @@ -3,7 +3,8 @@ include_once 'Sample_Header.php'; // Read contents $name = basename(__FILE__, '.php'); -$source = "resources/{$name}.odt"; +$source = __DIR__ . "/resources/{$name}.odt"; + echo date('H:i:s'), " Reading contents from `{$source}`", EOL; $phpWord = \PhpOffice\PhpWord\IOFactory::load($source, 'ODText'); diff --git a/samples/Sample_28_ReadRTF.php b/samples/Sample_28_ReadRTF.php new file mode 100644 index 00000000..76ac3d48 --- /dev/null +++ b/samples/Sample_28_ReadRTF.php @@ -0,0 +1,15 @@ +save("{$filename}.{$extension}"); - rename("{$filename}.{$extension}", "results/{$filename}.{$extension}"); + $xmlWriter->save(__DIR__ . "/{$filename}.{$extension}"); + rename(__DIR__ . "/{$filename}.{$extension}", __DIR__ . "/results/{$filename}.{$extension}"); } else { $result .= ' ... NOT DONE!'; } diff --git a/samples/resources/Sample_28_ReadRTF.rtf b/samples/resources/Sample_28_ReadRTF.rtf new file mode 100644 index 00000000..6f9ac0f8 --- /dev/null +++ b/samples/resources/Sample_28_ReadRTF.rtf @@ -0,0 +1,21 @@ +{\rtf1 +\ansi\ansicpg1252 +\deff0 +{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fnil\fcharset0 Times New Roman;}} +{\colortbl;\red255\green0\blue0;\red14\green0\blue0} +{\*\generator PhpWord;} + +{\info{\title }{\subject }{\category }{\keywords }{\comment }{\author }{\operator }{\creatim \yr2014\mo05\dy27\hr23\min36\sec45}{\revtim \yr2014\mo05\dy27\hr23\min36\sec45}{\company }{\manager }} +\deftab720\viewkind1\uc1\pard\nowidctlpar\lang1036\kerning1\fs20 +{Welcome to PhpWord}\par +\pard\nowidctlpar{\cf0\f0 Hello World!}\par +\par +\par +\pard\nowidctlpar{\cf0\f0\fs32\b\i I am styled by a font style definition.}\par +\pard\nowidctlpar{\cf0\f0 I am styled by a paragraph style definition.}\par +\pard\nowidctlpar\qc\sa100{\cf0\f0\fs32\b\i I am styled by both font and paragraph style.}\par +\pard\nowidctlpar{\cf1\f1\fs40\b\i\ul\strike\super I am inline styled.}\par +\par +{\field {\*\fldinst {HYPERLINK "http://www.google.com"}}{\fldrslt {Google}}}\par +\par +} \ No newline at end of file diff --git a/src/PhpWord/IOFactory.php b/src/PhpWord/IOFactory.php index 83076fcc..0d5fe689 100644 --- a/src/PhpWord/IOFactory.php +++ b/src/PhpWord/IOFactory.php @@ -51,7 +51,7 @@ abstract class IOFactory */ public static function createReader($name = 'Word2007') { - if (!in_array($name, array('ReaderInterface', 'Word2007', 'ODText'))) { + if (!in_array($name, array('ReaderInterface', 'Word2007', 'ODText', 'RTF'))) { throw new Exception("\"{$name}\" is not a valid reader."); } diff --git a/src/PhpWord/Reader/AbstractReader.php b/src/PhpWord/Reader/AbstractReader.php index 3cc2d490..c08914e5 100644 --- a/src/PhpWord/Reader/AbstractReader.php +++ b/src/PhpWord/Reader/AbstractReader.php @@ -39,7 +39,7 @@ abstract class AbstractReader implements ReaderInterface * * @var bool|resource */ - protected $fileHandle = true; + protected $fileHandle; /** * Read data only? diff --git a/src/PhpWord/Reader/RTF.php b/src/PhpWord/Reader/RTF.php new file mode 100644 index 00000000..9d5d813b --- /dev/null +++ b/src/PhpWord/Reader/RTF.php @@ -0,0 +1,51 @@ +canRead($docFile)) { + $doc = new Document(); + $doc->rtf = file_get_contents($docFile); + $doc->read($phpWord); + } else { + throw new \Exception("Cannot read {$docFile}."); + } + + return $phpWord; + } +} diff --git a/src/PhpWord/Reader/RTF/Document.php b/src/PhpWord/Reader/RTF/Document.php new file mode 100644 index 00000000..cb082fdc --- /dev/null +++ b/src/PhpWord/Reader/RTF/Document.php @@ -0,0 +1,393 @@ + 'markOpening', // { + 125 => 'markClosing', // } + 92 => 'markBackslash', // \ + 10 => 'markNewline', // LF + 13 => 'markNewline' // CR + ); + + $this->phpWord = $phpWord; + $this->section = $phpWord->addSection(); + $this->textrun = $this->section->addTextRun(); + $this->length = strlen($this->rtf); + + $this->flags['paragraph'] = true; // Set paragraph flag from the beginning + + // Walk each characters + while ($this->offset < $this->length) { + $char = $this->rtf[$this->offset]; + $ascii = ord($char); + + if (array_key_exists($ascii, $markers)) { // Marker found: {, }, \, LF, or CR + $markerFunction = $markers[$ascii]; + $this->$markerFunction(); + } else { + if ($this->isControl === false) { // Non control word: Push character + $this->pushText($char); + } else { + if (preg_match("/^[a-zA-Z0-9-]?$/", $char)) { // No delimiter: Buffer control + $this->control .= $char; + $this->isFirst = false; + } else { // Delimiter found: Parse buffered control + if ($this->isFirst) { + $this->isFirst = false; + } else { + if ($char == ' ') { // Discard space as a control word delimiter + $this->flushControl(true); + } + } + } + } + } + $this->offset++; + } + $this->flushText(); + } + + /** + * Mark opening braket `{` character + */ + private function markOpening() + { + $this->flush(true); + array_push($this->groups, $this->flags); + } + + /** + * Mark closing braket `}` character + */ + private function markClosing() + { + $this->flush(true); + $this->flags = array_pop($this->groups); + } + + /** + * Mark backslash `\` character + */ + private function markBackslash() + { + if ($this->isFirst) { + $this->setControl(false); + $this->text .= '\\'; + } else { + $this->flush(); + $this->setControl(true); + $this->control = ''; + } + } + + /** + * Mark newline character: Flush control word because it's not possible to span multiline + */ + private function markNewline() + { + if ($this->isControl) { + $this->flushControl(true); + } + } + + /** + * Flush control word or text + * + * @param bool $isControl + */ + private function flush($isControl = false) + { + if ($this->isControl) { + $this->flushControl($isControl); + } else { + $this->flushText(); + } + } + + /** + * Flush control word + * + * @param bool $isControl + */ + private function flushControl($isControl = false) + { + if (preg_match("/^([A-Za-z]+)(-?[0-9]*) ?$/", $this->control, $match) === 1) { + list(, $control, $parameter) = $match; + $this->parseControl($control, $parameter); + } + + if ($isControl === true) { + $this->setControl(false); + } + } + + /** + * Flush text in queue + */ + private function flushText() + { + if ($this->text != '') { + if (isset($this->flags['property'])) { // Set property + $this->flags['value'] = $this->text; + } else { // Set text + if ($this->flags['paragraph'] === true) { + $this->flags['paragraph'] = false; + $this->flags['text'] = $this->text; + } + } + + // Add text if it's not flagged as skipped + if (!isset($this->flags['skipped'])) { + $this->readText(); + } + + $this->text = ''; + } + } + + /** + * Reset control word and first char state + * + * @param bool $value + */ + private function setControl($value) + { + $this->isControl = $value; + $this->isFirst = $value; + } + + /** + * Push text into queue + * + * @param string $char + */ + private function pushText($char) + { + if ($char == '<') { + $this->text .= "<"; + } elseif ($char == '>') { + $this->text .= ">"; + } else { + $this->text .= $char; + } + } + + /** + * Parse control + * + * @param string $control + * @param string $parameter + */ + private function parseControl($control, $parameter) + { + $controls = array( + 'par' => array(self::PARA, 'paragraph', true), + 'b' => array(self::STYL, 'font', 'bold', true), + 'i' => array(self::STYL, 'font', 'italic', true), + 'u' => array(self::STYL, 'font', 'underline', true), + 'strike' => array(self::STYL, 'font', 'strikethrough',true), + 'fs' => array(self::STYL, 'font', 'size', $parameter), + 'qc' => array(self::STYL, 'paragraph', 'align', 'center'), + 'sa' => array(self::STYL, 'paragraph', 'spaceAfter', $parameter), + 'fonttbl' => array(self::SKIP, 'fonttbl', null), + 'colortbl' => array(self::SKIP, 'colortbl', null), + 'info' => array(self::SKIP, 'info', null), + 'generator' => array(self::SKIP, 'generator', null), + 'title' => array(self::SKIP, 'title', null), + 'subject' => array(self::SKIP, 'subject', null), + 'category' => array(self::SKIP, 'category', null), + 'keywords' => array(self::SKIP, 'keywords', null), + 'comment' => array(self::SKIP, 'comment', null), + 'shppict' => array(self::SKIP, 'pic', null), + 'fldinst' => array(self::SKIP, 'link', null), + ); + + if (array_key_exists($control, $controls)) { + list($function) = $controls[$control]; + if (method_exists($this, $function)) { + $directives = $controls[$control]; + array_shift($directives); // remove the function variable; we won't need it + $this->$function($directives); + } + } + } + + /** + * Read paragraph + * + * @param array $directives + */ + private function readParagraph($directives) + { + list($property, $value) = $directives; + $this->textrun = $this->section->addTextRun(); + $this->flags[$property] = $value; + } + + /** + * Read style + * + * @param array $directives + */ + private function readStyle($directives) + { + list($style, $property, $value) = $directives; + $this->flags['styles'][$style][$property] = $value; + } + + /** + * Read skip + * + * @param array $directives + */ + private function readSkip($directives) + { + list($property) = $directives; + $this->flags['property'] = $property; + $this->flags['skipped'] = true; + } + + /** + * Read text + */ + private function readText() + { + $text = $this->textrun->addText($this->text); + if (isset($this->flags['styles']['font'])) { + $text->getFontStyle()->setStyleByArray($this->flags['styles']['font']); + } + } +} diff --git a/src/PhpWord/Shared/XMLReader.php b/src/PhpWord/Shared/XMLReader.php index 60474ce9..153152ee 100644 --- a/src/PhpWord/Shared/XMLReader.php +++ b/src/PhpWord/Shared/XMLReader.php @@ -56,18 +56,30 @@ class XMLReader $zip = new ZipArchive(); $zip->open($zipFile); - $contents = $zip->getFromName($xmlFile); + $content = $zip->getFromName($xmlFile); $zip->close(); - if ($contents === false) { + if ($content === false) { return false; } else { - $this->dom = new \DOMDocument(); - $this->dom->loadXML($contents); - return $this->dom; + return $this->getDomFromString($content); } } + /** + * Get DOMDocument from content string + * + * @param string $content + * @return \DOMDocument + */ + public function getDomFromString($content) + { + $this->dom = new \DOMDocument(); + $this->dom->loadXML($content); + + return $this->dom; + } + /** * Get elements * diff --git a/src/PhpWord/Writer/RTF/Part/Header.php b/src/PhpWord/Writer/RTF/Part/Header.php index c401b500..15a0c303 100644 --- a/src/PhpWord/Writer/RTF/Part/Header.php +++ b/src/PhpWord/Writer/RTF/Part/Header.php @@ -123,7 +123,7 @@ class Header extends AbstractPart $content .= '{'; $content .= '\fonttbl'; foreach ($this->fontTable as $index => $font) { - $content .= "{\\f{$index}\\fnil\\fcharset0{$font};}"; + $content .= "{\\f{$index}\\fnil\\fcharset0 {$font};}"; } $content .= '}'; $content .= PHP_EOL; diff --git a/tests/PhpWord/Tests/Reader/ODTextTest.php b/tests/PhpWord/Tests/Reader/ODTextTest.php index 4120c11e..fc4d2e33 100644 --- a/tests/PhpWord/Tests/Reader/ODTextTest.php +++ b/tests/PhpWord/Tests/Reader/ODTextTest.php @@ -33,7 +33,7 @@ class ODTextTest extends \PHPUnit_Framework_TestCase public function testLoad() { $filename = __DIR__ . '/../_files/documents/reader.odt'; - $object = IOFactory::load($filename, 'ODText'); - $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $object); + $phpWord = IOFactory::load($filename, 'ODText'); + $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); } } diff --git a/tests/PhpWord/Tests/Reader/RTFTest.php b/tests/PhpWord/Tests/Reader/RTFTest.php new file mode 100644 index 00000000..c495db68 --- /dev/null +++ b/tests/PhpWord/Tests/Reader/RTFTest.php @@ -0,0 +1,51 @@ +assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); + } + + /** + * Test load exception + * + * @expectedException \Exception + * @expectedExceptionMessage Cannot read + */ + public function testLoadException() + { + $filename = __DIR__ . '/../_files/documents/foo.rtf'; + IOFactory::load($filename, 'RTF'); + } +} diff --git a/tests/PhpWord/Tests/Reader/Word2007Test.php b/tests/PhpWord/Tests/Reader/Word2007Test.php index 58890c9a..f2257012 100644 --- a/tests/PhpWord/Tests/Reader/Word2007Test.php +++ b/tests/PhpWord/Tests/Reader/Word2007Test.php @@ -28,40 +28,24 @@ use PhpOffice\PhpWord\Reader\Word2007; */ class Word2007Test extends \PHPUnit_Framework_TestCase { - /** - * Init - */ - public function tearDown() - { - } - /** * Test canRead() method */ public function testCanRead() { $object = new Word2007(); - $fqFilename = join( - DIRECTORY_SEPARATOR, - array(PHPWORD_TESTS_BASE_DIR, 'PhpWord', 'Tests', '_files', 'documents', 'reader.docx') - ); - $this->assertTrue($object->canRead($fqFilename)); + $filename = __DIR__ . '/../_files/documents/reader.docx'; + $this->assertTrue($object->canRead($filename)); } /** * Can read exception - * - * @expectedException \PhpOffice\PhpWord\Exception\Exception */ public function testCanReadFailed() { $object = new Word2007(); - $fqFilename = join( - DIRECTORY_SEPARATOR, - array(PHPWORD_TESTS_BASE_DIR, 'PhpWord', 'Tests', '_files', 'documents', 'foo.docx') - ); - $this->assertFalse($object->canRead($fqFilename)); - $object = IOFactory::load($fqFilename); + $filename = __DIR__ . '/../_files/documents/foo.docx'; + $this->assertFalse($object->canRead($filename)); } /** @@ -69,11 +53,8 @@ class Word2007Test extends \PHPUnit_Framework_TestCase */ public function testLoad() { - $fqFilename = join( - DIRECTORY_SEPARATOR, - array(PHPWORD_TESTS_BASE_DIR, 'PhpWord', 'Tests', '_files', 'documents', 'reader.docx') - ); - $object = IOFactory::load($fqFilename); - $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $object); + $filename = __DIR__ . '/../_files/documents/reader.docx'; + $phpWord = IOFactory::load($filename); + $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); } } diff --git a/tests/PhpWord/Tests/_files/documents/reader.rtf b/tests/PhpWord/Tests/_files/documents/reader.rtf new file mode 100644 index 00000000..400f43a5 --- /dev/null +++ b/tests/PhpWord/Tests/_files/documents/reader.rtf @@ -0,0 +1,21 @@ +{\rtf1 +\ansi\ansicpg1252 +\deff0 +{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fnil\fcharset0 Times New Roman;}} +{\colortbl;\red255\green0\blue0;\red14\green0\blue0} +{\*\generator PhpWord;} + +{\info{\title }{\subject }{\category }{\keywords }{\comment }{\author }{\operator }{\creatim \yr2014\mo05\dy27\hr23\min36\sec45}{\revtim \yr2014\mo05\dy27\hr23\min36\sec45}{\company }{\manager }} +\deftab720\viewkind1\uc1\pard\nowidctlpar\lang1036\kerning1\fs20 +{Welcome to PhpWord}\par +\pard\nowidctlpar{\cf0\f0 Hello World!}\par +\par +\par +\pard\nowidctlpar{\cf0\f0\fs32\b\i I am styled by a definition.}\par +\pard\nowidctlpar{\cf0\f0 I am styled by a paragraph style definition.}\par +\pard\nowidctlpar\qc\sa100{\cf0\f0\fs32\b\i I am styled by both font and paragraph style.}\par +\pard\nowidctlpar{\cf1\f1\fs40\b\i\ul\strike\super I am inline styled.}\par +\par +{\field {\*\fldinst {HYPERLINK "http://www.google.com"}}{\fldrslt {Google}}}\par +\par +} \ No newline at end of file