Merge pull request #252 from ivanlanin/rtf

Passed Travis. Merged.
This commit is contained in:
Ivan Lanin 2014-05-29 18:32:00 +07:00
commit 0fcea8209f
19 changed files with 593 additions and 45 deletions

View File

@ -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

View File

@ -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:

View File

@ -124,7 +124,7 @@ Readers
+---------------------------+----------------------+--------+-------+-------+
| | Custom | ✓ | | |
+---------------------------+----------------------+--------+-------+-------+
| **Element Type** | Text | ✓ | ✓ | |
| **Element Type** | Text | ✓ | ✓ | |
+---------------------------+----------------------+--------+-------+-------+
| | Text Run | ✓ | | |
+---------------------------+----------------------+--------+-------+-------+

View File

@ -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:

View File

@ -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);

View File

@ -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');

View File

@ -0,0 +1,15 @@
<?php
include_once 'Sample_Header.php';
// Read contents
$name = basename(__FILE__, '.php');
$source = __DIR__ . "/resources/{$name}.rtf";
echo date('H:i:s'), " Reading contents from `{$source}`", EOL;
$phpWord = \PhpOffice\PhpWord\IOFactory::load($source, 'RTF');
// Save file
echo write($phpWord, basename(__FILE__, '.php'), $writers);
if (!CLI) {
include_once 'Sample_Footer.php';
}

View File

@ -63,8 +63,8 @@ function write($phpWord, $filename, $writers)
$result .= date('H:i:s') . " Write to {$writer} format";
if (!is_null($extension)) {
$xmlWriter = IOFactory::createWriter($phpWord, $writer);
$xmlWriter->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!';
}

View File

@ -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
}

View File

@ -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.");
}

View File

@ -39,7 +39,7 @@ abstract class AbstractReader implements ReaderInterface
*
* @var bool|resource
*/
protected $fileHandle = true;
protected $fileHandle;
/**
* Read data only?

View File

@ -0,0 +1,51 @@
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @link https://github.com/PHPOffice/PHPWord
* @copyright 2010-2014 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Reader;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Reader\RTF\Document;
/**
* RTF Reader class
*
* @since 0.11.0
*/
class RTF extends AbstractReader implements ReaderInterface
{
/**
* Loads PhpWord from file
*
* @param string $docFile
* @throws \Exception
* @return \PhpOffice\PhpWord\PhpWord
*/
public function load($docFile)
{
$phpWord = new PhpWord();
if ($this->canRead($docFile)) {
$doc = new Document();
$doc->rtf = file_get_contents($docFile);
$doc->read($phpWord);
} else {
throw new \Exception("Cannot read {$docFile}.");
}
return $phpWord;
}
}

View File

@ -0,0 +1,393 @@
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @link https://github.com/PHPOffice/PHPWord
* @copyright 2010-2014 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Reader\RTF;
use PhpOffice\PhpWord\PhpWord;
/**
* RTF document reader
*
* References:
* - How to Write an RTF Reader http://latex2rtf.sourceforge.net/rtfspec_45.html
* - PHP rtfclass by Markus Fischer https://github.com/mfn/rtfclass
* - JavaScript RTF-parser by LazyGyu https://github.com/lazygyu/RTF-parser
*
* @since 0.11.0
* @SuppressWarnings(PHPMD.UnusedPrivateMethod)
*/
class Document
{
/** @const int */
const PARA = 'readParagraph';
const STYL = 'readStyle';
const SKIP = 'readSkip';
/**
* PhpWord object
*
* @var \PhpOffice\PhpWord\PhpWord
*/
private $phpWord;
/**
* Section object
*
* @var \PhpOffice\PhpWord\Element\Section
*/
private $section;
/**
* Textrun object
*
* @var \PhpOffice\PhpWord\Element\TextRun
*/
private $textrun;
/**
* RTF content
*
* @var string
*/
public $rtf;
/**
* Content length
*
* @var int
*/
private $length = 0;
/**
* Character index
*
* @var int
*/
private $offset = 0;
/**
* Current control word
*
* @var string
*/
private $control = '';
/**
* Text content
*
* @var string
*/
private $text = '';
/**
* Parsing a control word flag
*
* @var bool
*/
private $isControl = false;
/**
* First character flag: watch out for control symbols
*
* @var bool
*/
private $isFirst = false;
/**
* Group groups
*
* @var array
*/
private $groups = array();
/**
* Parser flags; not used
*
* @var array
*/
private $flags = array();
/**
* Parse RTF content
*
* - Marks controlling characters `{`, `}`, and `\`
* - Removes line endings
* - Builds control words and control symbols
* - Pushes every other character into the text queue
*
* @param \PhpOffice\PhpWord\PhpWord $phpWord
* @todo Use `fread` stream for scalability
*/
public function read(PhpWord &$phpWord)
{
$markers = array(
123 => '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 .= "&lt;";
} elseif ($char == '>') {
$this->text .= "&gt;";
} 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']);
}
}
}

View File

@ -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
*

View File

@ -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;

View File

@ -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);
}
}

View File

@ -0,0 +1,51 @@
<?php
/**
* This file is part of PHPWord - A pure PHP library for reading and writing
* word processing documents.
*
* PHPWord is free software distributed under the terms of the GNU Lesser
* General Public License version 3 as published by the Free Software Foundation.
*
* For the full copyright and license information, please read the LICENSE
* file that was distributed with this source code. For the full list of
* contributors, visit https://github.com/PHPOffice/PHPWord/contributors.
*
* @link https://github.com/PHPOffice/PHPWord
* @copyright 2010-2014 PHPWord contributors
* @license http://www.gnu.org/licenses/lgpl.txt LGPL version 3
*/
namespace PhpOffice\PhpWord\Tests\Reader;
use PhpOffice\PhpWord\IOFactory;
/**
* Test class for PhpOffice\PhpWord\Reader\RTF
*
* @coversDefaultClass \PhpOffice\PhpWord\Reader\RTF
* @runTestsInSeparateProcesses
*/
class RTFTest extends \PHPUnit_Framework_TestCase
{
/**
* Test load
*/
public function testLoad()
{
$filename = __DIR__ . '/../_files/documents/reader.rtf';
$phpWord = IOFactory::load($filename, 'RTF');
$this->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');
}
}

View File

@ -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);
}
}

View File

@ -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
}