diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..f3d033a7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# build config +/.scrutinizer.yml export-ignore +/.travis.yml export-ignore +/php_cs.dist export-ignore +/phpmd.xml.dist export-ignore +/phpstan.neon export-ignore + +/composer.lock export-ignore + +# git files +/.gitignore export-ignore +/.gitattributes export-ignore + +# project directories +/build export-ignore +/docs export-ignore +/samples export-ignore + +# tests +/phpunit.xml.dist export-ignore +/tests export-ignore \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2ac6e2b5..dd858cea 100644 --- a/.gitignore +++ b/.gitignore @@ -13,11 +13,11 @@ composer.phar vendor /report /build -/samples/resources /samples/results /.settings phpword.ini /.buildpath +/.scannerwork /.project /nbproject /.php_cs.cache diff --git a/.travis.yml b/.travis.yml index bd85b2d7..1d32cfda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,20 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - - php: 5.6 + - php: 7.0 env: COVERAGE=1 + - php: 5.3 + env: COMPOSER_MEMORY_LIMIT=2G + - php: 7.3 + env: DEPENDENCIES="--ignore-platform-reqs" + exclude: + - php: 5.3 + - php: 7.0 + - php: 7.3 cache: directories: @@ -32,12 +41,12 @@ before_install: before_script: ## Deactivate xdebug if we don't do code coverage - - if [ -z "$COVERAGE" ]; then phpenv config-rm xdebug.ini ; fi + - if [ -z "$COVERAGE" ]; then phpenv config-rm xdebug.ini || echo "xdebug not available" ; fi ## Composer - composer self-update - - travis_wait composer install --prefer-source + - travis_wait composer install --prefer-source $(if [ -n "$DEPENDENCIES" ]; then echo $DEPENDENCIES; fi) ## PHPDocumentor - - mkdir -p build/docs + ##- mkdir -p build/docs - mkdir -p build/coverage script: @@ -52,7 +61,7 @@ script: ## PHPLOC - if [ -z "$COVERAGE" ]; then ./vendor/bin/phploc src/ ; fi ## PHPDocumentor - - if [ -z "$COVERAGE" ]; then ./vendor/bin/phpdoc -q -d ./src -t ./build/docs --ignore "*/src/PhpWord/Shared/*/*" --template="responsive-twig" ; fi + ##- if [ -z "$COVERAGE" ]; then ./vendor/bin/phpdoc -q -d ./src -t ./build/docs --ignore "*/src/PhpWord/Shared/*/*" --template="responsive-twig" ; fi after_success: ## Coveralls diff --git a/CHANGELOG.md b/CHANGELOG.md index dfcce5a4..9ec1deef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,47 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -v0.15.0 (?? ??? 2018) +v0.17.0 (?? ??? 2019) +---------------------- +### Added +- Add RightToLeft table presentation. @troosan #1550 +- Set complex type in template @troosan #1565 +- Add support for page vertical alignment. @troosan #672 #1569 + +### Fixed +- Fix HTML border-color parsing. @troosan #1551 #1570 + +### Miscelaneous +- Use embedded http server to test loading of remote images @troosan # + +v0.16.0 (30 dec 2018) +---------------------- +### Added +- Add getVariableCount method in TemplateProcessor. @nicoder #1272 +- Add setting Chart Title and Legend visibility @Tom-Magill #1433 +- Add ability to pass a Style object in Section constructor @ndench #1416 +- Add support for hidden text @Alexmg86 #1527 +- Add support for setting images in TemplateProcessor @SailorMax #1170 +- Add "Plain Text" type to SDT (Structured Document Tags) @morrisdj #1541 +- Added possibility to index variables inside cloned block in TemplateProcessor @JPBetley #817 +- Added possibility to replace variables inside cloned block with values in TemplateProcessor @DIDoS #1392 + +### Fixed +- Fix regex in `cloneBlock` function @nicoder #1269 +- HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 +- Fix regex in fixBrokenMacros, make it less greedy @MuriloSo @brainwood @yurii-sio2 #1502 #1345 +- 240 twips are being added to line spacing, should not happen when using lineRule fixed @troosan #1509 #1505 +- Adding table layout to the generated HTML @aarangara #1441 +- Fix loading of Sharepoint document @Garrcomm #1498 +- RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals @Patrick64 #1493 +- Fix parsing of Office 365 documents @Timanx #1485 +- For RTF writers, sizes should should never have decimals @Samuel-BF #1536 +- Style Name Parsing fails if document generated by a non-english word version @begnini #1434 + +### Miscelaneous +- Get rid of duplicated code in TemplateProcessor @abcdmitry #1161 + +v0.15.0 (14 Jul 2018) ---------------------- ### Added - Parsing of `align` HTML attribute - @troosan #1231 @@ -23,6 +63,9 @@ v0.15.0 (?? ??? 2018) - Add support for table indent (tblInd) @Trainmaster #1343 - Added parsing of internal links in HTML reader @lalop #1336 - Several improvements to charts @JAEK-S #1332 +- Add parsing of html image in base64 format @jgpATs2w #1382 +- Added Support for Indentation & Tabs on RTF Writer. @smaug1985 #1405 +- Allows decimal numbers in HTML line-height style @jgpATs2w #1413 ### Fixed - Fix reading of docx default style - @troosan #1238 @@ -35,6 +78,9 @@ v0.15.0 (?? ??? 2018) - Fix colspan and rowspan for tables in HTML Writer @mattbolt #1292 - Fix parsing of Heading and Title formating @troosan @gthomas2 #465 - Fix Dateformat typo, fix hours casing, add Month-Day-Year formats @ComputerTinker #591 +- Support reading of w:drawing for documents produced by word 2011+ @gthomas2 #464 #1324 +- Fix missing column width in ODText writer @potofcoffee #413 +- Disable entity loader before parsing XML to avoid XXE injection @Tom4t0 #1427 ### Changed - Remove zend-stdlib dependency @Trainmaster #1284 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e62f2e6f..43ee0636 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ We want to create a high quality document writer and reader library that people - **Be brief, but be bold**. State your issues briefly. But speak out your ideas loudly, even if you can't or don't know how to implement it right away. The world will be better with limitless innovations. - **Follow PHP-FIG standards**. We follow PHP Standards Recommendations (PSRs) by [PHP Framework Interoperability Group](http://www.php-fig.org/). If you're not familiar with these standards, please, [familiarize yourself now](https://github.com/php-fig/fig-standards). Also, please, use [PHPCodeSniffer](http://pear.php.net/package/PHP_CodeSniffer/) to validate your code against PSRs. -- **Test your code**. Nobody else knows your code better than you. So, it's completely your mission to test the changes you made before pull request submission. We use [PHPUnit](https://phpunit.de/) for our testing purposes and recommend you using this tool too. [Here](https://phpunit.de/presentations.html) you can find PHPUnit best practices and additional information on effective unit testing, which helps us making PHPWord better day to day. Do not hesitate to smoke it carefully. It's a great investment in quality of your work, and it saves you years of life. +- **Test your code**. Nobody else knows your code better than you. So, it's completely your mission to test the changes you made before pull request submission. We use [PHPUnit](https://phpunit.de/) for our testing purposes and recommend you using this tool too. [Here](https://phpunit.readthedocs.io) you can find documentation on how to write tests with PHPUnit, which helps us making PHPWord better day to day. Do not hesitate to smoke it carefully. It's a great investment in quality of your work, and it saves you years of life. - **Request pull in separate branch**. Do not submit your request to the master branch. But create a separate branch named specifically for the issue that you addressed. Read [GitHub manual](https://help.github.com/articles/using-pull-requests) to find out more about this. If you are new to GitHub, read [this short manual](https://help.github.com/articles/fork-a-repo) to get yourself familiar with forks and how git works in general. [This video](http://www.youtube.com/watch?v=-zvHQXnBO6c) explains how to synchronize your Github Fork with the Branch of PHPWord. That's it. Thank you for your interest in PHPWord, and welcome! diff --git a/README.md b/README.md index 7531a6bc..f1d1d6ee 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,20 @@ # ![PHPWord](https://rawgit.com/PHPOffice/PHPWord/develop/docs/images/phpword.svg "PHPWord") +Master: [![Latest Stable Version](https://poser.pugx.org/phpoffice/phpword/v/stable.png)](https://packagist.org/packages/phpoffice/phpword) [![Build Status](https://travis-ci.org/PHPOffice/PHPWord.svg?branch=master)](https://travis-ci.org/PHPOffice/PHPWord) -[![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/badges/quality-score.png?s=b5997ce59ac2816b4514f3a38de9900f6d492c1d)](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/) -[![Coverage Status](https://coveralls.io/repos/github/PHPOffice/PHPWord/badge.svg?branch=develop)](https://coveralls.io/github/PHPOffice/PHPWord?branch=develop) +[![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/) +[![Coverage Status](https://coveralls.io/repos/github/PHPOffice/PHPWord/badge.svg?branch=master)](https://coveralls.io/github/PHPOffice/PHPWord?branch=master) [![Total Downloads](https://poser.pugx.org/phpoffice/phpword/downloads.png)](https://packagist.org/packages/phpoffice/phpword) [![License](https://poser.pugx.org/phpoffice/phpword/license.png)](https://packagist.org/packages/phpoffice/phpword) [![Join the chat at https://gitter.im/PHPOffice/PHPWord](https://img.shields.io/badge/GITTER-join%20chat-green.svg)](https://gitter.im/PHPOffice/PHPWord) +Develop: +[![Latest Development Version](https://img.shields.io/badge/unstable-dev--develop-orange.svg)](https://packagist.org/packages/phpoffice/phpword#dev-develop) +[![Build Status](https://travis-ci.org/PHPOffice/PHPWord.svg?branch=develop)](https://travis-ci.org/PHPOffice/PHPWord/branches) +[![Code Quality](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/badges/quality-score.png?b=develop)](https://scrutinizer-ci.com/g/PHPOffice/PHPWord/?branch=develop) +[![Coverage Status](https://coveralls.io/repos/github/PHPOffice/PHPWord/badge.svg?branch=develop)](https://coveralls.io/github/PHPOffice/PHPWord?branch=develop) + PHPWord is a library written in pure PHP that provides a set of classes to write to and read from different document file formats. The current version of PHPWord supports Microsoft [Office Open XML](http://en.wikipedia.org/wiki/Office_Open_XML) (OOXML or OpenXML), OASIS [Open Document Format for Office Applications](http://en.wikipedia.org/wiki/OpenDocument) (OpenDocument or ODF), [Rich Text Format](http://en.wikipedia.org/wiki/Rich_Text_Format) (RTF), HTML, and PDF. PHPWord is an open source project licensed under the terms of [LGPL version 3](https://github.com/PHPOffice/PHPWord/blob/develop/COPYING.LESSER). PHPWord is aimed to be a high quality software product by incorporating [continuous integration](https://travis-ci.org/PHPOffice/PHPWord) and [unit testing](http://phpoffice.github.io/PHPWord/coverage/develop/). You can learn more about PHPWord by reading the [Developers' Documentation](http://phpword.readthedocs.org/). @@ -81,7 +88,7 @@ You can of course also manually edit your composer.json file ```json { "require": { - "phpoffice/phpword": "v0.14.*" + "phpoffice/phpword": "v0.16.*" } } ``` @@ -161,7 +168,7 @@ $objWriter->save('helloWorld.html'); ``` More examples are provided in the [samples folder](samples/). For an easy access to those samples launch `php -S localhost:8000` in the samples directory then browse to [http://localhost:8000](http://localhost:8000) to view the samples. -You can also read the [Developers' Documentation](http://phpword.readthedocs.org/) and the [API Documentation](http://phpoffice.github.io/PHPWord/docs/master/) for more detail. +You can also read the [Developers' Documentation](http://phpword.readthedocs.org/) for more detail. ## Contributing diff --git a/composer.json b/composer.json index c29e901a..bd57d6e3 100644 --- a/composer.json +++ b/composer.json @@ -45,7 +45,7 @@ "php-cs-fixer fix --ansi --dry-run --diff", "phpcs --report-width=200 --report-summary --report-full samples/ src/ tests/ --ignore=src/PhpWord/Shared/PCLZip --standard=PSR2 -n", "phpmd src/,tests/ text ./phpmd.xml.dist --exclude pclzip.lib.php", - "@test" + "@test-no-coverage" ], "fix": [ "php-cs-fixer fix --ansi" @@ -61,18 +61,19 @@ "php": "^5.3.3 || ^7.0", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "phpoffice/common": "^0.2" + "phpoffice/common": "^0.2.9" }, "require-dev": { - "phpunit/phpunit": "^4.8.36 || ^5.0", - "phpdocumentor/phpdocumentor":"2.*", - "squizlabs/php_codesniffer": "^2.7", - "friendsofphp/php-cs-fixer": "^2.0", + "ext-zip": "*", + "ext-gd": "*", + "phpunit/phpunit": "^4.8.36 || ^7.0", + "squizlabs/php_codesniffer": "^2.9", + "friendsofphp/php-cs-fixer": "^2.2", "phpmd/phpmd": "2.*", "phploc/phploc": "2.* || 3.* || 4.*", "dompdf/dompdf":"0.8.*", "tecnickcom/tcpdf": "6.*", - "mpdf/mpdf": "5.* || 6.* || 7.*", + "mpdf/mpdf": "5.7.4 || 6.* || 7.*", "php-coveralls/php-coveralls": "1.1.0 || ^2.0" }, "suggest": { @@ -89,7 +90,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "0.15-dev" + "dev-develop": "0.17-dev" } } } diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md index 24ba001c..5430a996 100644 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -8,4 +8,4 @@ Fixes # (issue) - [ ] I have run `composer run-script check --timeout=0` and no errors were reported - [ ] The new code is covered by unit tests (check build/coverage for coverage report) -- [ ] I have update the documentation to describe the changes +- [ ] I have updated the documentation to describe the changes diff --git a/docs/conf.py b/docs/conf.py index 6b7cf8e8..d83c43f5 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -48,7 +48,7 @@ copyright = u'2014-2017, PHPWord Contributors' # built documents. # # The short X.Y version. -version = '0.14.0' +version = '0.16.0' # The full version, including alpha/beta/rc tags. release = version diff --git a/docs/containers.rst b/docs/containers.rst index dc194d59..9ee58efc 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -104,6 +104,12 @@ You can pass an optional parameter to specify where the header/footer should be - ``Footer::FIRST`` each first page of the section - ``Footer::EVEN`` each even page of the section. Will only be applied if the evenAndOddHeaders is set to true in phpWord->settings +To change the evenAndOddHeaders use the ``getSettings`` method to return the Settings object, and then call the ``setEvenAndOddHeaders`` method: + +.. code-block:: php + + $phpWord->getSettings()->setEvenAndOddHeaders(true); + Footers ------- diff --git a/docs/elements.rst b/docs/elements.rst index 74b1d56f..9d446b27 100644 --- a/docs/elements.rst +++ b/docs/elements.rst @@ -61,7 +61,7 @@ Legend: Texts ----- -Text can be added by using ``addText`` and ``addTextRun`` method. +Text can be added by using ``addText`` and ``addTextRun`` methods. ``addText`` is used for creating simple paragraphs that only contain texts with the same style. ``addTextRun`` is used for creating complex paragraphs that contain text with different style (some bold, other italics, etc) or other elements, e.g. images or links. The syntaxes are as follow: @@ -155,13 +155,18 @@ method or using the ``pageBreakBefore`` style of paragraph. Lists ----- -To add a list item use the function ``addListItem``. +Lists can be added by using ``addListItem`` and ``addListItemRun`` methods. +``addListItem`` is used for creating lists that only contain plain text. +``addListItemRun`` is used for creating complex list items that contains texts +with different style (some bold, other italics, etc) or other elements, e.g. +images or links. The syntaxes are as follow: Basic usage: .. code-block:: php $section->addListItem($text, [$depth], [$fontStyle], [$listStyle], [$paragraphStyle]); + $listItemRun = $section->addListItemRun([$depth], [$listStyle], [$paragraphStyle]) Parameters: @@ -172,6 +177,8 @@ Parameters: TYPE\_ALPHANUM, TYPE\_BULLET\_FILLED, etc. See list of constants in PHPWord\\Style\\ListItem. - ``$paragraphStyle``. See :ref:`paragraph-style`. +See ``Sample_09_Tables.php`` for more code sample. + Advanced usage: You can also create your own numbering style by changing the ``$listStyle`` parameter with the name of your numbering style. diff --git a/docs/styles.rst b/docs/styles.rst index 03366427..27f8ee66 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -29,8 +29,11 @@ Available Section style options: - ``marginRight``. Page margin right in *twip*. - ``marginBottom``. Page margin bottom in *twip*. - ``orientation``. Page orientation (``portrait``, which is default, or ``landscape``). + See ``\PhpOffice\PhpWord\Style\Section::ORIENTATION_...`` class constants for possible values - ``pageSizeH``. Page height in *twip*. Implicitly defined by ``orientation`` option. Any changes are discouraged. - ``pageSizeW``. Page width in *twip*. Implicitly defined by ``orientation`` option. Any changes are discouraged. +- ``vAlign``. Vertical Page Alignment + See ``\PhpOffice\PhpWord\SimpleType\VerticalJc`` for possible values .. _font-style: @@ -45,7 +48,7 @@ Available Font style options: - ``color``. Font color, e.g. *FF0000*. - ``doubleStrikethrough``. Double strikethrough, *true* or *false*. - ``fgColor``. Font highlight color, e.g. *yellow*, *green*, *blue*. - See ``\PhpOffice\PhpWord\Style\Font::FGCOLOR_...`` constants for more values + See ``\PhpOffice\PhpWord\Style\Font::FGCOLOR_...`` class constants for possible values - ``hint``. Font content type, *default*, *eastAsia*, or *cs*. - ``italic``. Italic, *true* or *false*. - ``name``. Font name, e.g. *Arial*. @@ -56,10 +59,11 @@ Available Font style options: - ``subScript``. Subscript, *true* or *false*. - ``superScript``. Superscript, *true* or *false*. - ``underline``. Underline, *single*, *dash*, *dotted*, etc. - See ``\PhpOffice\PhpWord\Style\Font::UNDERLINE_...`` constants for more values + See ``\PhpOffice\PhpWord\Style\Font::UNDERLINE_...`` class constants for possible values - ``lang``. Language, either a language code like *en-US*, *fr-BE*, etc. or an object (or as an array) if you need to set eastAsian or bidirectional languages See ``\PhpOffice\PhpWord\Style\Language`` class for some language codes. - ``position``. The text position, raised or lowered, in half points +- ``hidden``. Hidden text, *true* or *false*. .. _paragraph-style: @@ -69,7 +73,7 @@ Paragraph Available Paragraph style options: - ``alignment``. Supports all alignment modes since 1st Edition of ECMA-376 standard up till ISO/IEC 29500:2012. - See ``\PhpOffice\PhpWord\SimpleType\Jc`` class for the details. + See ``\PhpOffice\PhpWord\SimpleType\Jc`` class constants for possible values. - ``basedOn``. Parent style. - ``hanging``. Hanging in *twip*. - ``indent``. Indent in *twip*. @@ -80,8 +84,9 @@ Available Paragraph style options: - ``pageBreakBefore``. Start paragraph on next page, *true* or *false*. - ``spaceBefore``. Space before paragraph in *twip*. - ``spaceAfter``. Space after paragraph in *twip*. -- ``spacing``. Space between lines. +- ``spacing``. Space between lines in *twip*. If spacingLineRule is auto, 240 (height of 1 line) will be added, so if you want a double line height, set this to 240. - ``spacingLineRule``. Line Spacing Rule. *auto*, *exact*, *atLeast* + See ``\PhpOffice\PhpWord\SimpleType\LineSpacingRule`` class constants for possible values. - ``suppressAutoHyphens``. Hyphenation for paragraph, *true* or *false*. - ``tabs``. Set of custom tab stops. - ``widowControl``. Allow first/last line to display on a separate page, *true* or *false*. @@ -89,7 +94,7 @@ Available Paragraph style options: - ``bidi``. Right to Left Paragraph Layout, *true* or *false*. - ``shading``. Paragraph Shading. - ``textAlignment``. Vertical Character Alignment on Line. - See ``\PhpOffice\PhpWord\SimpleType\TextAlignment`` class for possible values. + See ``\PhpOffice\PhpWord\SimpleType\TextAlignment`` class constants for possible values. .. _table-style: @@ -99,17 +104,18 @@ Table Available Table style options: - ``alignment``. Supports all alignment modes since 1st Edition of ECMA-376 standard up till ISO/IEC 29500:2012. - See ``\PhpOffice\PhpWord\SimpleType\JcTable`` and ``\PhpOffice\PhpWord\SimpleType\Jc`` classes for the details. + See ``\PhpOffice\PhpWord\SimpleType\JcTable`` and ``\PhpOffice\PhpWord\SimpleType\Jc`` class constants for possible values. - ``bgColor``. Background color, e.g. '9966CC'. - ``border(Top|Right|Bottom|Left)Color``. Border color, e.g. '9966CC'. - ``border(Top|Right|Bottom|Left)Size``. Border size in *twip*. - ``cellMargin(Top|Right|Bottom|Left)``. Cell margin in *twip*. - ``indent``. Table indent from leading margin. Must be an instance of ``\PhpOffice\PhpWord\ComplexType\TblWidth``. -- ``width``. Table width in percent. +- ``width``. Table width in Fiftieths of a Percent or Twentieths of a Point. - ``unit``. The unit to use for the width. One of ``\PhpOffice\PhpWord\SimpleType\TblWidth``. Defaults to *auto*. - ``layout``. Table layout, either *fixed* or *autofit* See ``\PhpOffice\PhpWord\Style\Table`` for constants. - ``cellSpacing`` Cell spacing in *twip* - ``position`` Floating Table Positioning, see below for options +- ``bidiVisual`` Present table as Right-To-Left Floating Table Positioning options: @@ -168,7 +174,7 @@ Numbering level Available NumberingLevel style options: - ``alignment``. Supports all alignment modes since 1st Edition of ECMA-376 standard up till ISO/IEC 29500:2012. - See ``\PhpOffice\PhpWord\SimpleType\Jc`` class for the details. + See ``\PhpOffice\PhpWord\SimpleType\Jc`` class constants for possible values. - ``font``. Font name. - ``format``. Numbering format bullet\|decimal\|upperRoman\|lowerRoman\|upperLetter\|lowerLetter. - ``hanging``. See paragraph style. @@ -190,6 +196,14 @@ Available Chart style options: - ``width``. Width (in EMU). - ``height``. Height (in EMU). - ``3d``. Is 3D; applies to pie, bar, line, area, *true* or *false*. +- ``colors``. A list of colors to use in the chart. +- ``title``. The title for the chart. +- ``showLegend``. Show legend, *true* or *false*. +- ``categoryLabelPosition``. Label position for categories, *nextTo* (default), *low* or *high*. +- ``valueLabelPosition``. Label position for values, *nextTo* (default), *low* or *high*. +- ``categoryAxisTitle``. The title for the category axis. +- ``valueAxisTitle``. The title for the values axis. +- ``majorTickMarkPos``. The position for major tick marks, *in*, *out*, *cross*, *none* (default). - ``showAxisLabels``. Show labels for axis, *true* or *false*. - ``gridX``. Show Gridlines for X-Axis, *true* or *false*. - ``gridY``. Show Gridlines for Y-Axis, *true* or *false*. diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index af03b245..5b32aa18 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -4,22 +4,243 @@ Templates processing ==================== You can create an OOXML document template with included search-patterns (macros) which can be replaced by any value you wish. Only single-line values can be replaced. +Macros are defined like this: ``${search-pattern}``. +To load a template file, create a new instance of the TemplateProcessor. -To deal with a template file, use ``new TemplateProcessor`` statement. After TemplateProcessor instance creation the document template is copied into the temporary directory. Then you can use ``TemplateProcessor::setValue`` method to change the value of a search pattern. The search-pattern model is: ``${search-pattern}``. +.. code-block:: php + + $templateProcessor = new TemplateProcessor('Template.docx'); + +setValue +"""""""" +Given a template containing + +.. code-block:: clean + + Hello ${firstname} ${lastname}! + +The following will replace ``${firstname}`` with ``John``, and ``${lastname}`` with ``Doe`` . +The resulting document will now contain ``Hello John Doe!`` + +.. code-block:: php + + $templateProcessor->setValue('firstname', 'John'); + $templateProcessor->setValue('lastname', 'Doe'); + +setValues +""""""""" +You can also set multiple values by passing all of them in an array. + +.. code-block:: php + + $templateProcessor->setValues(array('firstname' => 'John', 'lastname' => 'Doe')); + +setImageValue +""""""""""""" +The search-pattern model for images can be like: + - ``${search-image-pattern}`` + - ``${search-image-pattern:[width]:[height]:[ratio]}`` + - ``${search-image-pattern:[width]x[height]}`` + - ``${search-image-pattern:size=[width]x[height]}`` + - ``${search-image-pattern:width=[width]:height=[height]:ratio=false}`` + +Where: + - [width] and [height] can be just numbers or numbers with measure, which supported by Word (cm, mm, in, pt, pc, px, %, em, ex) + - [ratio] uses only for ``false``, ``-`` or ``f`` to turn off respect aspect ration of image. By default template image size uses as 'container' size. Example: +.. code-block:: clean + + ${CompanyLogo} + ${UserLogo:50:50} ${Name} - ${City} - ${Street} + .. code-block:: php $templateProcessor = new TemplateProcessor('Template.docx'); $templateProcessor->setValue('Name', 'John Doe'); $templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street')); -It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform headers, main document part, and footers of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``). + $templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); + $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); -See ``Sample_07_TemplateCloneRow.php`` for example on how to create -multirow from a single row in a template by using ``TemplateProcessor::cloneRow``. +cloneBlock +"""""""""" +Given a template containing +See ``Sample_23_TemplateBlock.php`` for an example. -See ``Sample_23_TemplateBlock.php`` for example on how to clone a block -of text using ``TemplateProcessor::cloneBlock`` and delete a block of text using -``TemplateProcessor::deleteBlock``. +.. code-block:: clean + + ${block_name} + Customer: ${customer_name} + Address: ${customer_address} + ${/block_name} + +The following will duplicate everything between ``${block_name}`` and ``${/block_name}`` 3 times. + +.. code-block:: php + + $templateProcessor->cloneBlock('block_name', 3, true, true); + +The last parameter will rename any macro defined inside the block and add #1, #2, #3 ... to the macro name. +The result will be + +.. code-block:: clean + + Customer: ${customer_name#1} + Address: ${customer_address#1} + + Customer: ${customer_name#2} + Address: ${customer_address#2} + + Customer: ${customer_name#3} + Address: ${customer_address#3} + +It is also possible to pass an array with the values to replace the marcros with. +If an array with replacements is passed, the ``count`` argument is ignored, it is the size of the array that counts. + +.. code-block:: php + + $replacements = array( + array('customer_name' => 'Batman', 'customer_address' => 'Gotham City'), + array('customer_name' => 'Superman', 'customer_address' => 'Metropolis'), + ); + $templateProcessor->cloneBlock('block_name', 0, true, false, $replacements); + +The result will then be + +.. code-block:: clean + + Customer: Batman + Address: Gotham City + + Customer: Superman + Address: Metropolis + +replaceBlock +"""""""""""" +Given a template containing + +.. code-block:: clean + + ${block_name} + This block content will be replaced + ${/block_name} + +The following will replace everything between``${block_name}`` and ``${/block_name}`` with the value passed. + +.. code-block:: php + + $templateProcessor->replaceBlock('block_name', 'This is the replacement text.'); + +deleteBlock +""""""""""" +Same as previous, but it deletes the block + +.. code-block:: php + + $templateProcessor->deleteBlock('block_name'); + +cloneRow +"""""""" +Clones a table row in a template document. +See ``Sample_07_TemplateCloneRow.php`` for an example. + +.. code-block:: clean + + +-----------+----------------+ + | ${userId} | ${userName} | + | |----------------+ + | | ${userAddress} | + +-----------+----------------+ + +.. code-block:: php + + $templateProcessor->cloneRow('userId', 2); + +Will result in + +.. code-block:: clean + + +-------------+------------------+ + | ${userId#1} | ${userName#1} | + | |------------------+ + | | ${userAddress#1} | + +-------------+------------------+ + | ${userId#2} | ${userName#2} | + | |------------------+ + | | ${userAddress#2} | + +-------------+------------------+ + +cloneRowAndSetValues +"""""""""""""""""""" +Finds a row in a table row identified by `$search` param and clones it as many times as there are entries in `$values`. + +.. code-block:: clean + + +-----------+----------------+ + | ${userId} | ${userName} | + | |----------------+ + | | ${userAddress} | + +-----------+----------------+ + +.. code-block:: php + + $values = [ + ['userId' => 1, 'userName' => 'Batman', 'userAddress' => 'Gotham City'], + ['userId' => 2, 'userName' => 'Superman', 'userAddress' => 'Metropolis'], + ]; + $templateProcessor->cloneRowAndSetValues('userId', ); + +Will result in + +.. code-block:: clean + + +---+-------------+ + | 1 | Batman | + | |-------------+ + | | Gotham City | + +---+-------------+ + | 2 | Superman | + | |-------------+ + | | Metropolis | + +---+-------------+ + +applyXslStyleSheet +"""""""""""""""""" +Applies the XSL stylesheet passed to header part, footer part and main part + +.. code-block:: php + + $xslDomDocument = new \DOMDocument(); + $xslDomDocument->load('/path/to/my/stylesheet.xsl'); + $templateProcessor->applyXslStyleSheet($xslDomDocument); + +setComplexValue +""""""""""""""" +Raplaces a ${macro} with the ComplexType passed. +See ``Sample_40_TemplateSetComplexValue.php`` for examples. + +.. code-block:: php + + $inline = new TextRun(); + $inline->addText('by a red italic text', array('italic' => true, 'color' => 'red')); + $templateProcessor->setComplexValue('inline', $inline); + +setComplexBlock +""""""""""""""" +Raplaces a ${macro} with the ComplexType passed. +See ``Sample_40_TemplateSetComplexValue.php`` for examples. + +.. code-block:: php + + $table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP)); + $table->addRow(); + $table->addCell(150)->addText('Cell A1'); + $table->addCell(150)->addText('Cell A2'); + $table->addCell(150)->addText('Cell A3'); + $table->addRow(); + $table->addCell(150)->addText('Cell B1'); + $table->addCell(150)->addText('Cell B2'); + $table->addCell(150)->addText('Cell B3'); + $templateProcessor->setComplexBlock('table', $table); diff --git a/phpstan.neon b/phpstan.neon index 5ae6d0f2..666c63b9 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,7 +1,7 @@ includes: - vendor/phpstan/phpstan/conf/config.level1.neon parameters: - memory-limit: 200000 + memory-limit: 20000000 autoload_directories: - tests autoload_files: diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 015dd2ed..4a882446 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,8 +6,7 @@ convertNoticesToExceptions="true" convertWarningsToExceptions="true" processIsolation="false" - stopOnFailure="false" - syntaxCheck="false"> + stopOnFailure="false"> ./tests/PhpWord @@ -22,7 +21,8 @@ - + + \ No newline at end of file diff --git a/samples/Sample_01_SimpleText.php b/samples/Sample_01_SimpleText.php index 5a3393b3..8af44d20 100644 --- a/samples/Sample_01_SimpleText.php +++ b/samples/Sample_01_SimpleText.php @@ -1,6 +1,5 @@ addSection( ); $section->addText('This section uses other margins with folio papersize.'); +// The text of this section is vertically centered +$section = $phpWord->addSection( + array('vAlign' => VerticalJc::CENTER) +); +$section->addText('This section is vertically centered.'); + // New portrait section with Header & Footer $section = $phpWord->addSection( array( diff --git a/samples/Sample_04_Textrun.php b/samples/Sample_04_Textrun.php index 48978dd3..ecd0c88a 100644 --- a/samples/Sample_04_Textrun.php +++ b/samples/Sample_04_Textrun.php @@ -39,6 +39,9 @@ $textrun->addText(' Sample Object: '); $textrun->addObject('resources/_sheet.xls'); $textrun->addText(' Here is some more text. '); +$textrun = $section->addTextRun(); +$textrun->addText('This text is not visible.', array('hidden' => true)); + // Save file echo write($phpWord, basename(__FILE__, '.php'), $writers); if (!CLI) { diff --git a/samples/Sample_07_TemplateCloneRow.php b/samples/Sample_07_TemplateCloneRow.php index 81253d0a..42c53269 100644 --- a/samples/Sample_07_TemplateCloneRow.php +++ b/samples/Sample_07_TemplateCloneRow.php @@ -36,22 +36,46 @@ $templateProcessor->setValue('rowNumber#9', '9'); $templateProcessor->setValue('rowNumber#10', '10'); // Table with a spanned cell -$templateProcessor->cloneRow('userId', 3); +$values = array( + array( + 'userId' => 1, + 'userFirstName' => 'James', + 'userName' => 'Taylor', + 'userPhone' => '+1 428 889 773', + ), + array( + 'userId' => 2, + 'userFirstName' => 'Robert', + 'userName' => 'Bell', + 'userPhone' => '+1 428 889 774', + ), + array( + 'userId' => 3, + 'userFirstName' => 'Michael', + 'userName' => 'Ray', + 'userPhone' => '+1 428 889 775', + ), +); -$templateProcessor->setValue('userId#1', '1'); -$templateProcessor->setValue('userFirstName#1', 'James'); -$templateProcessor->setValue('userName#1', 'Taylor'); -$templateProcessor->setValue('userPhone#1', '+1 428 889 773'); +$templateProcessor->cloneRowAndSetValues('userId', $values); -$templateProcessor->setValue('userId#2', '2'); -$templateProcessor->setValue('userFirstName#2', 'Robert'); -$templateProcessor->setValue('userName#2', 'Bell'); -$templateProcessor->setValue('userPhone#2', '+1 428 889 774'); +//this is equivalent to cloning and settings values with cloneRowAndSetValues +// $templateProcessor->cloneRow('userId', 3); -$templateProcessor->setValue('userId#3', '3'); -$templateProcessor->setValue('userFirstName#3', 'Michael'); -$templateProcessor->setValue('userName#3', 'Ray'); -$templateProcessor->setValue('userPhone#3', '+1 428 889 775'); +// $templateProcessor->setValue('userId#1', '1'); +// $templateProcessor->setValue('userFirstName#1', 'James'); +// $templateProcessor->setValue('userName#1', 'Taylor'); +// $templateProcessor->setValue('userPhone#1', '+1 428 889 773'); + +// $templateProcessor->setValue('userId#2', '2'); +// $templateProcessor->setValue('userFirstName#2', 'Robert'); +// $templateProcessor->setValue('userName#2', 'Bell'); +// $templateProcessor->setValue('userPhone#2', '+1 428 889 774'); + +// $templateProcessor->setValue('userId#3', '3'); +// $templateProcessor->setValue('userFirstName#3', 'Michael'); +// $templateProcessor->setValue('userName#3', 'Ray'); +// $templateProcessor->setValue('userPhone#3', '+1 428 889 775'); echo date('H:i:s'), ' Saving the result document...', EOL; $templateProcessor->saveAs('results/Sample_07_TemplateCloneRow.docx'); diff --git a/samples/Sample_11_ReadWord97.php b/samples/Sample_11_ReadWord97.php index 68a30d36..a672269c 100644 --- a/samples/Sample_11_ReadWord97.php +++ b/samples/Sample_11_ReadWord97.php @@ -7,13 +7,8 @@ $source = "resources/{$name}.doc"; echo date('H:i:s'), " Reading contents from `{$source}`", EOL; $phpWord = \PhpOffice\PhpWord\IOFactory::load($source, 'MsDoc'); -// (Re)write contents -$writers = array('Word2007' => 'docx', 'ODText' => 'odt', 'RTF' => 'rtf'); -foreach ($writers as $writer => $extension) { - echo date('H:i:s'), " Write to {$writer} format", EOL; - $xmlWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, $writer); - $xmlWriter->save("{$name}.{$extension}"); - rename("{$name}.{$extension}", "results/{$name}.{$extension}"); +// Save file +echo write($phpWord, basename(__FILE__, '.php'), $writers); +if (!CLI) { + include_once 'Sample_Footer.php'; } - -include_once 'Sample_Footer.php'; diff --git a/samples/Sample_14_ListItem.php b/samples/Sample_14_ListItem.php index 774fd284..f40e9f6f 100644 --- a/samples/Sample_14_ListItem.php +++ b/samples/Sample_14_ListItem.php @@ -64,7 +64,7 @@ $section->addText('List with inline formatting.'); $listItemRun = $section->addListItemRun(); $listItemRun->addText('List item 1'); $listItemRun->addText(' in bold', array('bold' => true)); -$listItemRun = $section->addListItemRun(); +$listItemRun = $section->addListItemRun(1, $predefinedMultilevelStyle, $paragraphStyleName); $listItemRun->addText('List item 2'); $listItemRun->addText(' in italic', array('italic' => true)); $footnote = $listItemRun->addFootnote(); diff --git a/samples/Sample_23_TemplateBlock.php b/samples/Sample_23_TemplateBlock.php index ed986618..c0123f92 100644 --- a/samples/Sample_23_TemplateBlock.php +++ b/samples/Sample_23_TemplateBlock.php @@ -14,7 +14,7 @@ $templateProcessor->deleteBlock('DELETEME'); echo date('H:i:s'), ' Saving the result document...', EOL; $templateProcessor->saveAs('results/Sample_23_TemplateBlock.docx'); -echo getEndingNotes(array('Word2007' => 'docx'), 'results/Sample_23_TemplateBlock.docx'); +echo getEndingNotes(array('Word2007' => 'docx'), 'Sample_23_TemplateBlock'); if (!CLI) { include_once 'Sample_Footer.php'; } diff --git a/samples/Sample_26_Html.php b/samples/Sample_26_Html.php index e1823c43..6bd926fe 100644 --- a/samples/Sample_26_Html.php +++ b/samples/Sample_26_Html.php @@ -74,7 +74,7 @@ $html .= ' - +
12
12
This is bold text6
'; @@ -89,6 +89,9 @@ $html .= '
Cell in parent table
'; +$html .= '

The text below is not visible, click on show/hide to reveil it:

'; +$html .= '

This is hidden text

'; + \PhpOffice\PhpWord\Shared\Html::addHtml($section, $html, false, false); // Save file diff --git a/samples/Sample_36_RTL.php b/samples/Sample_36_RTL.php index 615557d7..ca93b14d 100644 --- a/samples/Sample_36_RTL.php +++ b/samples/Sample_36_RTL.php @@ -14,6 +14,29 @@ $textrun->addText('This is a Left to Right paragraph.'); $textrun = $section->addTextRun(array('alignment' => \PhpOffice\PhpWord\SimpleType\Jc::END)); $textrun->addText('سلام این یک پاراگراف راست به چپ است', array('rtl' => true)); +$section->addText('Table visually presented as RTL'); +$style = array('rtl' => true, 'size' => 12); +$tableStyle = array('borderSize' => 6, 'borderColor' => '000000', 'width' => 5000, 'unit' => \PhpOffice\PhpWord\SimpleType\TblWidth::PERCENT, 'bidiVisual' => true); + +$table = $section->addTable($tableStyle); +$cellHCentered = array('alignment' => \PhpOffice\PhpWord\SimpleType\Jc::CENTER); +$cellHEnd = array('alignment' => \PhpOffice\PhpWord\SimpleType\Jc::END); +$cellVCentered = array('valign' => \PhpOffice\PhpWord\Style\Cell::VALIGN_CENTER); + +//Vidually bidirectinal table +$table->addRow(); +$cell = $table->addCell(500, $cellVCentered); +$textrun = $cell->addTextRun($cellHCentered); +$textrun->addText('ردیف', $style); + +$cell = $table->addCell(11000); +$textrun = $cell->addTextRun($cellHEnd); +$textrun->addText('سوالات', $style); + +$cell = $table->addCell(500, $cellVCentered); +$textrun = $cell->addTextRun($cellHCentered); +$textrun->addText('بارم', $style); + // Save file echo write($phpWord, basename(__FILE__, '.php'), $writers); if (!CLI) { diff --git a/samples/Sample_40_TemplateSetComplexValue.php b/samples/Sample_40_TemplateSetComplexValue.php new file mode 100644 index 00000000..094823f7 --- /dev/null +++ b/samples/Sample_40_TemplateSetComplexValue.php @@ -0,0 +1,45 @@ +addText('This title has been set ', array('bold' => true, 'italic' => true, 'color' => 'blue')); +$title->addText('dynamically', array('bold' => true, 'italic' => true, 'color' => 'red', 'underline' => 'single')); +$templateProcessor->setComplexBlock('title', $title); + +$inline = new TextRun(); +$inline->addText('by a red italic text', array('italic' => true, 'color' => 'red')); +$templateProcessor->setComplexValue('inline', $inline); + +$table = new Table(array('borderSize' => 12, 'borderColor' => 'green', 'width' => 6000, 'unit' => TblWidth::TWIP)); +$table->addRow(); +$table->addCell(150)->addText('Cell A1'); +$table->addCell(150)->addText('Cell A2'); +$table->addCell(150)->addText('Cell A3'); +$table->addRow(); +$table->addCell(150)->addText('Cell B1'); +$table->addCell(150)->addText('Cell B2'); +$table->addCell(150)->addText('Cell B3'); +$templateProcessor->setComplexBlock('table', $table); + +$field = new Field('DATE', array('dateformat' => 'dddd d MMMM yyyy H:mm:ss'), array('PreserveFormat')); +$templateProcessor->setComplexValue('field', $field); + +// $link = new Link('https://github.com/PHPOffice/PHPWord'); +// $templateProcessor->setComplexValue('link', $link); + +echo date('H:i:s'), ' Saving the result document...', EOL; +$templateProcessor->saveAs('results/Sample_40_TemplateSetComplexValue.docx'); + +echo getEndingNotes(array('Word2007' => 'docx'), 'results/Sample_40_TemplateSetComplexValue.docx'); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/samples/index.php b/samples/index.php index 3dbc09ff..20b56b83 100644 --- a/samples/index.php +++ b/samples/index.php @@ -22,7 +22,7 @@ if (!CLI) { Read the Docs

-Requirement check:'; diff --git a/samples/resources/Sample_11_ReadWord2007.docx b/samples/resources/Sample_11_ReadWord2007.docx index 406cf1e1..f6526360 100644 Binary files a/samples/resources/Sample_11_ReadWord2007.docx and b/samples/resources/Sample_11_ReadWord2007.docx differ diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index 5593298b..b4af3812 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -11,5 +11,15 @@

Ordered (numbered) list:

  1. Item 1
  2. Item 2
+ +

Double height

+ +

Includes images

+ + + + + + diff --git a/samples/resources/Sample_40_TemplateSetComplexValue.docx b/samples/resources/Sample_40_TemplateSetComplexValue.docx new file mode 100644 index 00000000..7265908e Binary files /dev/null and b/samples/resources/Sample_40_TemplateSetComplexValue.docx differ diff --git a/samples/results/.gitignore b/samples/results/.gitignore old mode 100644 new mode 100755 diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 00000000..7741cfb4 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,17 @@ +# must be unique in a given SonarQube instance +sonar.projectKey=phpoffice:phpword +# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1. +sonar.projectName=PHPWord +sonar.projectVersion=0.16 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +# This property is optional if sonar.modules is set. +sonar.sources=src +sonar.tests=tests +sonar.php.coverage.reportPaths=build/logs/clover.xml +sonar.php.tests.reportPath=build/logs/logfile.xml + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 + +sonar.host.url=http://localhost:9000 \ No newline at end of file diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index ec990720..5e058667 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -31,11 +31,11 @@ namespace PhpOffice\PhpWord\Element; * @method Footnote addFootnote(mixed $pStyle = null) * @method Endnote addEndnote(mixed $pStyle = null) * @method CheckBox addCheckBox(string $name, $text, mixed $fStyle = null, mixed $pStyle = null) - * @method Title addTitle(string $text, int $depth = 1) + * @method Title addTitle(mixed $text, int $depth = 1) * @method TOC addTOC(mixed $fontStyle = null, mixed $tocStyle = null, int $minDepth = 1, int $maxDepth = 9) * @method PageBreak addPageBreak() * @method Table addTable(mixed $style = null) - * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false) + * @method Image addImage(string $source, mixed $style = null, bool $isWatermark = false, $name = null) * @method OLEObject addOLEObject(string $source, mixed $style = null) * @method TextBox addTextBox(mixed $style = null) * @method Field addField(string $type = null, array $properties = array(), array $options = array(), mixed $text = null) diff --git a/src/PhpWord/Element/AbstractElement.php b/src/PhpWord/Element/AbstractElement.php index 5ff85b8f..e3e54ed4 100644 --- a/src/PhpWord/Element/AbstractElement.php +++ b/src/PhpWord/Element/AbstractElement.php @@ -347,7 +347,7 @@ abstract class AbstractElement * * @param \PhpOffice\PhpWord\Element\AbstractElement $container */ - public function setParentContainer(AbstractElement $container) + public function setParentContainer(self $container) { $this->parentContainer = substr(get_class($container), strrpos(get_class($container), '\\') + 1); $this->parent = $container; diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index 03637067..bae87ff5 100644 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -65,6 +65,13 @@ class Image extends AbstractElement */ private $watermark; + /** + * Name of image + * + * @var string + */ + private $name; + /** * Image type * @@ -127,15 +134,17 @@ class Image extends AbstractElement * @param string $source * @param mixed $style * @param bool $watermark + * @param string $name * * @throws \PhpOffice\PhpWord\Exception\InvalidImageException * @throws \PhpOffice\PhpWord\Exception\UnsupportedImageTypeException */ - public function __construct($source, $style = null, $watermark = false) + public function __construct($source, $style = null, $watermark = false, $name = null) { $this->source = $source; - $this->setIsWatermark($watermark); $this->style = $this->setNewStyle(new ImageStyle(), $style, true); + $this->setIsWatermark($watermark); + $this->setName($name); $this->checkImage(); } @@ -170,6 +179,26 @@ class Image extends AbstractElement return $this->sourceType; } + /** + * Sets the image name + * + * @param string $value + */ + public function setName($value) + { + $this->name = $value; + } + + /** + * Get image name + * + * @return null|string + */ + public function getName() + { + return $this->name; + } + /** * Get image media ID * diff --git a/src/PhpWord/Element/OLEObject.php b/src/PhpWord/Element/OLEObject.php index c0c7f217..1a17b747 100644 --- a/src/PhpWord/Element/OLEObject.php +++ b/src/PhpWord/Element/OLEObject.php @@ -83,7 +83,7 @@ class OLEObject extends AbstractElement $this->style = $this->setNewStyle(new ImageStyle(), $style, true); $this->icon = realpath(__DIR__ . "/../resources/{$ext}.png"); - return $this; + return; } throw new InvalidObjectException(); diff --git a/src/PhpWord/Element/PreserveText.php b/src/PhpWord/Element/PreserveText.php index 1ce2dcdd..374f1a99 100644 --- a/src/PhpWord/Element/PreserveText.php +++ b/src/PhpWord/Element/PreserveText.php @@ -29,7 +29,7 @@ class PreserveText extends AbstractElement /** * Text content * - * @var string + * @var string|array */ private $text; @@ -64,8 +64,6 @@ class PreserveText extends AbstractElement if (isset($matches[0])) { $this->text = $matches; } - - return $this; } /** @@ -91,7 +89,7 @@ class PreserveText extends AbstractElement /** * Get Text content * - * @return string + * @return string|array */ public function getText() { diff --git a/src/PhpWord/Element/SDT.php b/src/PhpWord/Element/SDT.php index a866d1bd..5548f768 100644 --- a/src/PhpWord/Element/SDT.php +++ b/src/PhpWord/Element/SDT.php @@ -90,7 +90,7 @@ class SDT extends Text */ public function setType($value) { - $enum = array('comboBox', 'dropDownList', 'date'); + $enum = array('plainText', 'comboBox', 'dropDownList', 'date'); $this->type = $this->setEnumVal($value, $enum, 'comboBox'); return $this; diff --git a/src/PhpWord/Element/Section.php b/src/PhpWord/Element/Section.php index d612fc01..b495ef7b 100644 --- a/src/PhpWord/Element/Section.php +++ b/src/PhpWord/Element/Section.php @@ -59,14 +59,16 @@ class Section extends AbstractContainer * Create new instance * * @param int $sectionCount - * @param array $style + * @param null|array|\PhpOffice\PhpWord\Style $style */ public function __construct($sectionCount, $style = null) { $this->sectionId = $sectionCount; $this->setDocPart($this->container, $this->sectionId); - $this->style = new SectionStyle(); - $this->setStyle($style); + if (null === $style) { + $style = new SectionStyle(); + } + $this->style = $this->setNewStyle(new SectionStyle(), $style); } /** diff --git a/src/PhpWord/Element/Table.php b/src/PhpWord/Element/Table.php index 10c4db69..44fe3744 100644 --- a/src/PhpWord/Element/Table.php +++ b/src/PhpWord/Element/Table.php @@ -135,18 +135,40 @@ class Table extends AbstractElement public function countColumns() { $columnCount = 0; - if (is_array($this->rows)) { - $rowCount = count($this->rows); - for ($i = 0; $i < $rowCount; $i++) { - /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ - $row = $this->rows[$i]; - $cellCount = count($row->getCells()); - if ($columnCount < $cellCount) { - $columnCount = $cellCount; - } + + $rowCount = count($this->rows); + for ($i = 0; $i < $rowCount; $i++) { + /** @var \PhpOffice\PhpWord\Element\Row $row Type hint */ + $row = $this->rows[$i]; + $cellCount = count($row->getCells()); + if ($columnCount < $cellCount) { + $columnCount = $cellCount; } } return $columnCount; } + + /** + * The first declared cell width for each column + * + * @return int[] + */ + public function findFirstDefinedCellWidths() + { + $cellWidths = array(); + + foreach ($this->rows as $row) { + $cells = $row->getCells(); + if (count($cells) <= count($cellWidths)) { + continue; + } + $cellWidths = array(); + foreach ($cells as $cell) { + $cellWidths[] = $cell->getWidth(); + } + } + + return $cellWidths; + } } diff --git a/src/PhpWord/Element/Title.php b/src/PhpWord/Element/Title.php index 569cea92..d01f7f33 100644 --- a/src/PhpWord/Element/Title.php +++ b/src/PhpWord/Element/Title.php @@ -61,14 +61,12 @@ class Title extends AbstractElement */ public function __construct($text, $depth = 1) { - if (isset($text)) { - if (is_string($text)) { - $this->text = CommonText::toUTF8($text); - } elseif ($text instanceof TextRun) { - $this->text = $text; - } else { - throw new \InvalidArgumentException('Invalid text, should be a string or a TextRun'); - } + if (is_string($text)) { + $this->text = CommonText::toUTF8($text); + } elseif ($text instanceof TextRun) { + $this->text = $text; + } else { + throw new \InvalidArgumentException('Invalid text, should be a string or a TextRun'); } $this->depth = $depth; @@ -76,8 +74,6 @@ class Title extends AbstractElement if (array_key_exists($styleName, Style::getStyles())) { $this->style = str_replace('_', '', $styleName); } - - return $this; } /** diff --git a/src/PhpWord/Element/TrackChange.php b/src/PhpWord/Element/TrackChange.php index 410ffb7c..91c221f2 100644 --- a/src/PhpWord/Element/TrackChange.php +++ b/src/PhpWord/Element/TrackChange.php @@ -58,13 +58,13 @@ class TrackChange extends AbstractContainer * * @param string $changeType * @param string $author - * @param null|int|\DateTime $date + * @param null|int|bool|\DateTime $date */ public function __construct($changeType = null, $author = null, $date = null) { $this->changeType = $changeType; $this->author = $author; - if ($date !== null) { + if ($date !== null && $date !== false) { $this->date = ($date instanceof \DateTime) ? $date : new \DateTime('@' . $date); } } diff --git a/src/PhpWord/Metadata/DocInfo.php b/src/PhpWord/Metadata/DocInfo.php index 27ef89ae..8a2f7d31 100644 --- a/src/PhpWord/Metadata/DocInfo.php +++ b/src/PhpWord/Metadata/DocInfo.php @@ -507,7 +507,7 @@ class DocInfo case 'date': // Date return strtotime($propertyValue); case 'bool': // Boolean - return ($propertyValue == 'true') ? true : false; + return $propertyValue == 'true'; } return $propertyValue; diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 584ed83e..15aa3ff1 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -17,7 +17,7 @@ namespace PhpOffice\PhpWord\Metadata; -use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; +use PhpOffice\Common\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\SimpleType\DocProtect; /** @@ -113,7 +113,7 @@ class Protection /** * Set password * - * @param $password + * @param string $password * @return self */ public function setPassword($password) @@ -136,7 +136,7 @@ class Protection /** * Set count for hash iterations * - * @param $spinCount + * @param int $spinCount * @return self */ public function setSpinCount($spinCount) @@ -159,7 +159,7 @@ class Protection /** * Set algorithm * - * @param $algorithm + * @param string $algorithm * @return self */ public function setAlgorithm($algorithm) diff --git a/src/PhpWord/PhpWord.php b/src/PhpWord/PhpWord.php index b5cc0c51..a78df2c4 100644 --- a/src/PhpWord/PhpWord.php +++ b/src/PhpWord/PhpWord.php @@ -35,10 +35,10 @@ use PhpOffice\PhpWord\Exception\Exception; * @method int addChart(Element\Chart $chart) * @method int addComment(Element\Comment $comment) * - * @method Style\Paragraph addParagraphStyle(string $styleName, array $styles) + * @method Style\Paragraph addParagraphStyle(string $styleName, mixed $styles) * @method Style\Font addFontStyle(string $styleName, mixed $fontStyle, mixed $paragraphStyle = null) * @method Style\Font addLinkStyle(string $styleName, mixed $styles) - * @method Style\Font addTitleStyle(int $depth, mixed $fontStyle, mixed $paragraphStyle = null) + * @method Style\Font addTitleStyle(mixed $depth, mixed $fontStyle, mixed $paragraphStyle = null) * @method Style\Table addTableStyle(string $styleName, mixed $styleTable, mixed $styleFirstRow = null) * @method Style\Numbering addNumberingStyle(string $styleName, mixed $styles) */ diff --git a/src/PhpWord/Reader/MsDoc.php b/src/PhpWord/Reader/MsDoc.php index d4945229..1a9e4988 100644 --- a/src/PhpWord/Reader/MsDoc.php +++ b/src/PhpWord/Reader/MsDoc.php @@ -1619,7 +1619,7 @@ class MsDoc extends AbstractReader implements ReaderInterface break; // sprmCFData case 0x06: - $sprmCFData = dechex($operand) == 0x00 ? false : true; + $sprmCFData = dechex($operand) != 0x00; break; // sprmCFItalic case 0x36: @@ -2185,6 +2185,8 @@ class MsDoc extends AbstractReader implements ReaderInterface $sprmCPicLocation += $embeddedBlipRH['recLen']; break; + case self::OFFICEARTBLIPPNG: + break; default: // print_r(dechex($embeddedBlipRH['recType'])); } diff --git a/src/PhpWord/Reader/Word2007.php b/src/PhpWord/Reader/Word2007.php index deed3ce3..52030ef8 100644 --- a/src/PhpWord/Reader/Word2007.php +++ b/src/PhpWord/Reader/Word2007.php @@ -62,6 +62,9 @@ class Word2007 extends AbstractReader implements ReaderInterface foreach ($steps as $step) { $stepPart = $step['stepPart']; $stepItems = $step['stepItems']; + if (!isset($relationships[$stepPart])) { + continue; + } foreach ($relationships[$stepPart] as $relItem) { $relType = $relItem['type']; if (isset($stepItems[$relType])) { diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 6cdf2b3a..bb4a3a49 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -261,6 +261,20 @@ abstract class AbstractPart } $parent->addImage($imageSource); } + } elseif ($node->nodeName == 'w:drawing') { + // Office 2011 Image + $xmlReader->registerNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'); + $xmlReader->registerNamespace('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'); + $xmlReader->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture'); + $xmlReader->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + + $name = $xmlReader->getAttribute('name', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:nvPicPr/pic:cNvPr'); + $embedId = $xmlReader->getAttribute('r:embed', $node, 'wp:inline/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip'); + $target = $this->getMediaTarget($docPart, $embedId); + if (!is_null($target)) { + $imageSource = "zip://{$this->docFile}#{$target}"; + $parent->addImage($imageSource, null, false, $name); + } } elseif ($node->nodeName == 'w:object') { // Object $rId = $xmlReader->getAttribute('r:id', $node, 'o:OLEObject'); @@ -283,6 +297,8 @@ abstract class AbstractPart $target = $this->getMediaTarget($docPart, $rId); if (!is_null($target)) { $parent->addLink($target, $textContent, $fontStyle, $paragraphStyle); + } else { + $parent->addText($textContent, $fontStyle, $paragraphStyle); } } else { /** @var AbstractElement $element */ @@ -322,7 +338,7 @@ abstract class AbstractPart } elseif ('w:tr' == $tblNode->nodeName) { // Row $rowHeight = $xmlReader->getAttribute('w:val', $tblNode, 'w:trPr/w:trHeight'); $rowHRule = $xmlReader->getAttribute('w:hRule', $tblNode, 'w:trPr/w:trHeight'); - $rowHRule = $rowHRule == 'exact' ? true : false; + $rowHRule = $rowHRule == 'exact'; $rowStyle = array( 'tblHeader' => $xmlReader->elementExists('w:trPr/w:tblHeader', $tblNode), 'cantSplit' => $xmlReader->elementExists('w:trPr/w:cantSplit', $tblNode), @@ -430,6 +446,7 @@ abstract class AbstractPart 'rtl' => array(self::READ_TRUE, 'w:rtl'), 'lang' => array(self::READ_VALUE, 'w:lang'), 'position' => array(self::READ_VALUE, 'w:position'), + 'hidden' => array(self::READ_TRUE, 'w:vanish'), ); return $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); @@ -466,6 +483,7 @@ abstract class AbstractPart $styleDefs["border{$ucfSide}Style"] = array(self::READ_VALUE, "w:tblBorders/w:$side", 'w:val'); } $styleDefs['layout'] = array(self::READ_VALUE, 'w:tblLayout', 'w:type'); + $styleDefs['bidiVisual'] = array(self::READ_TRUE, 'w:bidiVisual'); $styleDefs['cellSpacing'] = array(self::READ_VALUE, 'w:tblCellSpacing', 'w:w'); $style = $this->readStyleDefs($xmlReader, $styleNode, $styleDefs); diff --git a/src/PhpWord/Reader/Word2007/Document.php b/src/PhpWord/Reader/Word2007/Document.php index 4e37541b..f0d1194a 100644 --- a/src/PhpWord/Reader/Word2007/Document.php +++ b/src/PhpWord/Reader/Word2007/Document.php @@ -106,6 +106,7 @@ class Document extends AbstractPart { $styleDefs = array( 'breakType' => array(self::READ_VALUE, 'w:type'), + 'vAlign' => array(self::READ_VALUE, 'w:vAlign'), 'pageSizeW' => array(self::READ_VALUE, 'w:pgSz', 'w:w'), 'pageSizeH' => array(self::READ_VALUE, 'w:pgSz', 'w:h'), 'orientation' => array(self::READ_VALUE, 'w:pgSz', 'w:orient'), diff --git a/src/PhpWord/Reader/Word2007/Settings.php b/src/PhpWord/Reader/Word2007/Settings.php index dbf34623..3084943b 100644 --- a/src/PhpWord/Reader/Word2007/Settings.php +++ b/src/PhpWord/Reader/Word2007/Settings.php @@ -81,7 +81,7 @@ class Settings extends AbstractPart * * @param XMLReader $xmlReader * @param PhpWord $phpWord - * @param \DOMNode $node + * @param \DOMElement $node */ protected function setThemeFontLang(XMLReader $xmlReader, PhpWord $phpWord, \DOMElement $node) { @@ -102,7 +102,7 @@ class Settings extends AbstractPart * * @param XMLReader $xmlReader * @param PhpWord $phpWord - * @param \DOMNode $node + * @param \DOMElement $node */ protected function setDocumentProtection(XMLReader $xmlReader, PhpWord $phpWord, \DOMElement $node) { @@ -119,7 +119,7 @@ class Settings extends AbstractPart * * @param XMLReader $xmlReader * @param PhpWord $phpWord - * @param \DOMNode $node + * @param \DOMElement $node */ protected function setProofState(XMLReader $xmlReader, PhpWord $phpWord, \DOMElement $node) { @@ -141,7 +141,7 @@ class Settings extends AbstractPart * * @param XMLReader $xmlReader * @param PhpWord $phpWord - * @param \DOMNode $node + * @param \DOMElement $node */ protected function setZoom(XMLReader $xmlReader, PhpWord $phpWord, \DOMElement $node) { @@ -158,7 +158,7 @@ class Settings extends AbstractPart * * @param XMLReader $xmlReader * @param PhpWord $phpWord - * @param \DOMNode $node + * @param \DOMElement $node */ protected function setRevisionView(XMLReader $xmlReader, PhpWord $phpWord, \DOMElement $node) { diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index f343ad92..97f29b43 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -64,11 +64,12 @@ class Styles extends AbstractPart if ($nodes->length > 0) { foreach ($nodes as $node) { $type = $xmlReader->getAttribute('w:type', $node); - $name = $xmlReader->getAttribute('w:styleId', $node); + $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); if (is_null($name)) { - $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); + $name = $xmlReader->getAttribute('w:styleId', $node); } - preg_match('/Heading(\d)/', $name, $headingMatches); + $headingMatches = array(); + preg_match('/Heading\s*(\d)/i', $name, $headingMatches); // $default = ($xmlReader->getAttribute('w:default', $node) == 1); switch ($type) { case 'paragraph': diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index c53f0030..7008ac5d 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -198,7 +198,7 @@ class Converter /** * Convert point to pixel * - * @param int $point + * @param float $point * @return float */ public static function pointToPixel($point = 1) @@ -217,6 +217,17 @@ class Converter return round($point / self::INCH_TO_POINT * self::INCH_TO_PIXEL * self::PIXEL_TO_EMU); } + /** + * Convert point to cm + * + * @param float $point + * @return float + */ + public static function pointToCm($point = 1) + { + return $point / self::INCH_TO_POINT * self::INCH_TO_CM; + } + /** * Convert EMU to pixel * @@ -299,6 +310,7 @@ class Converter if ($value == '0') { return 0; } + $matches = array(); if (preg_match('/^[+-]?([0-9]+\.?[0-9]*)?(px|em|ex|%|in|cm|mm|pt|pc)$/i', $value, $matches)) { $size = $matches[1]; $unit = $matches[2]; @@ -332,4 +344,37 @@ class Converter { return self::pointToTwip(self::cssToPoint($value)); } + + /** + * Transforms a size in CSS format (eg. 10px, 10px, ...) to pixel + * + * @param string $value + * @return float + */ + public static function cssToPixel($value) + { + return self::pointToPixel(self::cssToPoint($value)); + } + + /** + * Transforms a size in CSS format (eg. 10px, 10px, ...) to cm + * + * @param string $value + * @return float + */ + public static function cssToCm($value) + { + return self::pointToCm(self::cssToPoint($value)); + } + + /** + * Transforms a size in CSS format (eg. 10px, 10px, ...) to emu + * + * @param string $value + * @return float + */ + public static function cssToEmu($value) + { + return self::pointToEmu(self::cssToPoint($value)); + } } diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index cbdcecd5..89881822 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -20,8 +20,10 @@ namespace PhpOffice\PhpWord\Shared; use PhpOffice\PhpWord\Element\AbstractContainer; use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; +use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; +use PhpOffice\PhpWord\Style\Paragraph; /** * Common Html functions @@ -32,6 +34,7 @@ class Html { private static $listIndex = 0; private static $xpath; + private static $options; /** * Add HTML parts. @@ -44,13 +47,17 @@ class Html * @param string $html The code to parse * @param bool $fullHTML If it's a full HTML, no need to add 'body' tag * @param bool $preserveWhiteSpace If false, the whitespaces between nodes will be removed + * @param array $options: + * + IMG_SRC_SEARCH: optional to speed up images loading from remote url when files can be found locally + * + IMG_SRC_REPLACE: optional to speed up images loading from remote url when files can be found locally */ - public static function addHtml($element, $html, $fullHTML = false, $preserveWhiteSpace = true) + public static function addHtml($element, $html, $fullHTML = false, $preserveWhiteSpace = true, $options = null) { /* * @todo parse $stylesheet for default styles. Should result in an array based on id, class and element, * which could be applied when such an element occurs in the parseNode function. */ + self::$options = $options; // Preprocess: remove all line ends, decode HTML entity, // fix ampersand and angle brackets and add body tag for HTML fragments @@ -65,10 +72,11 @@ class Html } // Load DOM + libxml_disable_entity_loader(true); $dom = new \DOMDocument(); $dom->preserveWhiteSpace = $preserveWhiteSpace; $dom->loadXML($html); - self::$xpath = new \DOMXpath($dom); + self::$xpath = new \DOMXPath($dom); $node = $dom->getElementsByTagName('body'); self::parseNode($node->item(0), $element); @@ -141,6 +149,7 @@ class Html 'sup' => array('Property', null, null, $styles, null, 'superScript', true), 'sub' => array('Property', null, null, $styles, null, 'subScript', true), 'span' => array('Span', $node, null, $styles, null, null, null), + 'font' => array('Span', $node, null, $styles, null, null, null), 'table' => array('Table', $node, $element, $styles, null, null, null), 'tr' => array('Row', $node, $element, $styles, null, null, null), 'td' => array('Cell', $node, $element, $styles, null, null, null), @@ -506,6 +515,9 @@ class Html case 'text-align': $styles['alignment'] = self::mapAlign($cValue); break; + case 'display': + $styles['hidden'] = $cValue === 'none' || $cValue === 'hidden'; + break; case 'direction': $styles['rtl'] = $cValue === 'rtl'; break; @@ -523,18 +535,27 @@ class Html $styles['bgColor'] = trim($cValue, '#'); break; case 'line-height': - if (preg_match('/([0-9]+[a-z]+)/', $cValue, $matches)) { + $matches = array(); + if (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $cValue, $matches)) { + //matches number with a unit, e.g. 12px, 15pt, 20mm, ... $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::EXACT; - $spacing = Converter::cssToTwip($matches[1]) / \PhpOffice\PhpWord\Style\Paragraph::LINE_HEIGHT; + $spacing = Converter::cssToTwip($matches[1]); } elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) { + //matches percentages $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO; - $spacing = ((int) $matches[1]) / 100; + //we are subtracting 1 line height because the Spacing writer is adding one line + $spacing = ((((int) $matches[1]) / 100) * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT; } else { + //any other, wich is a multiplier. E.g. 1.2 $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO; - $spacing = $cValue; + //we are subtracting 1 line height because the Spacing writer is adding one line + $spacing = ($cValue * Paragraph::LINE_HEIGHT) - Paragraph::LINE_HEIGHT; } $styles['spacingLineRule'] = $spacingLineRule; - $styles['lineHeight'] = $spacing; + $styles['line-spacing'] = $spacing; + break; + case 'letter-spacing': + $styles['letter-spacing'] = Converter::cssToTwip($cValue); break; case 'text-indent': $styles['indentation']['firstLine'] = Converter::cssToTwip($cValue); @@ -560,7 +581,7 @@ class Html $styles['spaceAfter'] = Converter::cssToPoint($cValue); break; case 'border-color': - $styles['color'] = trim($cValue, '#'); + self::mapBorderColor($styles, $cValue); break; case 'border-width': $styles['borderSize'] = Converter::cssToPoint($cValue); @@ -648,7 +669,52 @@ class Html break; } } - $newElement = $element->addImage($src, $style); + $originSrc = $src; + if (strpos($src, 'data:image') !== false) { + $tmpDir = Settings::getTempDir() . '/'; + + $match = array(); + preg_match('/data:image\/(\w+);base64,(.+)/', $src, $match); + + $src = $imgFile = $tmpDir . uniqid() . '.' . $match[1]; + + $ifp = fopen($imgFile, 'wb'); + + if ($ifp !== false) { + fwrite($ifp, base64_decode($match[2])); + fclose($ifp); + } + } + $src = urldecode($src); + + if (!is_file($src) + && !is_null(self::$options) + && isset(self::$options['IMG_SRC_SEARCH']) + && isset(self::$options['IMG_SRC_REPLACE'])) { + $src = str_replace(self::$options['IMG_SRC_SEARCH'], self::$options['IMG_SRC_REPLACE'], $src); + } + + if (!is_file($src)) { + if ($imgBlob = @file_get_contents($src)) { + $tmpDir = Settings::getTempDir() . '/'; + $match = array(); + preg_match('/.+\.(\w+)$/', $src, $match); + $src = $tmpDir . uniqid() . '.' . $match[1]; + + $ifp = fopen($src, 'wb'); + + if ($ifp !== false) { + fwrite($ifp, $imgBlob); + fclose($ifp); + } + } + } + + if (is_file($src)) { + $newElement = $element->addImage($src, $style); + } else { + throw new \Exception("Could not load image $originSrc"); + } return $newElement; } @@ -672,6 +738,20 @@ class Html } } + private static function mapBorderColor(&$styles, $cssBorderColor) + { + $numColors = substr_count($cssBorderColor, '#'); + if ($numColors === 1) { + $styles['borderColor'] = trim($cssBorderColor, '#'); + } elseif ($numColors > 1) { + $colors = explode(' ', $cssBorderColor); + $borders = array('borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'); + for ($i = 0; $i < min(4, $numColors, count($colors)); $i++) { + $styles[$borders[$i]] = trim($colors[$i], '#'); + } + } + } + /** * Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc * @@ -690,8 +770,6 @@ class Html default: return Jc::START; } - - return null; } /** diff --git a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php b/src/PhpWord/Shared/Microsoft/PasswordEncoder.php deleted file mode 100644 index fc0c7ecd..00000000 --- a/src/PhpWord/Shared/Microsoft/PasswordEncoder.php +++ /dev/null @@ -1,235 +0,0 @@ - array(1, 'md2'), - self::ALGORITHM_MD4 => array(2, 'md4'), - self::ALGORITHM_MD5 => array(3, 'md5'), - self::ALGORITHM_SHA_1 => array(4, 'sha1'), - self::ALGORITHM_MAC => array(5, ''), // 'mac' -> not possible with hash() - self::ALGORITHM_RIPEMD => array(6, 'ripemd'), - self::ALGORITHM_RIPEMD_160 => array(7, 'ripemd160'), - self::ALGORITHM_HMAC => array(9, ''), //'hmac' -> not possible with hash() - self::ALGORITHM_SHA_256 => array(12, 'sha256'), - self::ALGORITHM_SHA_384 => array(13, 'sha384'), - self::ALGORITHM_SHA_512 => array(14, 'sha512'), - ); - - private static $initialCodeArray = array( - 0xE1F0, - 0x1D0F, - 0xCC9C, - 0x84C0, - 0x110C, - 0x0E10, - 0xF1CE, - 0x313E, - 0x1872, - 0xE139, - 0xD40F, - 0x84F9, - 0x280C, - 0xA96A, - 0x4EC3, - ); - - private static $encryptionMatrix = array( - array(0xAEFC, 0x4DD9, 0x9BB2, 0x2745, 0x4E8A, 0x9D14, 0x2A09), - array(0x7B61, 0xF6C2, 0xFDA5, 0xEB6B, 0xC6F7, 0x9DCF, 0x2BBF), - array(0x4563, 0x8AC6, 0x05AD, 0x0B5A, 0x16B4, 0x2D68, 0x5AD0), - array(0x0375, 0x06EA, 0x0DD4, 0x1BA8, 0x3750, 0x6EA0, 0xDD40), - array(0xD849, 0xA0B3, 0x5147, 0xA28E, 0x553D, 0xAA7A, 0x44D5), - array(0x6F45, 0xDE8A, 0xAD35, 0x4A4B, 0x9496, 0x390D, 0x721A), - array(0xEB23, 0xC667, 0x9CEF, 0x29FF, 0x53FE, 0xA7FC, 0x5FD9), - array(0x47D3, 0x8FA6, 0x0F6D, 0x1EDA, 0x3DB4, 0x7B68, 0xF6D0), - array(0xB861, 0x60E3, 0xC1C6, 0x93AD, 0x377B, 0x6EF6, 0xDDEC), - array(0x45A0, 0x8B40, 0x06A1, 0x0D42, 0x1A84, 0x3508, 0x6A10), - array(0xAA51, 0x4483, 0x8906, 0x022D, 0x045A, 0x08B4, 0x1168), - array(0x76B4, 0xED68, 0xCAF1, 0x85C3, 0x1BA7, 0x374E, 0x6E9C), - array(0x3730, 0x6E60, 0xDCC0, 0xA9A1, 0x4363, 0x86C6, 0x1DAD), - array(0x3331, 0x6662, 0xCCC4, 0x89A9, 0x0373, 0x06E6, 0x0DCC), - array(0x1021, 0x2042, 0x4084, 0x8108, 0x1231, 0x2462, 0x48C4), - ); - - private static $passwordMaxLength = 15; - - /** - * Create a hashed password that MS Word will be able to work with - * @see https://blogs.msdn.microsoft.com/vsod/2010/04/05/how-to-set-the-editing-restrictions-in-word-using-open-xml-sdk-2-0/ - * - * @param string $password - * @param string $algorithmName - * @param string $salt - * @param int $spinCount - * @return string - */ - public static function hashPassword($password, $algorithmName = self::ALGORITHM_SHA_1, $salt = null, $spinCount = 10000) - { - $origEncoding = mb_internal_encoding(); - mb_internal_encoding('UTF-8'); - - $password = mb_substr($password, 0, min(self::$passwordMaxLength, mb_strlen($password))); - - // Get the single-byte values by iterating through the Unicode characters of the truncated password. - // For each character, if the low byte is not equal to 0, take it. Otherwise, take the high byte. - $passUtf8 = mb_convert_encoding($password, 'UCS-2LE', 'UTF-8'); - $byteChars = array(); - - for ($i = 0; $i < mb_strlen($password); $i++) { - $byteChars[$i] = ord(substr($passUtf8, $i * 2, 1)); - - if ($byteChars[$i] == 0) { - $byteChars[$i] = ord(substr($passUtf8, $i * 2 + 1, 1)); - } - } - - // build low-order word and hig-order word and combine them - $combinedKey = self::buildCombinedKey($byteChars); - // build reversed hexadecimal string - $hex = str_pad(strtoupper(dechex($combinedKey & 0xFFFFFFFF)), 8, '0', \STR_PAD_LEFT); - $reversedHex = $hex[6] . $hex[7] . $hex[4] . $hex[5] . $hex[2] . $hex[3] . $hex[0] . $hex[1]; - - $generatedKey = mb_convert_encoding($reversedHex, 'UCS-2LE', 'UTF-8'); - - // Implementation Notes List: - // Word requires that the initial hash of the password with the salt not be considered in the count. - // The initial hash of salt + key is not included in the iteration count. - $algorithm = self::getAlgorithm($algorithmName); - $generatedKey = hash($algorithm, $salt . $generatedKey, true); - - for ($i = 0; $i < $spinCount; $i++) { - $generatedKey = hash($algorithm, $generatedKey . pack('CCCC', $i, $i >> 8, $i >> 16, $i >> 24), true); - } - $generatedKey = base64_encode($generatedKey); - - mb_internal_encoding($origEncoding); - - return $generatedKey; - } - - /** - * Get algorithm from self::$algorithmMapping - * - * @param string $algorithmName - * @return string - */ - private static function getAlgorithm($algorithmName) - { - $algorithm = self::$algorithmMapping[$algorithmName][1]; - if ($algorithm == '') { - $algorithm = 'sha1'; - } - - return $algorithm; - } - - /** - * Returns the algorithm ID - * - * @param string $algorithmName - * @return int - */ - public static function getAlgorithmId($algorithmName) - { - return self::$algorithmMapping[$algorithmName][0]; - } - - /** - * Build combined key from low-order word and high-order word - * - * @param array $byteChars byte array representation of password - * @return int - */ - private static function buildCombinedKey($byteChars) - { - $byteCharsLength = count($byteChars); - // Compute the high-order word - // Initialize from the initial code array (see above), depending on the passwords length. - $highOrderWord = self::$initialCodeArray[$byteCharsLength - 1]; - - // For each character in the password: - // For every bit in the character, starting with the least significant and progressing to (but excluding) - // the most significant, if the bit is set, XOR the key’s high-order word with the corresponding word from - // the Encryption Matrix - for ($i = 0; $i < $byteCharsLength; $i++) { - $tmp = self::$passwordMaxLength - $byteCharsLength + $i; - $matrixRow = self::$encryptionMatrix[$tmp]; - for ($intBit = 0; $intBit < 7; $intBit++) { - if (($byteChars[$i] & (0x0001 << $intBit)) != 0) { - $highOrderWord = ($highOrderWord ^ $matrixRow[$intBit]); - } - } - } - - // Compute low-order word - // Initialize with 0 - $lowOrderWord = 0; - // For each character in the password, going backwards - for ($i = $byteCharsLength - 1; $i >= 0; $i--) { - // low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR character - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteChars[$i]); - } - // Lastly, low-order word = (((low-order word SHR 14) AND 0x0001) OR (low-order word SHL 1) AND 0x7FFF)) XOR strPassword length XOR 0xCE4B. - $lowOrderWord = (((($lowOrderWord >> 14) & 0x0001) | (($lowOrderWord << 1) & 0x7FFF)) ^ $byteCharsLength ^ 0xCE4B); - - // Combine the Low and High Order Word - return self::int32(($highOrderWord << 16) + $lowOrderWord); - } - - /** - * Simulate behaviour of (signed) int32 - * - * @codeCoverageIgnore - * @param int $value - * @return int - */ - private static function int32($value) - { - $value = ($value & 0xFFFFFFFF); - - if ($value & 0x80000000) { - $value = -((~$value & 0xFFFFFFFF) + 1); - } - - return $value; - } -} diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index 2783e17e..bc71e74b 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -129,6 +129,7 @@ class ZipArchive { $result = true; $this->filename = $filename; + $this->tempDir = Settings::getTempDir(); if (!$this->usePclzip) { $zip = new \ZipArchive(); @@ -139,7 +140,6 @@ class ZipArchive $this->numFiles = $zip->numFiles; } else { $zip = new \PclZip($this->filename); - $this->tempDir = Settings::getTempDir(); $zipContent = $zip->listContent(); $this->numFiles = is_array($zipContent) ? count($zipContent) : 0; } @@ -245,14 +245,20 @@ class ZipArchive $pathRemoved = $filenameParts['dirname']; $pathAdded = $localnameParts['dirname']; - $res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded); + if (!$this->usePclzip) { + $pathAdded = $pathAdded . '/' . ltrim(str_replace('\\', '/', substr($filename, strlen($pathRemoved))), '/'); + //$res = $zip->addFile($filename, $pathAdded); + $res = $zip->addFromString($pathAdded, file_get_contents($filename)); // addFile can't use subfolders in some cases + } else { + $res = $zip->add($filename, PCLZIP_OPT_REMOVE_PATH, $pathRemoved, PCLZIP_OPT_ADD_PATH, $pathAdded); + } if ($tempFile) { // Remove temp file, if created unlink($this->tempDir . DIRECTORY_SEPARATOR . $localnameParts['basename']); } - return ($res == 0) ? false : true; + return $res != 0; } /** @@ -283,7 +289,7 @@ class ZipArchive // Remove temp file @unlink($this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename']); - return ($res == 0) ? false : true; + return $res != 0; } /** @@ -303,7 +309,7 @@ class ZipArchive if (is_null($entries)) { $result = $zip->extract(PCLZIP_OPT_PATH, $destination); - return ($result > 0) ? true : false; + return $result > 0; } // Extract by entries diff --git a/src/PhpWord/SimpleType/VerticalJc.php b/src/PhpWord/SimpleType/VerticalJc.php new file mode 100644 index 00000000..2a37de41 --- /dev/null +++ b/src/PhpWord/SimpleType/VerticalJc.php @@ -0,0 +1,36 @@ +vAlign = $this->setEnumVal($value, $enum, $this->vAlign); + VerticalJc::validate($value); + $this->vAlign = $this->setEnumVal($value, VerticalJc::values(), $this->vAlign); return $this; } diff --git a/src/PhpWord/Style/Chart.php b/src/PhpWord/Style/Chart.php index 5b02e636..06b4829c 100644 --- a/src/PhpWord/Style/Chart.php +++ b/src/PhpWord/Style/Chart.php @@ -52,6 +52,20 @@ class Chart extends AbstractStyle */ private $colors = array(); + /** + * Chart title + * + * @var string + */ + private $title = null; + + /** + * Chart legend visibility + * + * @var bool + */ + private $showLegend = false; + /** * A list of display options for data labels * @@ -97,9 +111,15 @@ class Chart extends AbstractStyle */ private $valueAxisTitle; + /** + * The position for major tick marks + * Possible values are 'in', 'out', 'cross', 'none' + * + * @var string + */ private $majorTickMarkPos = 'none'; - /* + /** * Show labels for axis * * @var bool @@ -221,6 +241,50 @@ class Chart extends AbstractStyle return $this; } + /** + * Get the chart title + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set the chart title + * + * @param string $value + */ + public function setTitle($value = null) + { + $this->title = $value; + + return $this; + } + + /** + * Get chart legend visibility + * + * @return bool + */ + public function isShowLegend() + { + return $this->showLegend; + } + + /** + * Set chart legend visibility + * + * @param bool $value + */ + public function setShowLegend($value = false) + { + $this->showLegend = $value; + + return $this; + } + /* * Show labels for axis * @@ -394,8 +458,8 @@ class Chart extends AbstractStyle } /** - * set the position for major tick marks - * @param string $position [description] + * Set the position for major tick marks + * @param string $position */ public function setMajorTickPosition($position) { @@ -403,7 +467,7 @@ class Chart extends AbstractStyle $this->majorTickMarkPos = $this->setEnumVal($position, $enum, $this->majorTickMarkPos); } - /* + /** * Show Gridlines for X-Axis * * @return bool diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php index c58cee49..09e6f1a2 100644 --- a/src/PhpWord/Style/Font.php +++ b/src/PhpWord/Style/Font.php @@ -80,7 +80,7 @@ class Font extends AbstractStyle * * @var array */ - protected $aliases = array('line-height' => 'lineHeight'); + protected $aliases = array('line-height' => 'lineHeight', 'letter-spacing' => 'spacing'); /** * Font style type @@ -122,14 +122,14 @@ class Font extends AbstractStyle * * @var bool */ - private $bold = false; + private $bold; /** * Italic * * @var bool */ - private $italic = false; + private $italic; /** * Undeline @@ -157,14 +157,14 @@ class Font extends AbstractStyle * * @var bool */ - private $strikethrough = false; + private $strikethrough; /** * Double strikethrough * * @var bool */ - private $doubleStrikethrough = false; + private $doubleStrikethrough; /** * Small caps @@ -172,7 +172,7 @@ class Font extends AbstractStyle * @var bool * @see http://www.schemacentral.com/sc/ooxml/e-w_smallCaps-1.html */ - private $smallCaps = false; + private $smallCaps; /** * All caps @@ -180,7 +180,7 @@ class Font extends AbstractStyle * @var bool * @see http://www.schemacentral.com/sc/ooxml/e-w_caps-1.html */ - private $allCaps = false; + private $allCaps; /** * Foreground/highlight @@ -235,7 +235,7 @@ class Font extends AbstractStyle * * @var bool */ - private $rtl = false; + private $rtl; /** * noProof (disables AutoCorrect) @@ -243,7 +243,7 @@ class Font extends AbstractStyle * @var bool * http://www.datypic.com/sc/ooxml/e-w_noProof-1.html */ - private $noProof = false; + private $noProof; /** * Languages @@ -252,6 +252,14 @@ class Font extends AbstractStyle */ private $lang; + /** + * Hidden text + * + * @var bool + * @see http://www.datypic.com/sc/ooxml/e-w_vanish-1.html + */ + private $hidden; + /** * Vertically Raised or Lowered Text * @@ -264,7 +272,7 @@ class Font extends AbstractStyle * Create new font style * * @param string $type Type of font - * @param array $paragraph Paragraph styles definition + * @param array|string|\PhpOffice\PhpWord\Style\AbstractStyle $paragraph Paragraph styles definition */ public function __construct($type = 'text', $paragraph = null) { @@ -299,6 +307,7 @@ class Font extends AbstractStyle 'smallCaps' => $this->isSmallCaps(), 'allCaps' => $this->isAllCaps(), 'fgColor' => $this->getFgColor(), + 'hidden' => $this->isHidden(), ), 'spacing' => array( 'scale' => $this->getScale(), @@ -938,6 +947,29 @@ class Font extends AbstractStyle return $this->getParagraph(); } + /** + * Get hidden text + * + * @return bool + */ + public function isHidden() + { + return $this->hidden; + } + + /** + * Set hidden text + * + * @param bool $value + * @return self + */ + public function setHidden($value = true) + { + $this->hidden = $this->setBoolVal($value, $this->hidden); + + return $this; + } + /** * Get position * diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 8bb60149..dd3ed819 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -47,6 +47,9 @@ final class Language extends AbstractStyle const HE_IL = 'he-IL'; const HE_IL_ID = 1037; + const IT_IT = 'it-IT'; + const IT_IT_ID = 1040; + const JA_JP = 'ja-JP'; const JA_JP_ID = 1041; @@ -62,6 +65,12 @@ final class Language extends AbstractStyle const PT_BR = 'pt-BR'; const PT_BR_ID = 1046; + const NL_NL = 'nl-NL'; + const NL_NL_ID = 1043; + + const UK_UA = 'uk-UA'; + const UK_UA_ID = 1058; + /** * Language ID, used for RTF document generation * @@ -120,8 +129,7 @@ final class Language extends AbstractStyle */ public function setLatin($latin) { - $this->validateLocale($latin); - $this->latin = $latin; + $this->latin = $this->validateLocale($latin); return $this; } @@ -170,8 +178,7 @@ final class Language extends AbstractStyle */ public function setEastAsia($eastAsia) { - $this->validateLocale($eastAsia); - $this->eastAsia = $eastAsia; + $this->eastAsia = $this->validateLocale($eastAsia); return $this; } @@ -195,8 +202,7 @@ final class Language extends AbstractStyle */ public function setBidirectional($bidirectional) { - $this->validateLocale($bidirectional); - $this->bidirectional = $bidirectional; + $this->bidirectional = $this->validateLocale($bidirectional); return $this; } @@ -215,12 +221,18 @@ final class Language extends AbstractStyle * Validates that the language passed is in the format xx-xx * * @param string $locale - * @return bool + * @return string */ private function validateLocale($locale) { - if ($locale !== null && strstr($locale, '-') === false) { + if (strlen($locale) === 2) { + return strtolower($locale) . '-' . strtoupper($locale); + } + + if ($locale !== null && $locale !== 'zxx' && strstr($locale, '-') === false) { throw new \InvalidArgumentException($locale . ' is not a valid language code'); } + + return $locale; } } diff --git a/src/PhpWord/Style/Paragraph.php b/src/PhpWord/Style/Paragraph.php index ac587686..6e9aaf15 100644 --- a/src/PhpWord/Style/Paragraph.php +++ b/src/PhpWord/Style/Paragraph.php @@ -61,7 +61,7 @@ class Paragraph extends Border * * @var array */ - protected $aliases = array('line-height' => 'lineHeight'); + protected $aliases = array('line-height' => 'lineHeight', 'line-spacing' => 'spacing'); /** * Parent style @@ -199,8 +199,6 @@ class Paragraph extends Border $key = Text::removeUnderscorePrefix($key); if ('indent' == $key || 'hanging' == $key) { $value = $value * 720; - } elseif ('spacing' == $key) { - $value += 240; // because line height of 1 matches 240 twips } return parent::setStyleValue($key, $value); @@ -479,7 +477,7 @@ class Paragraph extends Border /** * Get spacing between lines * - * @return int + * @return int|float */ public function getSpacing() { @@ -489,7 +487,7 @@ class Paragraph extends Border /** * Set spacing between lines * - * @param int $value + * @param int|float $value * @return self */ public function setSpacing($value = null) @@ -547,7 +545,8 @@ class Paragraph extends Border } $this->lineHeight = $lineHeight; - $this->setSpacing($lineHeight * self::LINE_HEIGHT); + $this->setSpacing(($lineHeight - 1) * self::LINE_HEIGHT); + $this->setSpacingLineRule(\PhpOffice\PhpWord\SimpleType\LineSpacingRule::AUTO); return $this; } diff --git a/src/PhpWord/Style/Section.php b/src/PhpWord/Style/Section.php index 162e08e0..ff9b0be0 100644 --- a/src/PhpWord/Style/Section.php +++ b/src/PhpWord/Style/Section.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\SimpleType\VerticalJc; + /** * Section settings */ @@ -166,6 +168,14 @@ class Section extends Border */ private $lineNumbering; + /** + * Vertical Text Alignment on Page + * One of \PhpOffice\PhpWord\SimpleType\VerticalJc + * + * @var string + */ + private $vAlign; + /** * Create new instance */ @@ -599,4 +609,28 @@ class Section extends Border return $this; } + + /** + * Get vertical alignment + * + * @return string + */ + public function getVAlign() + { + return $this->vAlign; + } + + /** + * Set vertical alignment + * + * @param string $value + * @return self + */ + public function setVAlign($value = null) + { + VerticalJc::validate($value); + $this->vAlign = $value; + + return $this; + } } diff --git a/src/PhpWord/Style/Table.php b/src/PhpWord/Style/Table.php index b622c78b..f777ac67 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -163,6 +163,21 @@ class Table extends Border /** @var TblWidthComplexType|null */ private $indent; + /** + * The width of each column, computed based on the max cell width of each column + * + * @var int[] + */ + private $columnWidths; + + /** + * Visually Right to Left Table + * + * @see http://www.datypic.com/sc/ooxml/e-w_bidiVisual-1.html + * @var bool + */ + private $bidiVisual = false; + /** * Create new table style * @@ -748,4 +763,48 @@ class Table extends Border return $this; } + + /** + * Get the columnWidths + * + * @return null|int[] + */ + public function getColumnWidths() + { + return $this->columnWidths; + } + + /** + * The column widths + * + * @param int[] $value + */ + public function setColumnWidths(array $value = null) + { + $this->columnWidths = $value; + } + + /** + * Get bidiVisual + * + * @return bool + */ + public function isBidiVisual() + { + return $this->bidiVisual; + } + + /** + * Set bidiVisual + * + * @param bool $bidi + * Set to true to visually present table as Right to Left + * @return self + */ + public function setBidiVisual($bidi) + { + $this->bidiVisual = $bidi; + + return $this; + } } diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 72446ae7..0f685bc4 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord; use PhpOffice\Common\Text; +use PhpOffice\Common\XMLWriter; use PhpOffice\PhpWord\Escaper\RegExp; use PhpOffice\PhpWord\Escaper\Xml; use PhpOffice\PhpWord\Exception\CopyFileException; @@ -48,6 +49,13 @@ class TemplateProcessor */ protected $tempDocumentMainPart; + /** + * Content of settings part (in XML format) of the temporary document + * + * @var string + */ + protected $tempDocumentSettingsPart; + /** * Content of headers (in XML format) of the temporary document * @@ -62,6 +70,27 @@ class TemplateProcessor */ protected $tempDocumentFooters = array(); + /** + * Document relations (in XML format) of the temporary document. + * + * @var string[] + */ + protected $tempDocumentRelations = array(); + + /** + * Document content types (in XML format) of the temporary document. + * + * @var string + */ + protected $tempDocumentContentTypes = ''; + + /** + * new inserted images list + * + * @var string[] + */ + protected $tempDocumentNewImages = array(); + /** * @since 0.12.0 Throws CreateTemporaryFileException and CopyFileException instead of Exception * @@ -75,12 +104,12 @@ class TemplateProcessor // Temporary document filename initialization $this->tempDocumentFilename = tempnam(Settings::getTempDir(), 'PhpWord'); if (false === $this->tempDocumentFilename) { - throw new CreateTemporaryFileException(); + throw new CreateTemporaryFileException(); // @codeCoverageIgnore } // Template file cloning if (false === copy($documentTemplate, $this->tempDocumentFilename)) { - throw new CopyFileException($documentTemplate, $this->tempDocumentFilename); + throw new CopyFileException($documentTemplate, $this->tempDocumentFilename); // @codeCoverageIgnore } // Temporary document content extraction @@ -88,19 +117,47 @@ class TemplateProcessor $this->zipClass->open($this->tempDocumentFilename); $index = 1; while (false !== $this->zipClass->locateName($this->getHeaderName($index))) { - $this->tempDocumentHeaders[$index] = $this->fixBrokenMacros( - $this->zipClass->getFromName($this->getHeaderName($index)) - ); + $this->tempDocumentHeaders[$index] = $this->readPartWithRels($this->getHeaderName($index)); $index++; } $index = 1; while (false !== $this->zipClass->locateName($this->getFooterName($index))) { - $this->tempDocumentFooters[$index] = $this->fixBrokenMacros( - $this->zipClass->getFromName($this->getFooterName($index)) - ); + $this->tempDocumentFooters[$index] = $this->readPartWithRels($this->getFooterName($index)); $index++; } - $this->tempDocumentMainPart = $this->fixBrokenMacros($this->zipClass->getFromName($this->getMainPartName())); + + $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName()); + $this->tempDocumentSettingsPart = $this->readPartWithRels($this->getSettingsPartName()); + $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName()); + } + + /** + * Expose zip class + * + * To replace an image: $templateProcessor->zip()->AddFromString("word/media/image1.jpg", file_get_contents($file));
+ * To read a file: $templateProcessor->zip()->getFromName("word/media/image1.jpg"); + * + * @return \PhpOffice\PhpWord\Shared\ZipArchive + */ + public function zip() + { + return $this->zipClass; + } + + /** + * @param string $fileName + * + * @return string + */ + protected function readPartWithRels($fileName) + { + $relsFileName = $this->getRelationsName($fileName); + $partRelations = $this->zipClass->getFromName($relsFileName); + if ($partRelations !== false) { + $this->tempDocumentRelations[$fileName] = $partRelations; + } + + return $this->fixBrokenMacros($this->zipClass->getFromName($fileName)); } /** @@ -113,6 +170,7 @@ class TemplateProcessor */ protected function transformSingleXml($xml, $xsltProcessor) { + libxml_disable_entity_loader(true); $domDocument = new \DOMDocument(); if (false === $domDocument->loadXML($xml)) { throw new Exception('Could not load the given XML document.'); @@ -138,6 +196,7 @@ class TemplateProcessor foreach ($xml as &$item) { $item = $this->transformSingleXml($item, $xsltProcessor); } + unset($item); } else { $xml = $this->transformSingleXml($xml, $xsltProcessor); } @@ -199,6 +258,46 @@ class TemplateProcessor return $subject; } + /** + * @param string $search + * @param \PhpOffice\PhpWord\Element\AbstractElement $complexType + */ + public function setComplexValue($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType) + { + $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; + + $xmlWriter = new XMLWriter(); + /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + $elementWriter = new $objectClass($xmlWriter, $complexType, true); + $elementWriter->write(); + + $where = $this->findContainingXmlBlockForMacro($search, 'w:r'); + $block = $this->getSlice($where['start'], $where['end']); + $textParts = $this->splitTextIntoTexts($block); + $this->replaceXmlBlock($search, $textParts, 'w:r'); + + $search = static::ensureMacroCompleted($search); + $this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:r'); + } + + /** + * @param string $search + * @param \PhpOffice\PhpWord\Element\AbstractElement $complexType + */ + public function setComplexBlock($search, \PhpOffice\PhpWord\Element\AbstractElement $complexType) + { + $elementName = substr(get_class($complexType), strrpos(get_class($complexType), '\\') + 1); + $objectClass = 'PhpOffice\\PhpWord\\Writer\\Word2007\\Element\\' . $elementName; + + $xmlWriter = new XMLWriter(); + /** @var \PhpOffice\PhpWord\Writer\Word2007\Element\AbstractElement $elementWriter */ + $elementWriter = new $objectClass($xmlWriter, $complexType, false); + $elementWriter->write(); + + $this->replaceXmlBlock($search, $xmlWriter->getData(), 'w:p'); + } + /** * @param mixed $search * @param mixed $replace @@ -208,18 +307,20 @@ class TemplateProcessor { if (is_array($search)) { foreach ($search as &$item) { - $item = self::ensureMacroCompleted($item); + $item = static::ensureMacroCompleted($item); } + unset($item); } else { - $search = self::ensureMacroCompleted($search); + $search = static::ensureMacroCompleted($search); } if (is_array($replace)) { foreach ($replace as &$item) { - $item = self::ensureUtf8Encoded($item); + $item = static::ensureUtf8Encoded($item); } + unset($item); } else { - $replace = self::ensureUtf8Encoded($replace); + $replace = static::ensureUtf8Encoded($replace); } if (Settings::isOutputEscapingEnabled()) { @@ -232,6 +333,312 @@ class TemplateProcessor $this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit); } + /** + * Set values from a one-dimensional array of "variable => value"-pairs. + * + * @param array $values + */ + public function setValues(array $values) + { + foreach ($values as $macro => $replace) { + $this->setValue($macro, $replace); + } + } + + private function getImageArgs($varNameWithArgs) + { + $varElements = explode(':', $varNameWithArgs); + array_shift($varElements); // first element is name of variable => remove it + + $varInlineArgs = array(); + // size format documentation: https://msdn.microsoft.com/en-us/library/documentformat.openxml.vml.shape%28v=office.14%29.aspx?f=255&MSPPError=-2147217396 + foreach ($varElements as $argIdx => $varArg) { + if (strpos($varArg, '=')) { // arg=value + list($argName, $argValue) = explode('=', $varArg, 2); + $argName = strtolower($argName); + if ($argName == 'size') { + list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $argValue, 2); + } else { + $varInlineArgs[strtolower($argName)] = $argValue; + } + } elseif (preg_match('/^([0-9]*[a-z%]{0,2}|auto)x([0-9]*[a-z%]{0,2}|auto)$/i', $varArg)) { // 60x40 + list($varInlineArgs['width'], $varInlineArgs['height']) = explode('x', $varArg, 2); + } else { // :60:40:f + switch ($argIdx) { + case 0: + $varInlineArgs['width'] = $varArg; + break; + case 1: + $varInlineArgs['height'] = $varArg; + break; + case 2: + $varInlineArgs['ratio'] = $varArg; + break; + } + } + } + + return $varInlineArgs; + } + + private function chooseImageDimension($baseValue, $inlineValue, $defaultValue) + { + $value = $baseValue; + if (is_null($value) && isset($inlineValue)) { + $value = $inlineValue; + } + if (!preg_match('/^([0-9]*(cm|mm|in|pt|pc|px|%|em|ex|)|auto)$/i', $value)) { + $value = null; + } + if (is_null($value)) { + $value = $defaultValue; + } + if (is_numeric($value)) { + $value .= 'px'; + } + + return $value; + } + + private function fixImageWidthHeightRatio(&$width, &$height, $actualWidth, $actualHeight) + { + $imageRatio = $actualWidth / $actualHeight; + + if (($width === '') && ($height === '')) { // defined size are empty + $width = $actualWidth . 'px'; + $height = $actualHeight . 'px'; + } elseif ($width === '') { // defined width is empty + $heightFloat = (float) $height; + $widthFloat = $heightFloat * $imageRatio; + $matches = array(); + preg_match("/\d([a-z%]+)$/", $height, $matches); + $width = $widthFloat . $matches[1]; + } elseif ($height === '') { // defined height is empty + $widthFloat = (float) $width; + $heightFloat = $widthFloat / $imageRatio; + $matches = array(); + preg_match("/\d([a-z%]+)$/", $width, $matches); + $height = $heightFloat . $matches[1]; + } else { // we have defined size, but we need also check it aspect ratio + $widthMatches = array(); + preg_match("/\d([a-z%]+)$/", $width, $widthMatches); + $heightMatches = array(); + preg_match("/\d([a-z%]+)$/", $height, $heightMatches); + // try to fix only if dimensions are same + if ($widthMatches[1] == $heightMatches[1]) { + $dimention = $widthMatches[1]; + $widthFloat = (float) $width; + $heightFloat = (float) $height; + $definedRatio = $widthFloat / $heightFloat; + + if ($imageRatio > $definedRatio) { // image wider than defined box + $height = ($widthFloat / $imageRatio) . $dimention; + } elseif ($imageRatio < $definedRatio) { // image higher than defined box + $width = ($heightFloat * $imageRatio) . $dimention; + } + } + } + } + + private function prepareImageAttrs($replaceImage, $varInlineArgs) + { + // get image path and size + $width = null; + $height = null; + $ratio = null; + if (is_array($replaceImage) && isset($replaceImage['path'])) { + $imgPath = $replaceImage['path']; + if (isset($replaceImage['width'])) { + $width = $replaceImage['width']; + } + if (isset($replaceImage['height'])) { + $height = $replaceImage['height']; + } + if (isset($replaceImage['ratio'])) { + $ratio = $replaceImage['ratio']; + } + } else { + $imgPath = $replaceImage; + } + + $width = $this->chooseImageDimension($width, isset($varInlineArgs['width']) ? $varInlineArgs['width'] : null, 115); + $height = $this->chooseImageDimension($height, isset($varInlineArgs['height']) ? $varInlineArgs['height'] : null, 70); + + $imageData = @getimagesize($imgPath); + if (!is_array($imageData)) { + throw new Exception(sprintf('Invalid image: %s', $imgPath)); + } + list($actualWidth, $actualHeight, $imageType) = $imageData; + + // fix aspect ratio (by default) + if (is_null($ratio) && isset($varInlineArgs['ratio'])) { + $ratio = $varInlineArgs['ratio']; + } + if (is_null($ratio) || !in_array(strtolower($ratio), array('', '-', 'f', 'false'))) { + $this->fixImageWidthHeightRatio($width, $height, $actualWidth, $actualHeight); + } + + $imageAttrs = array( + 'src' => $imgPath, + 'mime' => image_type_to_mime_type($imageType), + 'width' => $width, + 'height' => $height, + ); + + return $imageAttrs; + } + + private function addImageToRelations($partFileName, $rid, $imgPath, $imageMimeType) + { + // define templates + $typeTpl = ''; + $relationTpl = ''; + $newRelationsTpl = '' . "\n" . ''; + $newRelationsTypeTpl = ''; + $extTransform = array( + 'image/jpeg' => 'jpeg', + 'image/png' => 'png', + 'image/bmp' => 'bmp', + 'image/gif' => 'gif', + ); + + // get image embed name + if (isset($this->tempDocumentNewImages[$imgPath])) { + $imgName = $this->tempDocumentNewImages[$imgPath]; + } else { + // transform extension + if (isset($extTransform[$imageMimeType])) { + $imgExt = $extTransform[$imageMimeType]; + } else { + throw new Exception("Unsupported image type $imageMimeType"); + } + + // add image to document + $imgName = 'image_' . $rid . '_' . pathinfo($partFileName, PATHINFO_FILENAME) . '.' . $imgExt; + $this->zipClass->pclzipAddFile($imgPath, 'word/media/' . $imgName); + $this->tempDocumentNewImages[$imgPath] = $imgName; + + // setup type for image + $xmlImageType = str_replace(array('{IMG}', '{EXT}'), array($imgName, $imgExt), $typeTpl); + $this->tempDocumentContentTypes = str_replace('', $xmlImageType, $this->tempDocumentContentTypes) . ''; + } + + $xmlImageRelation = str_replace(array('{RID}', '{IMG}'), array($rid, $imgName), $relationTpl); + + if (!isset($this->tempDocumentRelations[$partFileName])) { + // create new relations file + $this->tempDocumentRelations[$partFileName] = $newRelationsTpl; + // and add it to content types + $xmlRelationsType = str_replace('{RELS}', $this->getRelationsName($partFileName), $newRelationsTypeTpl); + $this->tempDocumentContentTypes = str_replace('', $xmlRelationsType, $this->tempDocumentContentTypes) . ''; + } + + // add image to relations + $this->tempDocumentRelations[$partFileName] = str_replace('', $xmlImageRelation, $this->tempDocumentRelations[$partFileName]) . ''; + } + + /** + * @param mixed $search + * @param mixed $replace Path to image, or array("path" => xx, "width" => yy, "height" => zz) + * @param int $limit + */ + public function setImageValue($search, $replace, $limit = self::MAXIMUM_REPLACEMENTS_DEFAULT) + { + // prepare $search_replace + if (!is_array($search)) { + $search = array($search); + } + + $replacesList = array(); + if (!is_array($replace) || isset($replace['path'])) { + $replacesList[] = $replace; + } else { + $replacesList = array_values($replace); + } + + $searchReplace = array(); + foreach ($search as $searchIdx => $searchString) { + $searchReplace[$searchString] = isset($replacesList[$searchIdx]) ? $replacesList[$searchIdx] : $replacesList[0]; + } + + // collect document parts + $searchParts = array( + $this->getMainPartName() => &$this->tempDocumentMainPart, + ); + foreach (array_keys($this->tempDocumentHeaders) as $headerIndex) { + $searchParts[$this->getHeaderName($headerIndex)] = &$this->tempDocumentHeaders[$headerIndex]; + } + foreach (array_keys($this->tempDocumentFooters) as $headerIndex) { + $searchParts[$this->getFooterName($headerIndex)] = &$this->tempDocumentFooters[$headerIndex]; + } + + // define templates + // result can be verified via "Open XML SDK 2.5 Productivity Tool" (http://www.microsoft.com/en-us/download/details.aspx?id=30425) + $imgTpl = ''; + + foreach ($searchParts as $partFileName => &$partContent) { + $partVariables = $this->getVariablesForPart($partContent); + + foreach ($searchReplace as $searchString => $replaceImage) { + $varsToReplace = array_filter($partVariables, function ($partVar) use ($searchString) { + return ($partVar == $searchString) || preg_match('/^' . preg_quote($searchString) . ':/', $partVar); + }); + + foreach ($varsToReplace as $varNameWithArgs) { + $varInlineArgs = $this->getImageArgs($varNameWithArgs); + $preparedImageAttrs = $this->prepareImageAttrs($replaceImage, $varInlineArgs); + $imgPath = $preparedImageAttrs['src']; + + // get image index + $imgIndex = $this->getNextRelationsIndex($partFileName); + $rid = 'rId' . $imgIndex; + + // replace preparations + $this->addImageToRelations($partFileName, $rid, $imgPath, $preparedImageAttrs['mime']); + $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl); + + // replace variable + $varNameWithArgsFixed = static::ensureMacroCompleted($varNameWithArgs); + $matches = array(); + if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) { + $wholeTag = $matches[0]; + array_shift($matches); + list($openTag, $prefix, , $postfix, $closeTag) = $matches; + $replaceXml = $openTag . $prefix . $closeTag . $xmlImage . $openTag . $postfix . $closeTag; + // replace on each iteration, because in one tag we can have 2+ inline variables => before proceed next variable we need to change $partContent + $partContent = $this->setValueForPart($wholeTag, $replaceXml, $partContent, $limit); + } + } + } + } + } + + /** + * Returns count of all variables in template. + * + * @return array + */ + public function getVariableCount() + { + $variables = $this->getVariablesForPart($this->tempDocumentMainPart); + + foreach ($this->tempDocumentHeaders as $headerXML) { + $variables = array_merge( + $variables, + $this->getVariablesForPart($headerXML) + ); + } + + foreach ($this->tempDocumentFooters as $footerXML) { + $variables = array_merge( + $variables, + $this->getVariablesForPart($footerXML) + ); + } + + return array_count_values($variables); + } + /** * Returns array of all variables in template. * @@ -239,17 +646,7 @@ class TemplateProcessor */ public function getVariables() { - $variables = $this->getVariablesForPart($this->tempDocumentMainPart); - - foreach ($this->tempDocumentHeaders as $headerXML) { - $variables = array_merge($variables, $this->getVariablesForPart($headerXML)); - } - - foreach ($this->tempDocumentFooters as $footerXML) { - $variables = array_merge($variables, $this->getVariablesForPart($footerXML)); - } - - return array_unique($variables); + return array_keys($this->getVariableCount()); } /** @@ -262,9 +659,7 @@ class TemplateProcessor */ public function cloneRow($search, $numberOfClones) { - if ('${' !== substr($search, 0, 2) && '}' !== substr($search, -1)) { - $search = '${' . $search . '}'; - } + $search = static::ensureMacroCompleted($search); $tagPos = strpos($this->tempDocumentMainPart, $search); if (!$tagPos) { @@ -291,7 +686,7 @@ class TemplateProcessor // If tmpXmlRow doesn't contain continue, this row is no longer part of the spanned row. $tmpXmlRow = $this->getSlice($extraRowStart, $extraRowEnd); if (!preg_match('##', $tmpXmlRow) && - !preg_match('##', $tmpXmlRow)) { + !preg_match('##', $tmpXmlRow)) { break; } // This row was a spanned row, update $rowEnd and search for the next row. @@ -301,37 +696,62 @@ class TemplateProcessor } $result = $this->getSlice(0, $rowStart); - for ($i = 1; $i <= $numberOfClones; $i++) { - $result .= preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlRow); - } + $result .= implode($this->indexClonedVariables($numberOfClones, $xmlRow)); $result .= $this->getSlice($rowEnd); $this->tempDocumentMainPart = $result; } + /** + * Clones a table row and populates it's values from a two-dimensional array in a template document. + * + * @param string $search + * @param array $values + */ + public function cloneRowAndSetValues($search, $values) + { + $this->cloneRow($search, count($values)); + + foreach ($values as $rowKey => $rowData) { + $rowNumber = $rowKey + 1; + foreach ($rowData as $macro => $replace) { + $this->setValue($macro . '#' . $rowNumber, $replace); + } + } + } + /** * Clone a block. * * @param string $blockname - * @param int $clones + * @param int $clones How many time the block should be cloned * @param bool $replace + * @param bool $indexVariables If true, any variables inside the block will be indexed (postfixed with #1, #2, ...) + * @param array $variableReplacements Array containing replacements for macros found inside the block to clone * * @return string|null */ - public function cloneBlock($blockname, $clones = 1, $replace = true) + public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null) { $xmlBlock = null; + $matches = array(); preg_match( - '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', + '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, $matches ); if (isset($matches[3])) { $xmlBlock = $matches[3]; - $cloned = array(); - for ($i = 1; $i <= $clones; $i++) { - $cloned[] = $xmlBlock; + if ($indexVariables) { + $cloned = $this->indexClonedVariables($clones, $xmlBlock); + } elseif ($variableReplacements !== null && is_array($variableReplacements)) { + $cloned = $this->replaceClonedVariables($variableReplacements, $xmlBlock); + } else { + $cloned = array(); + for ($i = 1; $i <= $clones; $i++) { + $cloned[] = $xmlBlock; + } } if ($replace) { @@ -354,6 +774,7 @@ class TemplateProcessor */ public function replaceBlock($blockname, $replacement) { + $matches = array(); preg_match( '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, @@ -379,6 +800,22 @@ class TemplateProcessor $this->replaceBlock($blockname, ''); } + /** + * Automatically Recalculate Fields on Open + * + * @param bool $update + */ + public function setUpdateFields($update = true) + { + $string = $update ? 'true' : 'false'; + $matches = array(); + if (preg_match('//', $this->tempDocumentSettingsPart, $matches)) { + $this->tempDocumentSettingsPart = str_replace($matches[0], '', $this->tempDocumentSettingsPart); + } else { + $this->tempDocumentSettingsPart = str_replace('', '', $this->tempDocumentSettingsPart); + } + } + /** * Saves the result document. * @@ -389,23 +826,39 @@ class TemplateProcessor public function save() { foreach ($this->tempDocumentHeaders as $index => $xml) { - $this->zipClass->addFromString($this->getHeaderName($index), $xml); + $this->savePartWithRels($this->getHeaderName($index), $xml); } - $this->zipClass->addFromString($this->getMainPartName(), $this->tempDocumentMainPart); + $this->savePartWithRels($this->getMainPartName(), $this->tempDocumentMainPart); + $this->savePartWithRels($this->getSettingsPartName(), $this->tempDocumentSettingsPart); foreach ($this->tempDocumentFooters as $index => $xml) { - $this->zipClass->addFromString($this->getFooterName($index), $xml); + $this->savePartWithRels($this->getFooterName($index), $xml); } + $this->zipClass->addFromString($this->getDocumentContentTypesName(), $this->tempDocumentContentTypes); + // Close zip file if (false === $this->zipClass->close()) { - throw new Exception('Could not close zip file.'); + throw new Exception('Could not close zip file.'); // @codeCoverageIgnore } return $this->tempDocumentFilename; } + /** + * @param string $fileName + * @param string $xml + */ + protected function savePartWithRels($fileName, $xml) + { + $this->zipClass->addFromString($fileName, $xml); + if (isset($this->tempDocumentRelations[$fileName])) { + $relsFileName = $this->getRelationsName($fileName); + $this->zipClass->addFromString($relsFileName, $this->tempDocumentRelations[$fileName]); + } + } + /** * Saves the result document to the user defined file. * @@ -441,17 +894,13 @@ class TemplateProcessor */ protected function fixBrokenMacros($documentPart) { - $fixedDocumentPart = $documentPart; - - $fixedDocumentPart = preg_replace_callback( - '|\$[^{]*\{[^}]*\}|U', + return preg_replace_callback( + '/\$(?:\{|[^{$]*\>\{)[^}$]*\}/U', function ($match) { return strip_tags($match[0]); }, - $fixedDocumentPart + $documentPart ); - - return $fixedDocumentPart; } /** @@ -484,6 +933,7 @@ class TemplateProcessor */ protected function getVariablesForPart($documentPartXML) { + $matches = array(); preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); return $matches[1]; @@ -502,11 +952,30 @@ class TemplateProcessor } /** + * Usually, the name of main part document will be 'document.xml'. However, some .docx files (possibly those from Office 365, experienced also on documents from Word Online created from blank templates) have file 'document22.xml' in their zip archive instead of 'document.xml'. This method searches content types file to correctly determine the file name. + * * @return string */ protected function getMainPartName() { - return 'word/document.xml'; + $contentTypes = $this->zipClass->getFromName('[Content_Types].xml'); + + $pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~'; + + $matches = array(); + preg_match($pattern, $contentTypes, $matches); + + return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'; + } + + /** + * The name of the file containing the Settings part + * + * @return string + */ + protected function getSettingsPartName() + { + return 'word/settings.xml'; } /** @@ -521,6 +990,35 @@ class TemplateProcessor return sprintf('word/footer%d.xml', $index); } + /** + * Get the name of the relations file for document part. + * + * @param string $documentPartName + * + * @return string + */ + protected function getRelationsName($documentPartName) + { + return 'word/_rels/' . pathinfo($documentPartName, PATHINFO_BASENAME) . '.rels'; + } + + protected function getNextRelationsIndex($documentPartName) + { + if (isset($this->tempDocumentRelations[$documentPartName])) { + return substr_count($this->tempDocumentRelations[$documentPartName], 'tempDocumentMainPart, $startPosition, ($endPosition - $startPosition)); } + + /** + * Replaces variable names in cloned + * rows/blocks with indexed names + * + * @param int $count + * @param string $xmlBlock + * + * @return string + */ + protected function indexClonedVariables($count, $xmlBlock) + { + $results = array(); + for ($i = 1; $i <= $count; $i++) { + $results[] = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock); + } + + return $results; + } + + /** + * Raplaces variables with values from array, array keys are the variable names + * + * @param array $variableReplacements + * @param string $xmlBlock + * + * @return string[] + */ + protected function replaceClonedVariables($variableReplacements, $xmlBlock) + { + $results = array(); + foreach ($variableReplacements as $replacementArray) { + $localXmlBlock = $xmlBlock; + foreach ($replacementArray as $search => $replacement) { + $localXmlBlock = $this->setValueForPart(self::ensureMacroCompleted($search), $replacement, $localXmlBlock, self::MAXIMUM_REPLACEMENTS_DEFAULT); + } + $results[] = $localXmlBlock; + } + + return $results; + } + + /** + * 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 (is_array($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 bool|int[] FALSE if not found, otherwise array with start and end + */ + protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') + { + $macroPos = $this->findMacro($macro); + if (0 > $macroPos) { + return false; + } + $start = $this->findXmlBlockStart($macroPos, $blockType); + if (0 > $start) { + return false; + } + $end = $this->findXmlBlockEnd($start, $blockType); + //if not found or if resulting string does not contain the macro we are searching for + if (0 > $end || strstr($this->getSlice($start, $end), $macro) === false) { + 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 int $offset Offset from which to start searching + * @return int -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 int $offset Search position + * @param string $blockType XML Block tag + * @return int -1 if block start not found + */ + protected function findXmlBlockStart($offset, $blockType) + { + $reverseOffset = (strlen($this->tempDocumentMainPart) - $offset) * -1; + // first try XML tag with attributes + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', $reverseOffset); + // if not found, or if found but contains the XML tag without attribute + if (false === $blockStart || strrpos($this->getSlice($blockStart, $offset), '<' . $blockType . '>')) { + // also try XML tag without attributes + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', $reverseOffset); + } + + return ($blockStart === false) ? -1 : $blockStart; + } + + /** + * Find the nearest block end position after $offset + * + * @param int $offset Search position + * @param string $blockType XML Block tag + * @return int -1 if block end not found + */ + protected function findXmlBlockEnd($offset, $blockType) + { + $blockEndStart = strpos($this->tempDocumentMainPart, '', $offset); + // return position of end of tag if found, otherwise -1 + + return ($blockEndStart === false) ? -1 : $blockEndStart + 3 + strlen($blockType); + } + + /** + * Splits a w:r/w:t into a list of w:r where each ${macro} is in a separate w:r + * + * @param string $text + * @return string + */ + protected function splitTextIntoTexts($text) + { + if (!$this->textNeedsSplitting($text)) { + return $text; + } + $matches = array(); + if (preg_match('/()/i', $text, $matches)) { + $extractedStyle = $matches[0]; + } else { + $extractedStyle = ''; + } + + $unformattedText = preg_replace('/>\s+<', $text); + $result = str_replace(array('${', '}'), array('' . $extractedStyle . '${', '}' . $extractedStyle . ''), $unformattedText); + + return str_replace(array('' . $extractedStyle . '', '', ''), array('', '', ''), $result); + } + + /** + * Returns true if string contains a macro that is not in it's own w:r + * + * @param string $text + * @return bool + */ + protected function textNeedsSplitting($text) + { + return preg_match('/[^>]\${|}[^<]/i', $text) == 1; + } } diff --git a/src/PhpWord/Writer/AbstractWriter.php b/src/PhpWord/Writer/AbstractWriter.php index 7e0d511a..2c1ad294 100644 --- a/src/PhpWord/Writer/AbstractWriter.php +++ b/src/PhpWord/Writer/AbstractWriter.php @@ -220,7 +220,7 @@ abstract class AbstractWriter implements WriterInterface // Temporary file $this->originalFilename = $filename; - if (strtolower($filename) == 'php://output' || strtolower($filename) == 'php://stdout') { + if (strpos(strtolower($filename), 'php://') === 0) { $filename = tempnam(Settings::getTempDir(), 'PhpWord'); if (false === $filename) { $filename = $this->originalFilename; // @codeCoverageIgnore diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index 2f10df41..a6f14792 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -39,7 +39,8 @@ class Table extends AbstractElement $rows = $this->element->getRows(); $rowCount = count($rows); if ($rowCount > 0) { - $content .= '' . PHP_EOL; + $content .= 'element->getStyle()) . '>' . PHP_EOL; + for ($i = 0; $i < $rowCount; $i++) { /** @var $row \PhpOffice\PhpWord\Element\Row Type hint */ $rowStyle = $rows[$i]->getStyle(); @@ -112,4 +113,29 @@ class Table extends AbstractElement return $content; } + + /** + * Translates Table style in CSS equivalent + * + * @param string|\PhpOffice\PhpWord\Style\Table|null $tableStyle + * @return string + */ + private function getTableStyle($tableStyle = null) + { + if ($tableStyle == null) { + return ''; + } + if (is_string($tableStyle)) { + $style = ' class="' . $tableStyle; + } else { + $style = ' style="'; + if ($tableStyle->getLayout() == \PhpOffice\PhpWord\Style\Table::LAYOUT_FIXED) { + $style .= 'table-layout: fixed;'; + } elseif ($tableStyle->getLayout() == \PhpOffice\PhpWord\Style\Table::LAYOUT_AUTO) { + $style .= 'table-layout: auto;'; + } + } + + return $style . '"'; + } } diff --git a/src/PhpWord/Writer/HTML/Element/Title.php b/src/PhpWord/Writer/HTML/Element/Title.php index 7307ce0c..04ed61f5 100644 --- a/src/PhpWord/Writer/HTML/Element/Title.php +++ b/src/PhpWord/Writer/HTML/Element/Title.php @@ -45,7 +45,7 @@ class Title extends AbstractElement $text = $this->escaper->escapeHtml($text); } } elseif ($text instanceof \PhpOffice\PhpWord\Element\AbstractContainer) { - $writer = new Container($this->parentWriter, $this->element); + $writer = new Container($this->parentWriter, $text); $text = $writer->write(); } diff --git a/src/PhpWord/Writer/HTML/Style/Font.php b/src/PhpWord/Writer/HTML/Style/Font.php index 1aeaa347..75c98b9b 100644 --- a/src/PhpWord/Writer/HTML/Style/Font.php +++ b/src/PhpWord/Writer/HTML/Style/Font.php @@ -60,6 +60,7 @@ class Font extends AbstractStyle $css['text-decoration'] .= $this->getValueIf($lineThrough, 'line-through '); $css['text-transform'] = $this->getValueIf($style->isAllCaps(), 'uppercase'); $css['font-variant'] = $this->getValueIf($style->isSmallCaps(), 'small-caps'); + $css['display'] = $this->getValueIf($style->isHidden(), 'none'); $spacing = $style->getSpacing(); $css['letter-spacing'] = $this->getValueIf(!is_null($spacing), ($spacing / 20) . 'pt'); diff --git a/src/PhpWord/Writer/ODText/Element/Table.php b/src/PhpWord/Writer/ODText/Element/Table.php index 8a21ee1b..088330ae 100644 --- a/src/PhpWord/Writer/ODText/Element/Table.php +++ b/src/PhpWord/Writer/ODText/Element/Table.php @@ -17,6 +17,10 @@ namespace PhpOffice\PhpWord\Writer\ODText\Element; +use PhpOffice\Common\XMLWriter; +use PhpOffice\PhpWord\Element\Row as RowElement; +use PhpOffice\PhpWord\Element\Table as TableElement; + /** * Table element writer * @@ -36,32 +40,59 @@ class Table extends AbstractElement } $rows = $element->getRows(); $rowCount = count($rows); - $colCount = $element->countColumns(); if ($rowCount > 0) { $xmlWriter->startElement('table:table'); $xmlWriter->writeAttribute('table:name', $element->getElementId()); $xmlWriter->writeAttribute('table:style', $element->getElementId()); - $xmlWriter->startElement('table:table-column'); - $xmlWriter->writeAttribute('table:number-columns-repeated', $colCount); - $xmlWriter->endElement(); // table:table-column + // Write columns + $this->writeColumns($xmlWriter, $element); + // Write rows foreach ($rows as $row) { - $xmlWriter->startElement('table:table-row'); - /** @var $row \PhpOffice\PhpWord\Element\Row Type hint */ - foreach ($row->getCells() as $cell) { - $xmlWriter->startElement('table:table-cell'); - $xmlWriter->writeAttribute('office:value-type', 'string'); - - $containerWriter = new Container($xmlWriter, $cell); - $containerWriter->write(); - - $xmlWriter->endElement(); // table:table-cell - } - $xmlWriter->endElement(); // table:table-row + $this->writeRow($xmlWriter, $row); } $xmlWriter->endElement(); // table:table } } + + /** + * Write column. + * + * @param \PhpOffice\Common\XMLWriter $xmlWriter + * @param \PhpOffice\PhpWord\Element\Table $element + */ + private function writeColumns(XMLWriter $xmlWriter, TableElement $element) + { + $colCount = $element->countColumns(); + + for ($i = 0; $i < $colCount; $i++) { + $xmlWriter->startElement('table:table-column'); + $xmlWriter->writeAttribute('table:style-name', $element->getElementId() . '.' . $i); + $xmlWriter->endElement(); + } + } + + /** + * Write row. + * + * @param \PhpOffice\Common\XMLWriter $xmlWriter + * @param \PhpOffice\PhpWord\Element\Row $row + */ + private function writeRow(XMLWriter $xmlWriter, RowElement $row) + { + $xmlWriter->startElement('table:table-row'); + /** @var $row \PhpOffice\PhpWord\Element\Row Type hint */ + foreach ($row->getCells() as $cell) { + $xmlWriter->startElement('table:table-cell'); + $xmlWriter->writeAttribute('office:value-type', 'string'); + + $containerWriter = new Container($xmlWriter, $cell); + $containerWriter->write(); + + $xmlWriter->endElement(); // table:table-cell + } + $xmlWriter->endElement(); // table:table-row + } } diff --git a/src/PhpWord/Writer/ODText/Part/Content.php b/src/PhpWord/Writer/ODText/Part/Content.php index a50eea7b..99ee9353 100644 --- a/src/PhpWord/Writer/ODText/Part/Content.php +++ b/src/PhpWord/Writer/ODText/Part/Content.php @@ -239,6 +239,7 @@ class Content extends AbstractPart $style->setStyleName('fr' . $element->getMediaIndex()); $this->autoStyles['Image'][] = $style; } elseif ($element instanceof Table) { + /** @var \PhpOffice\PhpWord\Style\Table $style */ $style = $element->getStyle(); if ($style === null) { $style = new TableStyle(); @@ -246,6 +247,7 @@ class Content extends AbstractPart $style = Style::getStyle($style); } $style->setStyleName($element->getElementId()); + $style->setColumnWidths($element->findFirstDefinedCellWidths()); $this->autoStyles['Table'][] = $style; } } diff --git a/src/PhpWord/Writer/ODText/Style/Font.php b/src/PhpWord/Writer/ODText/Style/Font.php index 7c7d20dd..29657c5a 100644 --- a/src/PhpWord/Writer/ODText/Style/Font.php +++ b/src/PhpWord/Writer/ODText/Style/Font.php @@ -75,6 +75,9 @@ class Font extends AbstractStyle $xmlWriter->writeAttributeIf($style->isSmallCaps(), 'fo:font-variant', 'small-caps'); $xmlWriter->writeAttributeIf($style->isAllCaps(), 'fo:text-transform', 'uppercase'); + //Hidden text + $xmlWriter->writeAttributeIf($style->isHidden(), 'text:display', 'none'); + // Superscript/subscript $xmlWriter->writeAttributeIf($style->isSuperScript(), 'style:text-position', 'super'); $xmlWriter->writeAttributeIf($style->isSubScript(), 'style:text-position', 'sub'); diff --git a/src/PhpWord/Writer/ODText/Style/Paragraph.php b/src/PhpWord/Writer/ODText/Style/Paragraph.php index 223d02f0..f247dcc1 100644 --- a/src/PhpWord/Writer/ODText/Style/Paragraph.php +++ b/src/PhpWord/Writer/ODText/Style/Paragraph.php @@ -54,6 +54,10 @@ class Paragraph extends AbstractStyle $xmlWriter->writeAttribute('fo:margin-bottom', $marginBottom . 'cm'); $xmlWriter->writeAttribute('fo:text-align', $style->getAlignment()); } + + //Right to left + $xmlWriter->writeAttributeIf($style->isBidi(), 'style:writing-mode', 'rl-tb'); + $xmlWriter->endElement(); //style:paragraph-properties $xmlWriter->endElement(); //style:style diff --git a/src/PhpWord/Writer/ODText/Style/Table.php b/src/PhpWord/Writer/ODText/Style/Table.php index 249321cf..646f2e44 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -43,7 +43,22 @@ class Table extends AbstractStyle //$xmlWriter->writeAttribute('style:width', 'table'); $xmlWriter->writeAttribute('style:rel-width', 100); $xmlWriter->writeAttribute('table:align', 'center'); + $xmlWriter->writeAttributeIf($style->isBidiVisual(), 'style:writing-mode', 'rl-tb'); $xmlWriter->endElement(); // style:table-properties $xmlWriter->endElement(); // style:style + + $cellWidths = $style->getColumnWidths(); + $countCellWidths = $cellWidths === null ? 0 : count($cellWidths); + + for ($i = 0; $i < $countCellWidths; $i++) { + $width = $cellWidths[$i]; + $xmlWriter->startElement('style:style'); + $xmlWriter->writeAttribute('style:name', $style->getStyleName() . '.' . $i); + $xmlWriter->writeAttribute('style:family', 'table-column'); + $xmlWriter->startElement('style:table-column-properties'); + $xmlWriter->writeAttribute('style:column-width', number_format($width * 0.0017638889, 2, '.', '') . 'cm'); + $xmlWriter->endElement(); // style:table-column-properties + $xmlWriter->endElement(); // style:style + } } } diff --git a/src/PhpWord/Writer/RTF/Style/Border.php b/src/PhpWord/Writer/RTF/Style/Border.php index 08dcf018..db97c28d 100644 --- a/src/PhpWord/Writer/RTF/Style/Border.php +++ b/src/PhpWord/Writer/RTF/Style/Border.php @@ -92,7 +92,7 @@ class Border extends AbstractStyle $content .= '\pgbrdr' . substr($side, 0, 1); $content .= '\brdrs'; // Single-thickness border; @todo Get other type of border - $content .= '\brdrw' . $width; // Width + $content .= '\brdrw' . round($width); // Width $content .= '\brdrcf' . $colorIndex; // Color $content .= '\brsp480'; // Space in twips between borders and the paragraph (24pt, following OOXML) $content .= ' '; diff --git a/src/PhpWord/Writer/RTF/Style/Font.php b/src/PhpWord/Writer/RTF/Style/Font.php index 8c729425..b9001ea0 100644 --- a/src/PhpWord/Writer/RTF/Style/Font.php +++ b/src/PhpWord/Writer/RTF/Style/Font.php @@ -53,7 +53,7 @@ class Font extends AbstractStyle $content .= '\f' . $this->nameIndex; $size = $style->getSize(); - $content .= $this->getValueIf(is_numeric($size), '\fs' . ($size * 2)); + $content .= $this->getValueIf(is_numeric($size), '\fs' . round($size * 2)); $content .= $this->getValueIf($style->isBold(), '\b'); $content .= $this->getValueIf($style->isItalic(), '\i'); diff --git a/src/PhpWord/Writer/RTF/Style/Indentation.php b/src/PhpWord/Writer/RTF/Style/Indentation.php new file mode 100644 index 00000000..50e8ad99 --- /dev/null +++ b/src/PhpWord/Writer/RTF/Style/Indentation.php @@ -0,0 +1,45 @@ +getStyle(); + if (!$style instanceof \PhpOffice\PhpWord\Style\Indentation) { + return ''; + } + + $content = '\fi' . round($style->getFirstLine()); + $content .= '\li' . round($style->getLeft()); + $content .= '\ri' . round($style->getRight()); + + return $content . ' '; + } +} diff --git a/src/PhpWord/Writer/RTF/Style/Paragraph.php b/src/PhpWord/Writer/RTF/Style/Paragraph.php index 3b8690cd..8ef3e146 100644 --- a/src/PhpWord/Writer/RTF/Style/Paragraph.php +++ b/src/PhpWord/Writer/RTF/Style/Paragraph.php @@ -64,8 +64,48 @@ class Paragraph extends AbstractStyle if (isset($alignments[$style->getAlignment()])) { $content .= $alignments[$style->getAlignment()]; } - $content .= $this->getValueIf($spaceBefore !== null, '\sb' . $spaceBefore); - $content .= $this->getValueIf($spaceAfter !== null, '\sa' . $spaceAfter); + $content .= $this->writeIndentation($style->getIndentation()); + $content .= $this->getValueIf($spaceBefore !== null, '\sb' . round($spaceBefore)); + $content .= $this->getValueIf($spaceAfter !== null, '\sa' . round($spaceAfter)); + + $styles = $style->getStyleValues(); + $content .= $this->writeTabs($styles['tabs']); + + return $content; + } + + /** + * Writes an \PhpOffice\PhpWord\Style\Indentation + * + * @param null|\PhpOffice\PhpWord\Style\Indentation $indent + * @return string + */ + private function writeIndentation($indent = null) + { + if (isset($indent) && $indent instanceof \PhpOffice\PhpWord\Style\Indentation) { + $writer = new Indentation($indent); + + return $writer->write(); + } + + return ''; + } + + /** + * Writes tabs + * + * @param \PhpOffice\PhpWord\Style\Tab[] $tabs + * @return string + */ + private function writeTabs($tabs = null) + { + $content = ''; + if (!empty($tabs)) { + foreach ($tabs as $tab) { + $styleWriter = new Tab($tab); + $content .= $styleWriter->write(); + } + } return $content; } diff --git a/src/PhpWord/Writer/RTF/Style/Section.php b/src/PhpWord/Writer/RTF/Style/Section.php index 5c34fa86..190bb670 100644 --- a/src/PhpWord/Writer/RTF/Style/Section.php +++ b/src/PhpWord/Writer/RTF/Style/Section.php @@ -43,16 +43,16 @@ class Section extends AbstractStyle $content .= '\sectd '; // Size & margin - $content .= $this->getValueIf($style->getPageSizeW() !== null, '\pgwsxn' . $style->getPageSizeW()); - $content .= $this->getValueIf($style->getPageSizeH() !== null, '\pghsxn' . $style->getPageSizeH()); + $content .= $this->getValueIf($style->getPageSizeW() !== null, '\pgwsxn' . round($style->getPageSizeW())); + $content .= $this->getValueIf($style->getPageSizeH() !== null, '\pghsxn' . round($style->getPageSizeH())); $content .= ' '; - $content .= $this->getValueIf($style->getMarginTop() !== null, '\margtsxn' . $style->getMarginTop()); - $content .= $this->getValueIf($style->getMarginRight() !== null, '\margrsxn' . $style->getMarginRight()); - $content .= $this->getValueIf($style->getMarginBottom() !== null, '\margbsxn' . $style->getMarginBottom()); - $content .= $this->getValueIf($style->getMarginLeft() !== null, '\marglsxn' . $style->getMarginLeft()); - $content .= $this->getValueIf($style->getHeaderHeight() !== null, '\headery' . $style->getHeaderHeight()); - $content .= $this->getValueIf($style->getFooterHeight() !== null, '\footery' . $style->getFooterHeight()); - $content .= $this->getValueIf($style->getGutter() !== null, '\guttersxn' . $style->getGutter()); + $content .= $this->getValueIf($style->getMarginTop() !== null, '\margtsxn' . round($style->getMarginTop())); + $content .= $this->getValueIf($style->getMarginRight() !== null, '\margrsxn' . round($style->getMarginRight())); + $content .= $this->getValueIf($style->getMarginBottom() !== null, '\margbsxn' . round($style->getMarginBottom())); + $content .= $this->getValueIf($style->getMarginLeft() !== null, '\marglsxn' . round($style->getMarginLeft())); + $content .= $this->getValueIf($style->getHeaderHeight() !== null, '\headery' . round($style->getHeaderHeight())); + $content .= $this->getValueIf($style->getFooterHeight() !== null, '\footery' . round($style->getFooterHeight())); + $content .= $this->getValueIf($style->getGutter() !== null, '\guttersxn' . round($style->getGutter())); $content .= ' '; // Borders diff --git a/src/PhpWord/Writer/RTF/Style/Tab.php b/src/PhpWord/Writer/RTF/Style/Tab.php new file mode 100644 index 00000000..a21b13d3 --- /dev/null +++ b/src/PhpWord/Writer/RTF/Style/Tab.php @@ -0,0 +1,49 @@ +getStyle(); + if (!$style instanceof \PhpOffice\PhpWord\Style\Tab) { + return; + } + $tabs = array( + \PhpOffice\PhpWord\Style\Tab::TAB_STOP_RIGHT => '\tqr', + \PhpOffice\PhpWord\Style\Tab::TAB_STOP_CENTER => '\tqc', + \PhpOffice\PhpWord\Style\Tab::TAB_STOP_DECIMAL => '\tqdec', + ); + $content = ''; + if (isset($tabs[$style->getType()])) { + $content .= $tabs[$style->getType()]; + } + $content .= '\tx' . round($style->getPosition()); + + return $content; + } +} diff --git a/src/PhpWord/Writer/Word2007/Element/Image.php b/src/PhpWord/Writer/Word2007/Element/Image.php index 3614ec18..5bebb89c 100644 --- a/src/PhpWord/Writer/Word2007/Element/Image.php +++ b/src/PhpWord/Writer/Word2007/Element/Image.php @@ -103,7 +103,9 @@ class Image extends AbstractElement $style->setPositioning('absolute'); $styleWriter = new ImageStyleWriter($xmlWriter, $style); - $xmlWriter->startElement('w:p'); + if (!$this->withoutP) { + $xmlWriter->startElement('w:p'); + } $xmlWriter->startElement('w:r'); $xmlWriter->startElement('w:pict'); $xmlWriter->startElement('v:shape'); @@ -118,6 +120,8 @@ class Image extends AbstractElement $xmlWriter->endElement(); // v:shape $xmlWriter->endElement(); // w:pict $xmlWriter->endElement(); // w:r - $xmlWriter->endElement(); // w:p + if (!$this->withoutP) { + $xmlWriter->endElement(); // w:p + } } } diff --git a/src/PhpWord/Writer/Word2007/Element/SDT.php b/src/PhpWord/Writer/Word2007/Element/SDT.php index 21020a0f..edf89b53 100644 --- a/src/PhpWord/Writer/Word2007/Element/SDT.php +++ b/src/PhpWord/Writer/Word2007/Element/SDT.php @@ -73,6 +73,18 @@ class SDT extends Text $this->endElementP(); // w:p } + /** + * Write text. + * + * @see http://www.datypic.com/sc/ooxml/t-w_CT_SdtText.html + * @param \PhpOffice\Common\XMLWriter $xmlWriter + */ + private function writePlainText(XMLWriter $xmlWriter) + { + $xmlWriter->startElement('w:text'); + $xmlWriter->endElement(); // w:text + } + /** * Write combo box. * diff --git a/src/PhpWord/Writer/Word2007/Element/Table.php b/src/PhpWord/Writer/Word2007/Element/Table.php index 25a48ab2..c365b028 100644 --- a/src/PhpWord/Writer/Word2007/Element/Table.php +++ b/src/PhpWord/Writer/Word2007/Element/Table.php @@ -76,21 +76,7 @@ class Table extends AbstractElement */ private function writeColumns(XMLWriter $xmlWriter, TableElement $element) { - $rows = $element->getRows(); - $rowCount = count($rows); - - $cellWidths = array(); - for ($i = 0; $i < $rowCount; $i++) { - $row = $rows[$i]; - $cells = $row->getCells(); - if (count($cells) <= count($cellWidths)) { - continue; - } - $cellWidths = array(); - foreach ($cells as $cell) { - $cellWidths[] = $cell->getWidth(); - } - } + $cellWidths = $element->findFirstDefinedCellWidths(); $xmlWriter->startElement('w:tblGrid'); foreach ($cellWidths as $width) { diff --git a/src/PhpWord/Writer/Word2007/Element/Title.php b/src/PhpWord/Writer/Word2007/Element/Title.php index 858ecfef..6a05a34d 100644 --- a/src/PhpWord/Writer/Word2007/Element/Title.php +++ b/src/PhpWord/Writer/Word2007/Element/Title.php @@ -47,6 +47,7 @@ class Title extends AbstractElement $xmlWriter->endElement(); } + $bookmarkRId = null; if ($element->getDepth() !== 0) { $rId = $element->getRelationId(); $bookmarkRId = $element->getPhpWord()->addBookmark(); diff --git a/src/PhpWord/Writer/Word2007/Part/Chart.php b/src/PhpWord/Writer/Word2007/Part/Chart.php index 17c1fd54..812d3bf1 100644 --- a/src/PhpWord/Writer/Word2007/Part/Chart.php +++ b/src/PhpWord/Writer/Word2007/Part/Chart.php @@ -105,8 +105,6 @@ class Chart extends AbstractPart { $xmlWriter->startElement('c:chart'); - $xmlWriter->writeElementBlock('c:autoTitleDeleted', 'val', 1); - $this->writePlotArea($xmlWriter); $xmlWriter->endElement(); // c:chart @@ -131,6 +129,34 @@ class Chart extends AbstractPart $style = $this->element->getStyle(); $this->options = $this->types[$type]; + $title = $style->getTitle(); + $showLegend = $style->isShowLegend(); + + //Chart title + if ($title) { + $xmlWriter->startElement('c:title'); + $xmlWriter->startElement('c:tx'); + $xmlWriter->startElement('c:rich'); + $xmlWriter->writeRaw(' + + + + + ' . $title . ' + + '); + $xmlWriter->endElement(); // c:rich + $xmlWriter->endElement(); // c:tx + $xmlWriter->endElement(); // c:title + } else { + $xmlWriter->writeElementBlock('c:autoTitleDeleted', 'val', 1); + } + + //Chart legend + if ($showLegend) { + $xmlWriter->writeRaw(''); + } + $xmlWriter->startElement('c:plotArea'); $xmlWriter->writeElement('c:layout'); @@ -330,11 +356,11 @@ class Chart extends AbstractPart $valueAxisTitle = $style->getValueAxisTitle(); if ($axisType == 'c:catAx') { - if (isset($categoryAxisTitle)) { + if (!is_null($categoryAxisTitle)) { $this->writeAxisTitle($xmlWriter, $categoryAxisTitle); } } elseif ($axisType == 'c:valAx') { - if (isset($valueAxisTitle)) { + if (!is_null($valueAxisTitle)) { $this->writeAxisTitle($xmlWriter, $valueAxisTitle); } } diff --git a/src/PhpWord/Writer/Word2007/Part/Settings.php b/src/PhpWord/Writer/Word2007/Part/Settings.php index 42d3a5d5..b764642a 100644 --- a/src/PhpWord/Writer/Word2007/Part/Settings.php +++ b/src/PhpWord/Writer/Word2007/Part/Settings.php @@ -17,9 +17,9 @@ namespace PhpOffice\PhpWord\Writer\Word2007\Part; +use PhpOffice\Common\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\ComplexType\ProofState; use PhpOffice\PhpWord\ComplexType\TrackChangesView; -use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\Style\Language; /** diff --git a/src/PhpWord/Writer/Word2007/Style/AbstractStyle.php b/src/PhpWord/Writer/Word2007/Style/AbstractStyle.php index 3236cead..fcd4aeb6 100644 --- a/src/PhpWord/Writer/Word2007/Style/AbstractStyle.php +++ b/src/PhpWord/Writer/Word2007/Style/AbstractStyle.php @@ -121,6 +121,21 @@ abstract class AbstractStyle } } + /** + * Writes boolean as 0 or 1 + * + * @param bool $value + * @return null|string + */ + protected function writeOnOf($value = null) + { + if ($value === null) { + return null; + } + + return $value ? '1' : '0'; + } + /** * Assemble style array into style string * diff --git a/src/PhpWord/Writer/Word2007/Style/Font.php b/src/PhpWord/Writer/Word2007/Style/Font.php index 58282d15..dd4fac4f 100644 --- a/src/PhpWord/Writer/Word2007/Style/Font.php +++ b/src/PhpWord/Writer/Word2007/Style/Font.php @@ -107,18 +107,21 @@ class Font extends AbstractStyle $xmlWriter->writeElementIf($size !== null, 'w:szCs', 'w:val', $size * 2); // Bold, italic - $xmlWriter->writeElementIf($style->isBold(), 'w:b'); - $xmlWriter->writeElementIf($style->isBold(), 'w:bCs'); - $xmlWriter->writeElementIf($style->isItalic(), 'w:i'); - $xmlWriter->writeElementIf($style->isItalic(), 'w:iCs'); + $xmlWriter->writeElementIf($style->isBold() !== null, 'w:b', 'w:val', $this->writeOnOf($style->isBold())); + $xmlWriter->writeElementIf($style->isBold() !== null, 'w:bCs', 'w:val', $this->writeOnOf($style->isBold())); + $xmlWriter->writeElementIf($style->isItalic() !== null, 'w:i', 'w:val', $this->writeOnOf($style->isItalic())); + $xmlWriter->writeElementIf($style->isItalic() !== null, 'w:iCs', 'w:val', $this->writeOnOf($style->isItalic())); // Strikethrough, double strikethrough - $xmlWriter->writeElementIf($style->isStrikethrough(), 'w:strike'); - $xmlWriter->writeElementIf($style->isDoubleStrikethrough(), 'w:dstrike'); + $xmlWriter->writeElementIf($style->isStrikethrough() !== null, 'w:strike', 'w:val', $this->writeOnOf($style->isStrikethrough())); + $xmlWriter->writeElementIf($style->isDoubleStrikethrough() !== null, 'w:dstrike', 'w:val', $this->writeOnOf($style->isDoubleStrikethrough())); // Small caps, all caps - $xmlWriter->writeElementIf($style->isSmallCaps(), 'w:smallCaps'); - $xmlWriter->writeElementIf($style->isAllCaps(), 'w:caps'); + $xmlWriter->writeElementIf($style->isSmallCaps() !== null, 'w:smallCaps', 'w:val', $this->writeOnOf($style->isSmallCaps())); + $xmlWriter->writeElementIf($style->isAllCaps() !== null, 'w:caps', 'w:val', $this->writeOnOf($style->isAllCaps())); + + //Hidden text + $xmlWriter->writeElementIf($style->isHidden(), 'w:vanish', 'w:val', $this->writeOnOf($style->isHidden())); // Underline $xmlWriter->writeElementIf($style->getUnderline() != 'none', 'w:u', 'w:val', $style->getUnderline()); @@ -136,7 +139,7 @@ class Font extends AbstractStyle $xmlWriter->writeElementIf($style->getKerning() !== null, 'w:kern', 'w:val', $style->getKerning() * 2); // noProof - $xmlWriter->writeElementIf($style->isNoProof() !== false, 'w:noProof'); + $xmlWriter->writeElementIf($style->isNoProof() !== null, 'w:noProof', $this->writeOnOf($style->isNoProof())); // Background-Color $shading = $style->getShading(); diff --git a/src/PhpWord/Writer/Word2007/Style/Frame.php b/src/PhpWord/Writer/Word2007/Style/Frame.php index ea5abf78..10e5b151 100644 --- a/src/PhpWord/Writer/Word2007/Style/Frame.php +++ b/src/PhpWord/Writer/Word2007/Style/Frame.php @@ -61,6 +61,7 @@ class Frame extends AbstractStyle 'hPos' => 'mso-position-horizontal', 'vPos' => 'mso-position-vertical', 'hPosRelTo' => 'mso-position-horizontal-relative', + 'vPosRelTo' => 'mso-position-vertical-relative', ); $posStyles = $this->getStyles($style, $properties); diff --git a/src/PhpWord/Writer/Word2007/Style/Section.php b/src/PhpWord/Writer/Word2007/Style/Section.php index af77396d..1122b6ff 100644 --- a/src/PhpWord/Writer/Word2007/Style/Section.php +++ b/src/PhpWord/Writer/Word2007/Style/Section.php @@ -48,6 +48,10 @@ class Section extends AbstractStyle $xmlWriter->writeAttribute('w:h', $style->getPageSizeH()); $xmlWriter->endElement(); // w:pgSz + // Vertical alignment + $vAlign = $style->getVAlign(); + $xmlWriter->writeElementIf(!is_null($vAlign), 'w:vAlign', 'w:val', $vAlign); + // Margins $margins = array( 'w:top' => array('getMarginTop', SectionStyle::DEFAULT_MARGIN), diff --git a/src/PhpWord/Writer/Word2007/Style/Spacing.php b/src/PhpWord/Writer/Word2007/Style/Spacing.php index 0185cbcc..fdfb89ab 100644 --- a/src/PhpWord/Writer/Word2007/Style/Spacing.php +++ b/src/PhpWord/Writer/Word2007/Style/Spacing.php @@ -44,6 +44,10 @@ class Spacing extends AbstractStyle $xmlWriter->writeAttributeIf(!is_null($after), 'w:after', $this->convertTwip($after)); $line = $style->getLine(); + //if linerule is auto, the spacing is supposed to include the height of the line itself, which is 240 twips + if (null !== $line && 'auto' === $style->getLineRule()) { + $line += \PhpOffice\PhpWord\Style\Paragraph::LINE_HEIGHT; + } $xmlWriter->writeAttributeIf(!is_null($line), 'w:line', $line); $xmlWriter->writeAttributeIf(!is_null($line), 'w:lineRule', $style->getLineRule()); diff --git a/src/PhpWord/Writer/Word2007/Style/Table.php b/src/PhpWord/Writer/Word2007/Style/Table.php index 7f49be7c..443d6705 100644 --- a/src/PhpWord/Writer/Word2007/Style/Table.php +++ b/src/PhpWord/Writer/Word2007/Style/Table.php @@ -86,6 +86,9 @@ class Table extends AbstractStyle $styleWriter = new TablePosition($xmlWriter, $style->getPosition()); $styleWriter->write(); + //Right to left + $xmlWriter->writeElementIf($style->isBidiVisual() !== null, 'w:bidiVisual', 'w:val', $this->writeOnOf($style->isBidiVisual())); + $this->writeMargin($xmlWriter, $style); $this->writeBorder($xmlWriter, $style); diff --git a/tests/PhpWord/Element/CellTest.php b/tests/PhpWord/Element/CellTest.php index d4aaa488..7e63967a 100644 --- a/tests/PhpWord/Element/CellTest.php +++ b/tests/PhpWord/Element/CellTest.php @@ -17,12 +17,14 @@ namespace PhpOffice\PhpWord\Element; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; + /** * Test class for PhpOffice\PhpWord\Element\Cell * * @runTestsInSeparateProcesses */ -class CellTest extends \PHPUnit\Framework\TestCase +class CellTest extends AbstractWebServerEmbeddedTest { /** * New instance @@ -165,7 +167,7 @@ class CellTest extends \PHPUnit\Framework\TestCase public function testAddImageSectionByUrl() { $oCell = new Cell(); - $element = $oCell->addImage('http://php.net/images/logos/php-med-trans-light.gif'); + $element = $oCell->addImage(self::getRemoteGifImageUrl()); $this->assertCount(1, $oCell->getElements()); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $element); diff --git a/tests/PhpWord/Element/FooterTest.php b/tests/PhpWord/Element/FooterTest.php index 9de2487a..b1ef4677 100644 --- a/tests/PhpWord/Element/FooterTest.php +++ b/tests/PhpWord/Element/FooterTest.php @@ -17,12 +17,14 @@ namespace PhpOffice\PhpWord\Element; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; + /** * Test class for PhpOffice\PhpWord\Element\Footer * * @runTestsInSeparateProcesses */ -class FooterTest extends \PHPUnit\Framework\TestCase +class FooterTest extends AbstractWebServerEmbeddedTest { /** * New instance @@ -116,7 +118,7 @@ class FooterTest extends \PHPUnit\Framework\TestCase public function testAddImageByUrl() { $oFooter = new Footer(1); - $element = $oFooter->addImage('http://php.net/images/logos/php-med-trans-light.gif'); + $element = $oFooter->addImage(self::getRemoteGifImageUrl()); $this->assertCount(1, $oFooter->getElements()); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $element); diff --git a/tests/PhpWord/Element/HeaderTest.php b/tests/PhpWord/Element/HeaderTest.php index e61175f1..4bbf7b74 100644 --- a/tests/PhpWord/Element/HeaderTest.php +++ b/tests/PhpWord/Element/HeaderTest.php @@ -17,12 +17,14 @@ namespace PhpOffice\PhpWord\Element; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; + /** * Test class for PhpOffice\PhpWord\Element\Header * * @runTestsInSeparateProcesses */ -class HeaderTest extends \PHPUnit\Framework\TestCase +class HeaderTest extends AbstractWebServerEmbeddedTest { /** * New instance @@ -125,7 +127,7 @@ class HeaderTest extends \PHPUnit\Framework\TestCase public function testAddImageByUrl() { $oHeader = new Header(1); - $element = $oHeader->addImage('http://php.net/images/logos/php-med-trans-light.gif'); + $element = $oHeader->addImage(self::getRemoteGifImageUrl()); $this->assertCount(1, $oHeader->getElements()); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $element); diff --git a/tests/PhpWord/Element/ImageTest.php b/tests/PhpWord/Element/ImageTest.php index 747a77ac..f56d0794 100644 --- a/tests/PhpWord/Element/ImageTest.php +++ b/tests/PhpWord/Element/ImageTest.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Element; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; use PhpOffice\PhpWord\SimpleType\Jc; /** @@ -24,7 +25,7 @@ use PhpOffice\PhpWord\SimpleType\Jc; * * @runTestsInSeparateProcesses */ -class ImageTest extends \PHPUnit\Framework\TestCase +class ImageTest extends AbstractWebServerEmbeddedTest { /** * New instance @@ -131,7 +132,7 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ public function testUnsupportedImage() { - //disable ssl verification, never do this in real application, you should pass the certiciate instead!!! + //disable ssl verification, never do this in real application, you should pass the certificiate instead!!! $arrContextOptions = array( 'ssl' => array( 'verify_peer' => false, @@ -139,7 +140,7 @@ class ImageTest extends \PHPUnit\Framework\TestCase ), ); stream_context_set_default($arrContextOptions); - $object = new Image('https://samples.libav.org/image-samples/RACECAR.BMP'); + $object = new Image(self::getRemoteBmpImageUrl()); $object->getSource(); } @@ -215,7 +216,7 @@ class ImageTest extends \PHPUnit\Framework\TestCase */ public function testConstructFromGd() { - $source = 'http://php.net/images/logos/php-icon.png'; + $source = self::getRemoteImageUrl(); $image = new Image($source); $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\Image', $image); diff --git a/tests/PhpWord/Element/SDTTest.php b/tests/PhpWord/Element/SDTTest.php index 6e40bae0..2328dd76 100644 --- a/tests/PhpWord/Element/SDTTest.php +++ b/tests/PhpWord/Element/SDTTest.php @@ -29,8 +29,8 @@ class SDTTest extends \PHPUnit\Framework\TestCase */ public function testConstruct() { - $types = array('comboBox', 'dropDownList', 'date'); - $type = $types[rand(0, 2)]; + $types = array('plainText', 'comboBox', 'dropDownList', 'date'); + $type = $types[rand(0, 3)]; $value = rand(0, 100); $alias = 'alias'; $tag = 'my_tag'; diff --git a/tests/PhpWord/Element/SectionTest.php b/tests/PhpWord/Element/SectionTest.php index 265307d7..83d1214e 100644 --- a/tests/PhpWord/Element/SectionTest.php +++ b/tests/PhpWord/Element/SectionTest.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWord\Element; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\Style\Section as SectionStyle; /** * @covers \PhpOffice\PhpWord\Element\Section @@ -27,6 +28,27 @@ use PhpOffice\PhpWord\Style; */ class SectionTest extends \PHPUnit\Framework\TestCase { + public function testConstructorWithDefaultStyle() + { + $section = new Section(0); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Section', $section->getStyle()); + } + + public function testConstructorWithArrayStyle() + { + $section = new Section(0, array('orientation' => 'landscape')); + $style = $section->getStyle(); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Section', $style); + $this->assertEquals('landscape', $style->getOrientation()); + } + + public function testConstructorWithObjectStyle() + { + $style = new SectionStyle(); + $section = new Section(0, $style); + $this->assertSame($style, $section->getStyle()); + } + /** * @covers ::setStyle */ diff --git a/tests/PhpWord/Element/TrackChangeTest.php b/tests/PhpWord/Element/TrackChangeTest.php index df86feb2..b6cea924 100644 --- a/tests/PhpWord/Element/TrackChangeTest.php +++ b/tests/PhpWord/Element/TrackChangeTest.php @@ -41,4 +41,22 @@ class TrackChangeTest extends \PHPUnit\Framework\TestCase $this->assertEquals($date, $oTrackChange->getDate()); $this->assertEquals(TrackChange::INSERTED, $oTrackChange->getChangeType()); } + + /** + * New instance with invalid \DateTime (produced by \DateTime::createFromFormat(...)) + */ + public function testConstructDefaultWithInvalidDate() + { + $author = 'Test User'; + $date = false; + $oTrackChange = new TrackChange(TrackChange::INSERTED, $author, $date); + + $oText = new Text('dummy text'); + $oText->setTrackChange($oTrackChange); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\Element\\TrackChange', $oTrackChange); + $this->assertEquals($author, $oTrackChange->getAuthor()); + $this->assertEquals($date, null); + $this->assertEquals(TrackChange::INSERTED, $oTrackChange->getChangeType()); + } } diff --git a/tests/PhpWord/MediaTest.php b/tests/PhpWord/MediaTest.php index 02492016..3cf62b59 100644 --- a/tests/PhpWord/MediaTest.php +++ b/tests/PhpWord/MediaTest.php @@ -24,7 +24,7 @@ use PhpOffice\PhpWord\Element\Image; * * @runTestsInSeparateProcesses */ -class MediaTest extends \PHPUnit\Framework\TestCase +class MediaTest extends AbstractWebServerEmbeddedTest { /** * Get section media elements @@ -49,7 +49,7 @@ class MediaTest extends \PHPUnit\Framework\TestCase { $local = __DIR__ . '/_files/images/mars.jpg'; $object = __DIR__ . '/_files/documents/sheet.xls'; - $remote = 'http://php.net/images/logos/php-med-trans-light.gif'; + $remote = self::getRemoteImageUrl(); Media::addElement('section', 'image', $local, new Image($local)); Media::addElement('section', 'image', $local, new Image($local)); Media::addElement('section', 'image', $remote, new Image($local)); @@ -77,7 +77,7 @@ class MediaTest extends \PHPUnit\Framework\TestCase public function testAddHeaderMediaElement() { $local = __DIR__ . '/_files/images/mars.jpg'; - $remote = 'http://php.net/images/logos/php-med-trans-light.gif'; + $remote = self::getRemoteImageUrl(); Media::addElement('header1', 'image', $local, new Image($local)); Media::addElement('header1', 'image', $local, new Image($local)); Media::addElement('header1', 'image', $remote, new Image($remote)); @@ -92,7 +92,7 @@ class MediaTest extends \PHPUnit\Framework\TestCase public function testAddFooterMediaElement() { $local = __DIR__ . '/_files/images/mars.jpg'; - $remote = 'http://php.net/images/logos/php-med-trans-light.gif'; + $remote = self::getRemoteImageUrl(); Media::addElement('footer1', 'image', $local, new Image($local)); Media::addElement('footer1', 'image', $local, new Image($local)); Media::addElement('footer1', 'image', $remote, new Image($remote)); diff --git a/tests/PhpWord/Reader/Word2007/ElementTest.php b/tests/PhpWord/Reader/Word2007/ElementTest.php index 75060625..cb72ef9f 100644 --- a/tests/PhpWord/Reader/Word2007/ElementTest.php +++ b/tests/PhpWord/Reader/Word2007/ElementTest.php @@ -236,4 +236,40 @@ class ElementTest extends AbstractTestReader $this->assertEquals('Title', $formattedTitle->getStyle()); $this->assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $formattedTitle->getText()); } + + /** + * Test reading Drawing + */ + public function testReadDrawing() + { + $documentXml = ' + + + + + + + + + + + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(array('document' => $documentXml)); + + $elements = $phpWord->getSection(0)->getElements(); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $elements[0]); + } } diff --git a/tests/PhpWord/Reader/Word2007/PartTest.php b/tests/PhpWord/Reader/Word2007/PartTest.php index 31a492b8..6b7d9294 100644 --- a/tests/PhpWord/Reader/Word2007/PartTest.php +++ b/tests/PhpWord/Reader/Word2007/PartTest.php @@ -160,4 +160,76 @@ class PartTest extends AbstractTestReader $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $endnote->getElement(0)); $this->assertEquals('This is an endnote', $endnote->getElement(0)->getText()); } + + public function testReadHeadingWithOverriddenStyle() + { + $documentXml = ' + + + + + This is a bold + + + + + + heading + + + but with parts not in bold + + '; + + $stylesXml = ' + + + + + + + + + + + + + + + + + + + + + + + + + '; + + $phpWord = $this->getDocumentFromString(array('document' => $documentXml, 'styles' => $stylesXml)); + + $elements = $phpWord->getSection(0)->getElements(); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Title', $elements[0]); + /** @var \PhpOffice\PhpWord\Element\Title $title */ + $title = $elements[0]; + $this->assertEquals('Heading1', $title->getStyle()); + + /** @var \PhpOffice\PhpWord\Element\Text $text */ + $text = $title->getText()->getElement(0); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $text); + $this->assertEquals('This is a bold ', $text->getText()); + + /** @var \PhpOffice\PhpWord\Element\Text $text */ + $text = $title->getText()->getElement(1); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $text); + $this->assertEquals('heading', $text->getText()); + $this->assertFalse($text->getFontStyle()->isBold()); + + /** @var \PhpOffice\PhpWord\Element\Text $text */ + $text = $title->getText()->getElement(2); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $text); + $this->assertEquals(' but with parts not in bold', $text->getText()); + } } diff --git a/tests/PhpWord/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index d64079fa..a7308f1a 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -19,6 +19,8 @@ namespace PhpOffice\PhpWord\Reader\Word2007; use PhpOffice\PhpWord\AbstractTestReader; use PhpOffice\PhpWord\SimpleType\TblWidth; +use PhpOffice\PhpWord\SimpleType\VerticalJc; +use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Table; use PhpOffice\PhpWord\Style\TablePosition; @@ -145,4 +147,83 @@ class StyleTest extends AbstractTestReader $this->assertSame(TblWidth::TWIP, $tableStyle->getIndent()->getType()); $this->assertSame(2160, $tableStyle->getIndent()->getValue()); } + + public function testReadTableRTL() + { + $documentXml = ' + + + + '; + + $phpWord = $this->getDocumentFromString(array('document' => $documentXml)); + + $elements = $phpWord->getSection(0)->getElements(); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Table', $elements[0]); + $this->assertInstanceOf('PhpOffice\PhpWord\Style\Table', $elements[0]->getStyle()); + /** @var \PhpOffice\PhpWord\Style\Table $tableStyle */ + $tableStyle = $elements[0]->getStyle(); + $this->assertTrue($tableStyle->isBidiVisual()); + } + + public function testReadHidden() + { + $documentXml = ' + + + + + This text is hidden + + '; + + $phpWord = $this->getDocumentFromString(array('document' => $documentXml)); + + $elements = $phpWord->getSection(0)->getElements(); + /** @var \PhpOffice\PhpWord\Element\TextRun $elements */ + $textRun = $elements[0]; + $this->assertInstanceOf('PhpOffice\PhpWord\Element\TextRun', $textRun); + $this->assertInstanceOf('PhpOffice\PhpWord\Element\Text', $textRun->getElement(0)); + $this->assertInstanceOf('PhpOffice\PhpWord\Style\Font', $textRun->getElement(0)->getFontStyle()); + /** @var \PhpOffice\PhpWord\Style\Font $fontStyle */ + $fontStyle = $textRun->getElement(0)->getFontStyle(); + $this->assertTrue($fontStyle->isHidden()); + } + + public function testReadHeading() + { + Style::resetStyles(); + + $documentXml = ' + + + + + + + + + + + + + '; + + $name = 'Heading_1'; + + $this->getDocumentFromString(array('styles' => $documentXml)); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Font', Style::getStyle($name)); + } + + public function testPageVerticalAlign() + { + $documentXml = ' + + '; + + $phpWord = $this->getDocumentFromString(array('document' => $documentXml)); + + $sectionStyle = $phpWord->getSection(0)->getStyle(); + $this->assertEquals(VerticalJc::CENTER, $sectionStyle->getVAlign()); + } } diff --git a/tests/PhpWord/Reader/Word2007Test.php b/tests/PhpWord/Reader/Word2007Test.php index 62d23a68..6d660ce5 100644 --- a/tests/PhpWord/Reader/Word2007Test.php +++ b/tests/PhpWord/Reader/Word2007Test.php @@ -62,6 +62,20 @@ class Word2007Test extends \PHPUnit\Framework\TestCase $this->assertEquals(100, $phpWord->getSettings()->getZoom()); $doc = TestHelperDOCX::getDocument($phpWord); - $this->assertFalse($doc->elementExists('/w:document/w:body/w:p/w:r[w:t/node()="italics"]/w:rPr/w:b')); + $this->assertEquals('0', $doc->getElementAttribute('/w:document/w:body/w:p/w:r[w:t/node()="italics"]/w:rPr/w:b', 'w:val')); + } + + /** + * Load a Word 2011 file + */ + public function testLoadWord2011() + { + $filename = __DIR__ . '/../_files/documents/reader-2011.docx'; + $phpWord = IOFactory::load($filename); + + $this->assertInstanceOf('PhpOffice\\PhpWord\\PhpWord', $phpWord); + + $doc = TestHelperDOCX::getDocument($phpWord); + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[3]/w:r/w:pict/v:shape/v:imagedata')); } } diff --git a/tests/PhpWord/Shared/ConverterTest.php b/tests/PhpWord/Shared/ConverterTest.php index 3798a07b..15be8ec1 100644 --- a/tests/PhpWord/Shared/ConverterTest.php +++ b/tests/PhpWord/Shared/ConverterTest.php @@ -29,6 +29,7 @@ class ConverterTest extends \PHPUnit\Framework\TestCase */ public function testUnitConversions() { + $values = array(); $values[] = 0; // zero value $values[] = rand(1, 100) / 100; // fraction number $values[] = rand(1, 100); // integer @@ -79,6 +80,9 @@ class ConverterTest extends \PHPUnit\Framework\TestCase $result = Converter::pointToTwip($value); $this->assertEquals($value * 20, $result); + $result = Converter::pointToCm($value); + $this->assertEquals($value * 0.035277778, $result, '', 0.00001); + $result = Converter::pointToPixel($value); $this->assertEquals($value / 72 * 96, $result); @@ -105,6 +109,7 @@ class ConverterTest extends \PHPUnit\Framework\TestCase public function testHtmlToRGB() { // Prepare test values [ original, expected ] + $values = array(); $values[] = array('#FF99DD', array(255, 153, 221)); // With # $values[] = array('FF99DD', array(255, 153, 221)); // 6 characters $values[] = array('F9D', array(255, 153, 221)); // 3 characters @@ -126,6 +131,7 @@ class ConverterTest extends \PHPUnit\Framework\TestCase $this->assertEquals(10, Converter::cssToPoint('10pt')); $this->assertEquals(7.5, Converter::cssToPoint('10px')); $this->assertEquals(720, Converter::cssToPoint('10in')); + $this->assertEquals(7.2, Converter::cssToPoint('0.1in')); $this->assertEquals(120, Converter::cssToPoint('10pc')); $this->assertEquals(28.346457, Converter::cssToPoint('10mm'), '', 0.000001); $this->assertEquals(283.464567, Converter::cssToPoint('10cm'), '', 0.000001); diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index b61418e0..5bc9e241 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Shared; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; use PhpOffice\PhpWord\Element\Section; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\LineSpacingRule; @@ -27,7 +28,7 @@ use PhpOffice\PhpWord\TestHelperDOCX; * Test class for PhpOffice\PhpWord\Shared\Html * @coversDefaultClass \PhpOffice\PhpWord\Shared\Html */ -class HtmlTest extends \PHPUnit\Framework\TestCase +class HtmlTest extends AbstractWebServerEmbeddedTest { /** * Test unit conversion functions with various numbers @@ -86,6 +87,21 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertCount(2, $section->getElements()); } + /** + * Test HTML entities + */ + public function testParseHtmlEntities() + { + \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true); + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, 'text with entities <my text>'); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/w:r/w:t')); + $this->assertEquals('text with entities ', $doc->getElement('/w:document/w:body/w:p[1]/w:r/w:t')->nodeValue); + } + /** * Test underline */ @@ -116,6 +132,21 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertEquals('single', $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:rPr/w:u', 'w:val')); } + /** + * Test font + */ + public function testParseFont() + { + $html = 'test'; + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr')); + //TODO check style + } + /** * Test line-height style */ @@ -126,6 +157,7 @@ class HtmlTest extends \PHPUnit\Framework\TestCase Html::addHtml($section, '

test

'); Html::addHtml($section, '

test

'); Html::addHtml($section, '

test

'); + Html::addHtml($section, '

test

'); $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[1]/w:pPr/w:spacing')); @@ -139,6 +171,10 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[3]/w:pPr/w:spacing')); $this->assertEquals(Paragraph::LINE_HEIGHT * 1.2, $doc->getElementAttribute('/w:document/w:body/w:p[3]/w:pPr/w:spacing', 'w:line')); $this->assertEquals(LineSpacingRule::AUTO, $doc->getElementAttribute('/w:document/w:body/w:p[3]/w:pPr/w:spacing', 'w:lineRule')); + + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[4]/w:pPr/w:spacing')); + $this->assertEquals(244.8, $doc->getElementAttribute('/w:document/w:body/w:p[4]/w:pPr/w:spacing', 'w:line')); + $this->assertEquals(LineSpacingRule::EXACT, $doc->getElementAttribute('/w:document/w:body/w:p[4]/w:pPr/w:spacing', 'w:lineRule')); } /** @@ -261,8 +297,8 @@ class HtmlTest extends \PHPUnit\Framework\TestCase
- - + + @@ -277,6 +313,17 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tr/w:tc')); $this->assertTrue($doc->elementExists('/w:document/w:body/w:tbl/w:tblPr/w:jc')); $this->assertEquals(Jc::START, $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tblPr/w:jc', 'w:val')); + + //check border colors + $this->assertEquals('00EE00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]/w:tcPr/w:tcBorders/w:top', 'w:color')); + $this->assertEquals('00EE00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]/w:tcPr/w:tcBorders/w:right', 'w:color')); + $this->assertEquals('00EE00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]/w:tcPr/w:tcBorders/w:bottom', 'w:color')); + $this->assertEquals('00EE00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[2]/w:tcPr/w:tcBorders/w:left', 'w:color')); + + $this->assertEquals('00AA00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]/w:tcPr/w:tcBorders/w:top', 'w:color')); + $this->assertEquals('00BB00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]/w:tcPr/w:tcBorders/w:right', 'w:color')); + $this->assertEquals('00CC00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]/w:tcPr/w:tcBorders/w:bottom', 'w:color')); + $this->assertEquals('00DD00', $doc->getElementAttribute('/w:document/w:body/w:tbl/w:tr[1]/w:tc[3]/w:tcPr/w:tcBorders/w:left', 'w:color')); } /** @@ -447,6 +494,78 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertStringMatchesFormat('%Smso-position-horizontal:left%S', $doc->getElementAttribute($baseXpath . '[2]/w:pict/v:shape', 'style')); } + /** + * Test parsing of remote img + */ + public function testParseRemoteImage() + { + $src = self::getRemoteImageUrl(); + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $baseXpath = '/w:document/w:body/w:p/w:r'; + $this->assertTrue($doc->elementExists($baseXpath . '/w:pict/v:shape')); + } + + /** + * Test parsing embedded image + */ + public function testParseEmbeddedImage() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $baseXpath = '/w:document/w:body/w:p/w:r'; + $this->assertTrue($doc->elementExists($baseXpath . '/w:pict/v:shape')); + } + + /** + * Test parsing of remote img that can be found locally + */ + public function testParseRemoteLocalImage() + { + $src = 'https://fakedomain.io/images/firefox.png'; + $localPath = __DIR__ . '/../_files/images/'; + $options = array( + 'IMG_SRC_SEARCH' => 'https://fakedomain.io/images/', + 'IMG_SRC_REPLACE' => $localPath, + ); + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html, false, true, $options); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $baseXpath = '/w:document/w:body/w:p/w:r'; + $this->assertTrue($doc->elementExists($baseXpath . '/w:pict/v:shape')); + } + + /** + * Test parsing of remote img that can be found locally + * + * @expectedException \Exception + */ + public function testCouldNotLoadImage() + { + $src = 'https://fakedomain.io/images/firefox.png'; + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html, false, true); + } + public function testParseLink() { $phpWord = new \PhpOffice\PhpWord\PhpWord(); @@ -482,4 +601,35 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); $this->assertFalse($doc->elementExists('/w:document/w:body/w:p[1]/w:pPr/w:jc')); } + + /** + * Tests parsing hidden text + */ + public function testParseHiddenText() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

This is some hidden text.

'; + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr/w:vanish')); + } + + /** + * Tests parsing letter spacing + */ + public function testParseLetterSpacing() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

This is some text with letter spacing.

'; + Html::addHtml($section, $html); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr/w:spacing')); + $this->assertEquals(150 * 15, $doc->getElement('/w:document/w:body/w:p/w:r/w:rPr/w:spacing')->getAttribute('w:val')); + } } diff --git a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php b/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php deleted file mode 100644 index 5a050c54..00000000 --- a/tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php +++ /dev/null @@ -1,91 +0,0 @@ - Cell::VALIGN_TOP, + 'valign' => VerticalJc::TOP, 'textDirection' => Cell::TEXT_DIR_BTLR, 'bgColor' => 'FFFF00', 'borderTopSize' => 120, diff --git a/tests/PhpWord/Style/FontTest.php b/tests/PhpWord/Style/FontTest.php index 6a934579..84916fc2 100644 --- a/tests/PhpWord/Style/FontTest.php +++ b/tests/PhpWord/Style/FontTest.php @@ -76,6 +76,7 @@ class FontTest extends \PHPUnit\Framework\TestCase 'spacing' => null, 'kerning' => null, 'lang' => null, + 'hidden' => false, ); foreach ($attributes as $key => $default) { $get = is_bool($default) ? "is{$key}" : "get{$key}"; @@ -117,6 +118,7 @@ class FontTest extends \PHPUnit\Framework\TestCase 'rtl' => true, 'noProof' => true, 'lang' => new Language(Language::EN_US), + 'hidden' => true, ); $object->setStyleByArray($attributes); foreach ($attributes as $key => $value) { diff --git a/tests/PhpWord/Style/LanguageTest.php b/tests/PhpWord/Style/LanguageTest.php index 99741cea..3bf516f8 100644 --- a/tests/PhpWord/Style/LanguageTest.php +++ b/tests/PhpWord/Style/LanguageTest.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Style; +use PHPUnit\Framework\Assert; + /** * Test class for PhpOffice\PhpWord\Style\Language * @@ -56,7 +58,20 @@ class LanguageTest extends \PHPUnit\Framework\TestCase */ public function testWrongLanguage() { + $language = new Language(); + $language->setLatin('fra'); + } + + /** + * Tests that a language can be set with just a 2 char code + */ + public function testShortLanguage() + { + //when $language = new Language(); $language->setLatin('fr'); + + //then + Assert::assertEquals('fr-FR', $language->getLatin()); } } diff --git a/tests/PhpWord/Style/PaperTest.php b/tests/PhpWord/Style/PaperTest.php index 688f31af..f8f00701 100644 --- a/tests/PhpWord/Style/PaperTest.php +++ b/tests/PhpWord/Style/PaperTest.php @@ -17,7 +17,6 @@ namespace PhpOffice\PhpWord\Style; -use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\TestHelperDOCX; /** diff --git a/tests/PhpWord/Style/ParagraphTest.php b/tests/PhpWord/Style/ParagraphTest.php index 62460738..4fa0ef5a 100644 --- a/tests/PhpWord/Style/ParagraphTest.php +++ b/tests/PhpWord/Style/ParagraphTest.php @@ -91,8 +91,6 @@ class ParagraphTest extends \PHPUnit\Framework\TestCase $object->setStyleValue("$key", $value); if ('indent' == $key || 'hanging' == $key) { $value = $value * 720; - } elseif ('spacing' == $key) { - $value += 240; } $this->assertEquals($value, $object->$get()); } diff --git a/tests/PhpWord/Style/SectionTest.php b/tests/PhpWord/Style/SectionTest.php index b26d1d94..59d18167 100644 --- a/tests/PhpWord/Style/SectionTest.php +++ b/tests/PhpWord/Style/SectionTest.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\SimpleType\VerticalJc; + /** * Test class for PhpOffice\PhpWord\Style\Section * @@ -328,4 +330,18 @@ class SectionTest extends \PHPUnit\Framework\TestCase $oSettings->setBreakType(); $this->assertNull($oSettings->getBreakType()); } + + /** + * Vertical page alignment + */ + public function testVerticalAlign() + { + // Section Settings + $oSettings = new Section(); + + $this->assertNull($oSettings->getVAlign()); + + $oSettings->setVAlign(VerticalJc::BOTH); + $this->assertEquals('both', $oSettings->getVAlign()); + } } diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index c762a609..4caca77a 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -17,6 +17,9 @@ namespace PhpOffice\PhpWord; +use PhpOffice\PhpWord\Element\Text; +use PhpOffice\PhpWord\Element\TextRun; + /** * @covers \PhpOffice\PhpWord\TemplateProcessor * @coversDefaultClass \PhpOffice\PhpWord\TemplateProcessor @@ -24,10 +27,24 @@ namespace PhpOffice\PhpWord; */ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase { + /** + * Construct test + * + * @covers ::__construct + * @test + */ + public function testTheConstruct() + { + $object = new TemplateProcessor(__DIR__ . '/_files/templates/blank.docx'); + $this->assertInstanceOf('PhpOffice\\PhpWord\\TemplateProcessor', $object); + $this->assertEquals(array(), $object->getVariables()); + } + /** * Template can be saved in temporary location. * * @covers ::save + * @covers ::zip * @test */ final public function testTemplateCanBeSavedInTemporaryLocation() @@ -41,6 +58,8 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $templateProcessor->applyXslStyleSheet($xslDomDocument, array('needle' => $needle)); } + $embeddingText = 'The quick Brown Fox jumped over the lazy^H^H^H^Htired unitTester'; + $templateProcessor->zip()->AddFromString('word/embeddings/fox.bin', $embeddingText); $documentFqfn = $templateProcessor->save(); $this->assertNotEmpty($documentFqfn, 'FQFN of the saved document is empty.'); @@ -60,6 +79,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $documentHeaderXml = $documentZip->getFromName('word/header1.xml'); $documentMainPartXml = $documentZip->getFromName('word/document.xml'); $documentFooterXml = $documentZip->getFromName('word/footer1.xml'); + $documentEmbedding = $documentZip->getFromName('word/embeddings/fox.bin'); if (false === $documentZip->close()) { throw new \Exception("Could not close zip file \"{$documentZip}\"."); } @@ -67,6 +87,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertNotEquals($templateHeaderXml, $documentHeaderXml); $this->assertNotEquals($templateMainPartXml, $documentMainPartXml); $this->assertNotEquals($templateFooterXml, $documentFooterXml); + $this->assertEquals($embeddingText, $documentEmbedding); return $documentFqfn; } @@ -178,6 +199,79 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertTrue($docFound); } + /** + * @covers ::setValue + * @covers ::cloneRow + * @covers ::saveAs + * @test + */ + public function testCloneRowAndSetValues() + { + $mainPart = ' + + + + + + + + ${userId} + + + + + + + ${userName} + + + + + + + + + + + + + + + ${userLocation} + + + + + '; + $templateProcessor = new TestableTemplateProcesor($mainPart); + + $this->assertEquals( + array('userId', 'userName', 'userLocation'), + $templateProcessor->getVariables() + ); + + $values = array( + array('userId' => 1, 'userName' => 'Batman', 'userLocation' => 'Gotham City'), + array('userId' => 2, 'userName' => 'Superman', 'userLocation' => 'Metropolis'), + ); + $templateProcessor->setValue('tableHeader', 'My clonable table'); + $templateProcessor->cloneRowAndSetValues('userId', $values); + $this->assertContains('Superman', $templateProcessor->getMainPart()); + $this->assertContains('Metropolis', $templateProcessor->getMainPart()); + } + + /** + * @expectedException \Exception + * @test + */ + public function testCloneNotExistingRowShouldThrowException() + { + $mainPart = 'text'; + $templateProcessor = new TestableTemplateProcesor($mainPart); + + $templateProcessor->cloneRow('fake_search', 2); + } + /** * @covers ::setValue * @covers ::saveAs @@ -187,7 +281,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); - $this->assertEquals(array('documentContent', 'headerValue', 'footerValue'), $templateProcessor->getVariables()); + $this->assertEquals(array('documentContent', 'headerValue:100:100', 'footerValue'), $templateProcessor->getVariables()); $macroNames = array('headerValue', 'documentContent', 'footerValue'); $macroValues = array('Header Value', 'Document text.', 'Footer Value'); @@ -200,6 +294,171 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertTrue($docFound); } + /** + * @covers ::setValue + * @test + */ + public function testSetValue() + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-merge.docx'); + Settings::setOutputEscapingEnabled(true); + $helloworld = "hello\nworld"; + $templateProcessor->setValue('userName', $helloworld); + $this->assertEquals( + array('tableHeader', 'userId', 'userLocation'), + $templateProcessor->getVariables() + ); + } + + public function testSetComplexValue() + { + $title = new TextRun(); + $title->addText('This is my title'); + + $firstname = new Text('Donald'); + $lastname = new Text('Duck'); + + $mainPart = ' + + + Hello ${document-title} + + + + + Hello ${firstname} ${lastname} + + '; + + $result = ' + + + + + This is my title + + + + + Hello + + + + Donald + + + + + + + Duck + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setComplexBlock('document-title', $title); + $templateProcessor->setComplexValue('firstname', $firstname); + $templateProcessor->setComplexValue('lastname', $lastname); + + $this->assertEquals(preg_replace('/>\s+<', $result), preg_replace('/>\s+<', $templateProcessor->getMainPart())); + } + + /** + * @covers ::setValues + * @test + */ + public function testSetValues() + { + $mainPart = ' + + + Hello ${firstname} ${lastname} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->setValues(array('firstname' => 'John', 'lastname' => 'Doe')); + + $this->assertContains('Hello John Doe', $templateProcessor->getMainPart()); + } + + /** + * @covers ::setImageValue + * @test + */ + public function testSetImageValue() + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/header-footer.docx'); + $imagePath = __DIR__ . '/_files/images/earth.jpg'; + + $variablesReplace = array( + 'headerValue' => $imagePath, + 'documentContent' => array('path' => $imagePath, 'width' => 500, 'height' => 500), + 'footerValue' => array('path' => $imagePath, 'width' => 100, 'height' => 50, 'ratio' => false), + ); + $templateProcessor->setImageValue(array_keys($variablesReplace), $variablesReplace); + + $docName = 'header-footer-images-test-result.docx'; + $templateProcessor->saveAs($docName); + + $this->assertFileExists($docName, "Generated file '{$docName}' not found!"); + + $expectedDocumentZip = new \ZipArchive(); + $expectedDocumentZip->open($docName); + $expectedContentTypesXml = $expectedDocumentZip->getFromName('[Content_Types].xml'); + $expectedDocumentRelationsXml = $expectedDocumentZip->getFromName('word/_rels/document.xml.rels'); + $expectedHeaderRelationsXml = $expectedDocumentZip->getFromName('word/_rels/header1.xml.rels'); + $expectedFooterRelationsXml = $expectedDocumentZip->getFromName('word/_rels/footer1.xml.rels'); + $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); + $expectedHeaderPartXml = $expectedDocumentZip->getFromName('word/header1.xml'); + $expectedFooterPartXml = $expectedDocumentZip->getFromName('word/footer1.xml'); + $expectedImage = $expectedDocumentZip->getFromName('word/media/image_rId11_document.jpeg'); + if (false === $expectedDocumentZip->close()) { + throw new \Exception("Could not close zip file \"{$docName}\"."); + } + + $this->assertNotEmpty($expectedImage, 'Embed image doesn\'t found.'); + $this->assertContains('/word/media/image_rId11_document.jpeg', $expectedContentTypesXml, '[Content_Types].xml missed "/word/media/image5_document.jpeg"'); + $this->assertContains('/word/_rels/header1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/header1.xml.rels"'); + $this->assertContains('/word/_rels/footer1.xml.rels', $expectedContentTypesXml, '[Content_Types].xml missed "/word/_rels/footer1.xml.rels"'); + $this->assertNotContains('${documentContent}', $expectedMainPartXml, 'word/document.xml has no image.'); + $this->assertNotContains('${headerValue}', $expectedHeaderPartXml, 'word/header1.xml has no image.'); + $this->assertNotContains('${footerValue}', $expectedFooterPartXml, 'word/footer1.xml has no image.'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedDocumentRelationsXml, 'word/_rels/document.xml.rels missed "media/image5_document.jpeg"'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedHeaderRelationsXml, 'word/_rels/header1.xml.rels missed "media/image5_document.jpeg"'); + $this->assertContains('media/image_rId11_document.jpeg', $expectedFooterRelationsXml, 'word/_rels/footer1.xml.rels missed "media/image5_document.jpeg"'); + + unlink($docName); + + // dynamic generated doc + $testFileName = 'images-test-sample.docx'; + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $section->addText('${Test:width=100:ratio=true}'); + $objWriter = \PhpOffice\PhpWord\IOFactory::createWriter($phpWord, 'Word2007'); + $objWriter->save($testFileName); + $this->assertFileExists($testFileName, "Generated file '{$testFileName}' not found!"); + + $resultFileName = 'images-test-result.docx'; + $templateProcessor = new \PhpOffice\PhpWord\TemplateProcessor($testFileName); + unlink($testFileName); + $templateProcessor->setImageValue('Test', $imagePath); + $templateProcessor->setImageValue('Test1', $imagePath); + $templateProcessor->setImageValue('Test2', $imagePath); + $templateProcessor->saveAs($resultFileName); + $this->assertFileExists($resultFileName, "Generated file '{$resultFileName}' not found!"); + + $expectedDocumentZip = new \ZipArchive(); + $expectedDocumentZip->open($resultFileName); + $expectedMainPartXml = $expectedDocumentZip->getFromName('word/document.xml'); + if (false === $expectedDocumentZip->close()) { + throw new \Exception("Could not close zip file \"{$resultFileName}\"."); + } + unlink($resultFileName); + + $this->assertNotContains('${Test}', $expectedMainPartXml, 'word/document.xml has no image.'); + } + /** * @covers ::cloneBlock * @covers ::deleteBlock @@ -211,16 +470,378 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/clone-delete-block.docx'); $this->assertEquals( - array('DELETEME', '/DELETEME', 'CLONEME', '/CLONEME'), + array('DELETEME', '/DELETEME', 'CLONEME', 'blockVariable', '/CLONEME'), $templateProcessor->getVariables() ); $docName = 'clone-delete-block-result.docx'; $templateProcessor->cloneBlock('CLONEME', 3); $templateProcessor->deleteBlock('DELETEME'); + $templateProcessor->setValue('blockVariable#3', 'Test'); $templateProcessor->saveAs($docName); $docFound = file_exists($docName); unlink($docName); $this->assertTrue($docFound); } + + /** + * @covers ::getVariableCount + * @test + */ + public function getVariableCountCountsHowManyTimesEachPlaceholderIsPresent() + { + // create template with placeholders + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $header = $section->addHeader(); + $header->addText('${a_field_that_is_present_three_times}'); + $footer = $section->addFooter(); + $footer->addText('${a_field_that_is_present_twice}'); + $section2 = $phpWord->addSection(); + $section2->addText(' + ${a_field_that_is_present_one_time} + ${a_field_that_is_present_three_times} + ${a_field_that_is_present_twice} + ${a_field_that_is_present_three_times} + '); + $objWriter = IOFactory::createWriter($phpWord); + $templatePath = 'test.docx'; + $objWriter->save($templatePath); + + $templateProcessor = new TemplateProcessor($templatePath); + $variableCount = $templateProcessor->getVariableCount(); + unlink($templatePath); + + $this->assertEquals( + array( + 'a_field_that_is_present_three_times' => 3, + 'a_field_that_is_present_twice' => 2, + 'a_field_that_is_present_one_time' => 1, + ), + $variableCount + ); + } + + /** + * @covers ::cloneBlock + * @test + */ + public function cloneBlockCanCloneABlockTwice() + { + // create template with placeholders and block + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $documentElements = array( + 'Title: ${title}', + '${subreport}', + '${subreport.id}: ${subreport.text}. ', + '${/subreport}', + ); + foreach ($documentElements as $documentElement) { + $section->addText($documentElement); + } + + $objWriter = IOFactory::createWriter($phpWord); + $templatePath = 'test.docx'; + $objWriter->save($templatePath); + + // replace placeholders and save the file + $templateProcessor = new TemplateProcessor($templatePath); + $templateProcessor->setValue('title', 'Some title'); + $templateProcessor->cloneBlock('subreport', 2); + $templateProcessor->setValue('subreport.id', '123', 1); + $templateProcessor->setValue('subreport.text', 'Some text', 1); + $templateProcessor->setValue('subreport.id', '456', 1); + $templateProcessor->setValue('subreport.text', 'Some other text', 1); + $templateProcessor->saveAs($templatePath); + + // assert the block has been cloned twice + // and the placeholders have been replaced correctly + $phpWord = IOFactory::load($templatePath); + $sections = $phpWord->getSections(); + /** @var \PhpOffice\PhpWord\Element\TextRun[] $actualElements */ + $actualElements = $sections[0]->getElements(); + unlink($templatePath); + $expectedElements = array( + 'Title: Some title', + '123: Some text. ', + '456: Some other text. ', + ); + $this->assertCount(count($expectedElements), $actualElements); + foreach ($expectedElements as $i => $expectedElement) { + $this->assertEquals( + $expectedElement, + $actualElements[$i]->getElement(0)->getText() + ); + } + } + + /** + * @covers ::cloneBlock + * @test + */ + public function testCloneBlock() + { + $mainPart = ' + + + + ${CLONEME} + + + + + This block will be cloned with ${variable} + + + + + ${/CLONEME} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->cloneBlock('CLONEME', 3); + + $this->assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned with ${variable}')); + } + + /** + * @covers ::cloneBlock + * @test + */ + public function testCloneBlockWithVariables() + { + $mainPart = ' + + + + ${CLONEME} + + + + + Address ${address}, Street ${street} + + + + + ${/CLONEME} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->cloneBlock('CLONEME', 3, true, true); + + $this->assertContains('Address ${address#1}, Street ${street#1}', $templateProcessor->getMainPart()); + $this->assertContains('Address ${address#2}, Street ${street#2}', $templateProcessor->getMainPart()); + $this->assertContains('Address ${address#3}, Street ${street#3}', $templateProcessor->getMainPart()); + } + + public function testCloneBlockWithVariableReplacements() + { + $mainPart = ' + + + + ${CLONEME} + + + + + City: ${city}, Street: ${street} + + + + + ${/CLONEME} + + '; + + $replacements = array( + array('city' => 'London', 'street' => 'Baker Street'), + array('city' => 'New York', 'street' => '5th Avenue'), + array('city' => 'Rome', 'street' => 'Via della Conciliazione'), + ); + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->cloneBlock('CLONEME', 0, true, false, $replacements); + + $this->assertContains('City: London, Street: Baker Street', $templateProcessor->getMainPart()); + $this->assertContains('City: New York, Street: 5th Avenue', $templateProcessor->getMainPart()); + $this->assertContains('City: Rome, Street: Via della Conciliazione', $templateProcessor->getMainPart()); + } + + /** + * Template macros can be fixed. + * + * @covers ::fixBrokenMacros + * @test + */ + public function testFixBrokenMacros() + { + $templateProcessor = new TestableTemplateProcesor(); + + $fixed = $templateProcessor->fixBrokenMacros('normal text'); + $this->assertEquals('normal text', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('${documentContent}'); + $this->assertEquals('${documentContent}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('${documentContent}'); + $this->assertEquals('${documentContent}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$1500${documentContent}'); + $this->assertEquals('$1500${documentContent}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$1500${documentContent}'); + $this->assertEquals('$1500${documentContent}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('25$ plus some info {hint}'); + $this->assertEquals('25$ plus some info {hint}', $fixed); + + $fixed = $templateProcessor->fixBrokenMacros('$15,000.00. ${variable_name}'); + $this->assertEquals('$15,000.00. ${variable_name}', $fixed); + } + + /** + * @covers ::getMainPartName + */ + public function testMainPartNameDetection() + { + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); + + $variables = array('test'); + + $this->assertEquals($variables, $templateProcessor->getVariables()); + } + + /** + * @covers ::getVariables + */ + public function testGetVariables() + { + $templateProcessor = new TestableTemplateProcesor(); + + $variables = $templateProcessor->getVariablesForPart('normal text'); + $this->assertEquals(array(), $variables); + + $variables = $templateProcessor->getVariablesForPart('${documentContent}'); + $this->assertEquals(array('documentContent'), $variables); + + $variables = $templateProcessor->getVariablesForPart('$15,000.00. ${variable_name}'); + $this->assertEquals(array('variable_name'), $variables); + } + + /** + * @covers ::textNeedsSplitting + */ + public function testTextNeedsSplitting() + { + $templateProcessor = new TestableTemplateProcesor(); + + $this->assertFalse($templateProcessor->textNeedsSplitting('${nothing-to-replace}')); + + $text = 'Hello ${firstname} ${lastname}'; + $this->assertTrue($templateProcessor->textNeedsSplitting($text)); + $splitText = $templateProcessor->splitTextIntoTexts($text); + $this->assertFalse($templateProcessor->textNeedsSplitting($splitText)); + } + + /** + * @covers ::splitTextIntoTexts + */ + public function testSplitTextIntoTexts() + { + $templateProcessor = new TestableTemplateProcesor(); + + $splitText = $templateProcessor->splitTextIntoTexts('${nothing-to-replace}'); + $this->assertEquals('${nothing-to-replace}', $splitText); + + $splitText = $templateProcessor->splitTextIntoTexts('Hello ${firstname} ${lastname}'); + $this->assertEquals('Hello ${firstname} ${lastname}', $splitText); + } + + public function testFindXmlBlockStart() + { + $toFind = ' + + + + + This whole paragraph will be replaced with my ${title} + '; + $mainPart = ' + + + + + + + ${value1} ${value2} + + + + + + + . + + + + + + + + + + ' . $toFind . ' + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $position = $templateProcessor->findContainingXmlBlockForMacro('${title}', 'w:r'); + + $this->assertEquals($toFind, $templateProcessor->getSlice($position['start'], $position['end'])); + } + + public function testShouldReturnFalseIfXmlBlockNotFound() + { + $mainPart = ' + + + + + + this is my text containing a ${macro} + + + '; + $templateProcessor = new TestableTemplateProcesor($mainPart); + + //non-existing macro + $result = $templateProcessor->findContainingXmlBlockForMacro('${fake-macro}', 'w:p'); + $this->assertFalse($result); + + //existing macro but not inside node looked for + $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:fake-node'); + $this->assertFalse($result); + + //existing macro but end tag not found after macro + $result = $templateProcessor->findContainingXmlBlockForMacro('${macro}', 'w:rPr'); + $this->assertFalse($result); + } + + public function testShouldMakeFieldsUpdateOnOpen() + { + $settingsPart = ' + + '; + $templateProcessor = new TestableTemplateProcesor(null, $settingsPart); + + $templateProcessor->setUpdateFields(true); + $this->assertContains('', $templateProcessor->getSettingsPart()); + + $templateProcessor->setUpdateFields(false); + $this->assertContains('', $templateProcessor->getSettingsPart()); + } } diff --git a/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index 6ef02754..101e226f 100644 --- a/tests/PhpWord/Writer/HTML/ElementTest.php +++ b/tests/PhpWord/Writer/HTML/ElementTest.php @@ -18,6 +18,7 @@ namespace PhpOffice\PhpWord\Writer\HTML; use PhpOffice\PhpWord\Element\Text as TextElement; +use PhpOffice\PhpWord\Element\TextRun; use PhpOffice\PhpWord\Element\TrackChange; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Writer\HTML; @@ -70,10 +71,10 @@ class ElementTest extends \PHPUnit\Framework\TestCase $text2->setTrackChange(new TrackChange(TrackChange::DELETED, 'another author', new \DateTime())); $dom = $this->getAsHTML($phpWord); - $xpath = new \DOMXpath($dom); + $xpath = new \DOMXPath($dom); - $this->assertTrue($xpath->query('/html/body/p[1]/ins')->length == 1); - $this->assertTrue($xpath->query('/html/body/p[2]/del')->length == 1); + $this->assertEquals(1, $xpath->query('/html/body/p[1]/ins')->length); + $this->assertEquals(1, $xpath->query('/html/body/p[2]/del')->length); } /** @@ -94,11 +95,11 @@ class ElementTest extends \PHPUnit\Framework\TestCase $cell22->addText('second cell'); $dom = $this->getAsHTML($phpWord); - $xpath = new \DOMXpath($dom); + $xpath = new \DOMXPath($dom); - $this->assertTrue($xpath->query('/html/body/table/tr[1]/td')->length == 1); + $this->assertEquals(1, $xpath->query('/html/body/table/tr[1]/td')->length); $this->assertEquals('2', $xpath->query('/html/body/table/tr/td[1]')->item(0)->attributes->getNamedItem('colspan')->textContent); - $this->assertTrue($xpath->query('/html/body/table/tr[2]/td')->length == 2); + $this->assertEquals(2, $xpath->query('/html/body/table/tr[2]/td')->length); $this->assertEquals('#6086B8', $xpath->query('/html/body/table/tr[1]/td')->item(0)->attributes->getNamedItem('bgcolor')->textContent); $this->assertEquals('#ffffff', $xpath->query('/html/body/table/tr[1]/td')->item(0)->attributes->getNamedItem('color')->textContent); @@ -128,11 +129,11 @@ class ElementTest extends \PHPUnit\Framework\TestCase $row3->addCell(500)->addText('third cell being spanned'); $dom = $this->getAsHTML($phpWord); - $xpath = new \DOMXpath($dom); + $xpath = new \DOMXPath($dom); - $this->assertTrue($xpath->query('/html/body/table/tr[1]/td')->length == 2); + $this->assertEquals(2, $xpath->query('/html/body/table/tr[1]/td')->length); $this->assertEquals('3', $xpath->query('/html/body/table/tr[1]/td[1]')->item(0)->attributes->getNamedItem('rowspan')->textContent); - $this->assertTrue($xpath->query('/html/body/table/tr[2]/td')->length == 1); + $this->assertEquals(1, $xpath->query('/html/body/table/tr[2]/td')->length); } private function getAsHTML(PhpWord $phpWord) @@ -143,4 +144,46 @@ class ElementTest extends \PHPUnit\Framework\TestCase return $dom; } + + public function testWriteTitleTextRun() + { + $expected = 'Title with TextRun'; + + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + + $textRun = new TextRun(); + $textRun->addText($expected); + + $section->addTitle($textRun); + + $htmlWriter = new HTML($phpWord); + $content = $htmlWriter->getContent(); + + $this->assertContains($expected, $content); + } + + /** + * Tests writing table with layout + */ + public function testWriteTableLayout() + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addTable(); + + $table1 = $section->addTable(array('layout' => \PhpOffice\PhpWord\Style\Table::LAYOUT_FIXED)); + $row1 = $table1->addRow(); + $row1->addCell()->addText('fixed layout table'); + + $table2 = $section->addTable(array('layout' => \PhpOffice\PhpWord\Style\Table::LAYOUT_AUTO)); + $row2 = $table2->addRow(); + $row2->addCell()->addText('auto layout table'); + + $dom = $this->getAsHTML($phpWord); + $xpath = new \DOMXPath($dom); + + $this->assertEquals('table-layout: fixed;', $xpath->query('/html/body/table[1]')->item(0)->attributes->getNamedItem('style')->textContent); + $this->assertEquals('table-layout: auto;', $xpath->query('/html/body/table[2]')->item(0)->attributes->getNamedItem('style')->textContent); + } } diff --git a/tests/PhpWord/Writer/HTMLTest.php b/tests/PhpWord/Writer/HTMLTest.php index 8868db5a..24a8bca3 100644 --- a/tests/PhpWord/Writer/HTMLTest.php +++ b/tests/PhpWord/Writer/HTMLTest.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Writer; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\Settings; use PhpOffice\PhpWord\SimpleType\Jc; @@ -26,7 +27,7 @@ use PhpOffice\PhpWord\SimpleType\Jc; * * @runTestsInSeparateProcesses */ -class HTMLTest extends \PHPUnit\Framework\TestCase +class HTMLTest extends AbstractWebServerEmbeddedTest { /** * Construct @@ -57,7 +58,7 @@ class HTMLTest extends \PHPUnit\Framework\TestCase { $localImage = __DIR__ . '/../_files/images/PhpWord.png'; $archiveImage = 'zip://' . __DIR__ . '/../_files/documents/reader.docx#word/media/image1.jpeg'; - $gdImage = 'http://php.net/images/logos/php-med-trans-light.gif'; + $gdImage = self::getRemoteGifImageUrl(); $objectSrc = __DIR__ . '/../_files/documents/sheet.xls'; $file = __DIR__ . '/../_files/temp.html'; diff --git a/tests/PhpWord/Writer/RTF/StyleTest.php b/tests/PhpWord/Writer/RTF/StyleTest.php index 5f04f1a8..317014c6 100644 --- a/tests/PhpWord/Writer/RTF/StyleTest.php +++ b/tests/PhpWord/Writer/RTF/StyleTest.php @@ -17,7 +17,9 @@ namespace PhpOffice\PhpWord\Writer\RTF; +use PhpOffice\PhpWord\Writer\RTF; use PhpOffice\PhpWord\Writer\RTF\Style\Border; +use PHPUnit\Framework\Assert; /** * Test class for PhpOffice\PhpWord\Writer\RTF\Style subnamespace @@ -29,7 +31,7 @@ class StyleTest extends \PHPUnit\Framework\TestCase */ public function testEmptyStyles() { - $styles = array('Font', 'Paragraph', 'Section'); + $styles = array('Font', 'Paragraph', 'Section', 'Tab', 'Indentation'); foreach ($styles as $style) { $objectClass = 'PhpOffice\\PhpWord\\Writer\\RTF\\Style\\' . $style; $object = new $objectClass(); @@ -55,4 +57,55 @@ class StyleTest extends \PHPUnit\Framework\TestCase $this->assertEquals($expected, $content); } + + public function testIndentation() + { + $indentation = new \PhpOffice\PhpWord\Style\Indentation(); + $indentation->setLeft(1); + $indentation->setRight(2); + $indentation->setFirstLine(3); + + $indentWriter = new \PhpOffice\PhpWord\Writer\RTF\Style\Indentation($indentation); + $indentWriter->setParentWriter(new RTF()); + $result = $indentWriter->write(); + + Assert::assertEquals('\fi3\li1\ri2 ', $result); + } + + public function testRightTab() + { + $tabRight = new \PhpOffice\PhpWord\Style\Tab(); + $tabRight->setType(\PhpOffice\PhpWord\Style\Tab::TAB_STOP_RIGHT); + $tabRight->setPosition(5); + + $tabWriter = new \PhpOffice\PhpWord\Writer\RTF\Style\Tab($tabRight); + $tabWriter->setParentWriter(new RTF()); + $result = $tabWriter->write(); + + Assert::assertEquals('\tqr\tx5', $result); + } + + public function testCenterTab() + { + $tabRight = new \PhpOffice\PhpWord\Style\Tab(); + $tabRight->setType(\PhpOffice\PhpWord\Style\Tab::TAB_STOP_CENTER); + + $tabWriter = new \PhpOffice\PhpWord\Writer\RTF\Style\Tab($tabRight); + $tabWriter->setParentWriter(new RTF()); + $result = $tabWriter->write(); + + Assert::assertEquals('\tqc\tx0', $result); + } + + public function testDecimalTab() + { + $tabRight = new \PhpOffice\PhpWord\Style\Tab(); + $tabRight->setType(\PhpOffice\PhpWord\Style\Tab::TAB_STOP_DECIMAL); + + $tabWriter = new \PhpOffice\PhpWord\Writer\RTF\Style\Tab($tabRight); + $tabWriter->setParentWriter(new RTF()); + $result = $tabWriter->write(); + + Assert::assertEquals('\tqdec\tx0', $result); + } } diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index 25c62ecc..703f4590 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -387,6 +387,7 @@ class ElementTest extends \PHPUnit\Framework\TestCase $section->addSDT('comboBox')->setListItems(array('1' => 'Choice 1', '2' => 'Choice 2'))->setValue('select value'); $section->addSDT('dropDownList'); $section->addSDT('date')->setAlias('date_alias')->setTag('my_tag'); + $section->addSDT('plainText'); $doc = TestHelperDOCX::getDocument($phpWord); @@ -405,6 +406,8 @@ class ElementTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists($path . '[3]/w:sdt/w:sdtPr/w:date')); $this->assertTrue($doc->elementExists($path . '[3]/w:sdt/w:sdtPr/w:alias')); $this->assertTrue($doc->elementExists($path . '[3]/w:sdt/w:sdtPr/w:tag')); + + $this->assertTrue($doc->elementExists($path . '[4]/w:sdt/w:sdtPr/w:text')); } /** @@ -492,4 +495,19 @@ class ElementTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists('/w:document/w:body/w:p[2]/w:pPr/w:pStyle')); $this->assertEquals('Heading1', $doc->getElementAttribute('/w:document/w:body/w:p[2]/w:pPr/w:pStyle', 'w:val')); } + + /** + * Test correct writing of text with ampersant in it + */ + public function testTextWithAmpersant() + { + \PhpOffice\PhpWord\Settings::setOutputEscapingEnabled(true); + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addText('this text contains an & (ampersant)'); + + $doc = TestHelperDOCX::getDocument($phpWord); + $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:t')); + $this->assertEquals('this text contains an & (ampersant)', $doc->getElement('/w:document/w:body/w:p/w:r/w:t')->nodeValue); + } } diff --git a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php index 8201d746..8a21e827 100644 --- a/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php +++ b/tests/PhpWord/Writer/Word2007/Part/SettingsTest.php @@ -17,11 +17,10 @@ namespace PhpOffice\PhpWord\Writer\Word2007\Part; +use PhpOffice\Common\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\ComplexType\ProofState; use PhpOffice\PhpWord\ComplexType\TrackChangesView; use PhpOffice\PhpWord\PhpWord; -use PhpOffice\PhpWord\Settings; -use PhpOffice\PhpWord\Shared\Microsoft\PasswordEncoder; use PhpOffice\PhpWord\SimpleType\Zoom; use PhpOffice\PhpWord\Style\Language; use PhpOffice\PhpWord\TestHelperDOCX; diff --git a/tests/PhpWord/Writer/Word2007/Style/ParagraphTest.php b/tests/PhpWord/Writer/Word2007/Style/ParagraphTest.php index 8443bbca..843f9880 100644 --- a/tests/PhpWord/Writer/Word2007/Style/ParagraphTest.php +++ b/tests/PhpWord/Writer/Word2007/Style/ParagraphTest.php @@ -51,6 +51,32 @@ class ParagraphTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists($path)); } + public function testLineSpacingExact() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $section->addText('test', null, array('spacing' => 240, 'spacingLineRule' => 'exact')); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $path = '/w:document/w:body/w:p/w:pPr/w:spacing'; + $this->assertTrue($doc->elementExists($path)); + $this->assertEquals('exact', $doc->getElementAttribute($path, 'w:lineRule')); + $this->assertEquals('240', $doc->getElementAttribute($path, 'w:line')); + } + + public function testLineSpacingAuto() + { + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $section->addText('test', null, array('spacing' => 240, 'spacingLineRule' => 'auto')); + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $path = '/w:document/w:body/w:p/w:pPr/w:spacing'; + $this->assertTrue($doc->elementExists($path)); + $this->assertEquals('auto', $doc->getElementAttribute($path, 'w:lineRule')); + $this->assertEquals('480', $doc->getElementAttribute($path, 'w:line')); + } + public function testSuppressAutoHyphens() { $paragraphStyle = new ParagraphStyle(); diff --git a/tests/PhpWord/Writer/Word2007/Style/TableTest.php b/tests/PhpWord/Writer/Word2007/Style/TableTest.php index ec3b2483..8e5cb634 100644 --- a/tests/PhpWord/Writer/Word2007/Style/TableTest.php +++ b/tests/PhpWord/Writer/Word2007/Style/TableTest.php @@ -141,4 +141,21 @@ class TableTest extends \PHPUnit\Framework\TestCase $this->assertSame($value, (int) $doc->getElementAttribute($path, 'w:w')); $this->assertSame($type, $doc->getElementAttribute($path, 'w:type')); } + + public function testRigthToLeft() + { + $tableStyle = new Table(); + $tableStyle->setBidiVisual(true); + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $table = $section->addTable($tableStyle); + $table->addRow(); + + $doc = TestHelperDOCX::getDocument($phpWord, 'Word2007'); + + $path = '/w:document/w:body/w:tbl/w:tblPr/w:bidiVisual'; + $this->assertTrue($doc->elementExists($path)); + $this->assertEquals('1', $doc->getElementAttribute($path, 'w:val')); + } } diff --git a/tests/PhpWord/Writer/Word2007Test.php b/tests/PhpWord/Writer/Word2007Test.php index 0db36fc1..563475b4 100644 --- a/tests/PhpWord/Writer/Word2007Test.php +++ b/tests/PhpWord/Writer/Word2007Test.php @@ -17,6 +17,7 @@ namespace PhpOffice\PhpWord\Writer; +use PhpOffice\PhpWord\AbstractWebServerEmbeddedTest; use PhpOffice\PhpWord\PhpWord; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\TestHelperDOCX; @@ -26,7 +27,7 @@ use PhpOffice\PhpWord\TestHelperDOCX; * * @runTestsInSeparateProcesses */ -class Word2007Test extends \PHPUnit\Framework\TestCase +class Word2007Test extends AbstractWebServerEmbeddedTest { /** * Tear down after each test @@ -75,7 +76,7 @@ class Word2007Test extends \PHPUnit\Framework\TestCase public function testSave() { $localImage = __DIR__ . '/../_files/images/earth.jpg'; - $remoteImage = 'http://php.net/images/logos/new-php-logo.png'; + $remoteImage = self::getRemoteGifImageUrl(); $phpWord = new PhpWord(); $phpWord->addFontStyle('Font', array('size' => 11)); $phpWord->addParagraphStyle('Paragraph', array('alignment' => Jc::CENTER)); diff --git a/tests/PhpWord/_files/documents/reader-2011.docx b/tests/PhpWord/_files/documents/reader-2011.docx new file mode 100644 index 00000000..be94eca5 Binary files /dev/null and b/tests/PhpWord/_files/documents/reader-2011.docx differ diff --git a/tests/PhpWord/_files/images/new-php-logo.png b/tests/PhpWord/_files/images/new-php-logo.png new file mode 100644 index 00000000..66490799 Binary files /dev/null and b/tests/PhpWord/_files/images/new-php-logo.png differ diff --git a/tests/PhpWord/_files/templates/clone-delete-block.docx b/tests/PhpWord/_files/templates/clone-delete-block.docx index 049d5ca4..986742e3 100644 Binary files a/tests/PhpWord/_files/templates/clone-delete-block.docx and b/tests/PhpWord/_files/templates/clone-delete-block.docx differ diff --git a/tests/PhpWord/_files/templates/document22-xml.docx b/tests/PhpWord/_files/templates/document22-xml.docx new file mode 100644 index 00000000..206d80f4 Binary files /dev/null and b/tests/PhpWord/_files/templates/document22-xml.docx differ diff --git a/tests/PhpWord/_files/templates/header-footer.docx b/tests/PhpWord/_files/templates/header-footer.docx index 647d5222..dc2e9e8b 100644 Binary files a/tests/PhpWord/_files/templates/header-footer.docx and b/tests/PhpWord/_files/templates/header-footer.docx differ diff --git a/tests/PhpWord/_includes/AbstractWebServerEmbeddedTest.php b/tests/PhpWord/_includes/AbstractWebServerEmbeddedTest.php new file mode 100644 index 00000000..9316a9fe --- /dev/null +++ b/tests/PhpWord/_includes/AbstractWebServerEmbeddedTest.php @@ -0,0 +1,80 @@ +start(); + while (!self::$httpServer->isRunning()) { + usleep(1000); + } + } + } + + public static function tearDownAfterClass() + { + if (self::isBuiltinServerSupported()) { + self::$httpServer->stop(); + } + } + + protected static function getBaseUrl() + { + return 'http://localhost:8080'; + } + + protected static function getRemoteImageUrl() + { + if (self::$httpServer) { + return self::getBaseUrl() . '/images/new-php-logo.png'; + } + + return 'http://php.net/images/logos/new-php-logo.png'; + } + + protected static function getRemoteGifImageUrl() + { + if (self::$httpServer) { + return self::getBaseUrl() . '/images/mario.gif'; + } + + return 'http://php.net/images/logos/php-med-trans-light.gif'; + } + + protected static function getRemoteBmpImageUrl() + { + if (self::$httpServer) { + return self::getBaseUrl() . '/images/duke_nukem.bmp'; + } + + return 'https://samples.libav.org/image-samples/RACECAR.BMP'; + } + + private static function isBuiltinServerSupported() + { + return version_compare(PHP_VERSION, '5.4.0', '>='); + } +} diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php new file mode 100644 index 00000000..80cc748f --- /dev/null +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -0,0 +1,86 @@ +tempDocumentMainPart = $mainPart; + $this->tempDocumentSettingsPart = $settingsPart; + } + + public function fixBrokenMacros($documentPart) + { + return parent::fixBrokenMacros($documentPart); + } + + public function splitTextIntoTexts($text) + { + return parent::splitTextIntoTexts($text); + } + + public function textNeedsSplitting($text) + { + return parent::textNeedsSplitting($text); + } + + public function getVariablesForPart($documentPartXML) + { + $documentPartXML = parent::fixBrokenMacros($documentPartXML); + + return parent::getVariablesForPart($documentPartXML); + } + + public function findXmlBlockStart($offset, $blockType) + { + return parent::findXmlBlockStart($offset, $blockType); + } + + public function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') + { + return parent::findContainingXmlBlockForMacro($macro, $blockType); + } + + public function getSlice($startPosition, $endPosition = 0) + { + return parent::getSlice($startPosition, $endPosition); + } + + /** + * @return string + */ + public function getMainPart() + { + return $this->tempDocumentMainPart; + } + + /** + * @return string + */ + public function getSettingsPart() + { + return $this->tempDocumentSettingsPart; + } +} diff --git a/tests/PhpWord/_includes/XmlDocument.php b/tests/PhpWord/_includes/XmlDocument.php index 8c937bf5..f51eaad8 100644 --- a/tests/PhpWord/_includes/XmlDocument.php +++ b/tests/PhpWord/_includes/XmlDocument.php @@ -37,9 +37,9 @@ class XmlDocument private $dom; /** - * DOMXpath object + * DOMXPath object * - * @var \DOMXpath + * @var \DOMXPath */ private $xpath; @@ -76,8 +76,10 @@ class XmlDocument $this->file = $file; $file = $this->path . '/' . $file; + libxml_disable_entity_loader(false); $this->dom = new \DOMDocument(); $this->dom->load($file); + libxml_disable_entity_loader(true); return $this->dom; } @@ -96,7 +98,7 @@ class XmlDocument } if (null === $this->xpath) { - $this->xpath = new \DOMXpath($this->dom); + $this->xpath = new \DOMXPath($this->dom); $this->xpath->registerNamespace('w14', 'http://schemas.microsoft.com/office/word/2010/wordml'); } @@ -161,7 +163,7 @@ class XmlDocument { $nodeList = $this->getNodeList($path, $file); - return !($nodeList->length == 0); + return $nodeList->length != 0; } /**
header aheader bheader cheader bheader c