From bcfb3e868c0ee086b8b746bd40984b212a294c8b Mon Sep 17 00:00:00 2001 From: Gerald Buttinger Date: Fri, 27 Nov 2015 14:30:22 +0100 Subject: [PATCH 001/142] Add methods setValuesFromArray and cloneRowFromArray to the TemplateProcessor-class and update samples and docs accordingly --- docs/templates-processing.rst | 16 ++++ .../Sample_37_TemplateCloneRowFromArray.php | 91 +++++++++++++++++++ src/PhpWord/TemplateProcessor.php | 34 +++++++ 3 files changed, 141 insertions(+) mode change 100644 => 100755 docs/templates-processing.rst create mode 100755 samples/Sample_37_TemplateCloneRowFromArray.php mode change 100644 => 100755 src/PhpWord/TemplateProcessor.php diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst old mode 100644 new mode 100755 index 6a65ea0d..12b65f80 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -15,11 +15,27 @@ Example: $templateProcessor->setValue('Name', 'Somebody someone'); $templateProcessor->setValue('Street', 'Coming-Undone-Street 32'); +You can also use ``TemplateProcessor::setValuesFromArray`` method to perform replacements from an array of "variable => value"-pairs. + +Example: + +.. code-block:: php + + $replacements = [ + 'Name' => 'Somebody someone', + 'Street' => 'Coming-Undone-Street 32' + ]; + $templateProcessor = new TemplateProcessor('Template.docx'); + $templateProcessor->setValuesFromArray($replacements); + It is not possible to directly add new OOXML elements to the template file being processed, but it is possible to transform main document part of the template using XSLT (see ``TemplateProcessor::applyXslStyleSheet``). See ``Sample_07_TemplateCloneRow.php`` for example on how to create multirow from a single row in a template by using ``TemplateProcessor::cloneRow``. +See ``Sample_37_TemplateCloneRowFromArray.php`` for example on how to create +multirow from a single row with a two-dimensional array as data-source in a template by using ``TemplateProcessor::cloneRowFromArray``. + 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``. diff --git a/samples/Sample_37_TemplateCloneRowFromArray.php b/samples/Sample_37_TemplateCloneRowFromArray.php new file mode 100755 index 00000000..5abf1103 --- /dev/null +++ b/samples/Sample_37_TemplateCloneRowFromArray.php @@ -0,0 +1,91 @@ + htmlspecialchars(date('l')), // On section/content + 'time' => htmlspecialchars(date('H:i')), // On footer + 'serverName' => htmlspecialchars(realpath(__DIR__)), // On header +]; +$templateProcessor->setValuesFromArray($replacements); + +// Simple table +$rows = [ + [ + 'rowNumber' => 1, + 'rowValue' => 'Sun' + ], + [ + 'rowNumber' => 2, + 'rowValue' => 'Mercury' + ], + [ + 'rowNumber' => 3, + 'rowValue' => 'Venus' + ], + [ + 'rowNumber' => 4, + 'rowValue' => 'Earth' + ], + [ + 'rowNumber' => 5, + 'rowValue' => 'Mars' + ], + [ + 'rowNumber' => 6, + 'rowValue' => 'Jupiter' + ], + [ + 'rowNumber' => 7, + 'rowValue' => 'Saturn' + ], + [ + 'rowNumber' => 8, + 'rowValue' => 'Uranus' + ], + [ + 'rowNumber' => 9, + 'rowValue' => 'Neptun' + ], + [ + 'rowNumber' => 10, + 'rowValue' => 'Pluto' + ] +]; +$templateProcessor->cloneRowFromArray('rowValue', $rows); + +// Table with a spanned cell +$rows = [ + [ + 'userId' => 1, + 'userFirstName' => 'James', + 'userName' => 'Taylor', + 'userPhone' => '+1 428 889 773' + ], + [ + 'userId' => 2, + 'userFirstName' => 'Robert', + 'userName' => 'Bell', + 'userPhone' => '+1 428 889 774' + ], + [ + 'userId' => 3, + 'userFirstName' => 'Michael', + 'userName' => 'Ray', + 'userPhone' => '+1 428 889 775' + ] +]; +$templateProcessor->cloneRowFromArray('userId', $rows); + + +echo date('H:i:s'), ' Saving the result document...', EOL; +$templateProcessor->saveAs('results/Sample_07_TemplateCloneRow.docx'); + +echo getEndingNotes(array('Word2007' => 'docx')); +if (!CLI) { + include_once 'Sample_Footer.php'; +} diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php old mode 100644 new mode 100755 index ce92bacf..654bcf8c --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -155,6 +155,20 @@ class TemplateProcessor } } + /** + * Set values from a one-dimensional array of "variable => value"-pairs. + * + * @param array $values + * + * @return void + */ + public function setValuesFromArray($values) + { + foreach ($values as $macro => $replace) { + $this->setValue($macro, $replace); + } + } + /** * Returns array of all variables in template. * @@ -234,6 +248,26 @@ class TemplateProcessor $this->tempDocumentMainPart = $result; } + /** + * Clone a table row and populates it's values from a two-dimensional array in a template document. + * + * @param string $search + * @param array $rows + * + * @return void + */ + public function cloneRowFromArray($search, $rows) + { + $this->cloneRow($search, count($rows)); + + foreach ($rows as $rowKey => $rowData) { + $rowNumber = $rowKey+1; + foreach ($rowData as $macro => $replace) { + $this->setValue($macro.'#'.$rowNumber,$replace); + } + } + } + /** * Clone a block. * From f51422ad8dbd212fc91932b509180fd31988f800 Mon Sep 17 00:00:00 2001 From: Phil Betley Date: Wed, 15 Jun 2016 14:46:07 -0400 Subject: [PATCH 002/142] add variable indexing for block cloning Use the same functionality from cloneRow to index variables inside cloned block sections --- src/PhpWord/TemplateProcessor.php | 27 +++++++++++++----- tests/PhpWord/TemplateProcessorTest.php | 3 +- .../_files/templates/clone-delete-block.docx | Bin 22510 -> 17676 bytes 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 5f131901..90e9f4e6 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -274,9 +274,7 @@ 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; @@ -302,10 +300,7 @@ class TemplateProcessor if (isset($matches[3])) { $xmlBlock = $matches[3]; - $cloned = array(); - for ($i = 1; $i <= $clones; $i++) { - $cloned[] = $xmlBlock; - } + $cloned = $this->indexClonedVariables($clones, $xmlBlock); if ($replace) { $this->tempDocumentMainPart = str_replace( @@ -545,4 +540,22 @@ class TemplateProcessor return substr($this->tempDocumentMainPart, $startPosition, ($endPosition - $startPosition)); } + + /** + * Replaces variable names in cloned + * rows/blocks with indexed names + * + * @param integer $count + * @param string $xmlBlock + * + * @return string + */ + protected function indexClonedVariables($count, $xmlBlock) + { + $results = []; + for ($i = 1; $i <= $count; $i++) { + $results[] = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock); + } + return $results; + } } diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index c5a4b1a3..bd9455f6 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -199,13 +199,14 @@ 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); diff --git a/tests/PhpWord/_files/templates/clone-delete-block.docx b/tests/PhpWord/_files/templates/clone-delete-block.docx index 049d5ca415074504f33050b94f3918c06a280720..986742e369258f906b7771c107e9c6943273b0e8 100644 GIT binary patch literal 17676 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 R008^>C;lnvmHdCa`ajLP=FR{B literal 22510 zcmeFZg;QitlP-)5?(Q%QHn=-9gTuh!?(S}lySu~SGRWZW?(XjHZjJNt+jn=r_r~sj zaPNtzCr)K$MP*l4p66tBR?A62e!&ET27?6y10w}nksG452L}UNh5`e_0E30l6t=Z- zGPZHjRdTa4cGPBYwYK`2_XXm64j9Cz{{OrFFP?$A#8I0bCR9;S(lbhOqq4zKUI`6E z1R$AS;TQ(j14893-de|Jli9Dh#8(1}BQE~8$K~&(v~DwNHg%!Uk>&7*QDO?cpoEbL znrC)5T4L(3FwNomTDfT?5gz=F4Ma);l(?Lgf$#~%8`(%vW|&^XV?MRAIR@OUEB3L} z!=3a=-*u2XiZoYxSK&8f6duC1ALBSYaf|HUHWFQPiF}-1+$DJgB>4=56Dfy{Sj%S6l3Z48O9)jEqAVQ)u7J=Kevxhxmuq@Bn$2H-I~nN9 zsb@8CcO9Q-z?t1IkHb)X72~7IA8jybs_}TwCv?#-M3r-Mi(P2J_*NJd?CRz}(8x=y zAk@tc<}#59OPlJg6Sb4#MkNu|i~*RpbBp{(ar?ygU|AsD(9)3xop-SNDYW$atKr$( z#?T1_%mZpXkJssnNFBffFhncq&lfx15aIR+2?qA@0SPAee|W0YSJ9^T&*iuA>4*rQ zo~r9$Y~{$v@DKh!5B}74edrn7kfoJi zO-rJ1<-J|7jlN8@D#npFL(-&uxmCVRZiK@XYP1AePzT1)t8%NlT~jMyC~{*9myeoD zF8G(DcY}8hS~KE)_W&JyzbO=?V_O`{sUP71jmS;GOweAeRH{3kynvysgYu#(%bU>; zR{ijD`Pha72oaCaJAnqLKC@W)lUOq}#osbkqYQcda~QNtf}1H$-j~JjczbPACv`%6 zXKM-Kyc5Nq^E3|Ywt&&jm%xhJG@F18?i;=$_VY&-ccfv7+c>}jPfAf;E`I{2QM3>4 zz@s@imFjZaCw=`-cd6}&AcFdAY4~&|LNHixpsj-uO$re=Rpid=seMl=ko9;U-KJa#H)v%0D(hXdjxBH*tWYUi;TLv4q?@fdP=dL0IG!Xcb-R>Ly=9i)$i@I5+>pz~&#{x;e z3LV)bKk_rOQ68(5uArz(J5wBV#A{4Q_ovt^PnfKk&P$i%tQ5~%KB(sXnXH5MGF8%2 zJmK8cCrof zkQWy39E1BUy|5{GX?2O(7u-Knq_W9_uElJrhzO*MK%yO<=x}Fs1zVznfZRMhPkN{e z2!SEae);HEo3Q&8WMVDnj`cYWM!|hK8U*OEj5F`#lxDN~B&1(_| zP!b+Aw32F=YC*C48Ex)tUbv|dv6QiX)tg@YI11C|UqZW#zm&-^m9+zyCX}ENUS%W} zpa$S4Jao!kRCsL~Lv*gqhD_L^#tz1e_T#WvM)1Tse(%04(A=B z-k9EoOW~fts_X}r+>$DvO;VG?n;JRHva^uRbVhr%Gv!F6Q@$7e zZZXlxCjV>jC0E?Dq*w!Wh#>FA5&f%6JhjagXkhv$pZMO})y)tCpp%d!(rkdNu+d_r zBUgw)mb;9IOYD*~7tPjNoD=>m^P)$bl>HK-V+ExW)8 zrh$ZT1Dlbn$ze`?txR$F@kV>M;&2X|zg+GF_j-<`yxyx|$rmeb>L_=LxPn3%LX|za z(dGmC`sPuxLc9aT{iKANMvjIK&*oQwkCFLIn(Kavm|3yEsU)_#Y{pxX-;Q;|9<(+U z`tou$^UmXiC#hwm4m(reqV>fBu9v}03Dq3@YYvZ=YIeBq-_Qsz30oi7mG9pD3wVnF zHc}_<+bj9`Ua!FnF;50~sUxQnuYQbLd8qKP%P1;8+GYCCk-zOsf@|jpw0pOxazPTE z*bDxmabpW;Nux{j=fXj+)Qae{P`Po*A`+10UlZ*us*-6R?ngW2h=WQ)6^pY~_c*bs zOa#$Oj;qxa;&dHnEV@M-B1vOGOQ;+wZdTs~ui>Al7m=Cmlk zHd9@3ifB}GG9JK=hPi#vckX4cR*j^%?CNST*x5odYHrAngkj-@#8zdYS*B(z$CX?n zgb>tx0kxT|ChPOCBI>bgxCBPZ%WQA)^&@8$iXAuro`bgjvO%3%)h&+p_(zXPjj%wE ztQZ(>8I~Yr+hY^Pxsx%Adg>)u3}SyBZi16+caTQKRH6cAD40xZYA81#xsV1GAO#8s z1oJX`DYO-%4*m5V-$G-vZYcV~hZi6jDj^9GMzkJX4criPh9)?R+FC+`7@K z6zE+fjh(n^T{m}e2qC|1#N;G^1HQVKZd9)@MWqPaCB-p^n>CafE?XIM1Ojk`xBlq2 z0}^J$0tqG*4MP~BbISZO;SGA&-C*}Z)Wmv$?1>&u;I|yz!Ft*9S(Oog+UOr+akc)i z%m?jzOU#C2_OaW&>yWi3Mig@#A|e0~D4iythuT#37|KaZ%qCjNp>6Ga;|2^UG;3pp zTXurV^-P;4(7RduU7Il791aekPI(T*XH|z_+W6q&i&n(;TVTAh?dj+q3Smut_sBMR z?SruwItW54ny^5F=(LUsNvLgayB@Ix+k@yc2@1`PDiyk%@p-N*b>BUlkReW6YSrb_?Lzc>g}PTjfM$t6d{@-Z$-juu6%tmD38-B^?dPCAbe_)&Ve(V@|~n*>jn2t zmx9OYX&sXrQy~JHhtLK3x-Cuh96PI;*jK!Hgfq7s_3&IR{mslqiB{mIWvEtmddhZ4 zj~0a(LyiMpk6|$iK5uQ1G#2%H(@M~(Z2GvqHPQBYa9guyKA4ruH{Kz}m4pOSDnp>) z3*2f#Yg+tkySMXGiou7^|4LNJ_2oO)pFzAF0T>w8e-V|FnX$Do<9{IYKW{r{>Qc5C z;)I>5CqUeDqSsKOcvzVJXe9bCd3|#BgqP7kn8}MWlL5F6WxFz^zcl6U-xX~6m8|1_ zoCJBN)hW>@J;J|Dp1HRh#&pLj6+n=*-9El;#$;AA>a-BBI*A1S;WKxuzg0$w>pKMh z)sqaY-Q;cDCjD!Nt~Y=&b3-ZOp3)%jy7g^C0WAy;;#tIII~j*M$wrxGCd8O-@-~sY zigWMV0DR%MvM&%MkejK>C-W!n6a#G6a{h*T9)?_h0D@lQGpkuFmn@Gz6Xy^B7$Irw z)!z|0aOdxYWsE}LoC4KHA-TZ?E)rTtgb}5n2~+A^q- zC&4m-0sd7BmR34T^p6B!iV;mVjjP8C@ZQkAULff+N~^=S}MM zccoqzv9Hq4U0R#Xw~@XRaA02%%-;{ltoIPeaab{&Y;GJoEzq}t8~5Hrd0zG$xc3ea z$n}}AX#6;J#g3!?Dm+oawM_uFnM&D(s7!6FDwbVr+>E`;t91va=O2OfD>m(qQK(m! zvmGBQrX{#*#MX>V!`^5R1wDeK?vzq}TZ|$i7IQ$G>+^0zQOxvkI~5hko4eWPV{c;; zxrCMrZd7Tq^JC{SBs;H@*a!65t!mRox9jt9T)XN0HiM*Oyj5FC@^-y9U26;YxDA0p zevIx4U}=*z{5I|E5Q*%H7|i`8*3Xqc+&j?xw*eI#g-lhUeNHP)&D0`Q>i%@Kowb3- zRkAPk7K9G*W;G@tvQJGO=E?$7vx|R1JsqLwmuh`k4bFh%5jMks@f!GmIKy&g^P|fk z_1+B|y|tML9XkjwkT8Ut6#keW@U2hKC0${((qj%s>n2yIk=nxW#7Qz6xy&jJO*@yl z&v-`xb;tV21ptfLcMl#@2F~&BR^NDmqapaps^jA^l zmBW$0Um9seCP|UG)bTGg6VoZLB1VN-TqicByDLdhAWA@EO z2Fe|j35O{#Y~$-CXSUr#w_zzYEM(|_fk({~qtxuz-}G1M8Jgrad&)qSn?skvYqr|) z=+`4Q<<+IhTgz+c*C?jB?QH~F{7}<-GCw)!oXHO1I7rSJWPh&|Hm0Ud6;;!k-$~BxSQMYv;8Z;jz z=plsFVW~YlU1n>{CoR~` zEkZPf3J0GlBkCXPm9+Wy zJ@5eszRm?bH)MI%)VZzic1z2Nb(T~q%rdsabI2C;Saw*{M%RC>*wA`XPVd==JZ3ao zQ+l*=={UcI3Vo>;t@TCy(#WM6;v4LgQ_DiRkBuPFtM$6ZtVmcgVXfs5!I=R?u zr8z!~_P00J-p1{pIuLVfX{*ys|2+O?XoB#;bKOl8+m3)!z^pj4B!1DOT+8~_3HE4& zj&%GW@V|1zN?6Pf|G0$RH>UBj}5*!B0Vx30^`Q;3%J|u!LNhvbP(lb`>@d?HJQI4XT)@k z3IcUFRGua*g9%!361^Q{kfrsx4Sl`l+{w4o77IVkB$~P@h?MioyW{4=EZ~I~kn&HP zqUlBRVNt6rF#7`Fbtx?(;{3BBiXt!KCH|0tAnch}%q$d16_D3o=7Ql){}TN@g}` z*&Mro*)n4W@0O>i%W-isFZ-AS+#!k49+i9gl;7PD_+P@YmVA8U@z!>8s8gopt^30` zCcv4t*^Vh1>`&39z!TMfH78cd50dY!XoN~K?YW<>JlB)A_h^u79*sD}AR?`JPl5MmU)OM0T(2fj2wrRz`ALiDHX6(s23!*teq+LvWY`pMfyY@*r|As&gy?zYutBlvX zRqpSuI(ub5k0a?bVe$-#cqYTd7iWN(?JFFX7(5BrwC9f~S7S!6dTnn06hY>(YNmPc8we;O3h+_|R9*}e zZfTe^Id#F#lNdv`OYJOmci;+C4?|wJlt){oms$8#N>!-)s z*{mipM71%1?MU-z0(woT`;vjA8zr|ol|u{O17dbGJxlfrC7{cWM!;Z9|9RMflNTNF zJ~hVv0Ef-*-^CnCk}%!NA|P~YM`u*SE3)#(DDINUl9(y|d0L{s=t|2JS9imMfnM=* zTH`=qp@V&PQBlRl-?~|BJF{cfETzdN`?o8K-t-VgqK;*~?vm)b{qMp7F(5qmz62e> zh9K*y=zvE&>*$fsx6Epb_LaZUNbaw2_EP8Kp;U}BjEADD)Fun3(yKF=vMTC@U?GzU zMi1344~>#8abv_C+T2hR2Wug}>5T1uTfo=g>8s%y-7a)r``L_^QdD?i7Zip3x^2|l+rqE? zJ9PAXNh7?^HcX}UUB;^IsMZW5zi}k8Tl*(ju60eJb-qHrn83GMsBa=lEmPHC4^IMO^jPQiW!7`@9}FF~0uari}qR;QNvLV7tj zpp{o77qAR9Pvl%fbZ-^-R*+vvU^uVRzEI3FbNuiczFp6Ncu@>ol8%^Vi<`kXRo;%ow@y6VnYGT8{Bnl14|%;qbE5Z91uE8+ zMtsZ66pwQ)?KT5aV+BadDr-8j>9P97|ac*=$1mQ3H!H|jc_nWmOnvW&lQR~VMkDKy#}An3@jk(EoG;--~KVboh= zI5QBssJ(1y6x>}r*92XmYH$C*jjS}28BEw;Zql;l+!d=;EVX;&mcRI#XRF-V({0{6 zke^T3u0v8;CL`l)visMew5?n@D~&7Nki-QyRjD?BIo#$)zXPkD0M&{M}El zE-*l1`k5Cg5_iyplrORPtC6?-+~`C<=gJ#sLOE4r{5Zzp>cW;@k^ci898Qz>da4rq@WiFrGPm&zQg%q(t9V#Rw% zF}`y6pjyO0mNjNwLi)k!;oXy`#aQP>r0ua1FXFVH(-V^?Bl0f3f*kLmhDg8p;j_%RAwKrx~4T|sz}B=y;muqA$-k5zv6JaQ~nW6(t_ znqdBSlXkh2#jokYx(7NA1*wPMM^U!y+Rxq8+L;rh`OgNz_sj*dv&+cy1CdIPM~aqg zMXL+b&L%}A0oN)<7WmaxQLZ+dl+P7BM;ecPl|%4a5#%6|-rM50(4@BxvjSo3dgw(f z7MrN!VfhMw0}%wk zBc|qDFq4()P+??x)KdN4pTOnTw549xcg;8CV6%*pX)~FC80xqTepLhijibH$Ags3g z0AaaCthFl_!*0V^H}FTaCc)$9@!6g=r3yMEbJ0j9<#uG&5-o7D=w8)I++Vb9K5M2s zvC#s#5L8m8Cwp8E<=SIg&8D?v>0i~UeA8a`5^eRH$T1{WcQBHzgm8N#&C6_43#%K@s;DKK5r)ybRAb9~Z7ylOHxuqOWu=YOI|%6OLVDV&{Ph1p z7SryK6ThK{%YKh~psG(eZwih7M?%gR(%|5ups7EY^!;y=gh0{{Xo9D&P09JN&0q6= zN%s%BcA;P5ZOUw^GoA}1y$rZ2Us3qh`;Uq-D55#2i7-l`TV0^*tS}N4ODC}Xq%V}|oKHhL%=HZ9VO=YdG1w>7oT_Evy z_w%kk93Uf1C1?e(HVt9}MV&L{4KsO4V z-*M;_yV>G+j6Q$`^biAyBA?X(ckb)PDGf85H-2NhvlP!MNa>@c!@`LYmQo?j1PzQ-e+caY zm#wDt9#7I<162~}u7DF)0^aMSWC}ybbSbxhD(m$aqDDuT+m-Q4@%SmLP12fb74+R3 z0~uxfd}0<-bMbwMmE)bG1Dxi5NPajlh->uLfIg=qi zSh8*FJ)L|f!5_szJ~qg&ojcE%lUDLc*Tu@^tkRFOEGg>V#GGwmy|oYK)9-TKYB(_)^(1$XHTO@RC@E2K zS5K$l0FuzaNvo-*H?X37^rvnI~x9+No|yc%C{vm(oD#x zoO=Rr4Ov>OMeePe?Ouir<81*FbRJBmgu58VGMXLZLa9$bbN2^$2)nbkJhTzhaGHXN-SLf zzKP(U?ICGJ^~fc4rMTX33m)$Zm~?4yE9!MuLflNu&TE{K#k~4)uMDchuESEdm6#Od zJnD=aHKqRb(N2DcGwqln0PiC#X`tNYLw^$7Iob0eg8aH^-#*MId3I33Ed^(z++-a- zTD+ZBCxl=eO|o5bU_dE$v9~$38XSb8IdkAWly8$IMxP94XxXsL%nA){Qc%uO1J@G~ z8`WQHD_(0=(KEdhJ>Rl(?d4}1VXyjT4@yLt-HdrBMw~tb-D@N~SIDK0;w(mQbN^|b zqCUb-Y?W&K+3$?QrS<4$TG>wS@gAWCnA~SP;{r|!2)Bb?@!ED5W|?B}4(btS3$$zs zjoN8s`C5Tp$~r-=HR*v3{Byrn$;j*4m-*bX0;zzGZn5Yct$c2AqmqeHI4<9=sdM?c z20Ha|s%@(iZtts#qb)i+afI5uLXDzAx!>5|UOm@I%Lpq zjkep9MK)F3@QWXIv?pRHFTl?a|_qQ^t<1nos4g^IY{q9nkbS=A6#6cSPuE*gX;7>|?TCz&R5m zz&Co6BBtA8KDRqPiBsCMA6@qhrYZo$&A5-QQ}84)w21<~UV`sn4-||(c86laBu#(~ zfl7q)cmcJyw|dC;W}oZXW5KPf_oi2|jsv;JOp3;$ zEXm&H)As&7Xm65>=iCFY4VOojh#Kn=?YT^lAYXJ7uKW|GWugfH zpK>OdaEKh_QkI>~QO!QE%S|*Id?R|2@0A_3^Xx9*?X-G@=#dVy8ghBj%%^#L>lP`6 z370Lq_riE34VT?0)??Z=xx=~@$#RH_bnJ$@i|u|9@ZlX%a6IRGwwhe1^G<+=d)Lf2 zRI3gk;+4D#_}nEvYU-W=e>a`B#|kU=y)UU|sHgVQ5n7)Q3c5kK^P9MZbo!RUs_Q+z z?dsaAC36H3v0DQc{X&O)WR8tIrOxc`uf5RDD8$m0fffOD1iMVQ)zX9=EZ1sKBjkj( z5{SO14`NZm1lw3@9U@6h~jF?HCibgeJ;4bOd*|tH_v46Xwz*u&w9Hg56S6 zLMN=USLdRHZO)Cpo>)vznEbM6&#+?h?9|?3vT1X?PEV@i3z&KWf%7mnI$`gN%70+Z zpzZnkjDWIl-h96Q8ca%#*5QGOVHxS94YpLI3r`s%7BEZ|t+{GfFO-GW`Zj3)pY zh;Fynb&osSbxq|w+4PV$dRLPj}!L3J3NW^cKxkU_C97^crdv_a|(!wkXrX# z6F>o+Y-~!PzkF*|csQZYV;P;$-XW@Ik{*yr>~+YQQH~_vE#Ma`1mG5XQ+ApLdMcJ)97Dhd zn}WRB+k17|mbucfT?RLF0f<~K8cU(Wc0ThvT{?V9ge5p)aS|UYjME5pm37WXL!NiA zfXB|KcQ22SP3`{snAzOfOwnx1&G4n(U2CGdN6UmxT*OLij1*loE2vej9uM@qdc-e2 z;5CuRA1ivTl@#3K{>V#fn{ps-2W@f=?ok)W=r)3)-~oDdZ<)o{WiW;^k|y^jZlm95 zf~4Vn+AJfyiq$m>!uHCcD25cn(|#PDjYqS=Ys_Y?{x#;kU)pk8diq?eud%oUdU>~r z^v9TOXGPd<3Q@1W%FH}ZVA)p5L{qQ^A1!$e1~H;ty`Q;PRPL?(xjy%eZZv)Tsh|GD z-yb`{v0J)eNKH9BMiq;(_I2H-GJu2Mu(fS;hw8*Tdy~Y%QD4ZkPBy#NuIc=Jscr2C zZ0AelTizsq;cBReI|lh&-2Q&N9;bttA=*el=)>E8q1Z#fW+xaZ*Fbzhnn6YplYc27 z>S0vWgwJi^^gR;_pKWs@tDW`%#bO7rYNi*86gc(m) z2OmjSh%6G#c-)@D;l*$2Ky001`cRo71Xu*~{7N-#5|wNBz>l%X)Xj&5Kj! zj(YqeD}Eic?J=bBvLzr9DgS+E!_PXNClw~^a&{0y8trCd@kDokFm3K8oXpb$6c2s= z9xrYm%#Cr%Sqz@ypmZ2m)#$*O?UVn&egUnjZm?TRY5_q-$CGI7}WFy7kYaZ&yT_Ypa=8P3CG z7fjm{BS5EbzAK`^Q}+%nApgi+kanS!9vy$h@BW?VBopY*s6cSB+eN%-VE@YDYuMUv z?~WYZ4lSNl>A1_9L2$JnnSpV|p_it~A-4n&h%)N5rY}sA(AI{mW_aGAI;wie|I7W&`G-k z5Ij++0h)Za@jN&wv_;Y43h&*o=Y=r_JnWjIzx!7{MsGHfY%!h3nslhLA{ScEp5Y0zKsE?f2l{Hl_IM12<+&Q81+3s`zu^DonY z8s>Q0+BhqsVpz8o2prI-0g?7W8_wMvpF&U3jn)bxEVsoZpiCUX-9|eh7m02|S;gJ^ zt?kFYd)MnK(V)y}YelxMm;T}=sJ!E#r=m0IOK0uLMF%wt;1A;vmQM`0);Oq$&#QZ< zvFX|1d&1~^${_N9DSK0uO^tLAW(z`9ltwliC^3}fp{LvGixR&ishwQykwBei&ljt#S z-p>c&sOv$9XNt>+2n;E9z7xjjt7tDFl(vf{mn;kz3C~twa9OhWe>yrw(2`qX9s2S^;0F0RF z=EFB`>TPf2DdG|fk6)ZCufGoCdt_Fpb94y#NO=1q5emR=xsLA~(RaG)-1M$g3FwBA z`&<5wlBH_`6juZ;KzKn_OhVgv&F-AECqK1^$a8P6e!D{dfEq}d^@DK0r};{?<(uJ27>i+d zr?dL`(f~%{aI-Be> z2y?Y#8yH?*`r;W!RVzaduww7k>AeSexNi4)$8-?3p<(0fYm|(P?J-IM0i4I9F%Aw1_He z{D7{mFz=rAY}qjRH=iJ&92N%Xt3ZH+DN{;Ju9hz?jB11X1Ze+w^7dZ%@Y%8EMu)U{ z$Cj*XldDVDGX&-xJNERdZSUu|McSVbjrg&rCkC#px0tcREb;ZH1s?)Kucn;I^xsrG z3+k#$d1Z5-106qDJug7_aBlE*r2tY{aIc={9NDHGxA5p;MKE!lteu)ua#`>W<9(b! zmtn;+++Q8TP}&DRh*5=|!|St0!MC3Iyt3flt@n!5Zy&o0Jd?56fKtA7Qy)HtAec0{ zI+A;N)AwPo^~rZv%pjPgAJg7*qZ6-K{}LQVwo9FQo@-d24sxGc(YkD}%8d;M@wd%9 z_;Hj&RxEU5mkaCGT}%YUO!M67oT4T!kP^Ip5R}3OL(hHae*Qf6zoY{yN#@0he`Z-k zBmG-Cprf&qlevwl<3AY|8LI2Hs~o64Bvl{XSGR@L+WqOd7v!vHAFI;uocspON)Mlr_5hS@<@n3K@Tm#jIX?t8$cWpua z8M2G0KP*x6KXi_s4_vc+Jl~}W7jGG79<8j)$5xeMd z*cGm*U1ssaLK=Q2w%NpfBM=JX`2{%{F_?+(&29sg4y2)Er-n-LEUg>Lux&QL-X)|grZ zh2JejM?qd(*zOmIgd<*w$-nLe4g@mbD}TN4IB>FgI_so{)%hNKN1vs36m0bcy+MYD z^Tffs-B2qBDX8QNP)u+7kw0;tBx5mux2-!baP7x0j}6xna4lQAdVplmsXr?PQuR)D za|=$fXVX5++VmB6z0!LAxW+ZK6A>zIy6fNhS^;*CG3otRWZS&Kd_5EU{2Rl;z?9-@ zjZuDoOHAxj=%mRXXhcFCu13u6Fd{u`@tC$bl@sA+*HlMSV{$&~-qA5086E*`Z zqSY6d5>;339`51*$m)7l(IScF=@LiRf49Djak&0+k)xg%I}!P@Y(UiEh@ z@>ZU*r_@)a86?f|&WG*IHeHCX`j%HxlB%Cp_%!cuvJ^q4h zm#Ats3I1O_=Hq7(<@{;jZHfl;05zE0>Bw4#k1dU*nI$k-#1p?TYG)Fg_4b(N_^+=+hPLXmlX; z!@(+D3+t1wKFa!VB){u5z~hZ$8@uTVGtxOP?_q1HMRnv(kDF|=i0DP6X7a?1)UB8fsc7t2#nxMX#{#|w~!WtOu6j&1Cw@I)@0N9SCD3Gk4Q zDOB3-;fB|>Th@lt`9GqFaQP!hL^H|tPlD(8wHwl%8TQPyMWnLg~nGM%a&7v=CBG`N%phP4PhbWCb`%(s3& z&++VlA%56*b*vp#MM04CC+5>X((mh*e?)j_l$lp`=n2Ak`2L<+gh_xlS^MiZ%LAhMs|FKl_IDJ z^%o#U`fG7l3~cIZi;X5Y;IW-&)rm4cIB14ei$e`f=M%u-xZDk*cfX|#(+QKgH(45E zC|ksQ$Y;0FQ^AeoB3$d>M#8+*(;evRV%3!SSs1_eMYhdR-fj; zdkf}Rcb)b8L!$lTGdtmbCSj9bHi%$+X6Ju=7R=A*@_!^@1C0$7|Lwt>IISxM$&3hg z1?;}GyYo7=E*`30{bBU&5yhzk)Uu{xl{Vv)bvsKi$&IFapgGGX5O2cbA*|abWFVfa zlCS?49yU3dq0FM*H4SA#xqCUV;2JYWMN^MGotWfTOG&^1nnQAmRw#OtkY=g#MD9pe4zXq-?o_^60 zfp2q(>#V;(Ezhru45MQjCb;q@kVBRyPI&j$g``rO6`j9M8X;eZK92 zJO@tdXznL>a;pT+8E-es$4Xh;JB*C!`sR;XP^AU48M$doPZgL#sK1cZ}sH`nTSpu&B0Y&|u`Q>E|w ztfk3SAsDD1o?IqGkj?g+|Mq;uyDdPbtLHHNy>j2q!Nji=F+LIX!7aV4(%^*1lN zTaOQiP+%+goEKpX{xo6C(iCA#@@G-{Ea+2&vN`$OSzKa{eEQ*AN-Phexfo|uS!S09 z7bIjoO)nvVOc(EHUn5%uwnOYehoH}z6Y;=$c6)ErO9Fe=s&suGFGXz|ep$`$L}dnA zV&&IEJN!|s(L_QvKK`R?|GlqpOutNOA+Pl`#>Ot+&aoKg0Lv(!$)di+?(s z^}4+W6qT4ns>OdNCav01)^>F#Coe<*>;{#raU1K+3M$6key^;h7PMkm&WP;44QzT* z5sCj(g>0sxia?Jph&qQCfxz8n9v`rUqgoHBsxegbcvb!N(oD z8tKvt#(|EWEN&T^33VTt3FR4^34JaPlP&SCWy||lsoQ>g_+B#i#}&9>Klib=&Xx2` ze!b-)-N$EY0m0eReRQi1yI4L;}>lRr&TXLL+N^%$O>NsxtTG|E>#-a>$UMPf^srH z66N{`Thb(7E}2Y;5J&aF&-bOKKPh_xMx#))L4T7PP~SqH=lA!IST&IWIU$K*83VQ? z8%EN9(AE)mc6SeDr}MQ=SQLSXNLOMiNn=B)57Jwk9k<7u=J>AV;ajgkM`o2_GAMXU zYN&`TNRpHySBJl(nA0Pk->z_-8{}DUu@J;k zUfcGE&!Cr#Pp>y0pwkn}%;&G@5DbbDA~%{~Vxp17#Z4kWj2wZ17CZ6X1|D%$Z8si& z0_cQ|DUaG0GC!PkW;^?cCgz{Je=D^KAhhi9Ir3DSQ2SUV=EJYY&}nTu%L>?KuB1ZV z5>FXu8DFi3uhji|{(V1g;;?nQ3kmnH+8u-A_kij?kA>--VCjh!WVw-xq+gzHPRY)9oyW zB=uGktG3#9Cm(W)nQ2`66fqjyRBw!0*2Z`~0uM5r-XfQ6yW)2;KfFaMrT5sy3&;fu zfHpT|?o`wQ5Sis$p}wOkQ^YCdIbISC{)w?x?0VQ+;&)ldk7#DOLSHnBAx+@Dzx*uj z;*%(j^~a_W$q9o0Z0Ui+l_{PgrcyE^`KBZRheq?~U-Ex?p!)rX8a`JeC=CV9qBjV81 z4Z>lqgnV`p3WTTrmudew&_DJF{ZA9qB7z_T;m_@bf*wePf)Y`4h2a0&wtsc`r;1L) zgo6Ix){Cfmu1FoE$dl0jJBdho>)6;JLJfL_I;-Q&`DUs-;edLDP%o-a@*UPjFuAM^rhl z$NgJGr{cN%XW~K4_#=p?Cw95daB8Z;Lb0Xblxy>-SZ5ZY@Q&=#k)JY%h57B_Wm#AU z_O8HI{5*ecBzgUcU}3z4`B-0Pc5*#!e!CwJ20fqR?uy`ICe_|yqW^9}f)lKOPPJ1N zR;nr`r5T&g`63BW6ZU}Y6&eaw6O;I!@;uPl z_!cYgE!8V@eHt}7R>obs!%fZ$^Ho=u!{^#Zc!KGu*G}6Bh#*3XSnlbwi>H3Crzca0X*D)XVDP@y>dU( zu$mAz*65-*ax8A5tgcrbFC1*zo4@<)^tXh_T4hmxAf!Y(=VusCg!J=G#3*;xE_Jw! zV&p&EH=iDMj*U73dwXu9wPU2LudFI^IajT<8EscC&4C5=E7Kf8r6j9A<^^R$C}_2YjHBpw$; zOn-kyVnm-OtIRn@%jq=QiscI^yiNt)UAH9^ni5yF!om z(xFjeWtT1T6;9cP?(PKH`i`AGN1BCd-3&=$tJtvZ1So{dA2)Q+LQumx<#hM`IEswt zz*nX7~5noO~7)oPAB?TiaMm%2yfL(}wFROUetQap}5f zav{pm?V@OD)XAfzZE(9vj+8F!eYj=}9s22xIMaF6m49z_ZpyMNK1Ad<&Rykp&RP6c)2peF!a%OS8yq^>}jU)$0zy3y2+nDvje_gH%M zW{dr9UABA)Qft=r?9|XTGs5zc656T)tpn=>HWM+k75~Fn0Ld)%}lS zbK0xXpPRZ~j8i^LU+$PK*BQk){3=Dm*}kV?dLNJu4H zr4`a?&=z^Bmy06zb7$mEj14WUf;J(1s+<$%B6`FH-BE-52zaJP5VA>v1Ul$8QhUCR zpV=9|$R7dV8Et4jkJ;{`*aGwIFzD95I1&0X_c+x>WQyx&8ef}nNoUp*EFBAPaZ4tO z$u0+AK=mFS4?C`HGyGH$R!p5 zi+#d59KB{U40(oV^rGtPrNRz<MPk%kYrqnr};DVd+|Hr0p>VZS{g%=^%T;a$4oP@}8MxCVmypaY{b- zEWkanSB$G^=?^qof=e7NZNR>cJb*x|ctfCoaB~%39Sy2GAFqm+(u~LwPKD3S26^a#SPxP3GsFtR)aGzCq?wGi8#WdW~Tijsm-tyxw#t4oIL8G8myFN#pd%ZxY_Kd*EifRCYO05 z<0J}WjjqWG$L@S~i2=seNkUPH>)>Jap~w;d>xY$m-SPq*Bh#u>*28YfuCS<8>4ZX; zc+W-XntDONh(jxdeEL>hTD#fxJ=#DCxRO;W~uV=KdKVdgj#~ z)cy6w*1>QEi3>nr|m+@pT@7z(&U$|_m>EJUxskG^o(W+YVw(c2&iDw@b%!kC8y{;pZjnK5{s;eC)Six6I@ zZ#HZ@hKa|4s<)f!eXBAHEndjzg6YbuZ}cj}EREB99;l&bUsbA46CxMqlpOq1D>a~i zgrU)%gcdIc#=OeA!`bCKAde0a=p2H^*&bb3JIiD3Y(I-gyr0P2i{7{x59JE?oO{Zm zk`cIsNJJ(JO4!yU3Ub(tLn3ahwScIFM!6%O4qUwusB$hRSwQ3K+p_A7wd;Sh&|LQX z(QALTcVJyC5Rb|vfjvh@o_v}P{WtUbSxG(?-phxEqLnWkS+qAE145fQ>Qcn%Crzqq zg$yZgH!Qo%_&lwf^0&+^PTHZgekIX-6DAQOm2~UmDlLUl82ZB6w(X5JS^ldoQ5bFh ztLjbDX^z{y3ozY#7j7@Lhfl$+<_^QBKBk=YbN17Xs>Fu-!XPMscu)5 z4EX~Le_#2s8@wmo#C%wAD5WYFV^OGGHbY0ZJoJwACa^a2+!*g&fuZvpGwwlq3`*|O zk|;GRHXZ12M3=~3+gh4>j9>>hPY`UV1%8cMwK+zKZh7i{@ctHk5bT>T^FWK{%9g4v z)z0>sMc0W=Sx(SacOs8GHnG-iO*tH&B19PrxL(xFTbJFeCV-WW=OKQsbL;WnUce5E zCj+w6Jh3<-f!-Hv*i0+;p0sVrS*)cojnujPMXj*zet>`FvwpVYG|`$*Gxd5)kra&Z zZGnFcRNdJUm~}{MSuDqXa_n9teLIM&+vPtaRSd%X7Ez8h=`&(aKzQbkijcGbu@=>6 z)b?c=uq6hA*Paw;3k9D?IC z<02Z(&yHSpHy-#5k1%{9ox0$67Ojqze7;>?CqIsUi<-@WkA=wQ1-Zg6-=#3DUd_1E z6k1giSk?GC=bW{dOo4qG5zJnb0WuEr5>-_qzVBk1l^e{62S|ehc#H!Tcs7AXw`jBQ z?Jxw$Lxw_YRg@B_=>OJm)Qs&I2iQ@85AJUB#mN-)n~rf#lXTo&Z{eT>YxL)J)>{Fz zV20x3JzANcQtAefqjBh5M&TBf#>!*uuBgR~PYwrpJfD=IVEgvO)HbJulkiTSvh-1g zQIo7jp2izX=bjQJcHlZPFVTh_lETG*29o(pGXyr0y;+ks2(bRVFmeqD`2V_y zG)2E(jSwgQmvWqN!+CA}2$y>>&)WXY1hpb9iuAF|*Fbo4zqyHAIJEj}WKA~6!*V^= z?mnL6c-If)I-+6bP1jkUG^9tVNp2i}6!fGCX%OuKSKH6@W-F~O4dmk5<#&sgdDA}U z8Ng5Jo5E`~f$nQgiN@POE?n{~u$(ww5?F*V@|utvS9IvSX$_XK^H?zm#Le<{j#(D& z#>su=T~YrE9U)ef$ZA2ZfurJ~!;vCv*zu4NGv8zx@9TG<%%v3}jBX&YZx`3k!vq;N zHemBs#0uUm8Sda8GO?4Q3pv3Q$Pz}^>*M~&yCO+*$Iopj z)RNaFwH@UeDf7;+*srg3{wv+?Jwj;8+KV@v9q23NzlWm`+zYm0C-%wtMzuaPeAkic)H-Hrsy9fhZ+)QoX1{;BBvIq|C)1Tnqx?#+ zap%fLz;t${-&q`9#;wR-qa^SD3 zaX$lJlSIJ&mxdfU6S;8M5tksz9sNT_j2uob=W+zckruIk!pX&5$SKHA`yWw!Bnia) zowfPp>_0gN`F^J(4q5r{!}xjk6FHo`aDN0Z1$~E;mGQ|L$g9>z4DXeH4B+n?HaYm$ zOw|#Tf&!@j(;xVew<3p=CkBoPs0_b<`M1jgaGi^(RMY3JNo; bpN9Y4yI7jikW7? Date: Thu, 16 Jun 2016 08:54:24 -0400 Subject: [PATCH 003/142] 5.3 array syntax --- src/PhpWord/TemplateProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 90e9f4e6..aafe63cd 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -552,7 +552,7 @@ class TemplateProcessor */ protected function indexClonedVariables($count, $xmlBlock) { - $results = []; + $results = array(); for ($i = 1; $i <= $count; $i++) { $results[] = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock); } From f7cb73e8a6260822731b14fbf837f3f2986476e5 Mon Sep 17 00:00:00 2001 From: Dmitry Lukashin Date: Thu, 19 Oct 2017 17:33:46 +0300 Subject: [PATCH 004/142] Get rid of duplicated code in TemplateProcessor.php --- src/PhpWord/TemplateProcessor.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 2f6d6258..27ffa458 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -268,9 +268,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) { From ba9e8958d71074f7d43ee83e20b1508e4a9886f7 Mon Sep 17 00:00:00 2001 From: Dmitry Lukashin Date: Thu, 19 Oct 2017 18:51:57 +0300 Subject: [PATCH 005/142] Replace self:: with static:: calls --- src/PhpWord/TemplateProcessor.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 27ffa458..13dcc71c 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -212,18 +212,18 @@ class TemplateProcessor { if (is_array($search)) { foreach ($search as &$item) { - $item = self::ensureMacroCompleted($item); + $item = static::ensureMacroCompleted($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); } } else { - $replace = self::ensureUtf8Encoded($replace); + $replace = static::ensureUtf8Encoded($replace); } if (Settings::isOutputEscapingEnabled()) { From 7792eb2769f8e489b4e8fcdc58003c9004c7fc7e Mon Sep 17 00:00:00 2001 From: Dmitry Lukashin Date: Thu, 19 Oct 2017 19:02:26 +0300 Subject: [PATCH 006/142] Break the references in foreach within TemplateProcessor --- src/PhpWord/TemplateProcessor.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 2f6d6258..095ad33b 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -138,6 +138,7 @@ class TemplateProcessor foreach ($xml as &$item) { $item = $this->transformSingleXml($item, $xsltProcessor); } + unset($item); } else { $xml = $this->transformSingleXml($xml, $xsltProcessor); } @@ -214,6 +215,7 @@ class TemplateProcessor foreach ($search as &$item) { $item = self::ensureMacroCompleted($item); } + unset($item); } else { $search = self::ensureMacroCompleted($search); } @@ -222,6 +224,7 @@ class TemplateProcessor foreach ($replace as &$item) { $item = self::ensureUtf8Encoded($item); } + unset($item); } else { $replace = self::ensureUtf8Encoded($replace); } From 4105a9aad1bd8e08cd3e7d5a74810a9e73f80fd4 Mon Sep 17 00:00:00 2001 From: Nicolas Dermine Date: Fri, 2 Feb 2018 16:19:21 +0100 Subject: [PATCH 007/142] improve `cloneBlock` regex it wrongly matched `\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', + '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, $matches ); diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 7b064ef7..122ed5b6 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -223,4 +223,56 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase unlink($docName); $this->assertTrue($docFound); } + + /** + * @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(); + $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]->getText() + ); + } + } } From 07e97c38cd8843392f83c20b9b109cc6e3cd57b7 Mon Sep 17 00:00:00 2001 From: Nicolas Dermine Date: Mon, 5 Feb 2018 17:45:24 +0100 Subject: [PATCH 008/142] add `getVariableCount` method to `TemplateProcessor` returns how many times each placeholder is present in the document almost the same code as `getVariables` useful when cloning a block a number of times and want to replace placeholders that are present more than once in the block (using the `$limit` parameter of `setValue`) --- src/PhpWord/TemplateProcessor.php | 26 +++++++++++++++++ tests/PhpWord/TemplateProcessorTest.php | 37 +++++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 5dd7b0bf..f5df06b1 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -232,6 +232,32 @@ class TemplateProcessor $this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $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. * diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 7b064ef7..187e5686 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -223,4 +223,41 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase 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); + + $variableCount = (new TemplateProcessor($templatePath))->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 + ); + } } From 623bd993d8bebeae4d4d4da21b19b7f7ffbfe8e2 Mon Sep 17 00:00:00 2001 From: Nicolas Dermine Date: Mon, 5 Feb 2018 17:49:23 +0100 Subject: [PATCH 009/142] refactor: use extracted method in original method --- src/PhpWord/TemplateProcessor.php | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index f5df06b1..2cc56728 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -265,17 +265,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()); } /** From 12ad6fa865cee81bd3b8f84c5f0c970a568b0973 Mon Sep 17 00:00:00 2001 From: Nicolas Dermine Date: Tue, 6 Feb 2018 09:30:24 +0100 Subject: [PATCH 010/142] adapt code for php 5.3 --- tests/PhpWord/TemplateProcessorTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 187e5686..28ade1a3 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -248,7 +248,8 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $templatePath = 'test.docx'; $objWriter->save($templatePath); - $variableCount = (new TemplateProcessor($templatePath))->getVariableCount(); + $templateProcessor = new TemplateProcessor($templatePath); + $variableCount = $templateProcessor->getVariableCount(); unlink($templatePath); $this->assertEquals( From 34bda105365cc4c8d9e8fe9c33f3d8b204c4ee79 Mon Sep 17 00:00:00 2001 From: gthomas2 Date: Tue, 6 Jan 2015 17:20:27 +0000 Subject: [PATCH 011/142] Fix images added in word 2011 --- src/PhpWord/Element/Image.php | 20 +++++++++++++++++++- src/PhpWord/Reader/Word2007/AbstractPart.php | 9 +++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Element/Image.php b/src/PhpWord/Element/Image.php index 03637067..0eb3d937 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 * @@ -131,11 +138,12 @@ class Image extends AbstractElement * @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->name = $name; $this->checkImage(); } @@ -170,6 +178,16 @@ class Image extends AbstractElement return $this->sourceType; } + /** + * Get image name + * + * @return null|string + */ + public function getName() + { + return $this->name; + } + /** * Get image media ID * diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index f64886cf..d8155eb5 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -260,6 +260,15 @@ abstract class AbstractPart } $parent->addImage($imageSource); } + } elseif ($node->nodeName == 'w:drawing') { + // Office 2011 Images + $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'); From 9b722a5b0cc03e7e46390fba9c011c88e0dca46d Mon Sep 17 00:00:00 2001 From: gthomas2 Date: Tue, 6 Jan 2015 17:36:11 +0000 Subject: [PATCH 012/142] Added missing namespaces --- src/PhpWord/Shared/XMLReader.php | 195 +++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 src/PhpWord/Shared/XMLReader.php diff --git a/src/PhpWord/Shared/XMLReader.php b/src/PhpWord/Shared/XMLReader.php new file mode 100644 index 00000000..44ee07cd --- /dev/null +++ b/src/PhpWord/Shared/XMLReader.php @@ -0,0 +1,195 @@ +open($zipFile); + $content = $zip->getFromName($xmlFile); + $zip->close(); + + if ($content === false) { + return false; + } else { + return $this->getDomFromString($content); + } + } + + /** + * Get DOMDocument from content string + * + * @param string $content + * @return \DOMDocument + */ + public function getDomFromString($content) + { + $this->dom = new \DOMDocument(); + $this->dom->loadXML($content); + + return $this->dom; + } + + /** + * Get elements + * + * @param string $path + * @param \DOMElement $contextNode + * @return \DOMNodeList + */ + public function getElements($path, \DOMElement $contextNode = null) + { + if ($this->dom === null) { + return array(); + } + if ($this->xpath === null) { + $this->xpath = new \DOMXpath($this->dom); + // GT Mod - required for reading images + $this->xpath->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); + $this->xpath->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture'); + } + + if (is_null($contextNode)) { + return $this->xpath->query($path); + } else { + return $this->xpath->query($path, $contextNode); + } + } + + /** + * Get element + * + * @param string $path + * @param \DOMElement $contextNode + * @return \DOMElement|null + */ + public function getElement($path, \DOMElement $contextNode = null) + { + $elements = $this->getElements($path, $contextNode); + if ($elements->length > 0) { + return $elements->item(0); + } else { + return null; + } + } + + /** + * Get element attribute + * + * @param string $attribute + * @param \DOMElement $contextNode + * @param string $path + * @return string|null + */ + public function getAttribute($attribute, \DOMElement $contextNode = null, $path = null) + { + $return = null; + if ($path !== null) { + $elements = $this->getElements($path, $contextNode); + if ($elements->length > 0) { + /** @var \DOMElement $node Type hint */ + $node = $elements->item(0); + $return = $node->getAttribute($attribute); + } + } else { + if ($contextNode !== null) { + $return = $contextNode->getAttribute($attribute); + } + } + + return ($return == '') ? null : $return; + } + + /** + * Get element value + * + * @param string $path + * @param \DOMElement $contextNode + * @return string|null + */ + public function getValue($path, \DOMElement $contextNode = null) + { + $elements = $this->getElements($path, $contextNode); + if ($elements->length > 0) { + return $elements->item(0)->nodeValue; + } else { + return null; + } + } + + /** + * Count elements + * + * @param string $path + * @param \DOMElement $contextNode + * @return integer + */ + public function countElements($path, \DOMElement $contextNode = null) + { + $elements = $this->getElements($path, $contextNode); + + return $elements->length; + } + + /** + * Element exists + * + * @param string $path + * @param \DOMElement $contextNode + * @return boolean + */ + public function elementExists($path, \DOMElement $contextNode = null) + { + return $this->getElements($path, $contextNode)->length > 0; + } +} From 566e625b85b80f503db69d3e0e635a817dc0ef10 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 25 Mar 2018 22:46:50 +0200 Subject: [PATCH 013/142] merge/add test/cleanup --- composer.json | 2 +- src/PhpWord/Element/AbstractContainer.php | 2 +- src/PhpWord/Element/Image.php | 15 +- src/PhpWord/Reader/Word2007/AbstractPart.php | 9 +- src/PhpWord/Shared/XMLReader.php | 195 ------------------ tests/PhpWord/Reader/Word2007/ElementTest.php | 36 ++++ tests/PhpWord/Reader/Word2007Test.php | 14 ++ .../PhpWord/_files/documents/reader-2011.docx | Bin 0 -> 36938 bytes 8 files changed, 73 insertions(+), 200 deletions(-) delete mode 100644 src/PhpWord/Shared/XMLReader.php create mode 100644 tests/PhpWord/_files/documents/reader-2011.docx diff --git a/composer.json b/composer.json index 742e4bc8..6dc9be2c 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "php": "^5.3.3 || ^7.0", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "phpoffice/common": "^0.2" + "phpoffice/common": "dev-develop" }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^5.0", diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index ec990720..204d4a73 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -35,7 +35,7 @@ namespace PhpOffice\PhpWord\Element; * @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/Image.php b/src/PhpWord/Element/Image.php index 0eb3d937..bae87ff5 100644 --- a/src/PhpWord/Element/Image.php +++ b/src/PhpWord/Element/Image.php @@ -134,6 +134,7 @@ 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 @@ -141,9 +142,9 @@ class Image extends AbstractElement 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->name = $name; + $this->setIsWatermark($watermark); + $this->setName($name); $this->checkImage(); } @@ -178,6 +179,16 @@ class Image extends AbstractElement return $this->sourceType; } + /** + * Sets the image name + * + * @param string $value + */ + public function setName($value) + { + $this->name = $value; + } + /** * Get image name * diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index d8155eb5..c7ec4ca7 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -261,7 +261,12 @@ abstract class AbstractPart $parent->addImage($imageSource); } } elseif ($node->nodeName == 'w:drawing') { - // Office 2011 Images + // 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); @@ -573,6 +578,8 @@ abstract class AbstractPart return $possibleAttribute; } } + + return null; } return $attributes; diff --git a/src/PhpWord/Shared/XMLReader.php b/src/PhpWord/Shared/XMLReader.php deleted file mode 100644 index 44ee07cd..00000000 --- a/src/PhpWord/Shared/XMLReader.php +++ /dev/null @@ -1,195 +0,0 @@ -open($zipFile); - $content = $zip->getFromName($xmlFile); - $zip->close(); - - if ($content === false) { - return false; - } else { - return $this->getDomFromString($content); - } - } - - /** - * Get DOMDocument from content string - * - * @param string $content - * @return \DOMDocument - */ - public function getDomFromString($content) - { - $this->dom = new \DOMDocument(); - $this->dom->loadXML($content); - - return $this->dom; - } - - /** - * Get elements - * - * @param string $path - * @param \DOMElement $contextNode - * @return \DOMNodeList - */ - public function getElements($path, \DOMElement $contextNode = null) - { - if ($this->dom === null) { - return array(); - } - if ($this->xpath === null) { - $this->xpath = new \DOMXpath($this->dom); - // GT Mod - required for reading images - $this->xpath->registerNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main'); - $this->xpath->registerNamespace('pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture'); - } - - if (is_null($contextNode)) { - return $this->xpath->query($path); - } else { - return $this->xpath->query($path, $contextNode); - } - } - - /** - * Get element - * - * @param string $path - * @param \DOMElement $contextNode - * @return \DOMElement|null - */ - public function getElement($path, \DOMElement $contextNode = null) - { - $elements = $this->getElements($path, $contextNode); - if ($elements->length > 0) { - return $elements->item(0); - } else { - return null; - } - } - - /** - * Get element attribute - * - * @param string $attribute - * @param \DOMElement $contextNode - * @param string $path - * @return string|null - */ - public function getAttribute($attribute, \DOMElement $contextNode = null, $path = null) - { - $return = null; - if ($path !== null) { - $elements = $this->getElements($path, $contextNode); - if ($elements->length > 0) { - /** @var \DOMElement $node Type hint */ - $node = $elements->item(0); - $return = $node->getAttribute($attribute); - } - } else { - if ($contextNode !== null) { - $return = $contextNode->getAttribute($attribute); - } - } - - return ($return == '') ? null : $return; - } - - /** - * Get element value - * - * @param string $path - * @param \DOMElement $contextNode - * @return string|null - */ - public function getValue($path, \DOMElement $contextNode = null) - { - $elements = $this->getElements($path, $contextNode); - if ($elements->length > 0) { - return $elements->item(0)->nodeValue; - } else { - return null; - } - } - - /** - * Count elements - * - * @param string $path - * @param \DOMElement $contextNode - * @return integer - */ - public function countElements($path, \DOMElement $contextNode = null) - { - $elements = $this->getElements($path, $contextNode); - - return $elements->length; - } - - /** - * Element exists - * - * @param string $path - * @param \DOMElement $contextNode - * @return boolean - */ - public function elementExists($path, \DOMElement $contextNode = null) - { - return $this->getElements($path, $contextNode)->length > 0; - } -} 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/Word2007Test.php b/tests/PhpWord/Reader/Word2007Test.php index 62d23a68..e4ea62de 100644 --- a/tests/PhpWord/Reader/Word2007Test.php +++ b/tests/PhpWord/Reader/Word2007Test.php @@ -64,4 +64,18 @@ class Word2007Test extends \PHPUnit\Framework\TestCase $doc = TestHelperDOCX::getDocument($phpWord); $this->assertFalse($doc->elementExists('/w:document/w:body/w:p/w:r[w:t/node()="italics"]/w:rPr/w:b')); } + + /** + * 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/_files/documents/reader-2011.docx b/tests/PhpWord/_files/documents/reader-2011.docx new file mode 100644 index 0000000000000000000000000000000000000000..be94eca5242caeb7c9beda45741d5bcfd3b5838e GIT binary patch literal 36938 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%0AfwqU0&g5gH4Gl!~;>i>16+#cyIzb&8cM|zK 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 Date: Wed, 18 Apr 2018 22:34:53 +0200 Subject: [PATCH 014/142] write column width in ODT writer --- CHANGELOG.md | 1 + src/PhpWord/Element/Table.php | 24 +++++++ src/PhpWord/Style/Table.php | 27 ++++++++ src/PhpWord/Writer/ODText/Element/Table.php | 63 ++++++++++++++----- src/PhpWord/Writer/ODText/Part/Content.php | 1 + src/PhpWord/Writer/ODText/Style/Table.php | 13 ++++ src/PhpWord/Writer/Word2007/Element/Table.php | 16 +---- 7 files changed, 114 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb07fcc2..7a49f25e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ 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 +- Fix missing column with in ODText writer @potofcoffee #413 ### Changed - Remove zend-stdlib dependency @Trainmaster #1284 diff --git a/src/PhpWord/Element/Table.php b/src/PhpWord/Element/Table.php index 10c4db69..16102119 100644 --- a/src/PhpWord/Element/Table.php +++ b/src/PhpWord/Element/Table.php @@ -149,4 +149,28 @@ class Table extends AbstractElement return $columnCount; } + + /** + * The first declared cell width for each column + * + * @return int[] + */ + public function findFirstDefinedCellWidths() + { + $cellWidths = array(); + if (is_array($this->rows)) { + 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/Style/Table.php b/src/PhpWord/Style/Table.php index feb028da..5d4a0150 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -159,6 +159,13 @@ class Table extends Border */ private $position; + /** + * The width of each column, computed based on the max cell width of each column + * + * @var int[] + */ + private $columnWidths; + /** * Create new table style * @@ -724,4 +731,24 @@ class Table extends Border return $this; } + + /** + * Get the columnWidths + * + * @return number[] + */ + public function getColumnWidths() + { + return $this->columnWidths; + } + + /** + * The column widths + * + * @param int[] $value + */ + public function setColumnWidths(array $value = null) + { + $this->columnWidths = $value; + } } 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..b705bb5e 100644 --- a/src/PhpWord/Writer/ODText/Part/Content.php +++ b/src/PhpWord/Writer/ODText/Part/Content.php @@ -246,6 +246,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/Table.php b/src/PhpWord/Writer/ODText/Style/Table.php index 249321cf..f5a3c431 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -45,5 +45,18 @@ class Table extends AbstractStyle $xmlWriter->writeAttribute('table:align', 'center'); $xmlWriter->endElement(); // style:table-properties $xmlWriter->endElement(); // style:style + + $cellWidths = $style->getColumnWidths(); + + for ($i = 0; $i < count($cellWidths); $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/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) { From 58c6c52ee94b5b1821f9c2fad7ee415fd4fca1c6 Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Wed, 23 May 2018 18:22:54 +0200 Subject: [PATCH 015/142] merged with local version --- src/PhpWord/Shared/Html.php | 51 +++++++++++++++++++++++++++-- tests/PhpWord/Shared/HtmlTest.php | 54 +++++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index cbdcecd5..b308ec3e 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -22,6 +22,7 @@ use PhpOffice\PhpWord\Element\Row; use PhpOffice\PhpWord\Element\Table; use PhpOffice\PhpWord\SimpleType\Jc; use PhpOffice\PhpWord\SimpleType\NumberFormat; +use PhpOffice\PhpWord\Settings; /** * Common Html functions @@ -32,6 +33,7 @@ class Html { private static $listIndex = 0; private static $xpath; + private static $options; /** * Add HTML parts. @@ -44,13 +46,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 @@ -141,6 +147,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), @@ -296,8 +303,9 @@ class Html * * @todo As soon as TableItem, RowItem and CellItem support relative width and height */ - private static function parseTable($node, $element, &$styles) + private static function parseTable($node, $element, &$styles ) { + $elementStyles = self::parseInlineStyle($node, $styles['table']); $newElement = $element->addTable($elementStyles); @@ -648,6 +656,45 @@ class Html break; } } + if( strpos( $src, "data:image" ) !== false ){ + if( ! is_dir( self::$imgdir ) ) + mkdir( self::$imgdir ) ; + + $match = array(); + preg_match( '/data:image\/(\w+);base64,(.+)/', $src, $match ); + + $src = $imgFile = self::$imgdir . uniqid() . "." . $match[1]; + + $ifp = fopen( $imgFile, "wb"); + + 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().'/'; + if( ! is_dir( $tmpDir ) ) + mkdir( $tmpDir ) ; + $match = array(); + preg_match( '/.+\.(\w+)$/', $src, $match ); + $src = $tmpDir . uniqid() . "." . $match[1]; + + $ifp = fopen( $src, "wb"); + + fwrite($ifp, $imgBlob ); + fclose($ifp); + } + } $newElement = $element->addImage($src, $style); return $newElement; diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index b61418e0..63295d62 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -115,6 +115,20 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr/w:u')); $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 @@ -447,6 +461,46 @@ 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 = 'https://phpword.readthedocs.io/en/latest/_images/phpword.png'; + + $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= [ + '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')); + } + public function testParseLink() { $phpWord = new \PhpOffice\PhpWord\PhpWord(); From d54cc6efeea1a43419d9b97a759a796c1124aa6e Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Wed, 23 May 2018 18:35:12 +0200 Subject: [PATCH 016/142] fix lint --- samples/resources/Sample_30_ReadHTML.html | 6 ++++++ tests/PhpWord/Shared/HtmlTest.php | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index 5593298b..ea982218 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -11,5 +11,11 @@
  • Item 1
  • Item 2
    • Item 2.1
    • Item 2.1

Ordered (numbered) list:

  1. Item 1
  2. Item 2
+ + +

Double height

+ +

Includes images

+ diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 63295d62..9b5def85 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -485,10 +485,10 @@ class HtmlTest extends \PHPUnit\Framework\TestCase { $src = 'https://fakedomain.io/images/firefox.png'; $localPath = __DIR__ . '/../_files/images/'; - $options= [ + $options= array( 'IMG_SRC_SEARCH'=> 'https://fakedomain.io/images/', 'IMG_SRC_REPLACE'=> $localPath - ]; + ); $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); From a228811a611fe015c3bdb916d33ec579aa5697b4 Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Wed, 23 May 2018 18:48:28 +0200 Subject: [PATCH 017/142] fixes --- src/PhpWord/Shared/Html.php | 66 +++++++++++++++---------------- tests/PhpWord/Shared/HtmlTest.php | 8 ++-- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index b308ec3e..24f695c7 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -20,9 +20,9 @@ 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\Settings; /** * Common Html functions @@ -50,7 +50,7 @@ class Html * + 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, $options = null ) + 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, @@ -303,9 +303,8 @@ class Html * * @todo As soon as TableItem, RowItem and CellItem support relative width and height */ - private static function parseTable($node, $element, &$styles ) + private static function parseTable($node, $element, &$styles) { - $elementStyles = self::parseInlineStyle($node, $styles['table']); $newElement = $element->addTable($elementStyles); @@ -656,45 +655,46 @@ class Html break; } } - if( strpos( $src, "data:image" ) !== false ){ - if( ! is_dir( self::$imgdir ) ) - mkdir( self::$imgdir ) ; + if (strpos($src, 'data:image') !== false) { + if (!is_dir(self::$imgdir)) { + mkdir(self::$imgdir); + } - $match = array(); - preg_match( '/data:image\/(\w+);base64,(.+)/', $src, $match ); + $match = array(); + preg_match('/data:image\/(\w+);base64,(.+)/', $src, $match); - $src = $imgFile = self::$imgdir . uniqid() . "." . $match[1]; + $src = $imgFile = self::$imgdir . uniqid() . '.' . $match[1]; - $ifp = fopen( $imgFile, "wb"); + $ifp = fopen($imgFile, 'wb'); - fwrite($ifp, base64_decode( $match[2] ) ); - fclose($ifp); + fwrite($ifp, base64_decode($match[2])); + fclose($ifp); + } + $src = urldecode($src); - } - $src= urldecode($src); - - if( ! is_file( $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 ); - } + && 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().'/'; - if( ! is_dir( $tmpDir ) ) - mkdir( $tmpDir ) ; - $match = array(); - preg_match( '/.+\.(\w+)$/', $src, $match ); - $src = $tmpDir . uniqid() . "." . $match[1]; + if (!is_file($src)) { + if ($imgBlob = file_get_contents($src)) { + $tmpDir = Settings::getTempDir() . '/'; + if (!is_dir($tmpDir)) { + mkdir($tmpDir); + } + $match = array(); + preg_match('/.+\.(\w+)$/', $src, $match); + $src = $tmpDir . uniqid() . '.' . $match[1]; - $ifp = fopen( $src, "wb"); + $ifp = fopen($src, 'wb'); - fwrite($ifp, $imgBlob ); - fclose($ifp); - } - } + fwrite($ifp, $imgBlob); + fclose($ifp); + } + } $newElement = $element->addImage($src, $style); return $newElement; diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 9b5def85..8c42544d 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -115,6 +115,7 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists('/w:document/w:body/w:p/w:r/w:rPr/w:u')); $this->assertEquals('single', $doc->getElementAttribute('/w:document/w:body/w:p/w:r/w:rPr/w:u', 'w:val')); } + /** * Test font */ @@ -478,6 +479,7 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $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 */ @@ -485,9 +487,9 @@ class HtmlTest extends \PHPUnit\Framework\TestCase { $src = 'https://fakedomain.io/images/firefox.png'; $localPath = __DIR__ . '/../_files/images/'; - $options= array( - 'IMG_SRC_SEARCH'=> 'https://fakedomain.io/images/', - 'IMG_SRC_REPLACE'=> $localPath + $options = array( + 'IMG_SRC_SEARCH' => 'https://fakedomain.io/images/', + 'IMG_SRC_REPLACE' => $localPath, ); $phpWord = new \PhpOffice\PhpWord\PhpWord(); From 46b7bea0975eb70a4cd6d8469d7fa859e277ec6e Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Thu, 24 May 2018 07:19:45 +0200 Subject: [PATCH 018/142] increased test coverage of new lines --- src/PhpWord/Shared/Html.php | 9 ++------- tests/PhpWord/Shared/HtmlTest.php | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 24f695c7..97b27018 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -656,14 +656,12 @@ class Html } } if (strpos($src, 'data:image') !== false) { - if (!is_dir(self::$imgdir)) { - mkdir(self::$imgdir); - } + $tmpDir = Settings::getTempDir() . '/'; $match = array(); preg_match('/data:image\/(\w+);base64,(.+)/', $src, $match); - $src = $imgFile = self::$imgdir . uniqid() . '.' . $match[1]; + $src = $imgFile = $tmpDir . uniqid() . '.' . $match[1]; $ifp = fopen($imgFile, 'wb'); @@ -682,9 +680,6 @@ class Html if (!is_file($src)) { if ($imgBlob = file_get_contents($src)) { $tmpDir = Settings::getTempDir() . '/'; - if (!is_dir($tmpDir)) { - mkdir($tmpDir); - } $match = array(); preg_match('/.+\.(\w+)$/', $src, $match); $src = $tmpDir . uniqid() . '.' . $match[1]; diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 8c42544d..6925e3c2 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -480,6 +480,22 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $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 */ From a89e4c93a78c964bd3b7e6dc5f1ab38bb0d18525 Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Fri, 25 May 2018 08:01:17 +0200 Subject: [PATCH 019/142] added exception control to file_get_contents error --- samples/resources/Sample_30_ReadHTML.html | 6 +++++- samples/results/.gitignore | 0 src/PhpWord/Shared/Html.php | 10 ++++++++-- tests/PhpWord/Shared/HtmlTest.php | 14 ++++++++++++++ 4 files changed, 27 insertions(+), 3 deletions(-) mode change 100644 => 100755 samples/results/.gitignore diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index ea982218..b3d2fad2 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -12,10 +12,14 @@

Ordered (numbered) list:

  1. Item 1
  2. Item 2
-

Double height

Includes images

+ + + + + diff --git a/samples/results/.gitignore b/samples/results/.gitignore old mode 100644 new mode 100755 diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 97b27018..23172769 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -655,6 +655,7 @@ class Html break; } } + $origin_src= $src; if (strpos($src, 'data:image') !== false) { $tmpDir = Settings::getTempDir() . '/'; @@ -678,7 +679,7 @@ class Html } if (!is_file($src)) { - if ($imgBlob = file_get_contents($src)) { + if ($imgBlob = @file_get_contents($src)) { $tmpDir = Settings::getTempDir() . '/'; $match = array(); preg_match('/.+\.(\w+)$/', $src, $match); @@ -690,7 +691,12 @@ class Html fclose($ifp); } } - $newElement = $element->addImage($src, $style); + + if (is_file($src)){ + $newElement = $element->addImage($src, $style); + }else{ + throw new \Exception("Could not load image $origin_src"); + } return $newElement; } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 6925e3c2..481b5d75 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -519,6 +519,20 @@ class HtmlTest extends \PHPUnit\Framework\TestCase $this->assertTrue($doc->elementExists($baseXpath . '/w:pict/v:shape')); } + /** + * Test parsing of remote img that can be found locally + */ + public function testCouldNotLoadImage() + { + $src = 'https://fakedomain.io/images/firefox.png'; + $this->expectException(\Exception::class); + + $phpWord = new \PhpOffice\PhpWord\PhpWord(); + $section = $phpWord->addSection(); + $html = '

'; + Html::addHtml($section, $html, false, true); + } + public function testParseLink() { $phpWord = new \PhpOffice\PhpWord\PhpWord(); From 65a594d2713b6ff876ca0a1d230404a0d160b6c9 Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Fri, 25 May 2018 09:29:58 +0200 Subject: [PATCH 020/142] cs-fixer fixes --- src/PhpWord/Shared/Html.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 23172769..58ce2412 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -655,7 +655,7 @@ class Html break; } } - $origin_src= $src; + $originSrc = $src; if (strpos($src, 'data:image') !== false) { $tmpDir = Settings::getTempDir() . '/'; @@ -692,10 +692,10 @@ class Html } } - if (is_file($src)){ - $newElement = $element->addImage($src, $style); - }else{ - throw new \Exception("Could not load image $origin_src"); + if (is_file($src)) { + $newElement = $element->addImage($src, $style); + } else { + throw new \Exception("Could not load image $originSrc"); } return $newElement; From 0c9626cedd0efe20de858c96cc9ba3fb2e2d78f1 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 27 May 2018 20:38:25 +0200 Subject: [PATCH 021/142] add .gitattributes --- .gitattributes | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .gitattributes 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 From da604a80c43241b28c7cf6d07977f1533a70d39c Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 27 May 2018 20:53:42 +0200 Subject: [PATCH 022/142] use annotation instead --- tests/PhpWord/Shared/HtmlTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 481b5d75..32f4bf46 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -521,11 +521,12 @@ class HtmlTest extends \PHPUnit\Framework\TestCase /** * Test parsing of remote img that can be found locally + * + * @expectedException \Exception */ public function testCouldNotLoadImage() { $src = 'https://fakedomain.io/images/firefox.png'; - $this->expectException(\Exception::class); $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); From c22f7eab5e2a9e764946ddf77c8f82d21811871a Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 27 May 2018 21:27:45 +0200 Subject: [PATCH 023/142] add check on opened file --- src/PhpWord/Shared/Html.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 58ce2412..239cfd1d 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -666,8 +666,10 @@ class Html $ifp = fopen($imgFile, 'wb'); - fwrite($ifp, base64_decode($match[2])); - fclose($ifp); + if ($ifp !== false) { + fwrite($ifp, base64_decode($match[2])); + fclose($ifp); + } } $src = urldecode($src); @@ -687,8 +689,10 @@ class Html $ifp = fopen($src, 'wb'); - fwrite($ifp, $imgBlob); - fclose($ifp); + if ($ifp !== false) { + fwrite($ifp, $imgBlob); + fclose($ifp); + } } } From 82f3a2ab44f9de047087e5307fdb9269fb346e0c Mon Sep 17 00:00:00 2001 From: Edvin Hultberg Date: Thu, 31 May 2018 00:21:08 +0200 Subject: [PATCH 024/142] Update documentation for Style::add*Style methods (#1383) * Update documentation for Style::add*Style methods * remove phpDocumentor, simplify dependencies --- .travis.yml | 4 ++-- composer.json | 7 +++---- src/PhpWord/Element/AbstractElement.php | 2 +- src/PhpWord/PhpWord.php | 4 ++-- src/PhpWord/Style.php | 20 ++++++++++---------- src/PhpWord/Style/Font.php | 2 +- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index bd85b2d7..c72dd8c2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ before_script: - composer self-update - travis_wait composer install --prefer-source ## PHPDocumentor - - mkdir -p build/docs + ##- mkdir -p build/docs - mkdir -p build/coverage script: @@ -52,7 +52,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/composer.json b/composer.json index c29e901a..dd3a2de8 100644 --- a/composer.json +++ b/composer.json @@ -65,14 +65,13 @@ }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^5.0", - "phpdocumentor/phpdocumentor":"2.*", - "squizlabs/php_codesniffer": "^2.7", - "friendsofphp/php-cs-fixer": "^2.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": { 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/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/Style.php b/src/PhpWord/Style.php index 47242621..62783b63 100644 --- a/src/PhpWord/Style.php +++ b/src/PhpWord/Style.php @@ -39,7 +39,7 @@ class Style * Add paragraph style * * @param string $styleName - * @param array $styles + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $styles * @return \PhpOffice\PhpWord\Style\Paragraph */ public static function addParagraphStyle($styleName, $styles) @@ -51,8 +51,8 @@ class Style * Add font style * * @param string $styleName - * @param array $fontStyle - * @param array $paragraphStyle + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $fontStyle + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $paragraphStyle * @return \PhpOffice\PhpWord\Style\Font */ public static function addFontStyle($styleName, $fontStyle, $paragraphStyle = null) @@ -64,7 +64,7 @@ class Style * Add link style * * @param string $styleName - * @param array $styles + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $styles * @return \PhpOffice\PhpWord\Style\Font */ public static function addLinkStyle($styleName, $styles) @@ -76,7 +76,7 @@ class Style * Add numbering style * * @param string $styleName - * @param array $styleValues + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $styleValues * @return \PhpOffice\PhpWord\Style\Numbering * @since 0.10.0 */ @@ -88,14 +88,14 @@ class Style /** * Add title style * - * @param int $depth - * @param array $fontStyle - * @param array $paragraphStyle + * @param int|null $depth Provide null to set title font + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $fontStyle + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $paragraphStyle * @return \PhpOffice\PhpWord\Style\Font */ public static function addTitleStyle($depth, $fontStyle, $paragraphStyle = null) { - if ($depth == null) { + if (empty($depth)) { $styleName = 'Title'; } else { $styleName = "Heading_{$depth}"; @@ -141,7 +141,7 @@ class Style /** * Set default paragraph style * - * @param array $styles Paragraph style definition + * @param array|\PhpOffice\PhpWord\Style\AbstractStyle $styles Paragraph style definition * @return \PhpOffice\PhpWord\Style\Paragraph */ public static function setDefaultParagraphStyle($styles) diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php index c58cee49..e9f3c9d6 100644 --- a/src/PhpWord/Style/Font.php +++ b/src/PhpWord/Style/Font.php @@ -264,7 +264,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) { From e76487172b78af26e44b250f85d41fce541f5084 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 May 2018 00:31:41 +0200 Subject: [PATCH 025/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfcce5a4..e7b7ed68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ 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 ### Fixed - Fix reading of docx default style - @troosan #1238 From 1a06173e1b933cbb8945949aca3d6a90179b496e Mon Sep 17 00:00:00 2001 From: javier Date: Thu, 31 May 2018 01:28:14 +0200 Subject: [PATCH 026/142] Add parsing of html image in base64 format (#1382) * increased test coverage of new lines * added exception control to file_get_contents error * update changelog --- CHANGELOG.md | 1 + samples/resources/Sample_30_ReadHTML.html | 10 +++ samples/results/.gitignore | 0 src/PhpWord/Shared/Html.php | 56 ++++++++++++++- tests/PhpWord/Shared/HtmlTest.php | 87 +++++++++++++++++++++++ 5 files changed, 152 insertions(+), 2 deletions(-) mode change 100644 => 100755 samples/results/.gitignore diff --git a/CHANGELOG.md b/CHANGELOG.md index dfcce5a4..e7b7ed68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ 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 ### Fixed - Fix reading of docx default style - @troosan #1238 diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index 5593298b..b3d2fad2 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -11,5 +11,15 @@
  • Item 1
  • Item 2
    • Item 2.1
    • Item 2.1

Ordered (numbered) list:

  1. Item 1
  2. Item 2
+ +

Double height

+ +

Includes images

+ + + + + + diff --git a/samples/results/.gitignore b/samples/results/.gitignore old mode 100644 new mode 100755 diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index cbdcecd5..239cfd1d 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -20,6 +20,7 @@ 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; @@ -32,6 +33,7 @@ class Html { private static $listIndex = 0; private static $xpath; + private static $options; /** * Add HTML parts. @@ -44,13 +46,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 @@ -141,6 +147,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), @@ -648,7 +655,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; } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index b61418e0..32f4bf46 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -116,6 +116,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 */ @@ -447,6 +462,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 = 'https://phpword.readthedocs.io/en/latest/_images/phpword.png'; + + $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(); From aa271091016a2ae8a793b404fcb170d37b87bbd3 Mon Sep 17 00:00:00 2001 From: Omar Piani Date: Thu, 31 May 2018 11:43:20 +0200 Subject: [PATCH 027/142] Update Language.php --- src/PhpWord/Style/Language.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 4e9220fd..21c416af 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -46,6 +46,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; From 0bd7c0b301494f27e07b5984a5c24ac2d3f934b0 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 May 2018 19:50:43 +0200 Subject: [PATCH 028/142] Add constants for Italian --- src/PhpWord/Style/Language.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 21c416af..5284bbf3 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -46,7 +46,7 @@ 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; From 4e37afa15da62cdd695a18db2a40d86615086e9d Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 May 2018 20:10:49 +0200 Subject: [PATCH 029/142] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfb1cb5c..221377df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,7 +34,7 @@ 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 -- Fix missing column with in ODText writer @potofcoffee #413 +- Fix missing column width in ODText writer @potofcoffee #413 ### Changed - Remove zend-stdlib dependency @Trainmaster #1284 From d9ddc162a362cae5ef3842e5d44e052aa946a1e9 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 18 Apr 2018 22:34:53 +0200 Subject: [PATCH 030/142] write column width in ODT writer --- CHANGELOG.md | 1 + src/PhpWord/Element/Table.php | 24 +++++++ src/PhpWord/Style/Table.php | 27 ++++++++ src/PhpWord/Writer/ODText/Element/Table.php | 63 ++++++++++++++----- src/PhpWord/Writer/ODText/Part/Content.php | 1 + src/PhpWord/Writer/ODText/Style/Table.php | 13 ++++ src/PhpWord/Writer/Word2007/Element/Table.php | 16 +---- 7 files changed, 114 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7b7ed68..868a4eba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ 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 +- Fix missing column with in ODText writer @potofcoffee #413 ### Changed - Remove zend-stdlib dependency @Trainmaster #1284 diff --git a/src/PhpWord/Element/Table.php b/src/PhpWord/Element/Table.php index 10c4db69..16102119 100644 --- a/src/PhpWord/Element/Table.php +++ b/src/PhpWord/Element/Table.php @@ -149,4 +149,28 @@ class Table extends AbstractElement return $columnCount; } + + /** + * The first declared cell width for each column + * + * @return int[] + */ + public function findFirstDefinedCellWidths() + { + $cellWidths = array(); + if (is_array($this->rows)) { + 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/Style/Table.php b/src/PhpWord/Style/Table.php index b622c78b..0f7bf7dc 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -163,6 +163,13 @@ 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; + /** * Create new table style * @@ -748,4 +755,24 @@ class Table extends Border return $this; } + + /** + * Get the columnWidths + * + * @return number[] + */ + public function getColumnWidths() + { + return $this->columnWidths; + } + + /** + * The column widths + * + * @param int[] $value + */ + public function setColumnWidths(array $value = null) + { + $this->columnWidths = $value; + } } 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..b705bb5e 100644 --- a/src/PhpWord/Writer/ODText/Part/Content.php +++ b/src/PhpWord/Writer/ODText/Part/Content.php @@ -246,6 +246,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/Table.php b/src/PhpWord/Writer/ODText/Style/Table.php index 249321cf..f5a3c431 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -45,5 +45,18 @@ class Table extends AbstractStyle $xmlWriter->writeAttribute('table:align', 'center'); $xmlWriter->endElement(); // style:table-properties $xmlWriter->endElement(); // style:style + + $cellWidths = $style->getColumnWidths(); + + for ($i = 0; $i < count($cellWidths); $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/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) { From da43a880e36fc43c6d715cfbb5adb89eee02350f Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 3 Jun 2018 00:22:08 +0200 Subject: [PATCH 031/142] Address scrutinizer issues --- src/PhpWord/Element/Table.php | 36 ++++++++++------------ src/PhpWord/Writer/ODText/Part/Content.php | 1 + src/PhpWord/Writer/ODText/Style/Table.php | 3 +- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/PhpWord/Element/Table.php b/src/PhpWord/Element/Table.php index 16102119..44fe3744 100644 --- a/src/PhpWord/Element/Table.php +++ b/src/PhpWord/Element/Table.php @@ -135,15 +135,14 @@ 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; } } @@ -158,16 +157,15 @@ class Table extends AbstractElement public function findFirstDefinedCellWidths() { $cellWidths = array(); - if (is_array($this->rows)) { - foreach ($this->rows as $row) { - $cells = $row->getCells(); - if (count($cells) <= count($cellWidths)) { - continue; - } - $cellWidths = array(); - foreach ($cells as $cell) { - $cellWidths[] = $cell->getWidth(); - } + + foreach ($this->rows as $row) { + $cells = $row->getCells(); + if (count($cells) <= count($cellWidths)) { + continue; + } + $cellWidths = array(); + foreach ($cells as $cell) { + $cellWidths[] = $cell->getWidth(); } } diff --git a/src/PhpWord/Writer/ODText/Part/Content.php b/src/PhpWord/Writer/ODText/Part/Content.php index b705bb5e..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(); diff --git a/src/PhpWord/Writer/ODText/Style/Table.php b/src/PhpWord/Writer/ODText/Style/Table.php index f5a3c431..5ddee25a 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -47,8 +47,9 @@ class Table extends AbstractStyle $xmlWriter->endElement(); // style:style $cellWidths = $style->getColumnWidths(); + $countCellWidths = count($cellWidths); - for ($i = 0; $i < count($cellWidths); $i++) { + for ($i = 0; $i < $countCellWidths; $i++) { $width = $cellWidths[$i]; $xmlWriter->startElement('style:style'); $xmlWriter->writeAttribute('style:name', $style->getStyleName() . '.' . $i); From 5b9719071e1454bdbd4faa5940ba81636334ca73 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 3 Jun 2018 22:36:30 +0200 Subject: [PATCH 032/142] remove unused imports --- samples/Sample_01_SimpleText.php | 1 - tests/PhpWord/Style/PaperTest.php | 1 - tests/PhpWord/Writer/Word2007/Part/SettingsTest.php | 1 - 3 files changed, 3 deletions(-) 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 @@ Date: Wed, 13 Jun 2018 17:41:17 +0200 Subject: [PATCH 033/142] fix when style line-height size comes in decimal number --- samples/resources/Sample_30_ReadHTML.html | 9 ++++++--- src/PhpWord/Shared/Html.php | 2 +- tests/PhpWord/Shared/ConverterTest.php | 1 + tests/PhpWord/Shared/HtmlTest.php | 5 +++++ 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index b3d2fad2..3ccbb5f2 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -17,9 +17,12 @@

Includes images

- - - +

Nested p and i

+

+ Contenido + B.O. de la Provincia de Barcelona + , 6-9-1939, citado en Díaz Plaja, F.: "España 1939-1979", +

diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 58ce2412..f5a7e567 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -530,7 +530,7 @@ class Html $styles['bgColor'] = trim($cValue, '#'); break; case 'line-height': - if (preg_match('/([0-9]+[a-z]+)/', $cValue, $matches)) { + if (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $cValue, $matches)) { $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::EXACT; $spacing = Converter::cssToTwip($matches[1]) / \PhpOffice\PhpWord\Style\Paragraph::LINE_HEIGHT; } elseif (preg_match('/([0-9]+)%/', $cValue, $matches)) { diff --git a/tests/PhpWord/Shared/ConverterTest.php b/tests/PhpWord/Shared/ConverterTest.php index 3798a07b..fbe92c25 100644 --- a/tests/PhpWord/Shared/ConverterTest.php +++ b/tests/PhpWord/Shared/ConverterTest.php @@ -126,6 +126,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 481b5d75..ac273ad0 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -141,6 +141,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')); @@ -154,6 +155,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')); } /** From c93880b1250b06a73b557617f693c3cfd30dd59d Mon Sep 17 00:00:00 2001 From: Javier Garcia Date: Wed, 13 Jun 2018 18:14:13 +0200 Subject: [PATCH 034/142] clean sample resource --- samples/resources/Sample_30_ReadHTML.html | 7 ------- 1 file changed, 7 deletions(-) diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index 3ccbb5f2..0ff1a414 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -17,12 +17,5 @@

Includes images

-

Nested p and i

-

- Contenido - B.O. de la Provincia de Barcelona - , 6-9-1939, citado en Díaz Plaja, F.: "España 1939-1979", -

- From 94cf1aecb6fa9dce566a4b88aeb282b33e9cc5b7 Mon Sep 17 00:00:00 2001 From: Jonathan Jefferies Date: Fri, 13 Jul 2018 23:12:34 +0100 Subject: [PATCH 035/142] Require `ext-zip` for development (#1419) * Require `ext-zip` for development * Require `ext-gd` for development --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index dd3a2de8..9a14618e 100644 --- a/composer.json +++ b/composer.json @@ -64,6 +64,8 @@ "phpoffice/common": "^0.2" }, "require-dev": { + "ext-zip": "*", + "ext-gd": "*", "phpunit/phpunit": "^4.8.36 || ^5.0", "squizlabs/php_codesniffer": "^2.9", "friendsofphp/php-cs-fixer": "^2.2", From 536a1b89d73c1f7a14322839cd5598eddb3d1cfc Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 00:50:01 +0200 Subject: [PATCH 036/142] disable entity loader --- composer.json | 2 +- src/PhpWord/Shared/Html.php | 1 + src/PhpWord/TemplateProcessor.php | 1 + tests/PhpWord/_includes/XmlDocument.php | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9a14618e..4bba86b3 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,7 @@ "require-dev": { "ext-zip": "*", "ext-gd": "*", - "phpunit/phpunit": "^4.8.36 || ^5.0", + "phpunit/phpunit": "^4.8.36 || ^7.0", "squizlabs/php_codesniffer": "^2.9", "friendsofphp/php-cs-fixer": "^2.2", "phpmd/phpmd": "2.*", diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 239cfd1d..a4281678 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -71,6 +71,7 @@ class Html } // Load DOM + libxml_disable_entity_loader(true); $dom = new \DOMDocument(); $dom->preserveWhiteSpace = $preserveWhiteSpace; $dom->loadXML($html); diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 72446ae7..946d6691 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -113,6 +113,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.'); diff --git a/tests/PhpWord/_includes/XmlDocument.php b/tests/PhpWord/_includes/XmlDocument.php index 8c937bf5..81de7eff 100644 --- a/tests/PhpWord/_includes/XmlDocument.php +++ b/tests/PhpWord/_includes/XmlDocument.php @@ -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; } From 96b21badaf99442f2a1de5a1b2298ae371d0e901 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 00:54:41 +0200 Subject: [PATCH 037/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cdd78870..d2b009dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ v0.15.0 (?? ??? 2018) - Fix parsing of Heading and Title formating @troosan @gthomas2 #465 - Fix Dateformat typo, fix hours casing, add Month-Day-Year formats @ComputerTinker #591 - 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 From 4c4c6f43ca446c0998f925f52b3b1aa3fe5289bb Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 00:59:54 +0200 Subject: [PATCH 038/142] remove options not compatible with latest phpunit version --- phpunit.xml.dist | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 015dd2ed..17fcfa39 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,7 @@ - + \ No newline at end of file From 3906be19ee92729a4e12bdbd499f031aae12bf03 Mon Sep 17 00:00:00 2001 From: smaug1985 Date: Sat, 14 Jul 2018 02:13:45 +0200 Subject: [PATCH 039/142] Added Support for Indentation & Tabs on RTF Writer. (#1405) * Added Support for Indentation & Tabs on RTF Writer. * add decimal tab writer + tests * Update CHANGELOG.md --- CHANGELOG.md | 1 + src/PhpWord/Writer/RTF/Style/Indentation.php | 45 ++++++++++++++++ src/PhpWord/Writer/RTF/Style/Paragraph.php | 40 ++++++++++++++ src/PhpWord/Writer/RTF/Style/Tab.php | 49 +++++++++++++++++ tests/PhpWord/Writer/RTF/StyleTest.php | 55 +++++++++++++++++++- 5 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 src/PhpWord/Writer/RTF/Style/Indentation.php create mode 100644 src/PhpWord/Writer/RTF/Style/Tab.php diff --git a/CHANGELOG.md b/CHANGELOG.md index d2b009dc..03387b37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ v0.15.0 (?? ??? 2018) - 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 ### Fixed - Fix reading of docx default style - @troosan #1238 diff --git a/src/PhpWord/Writer/RTF/Style/Indentation.php b/src/PhpWord/Writer/RTF/Style/Indentation.php new file mode 100644 index 00000000..5c80af0c --- /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' . $style->getFirstLine(); + $content .= '\li' . $style->getLeft(); + $content .= '\ri' . $style->getRight(); + + return $content . ' '; + } +} diff --git a/src/PhpWord/Writer/RTF/Style/Paragraph.php b/src/PhpWord/Writer/RTF/Style/Paragraph.php index 3b8690cd..cb50a31b 100644 --- a/src/PhpWord/Writer/RTF/Style/Paragraph.php +++ b/src/PhpWord/Writer/RTF/Style/Paragraph.php @@ -64,9 +64,49 @@ class Paragraph extends AbstractStyle if (isset($alignments[$style->getAlignment()])) { $content .= $alignments[$style->getAlignment()]; } + $content .= $this->writeIndentation($style->getIndentation()); $content .= $this->getValueIf($spaceBefore !== null, '\sb' . $spaceBefore); $content .= $this->getValueIf($spaceAfter !== null, '\sa' . $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/Tab.php b/src/PhpWord/Writer/RTF/Style/Tab.php new file mode 100644 index 00000000..fe1f9363 --- /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' . $style->getPosition(); + + return $content; + } +} 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); + } } From e028aef6d2cb6dd27a24be6334c14c38f5849dbc Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 02:26:47 +0200 Subject: [PATCH 040/142] update to final phpoffice/common version --- CHANGELOG.md | 1 + composer.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0b39ce1..1f1e194b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ 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 ### Changed - Remove zend-stdlib dependency @Trainmaster #1284 diff --git a/composer.json b/composer.json index 6dc9be2c..1844e82e 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ "php": "^5.3.3 || ^7.0", "ext-xml": "*", "zendframework/zend-escaper": "^2.2", - "phpoffice/common": "dev-develop" + "phpoffice/common": "^0.2.9" }, "require-dev": { "phpunit/phpunit": "^4.8.36 || ^5.0", From adc1428607d73018ec0d088196953e99c9045c58 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 02:51:08 +0200 Subject: [PATCH 041/142] use PasswordEncoder from phpoffice/common instead --- src/PhpWord/Metadata/Protection.php | 2 +- .../Shared/Microsoft/PasswordEncoder.php | 235 ------------------ src/PhpWord/Writer/Word2007/Part/Settings.php | 2 +- .../Shared/Microsoft/PasswordEncoderTest.php | 91 ------- .../Writer/Word2007/Part/SettingsTest.php | 2 +- 5 files changed, 3 insertions(+), 329 deletions(-) delete mode 100644 src/PhpWord/Shared/Microsoft/PasswordEncoder.php delete mode 100644 tests/PhpWord/Shared/Microsoft/PasswordEncoderTest.php diff --git a/src/PhpWord/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 584ed83e..197f8022 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; /** 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/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/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 @@ - Date: Sat, 14 Jul 2018 02:54:17 +0200 Subject: [PATCH 042/142] fix phpstan issues --- phpstan.neon | 2 +- src/PhpWord/Element/Title.php | 14 ++++++-------- src/PhpWord/Shared/Html.php | 2 +- src/PhpWord/Writer/Word2007/Element/Title.php | 1 + src/PhpWord/Writer/Word2007/Part/Chart.php | 4 ++-- tests/PhpWord/Writer/HTML/ElementTest.php | 6 +++--- tests/PhpWord/_includes/XmlDocument.php | 6 +++--- 7 files changed, 17 insertions(+), 18 deletions(-) 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/src/PhpWord/Element/Title.php b/src/PhpWord/Element/Title.php index 569cea92..eb261b1f 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; diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index a4281678..f725c366 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -75,7 +75,7 @@ class Html $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); 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..5a3ef276 100644 --- a/src/PhpWord/Writer/Word2007/Part/Chart.php +++ b/src/PhpWord/Writer/Word2007/Part/Chart.php @@ -330,11 +330,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/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index 90044b92..b76ddded 100644 --- a/tests/PhpWord/Writer/HTML/ElementTest.php +++ b/tests/PhpWord/Writer/HTML/ElementTest.php @@ -70,7 +70,7 @@ 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); @@ -94,7 +94,7 @@ 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('2', $xpath->query('/html/body/table/tr/td[1]')->item(0)->attributes->getNamedItem('colspan')->textContent); @@ -123,7 +123,7 @@ 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('3', $xpath->query('/html/body/table/tr[1]/td[1]')->item(0)->attributes->getNamedItem('rowspan')->textContent); diff --git a/tests/PhpWord/_includes/XmlDocument.php b/tests/PhpWord/_includes/XmlDocument.php index 81de7eff..7062ebbf 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; @@ -98,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'); } From 6475812e828be030a39adc8c8d9a925bd9d163b7 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 03:28:09 +0200 Subject: [PATCH 043/142] fix documentation --- src/PhpWord/Element/OLEObject.php | 2 +- src/PhpWord/Element/PreserveText.php | 6 ++---- src/PhpWord/Element/Title.php | 2 -- src/PhpWord/Metadata/Protection.php | 6 +++--- src/PhpWord/Reader/Word2007/Settings.php | 10 +++++----- src/PhpWord/Writer/RTF/Style/Indentation.php | 2 +- 6 files changed, 12 insertions(+), 16 deletions(-) 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/Title.php b/src/PhpWord/Element/Title.php index eb261b1f..d01f7f33 100644 --- a/src/PhpWord/Element/Title.php +++ b/src/PhpWord/Element/Title.php @@ -74,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/Metadata/Protection.php b/src/PhpWord/Metadata/Protection.php index 197f8022..15aa3ff1 100644 --- a/src/PhpWord/Metadata/Protection.php +++ b/src/PhpWord/Metadata/Protection.php @@ -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/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/Writer/RTF/Style/Indentation.php b/src/PhpWord/Writer/RTF/Style/Indentation.php index 5c80af0c..dd52230e 100644 --- a/src/PhpWord/Writer/RTF/Style/Indentation.php +++ b/src/PhpWord/Writer/RTF/Style/Indentation.php @@ -33,7 +33,7 @@ class Indentation extends AbstractStyle { $style = $this->getStyle(); if (!$style instanceof \PhpOffice\PhpWord\Style\Indentation) { - return; + return ''; } $content = '\fi' . $style->getFirstLine(); From 54155bf5a99102378f7c399ea60f898fa2abeeff Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 17:16:24 +0200 Subject: [PATCH 044/142] Update Sample_30_ReadHTML.html --- samples/resources/Sample_30_ReadHTML.html | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/samples/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index 0ff1a414..b3d2fad2 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -17,5 +17,9 @@

Includes images

+ + + + From 87498e43e18f31e3161256ac0f70092c86718914 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 17:21:30 +0200 Subject: [PATCH 045/142] Allow passing short lang code --- CHANGELOG.md | 3 ++- src/PhpWord/Style/Language.php | 17 ++++++++++------- tests/PhpWord/Style/LanguageTest.php | 15 +++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03387b37..56a1d9ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ 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.15.0 (14 Jul 2018) ---------------------- ### Added - Parsing of `align` HTML attribute - @troosan #1231 @@ -25,6 +25,7 @@ v0.15.0 (?? ??? 2018) - 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 diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 4a50a595..d7a76f78 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -123,8 +123,7 @@ final class Language extends AbstractStyle */ public function setLatin($latin) { - $this->validateLocale($latin); - $this->latin = $latin; + $this->latin = $this->validateLocale($latin); return $this; } @@ -173,8 +172,7 @@ final class Language extends AbstractStyle */ public function setEastAsia($eastAsia) { - $this->validateLocale($eastAsia); - $this->eastAsia = $eastAsia; + $this->eastAsia = $this->validateLocale($eastAsia); return $this; } @@ -198,8 +196,7 @@ final class Language extends AbstractStyle */ public function setBidirectional($bidirectional) { - $this->validateLocale($bidirectional); - $this->bidirectional = $bidirectional; + $this->bidirectional = $this->validateLocale($bidirectional); return $this; } @@ -218,12 +215,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 (strlen($locale) === 2) { + return strtolower($locale) . '-' . strtoupper($locale); + } + if ($locale !== null && strstr($locale, '-') === false) { throw new \InvalidArgumentException($locale . ' is not a valid language code'); } + + return $locale; } } 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()); } } From 8fa1d4f2244c081ee009b16dddf9a69296c8adfe Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 17:47:34 +0200 Subject: [PATCH 046/142] run checks with php 7.0 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c72dd8c2..db77ff05 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,7 +13,7 @@ php: matrix: include: - - php: 5.6 + - php: 7.0 env: COVERAGE=1 cache: From 4c9e75088a9f0794ec038d1f4cfccc268cdaea08 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 14 Jul 2018 18:36:29 +0200 Subject: [PATCH 047/142] alias for develop branch [ci skip] --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 9f0e8a43..5d8a855b 100644 --- a/composer.json +++ b/composer.json @@ -90,7 +90,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "0.15-dev" + "dev-develop": "0.16-dev" } } } From 198165ce5984c4b94550e15efb4a57f8e84997ff Mon Sep 17 00:00:00 2001 From: Nicolas Dermine Date: Mon, 16 Jul 2018 19:49:30 +0200 Subject: [PATCH 048/142] allow to override TemplateProcessor#ensureUtf8Encoded the method is `protected`, but since it is called with `self` instead of `static` it does not allow for subclasses to override it --- src/PhpWord/TemplateProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 946d6691..74cd4639 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -217,10 +217,10 @@ class TemplateProcessor if (is_array($replace)) { foreach ($replace as &$item) { - $item = self::ensureUtf8Encoded($item); + $item = static::ensureUtf8Encoded($item); } } else { - $replace = self::ensureUtf8Encoded($replace); + $replace = static::ensureUtf8Encoded($replace); } if (Settings::isOutputEscapingEnabled()) { From 0c3eb4bafc9b28313fb31c48c6bfc958a362b47f Mon Sep 17 00:00:00 2001 From: Tom-Magill <41332981+Tom-Magill@users.noreply.github.com> Date: Tue, 17 Jul 2018 14:10:02 +0100 Subject: [PATCH 049/142] Update Chart.php --- src/PhpWord/Style/Chart.php | 58 +++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/src/PhpWord/Style/Chart.php b/src/PhpWord/Style/Chart.php index 5b02e636..5c96afd2 100644 --- a/src/PhpWord/Style/Chart.php +++ b/src/PhpWord/Style/Chart.php @@ -51,6 +51,20 @@ class Chart extends AbstractStyle * @var array */ 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 @@ -220,6 +234,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 getShowLegend() + { + return $this->showLegend; + } + + /** + * Set chart legend visibility + * + * @param bool $value + */ + public function setShowLegend($value = false) + { + $this->showLegend = $value; + + return $this; + } /* * Show labels for axis From 139242612d750f0258472cf0bbc1f7044610785d Mon Sep 17 00:00:00 2001 From: Tom-Magill <41332981+Tom-Magill@users.noreply.github.com> Date: Tue, 17 Jul 2018 14:11:55 +0100 Subject: [PATCH 050/142] Update Chart.php --- src/PhpWord/Writer/Word2007/Part/Chart.php | 32 ++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Writer/Word2007/Part/Chart.php b/src/PhpWord/Writer/Word2007/Part/Chart.php index 5a3ef276..e14a708b 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 @@ -130,6 +128,36 @@ class Chart extends AbstractPart $type = $this->element->getType(); $style = $this->element->getStyle(); $this->options = $this->types[$type]; + + $title = $style->getTitle(); + $showLegend = $style->getShowLegend(); + + //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'); From f5e38076543b989b62e572cf6d5f57f9e22e3950 Mon Sep 17 00:00:00 2001 From: Humberto Pereira Date: Tue, 17 Jul 2018 19:35:31 -0400 Subject: [PATCH 051/142] Improving Style Parsing - handling Heading style --- src/PhpWord/Reader/Word2007/Styles.php | 6 ++--- tests/PhpWord/Reader/Word2007/StyleTest.php | 26 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index f343ad92..140a3f09 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -64,11 +64,11 @@ 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); + preg_match('/Heading\s*(\d)/i', $name, $headingMatches); // $default = ($xmlReader->getAttribute('w:default', $node) == 1); switch ($type) { case 'paragraph': diff --git a/tests/PhpWord/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index d64079fa..561067ee 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -19,6 +19,7 @@ namespace PhpOffice\PhpWord\Reader\Word2007; use PhpOffice\PhpWord\AbstractTestReader; use PhpOffice\PhpWord\SimpleType\TblWidth; +use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Table; use PhpOffice\PhpWord\Style\TablePosition; @@ -145,4 +146,29 @@ class StyleTest extends AbstractTestReader $this->assertSame(TblWidth::TWIP, $tableStyle->getIndent()->getType()); $this->assertSame(2160, $tableStyle->getIndent()->getValue()); } + + public function testReadHeading() + { + Style::resetStyles(); + + $documentXml = ' + + + + + + + + + + + + + '; + + $name = 'Heading_1'; + + $this->getDocumentFromString(array('styles' => $documentXml)); + $this->assertInstanceOf('PhpOffice\\PhpWord\\Style\\Font', Style::getStyle($name)); + } } From 8c62cea580e5d9aa7b5c496ccad98114b7466422 Mon Sep 17 00:00:00 2001 From: Humberto Pereira Date: Tue, 17 Jul 2018 21:10:53 -0400 Subject: [PATCH 052/142] Fix Writer losing text when Title contains a TextRun instead a string. --- src/PhpWord/Writer/HTML/Element/Title.php | 2 +- tests/PhpWord/Writer/HTML/ElementTest.php | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) 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/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index b76ddded..7a6397ef 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; @@ -138,4 +139,22 @@ 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->assertTrue(strpos($content, $expected) !== false); + } } From e07c6559a9d1bdac676f00e2c290cb2c99e1a7e0 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 19 Jul 2018 00:52:22 +0200 Subject: [PATCH 053/142] adapt test --- tests/PhpWord/TemplateProcessorTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 770428cb..ea739561 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -260,6 +260,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase // 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( @@ -271,7 +272,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase foreach ($expectedElements as $i => $expectedElement) { $this->assertEquals( $expectedElement, - $actualElements[$i]->getText() + $actualElements[$i]->getElement(0)->getText() ); } } From 1951db58c18e6a0492db5a1b46dd5963c12b052f Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 19 Jul 2018 01:16:25 +0200 Subject: [PATCH 054/142] update changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a166345c..d822357c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,13 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +v0.16.0 (xx xxx 2018) +---------------------- +### Added + +### Fixed +- Fix regex in `cloneBlock` function @nicoder #1269 + v0.15.0 (14 Jul 2018) ---------------------- ### Added From d09da0b6f23b5bde65ced648377190291c2b9535 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 19 Jul 2018 02:01:40 +0200 Subject: [PATCH 055/142] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d822357c..abf34834 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ v0.16.0 (xx xxx 2018) ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 +- HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 v0.15.0 (14 Jul 2018) ---------------------- From e61c40e71d8670d3334afeb6e2478d8ee8ef1325 Mon Sep 17 00:00:00 2001 From: Abubakkar Rangara <> Date: Tue, 24 Jul 2018 13:59:16 +0100 Subject: [PATCH 056/142] Adding table layout to the generated HTML if element has layout style. This is useful when using creating PDF from PHPWord (e.g. using dompdf), otherwise the PDF does not contain any layout for table. --- src/PhpWord/Writer/HTML/Element/Table.php | 4 +++- tests/PhpWord/Writer/HTML/ElementTest.php | 19 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index 844066f4..068f489a 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -39,7 +39,9 @@ class Table extends AbstractElement $rows = $this->element->getRows(); $rowCount = count($rows); if ($rowCount > 0) { - $content .= '' . PHP_EOL; + $tableStyle = $this->element->getStyle(); + $tableLayout = $tableStyle === null ? '' : $tableStyle->getLayout(); + $content .= ''. PHP_EOL; for ($i = 0; $i < $rowCount; $i++) { /** @var $row \PhpOffice\PhpWord\Element\Row Type hint */ $rowStyle = $rows[$i]->getStyle(); diff --git a/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index 7a6397ef..1f286c5f 100644 --- a/tests/PhpWord/Writer/HTML/ElementTest.php +++ b/tests/PhpWord/Writer/HTML/ElementTest.php @@ -157,4 +157,23 @@ class ElementTest extends \PHPUnit\Framework\TestCase $this->assertTrue(strpos($content, $expected) !== false); } + + /** + * Tests writing table with layout + */ + public function testWriteTableLayout() + { + $phpWord = new PhpWord(); + $section = $phpWord->addSection(); + $section->addTable(); + $table = $section->addTable(array('layout' => 'fixed')); + + $row1 = $table->addRow(); + $row1->addCell()->addText('fixed layout table'); + + $dom = $this->getAsHTML($phpWord); + $xpath = new \DOMXPath($dom); + + $this->assertEquals('table-layout: fixed', $xpath->query('/html/body/table')->item(0)->attributes->getNamedItem('style')->textContent); + } } From 4b9ae18d5aefee34bb631b96a752dfca6be56b4d Mon Sep 17 00:00:00 2001 From: Abubakkar Rangara <> Date: Tue, 24 Jul 2018 14:23:23 +0100 Subject: [PATCH 057/142] Adding table layout to the generated HTML - fixed php-cs-fixer error --- src/PhpWord/Writer/HTML/Element/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index 068f489a..50c5a777 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -41,7 +41,7 @@ class Table extends AbstractElement if ($rowCount > 0) { $tableStyle = $this->element->getStyle(); $tableLayout = $tableStyle === null ? '' : $tableStyle->getLayout(); - $content .= ''. PHP_EOL; + $content .= '' . PHP_EOL; for ($i = 0; $i < $rowCount; $i++) { /** @var $row \PhpOffice\PhpWord\Element\Row Type hint */ $rowStyle = $rows[$i]->getStyle(); From 677e3f6a19bcd5d3f4a81e6a0059bbce773e8ee7 Mon Sep 17 00:00:00 2001 From: Maxim Bulygin Date: Tue, 31 Jul 2018 18:25:29 +0300 Subject: [PATCH 058/142] writer / word2007 / support valign and watermark withouth paragraph --- src/PhpWord/Writer/Word2007/Element/Image.php | 8 ++++++-- src/PhpWord/Writer/Word2007/Style/Frame.php | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) 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/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); From 683d91990ff53f46c2ad3dd8ceaeeffc28a62ce1 Mon Sep 17 00:00:00 2001 From: vblinden Date: Mon, 3 Sep 2018 12:30:05 +0200 Subject: [PATCH 059/142] Added Dutch (nl-NL) --- src/PhpWord/Style/Language.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index d7a76f78..8a3b0315 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -64,6 +64,9 @@ 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; /** * Language ID, used for RTF document generation From d8c0441975e2032ba86bb82ecdfc754d15c92cd9 Mon Sep 17 00:00:00 2001 From: vblinden Date: Mon, 3 Sep 2018 13:32:00 +0200 Subject: [PATCH 060/142] Fix indenting --- src/PhpWord/Style/Language.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 8a3b0315..412a76a7 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -64,7 +64,7 @@ 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; From 45ee8b7faf0bb41b6cf8d1f5bf21cfd5e07353aa Mon Sep 17 00:00:00 2001 From: kisabelle Date: Fri, 21 Sep 2018 11:53:16 -0700 Subject: [PATCH 061/142] Add documentation for enabling evenAndOddHeaders --- docs/containers.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/containers.rst b/docs/containers.rst index dc194d59..34b2a9a3 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 ------- From 7f55816ebaa2777cefd759f6628e7753c91e0faf Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Thu, 11 Oct 2018 08:55:38 +0200 Subject: [PATCH 062/142] detect actual filename of document xml (prevent mismatching document22.xml as in #1253) --- src/PhpWord/TemplateProcessor.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 7a5eaf55..86c9e1c9 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -507,7 +507,13 @@ class TemplateProcessor */ 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"~'; + + preg_match($pattern, $contentTypes, $m); + + return (array_key_exists(1, $m) ? $m[1] : 'word/document.xml'); } /** From 7eb19c8f76c6a0a4ba56bc779d3f6024dc172822 Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Thu, 11 Oct 2018 09:40:12 +0200 Subject: [PATCH 063/142] add test case for issue #1253 --- tests/PhpWord/TemplateProcessorTest.php | 10 ++++++++++ .../_files/templates/document22-xml.docx | Bin 0 -> 11126 bytes 2 files changed, 10 insertions(+) create mode 100644 tests/PhpWord/_files/templates/document22-xml.docx diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index ea739561..2b3a9fd1 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -276,4 +276,14 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase ); } } + + public function testMainPartNameDetection() + { + $templatePath = 'templates/document22-xml.docx'; + $templateProcessor = new TemplateProcessor($templatePath); + + $variables = array('test'); + + $this->assertEquals($variables, $templateProcessor->getVariables()); + } } diff --git a/tests/PhpWord/_files/templates/document22-xml.docx b/tests/PhpWord/_files/templates/document22-xml.docx new file mode 100644 index 0000000000000000000000000000000000000000..206d80f46063d4f9ec4594b191bbd6f493879b8e GIT binary patch literal 11126 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 literal 0 HcmV?d00001 From e19de8e8a481ce3fb1aa84ff236952c22debb277 Mon Sep 17 00:00:00 2001 From: Martin Hanzl Date: Thu, 11 Oct 2018 11:28:44 +0200 Subject: [PATCH 064/142] #1253 - add explanatory comment --- src/PhpWord/TemplateProcessor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 86c9e1c9..ced3880e 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -503,6 +503,8 @@ 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() From 28505b0b77573d9f36936bf481ff8aef11eeec3a Mon Sep 17 00:00:00 2001 From: Ralph02 Date: Thu, 25 Oct 2018 11:23:53 +0100 Subject: [PATCH 065/142] RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals --- src/PhpWord/Writer/RTF/Style/Section.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Writer/RTF/Style/Section.php b/src/PhpWord/Writer/RTF/Style/Section.php index 5c34fa86..ee6efcf3 100644 --- a/src/PhpWord/Writer/RTF/Style/Section.php +++ b/src/PhpWord/Writer/RTF/Style/Section.php @@ -43,8 +43,8 @@ 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()); From 54eb6e6f2cf73d9983b08d80e2da9de8311fd6bc Mon Sep 17 00:00:00 2001 From: Stefan Thoolen Date: Tue, 6 Nov 2018 14:24:56 +0100 Subject: [PATCH 066/142] Fix for undefined index PHP Notice: Undefined index: document in /home/stefan/Projects/garrcomm/PHPWord/src/PhpWord/Reader/Word2007.php on line 65 PHP Warning: Invalid argument supplied for foreach() in /home/stefan/Projects/garrcomm/PHPWord/src/PhpWord/Reader/Word2007.php on line 65 --- src/PhpWord/Reader/Word2007.php | 3 +++ 1 file changed, 3 insertions(+) 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])) { From 768a07071503cd24b8089f04899bc5f6d43bf61e Mon Sep 17 00:00:00 2001 From: Gordon Franke Date: Mon, 12 Nov 2018 08:02:15 +0100 Subject: [PATCH 067/142] add/align possible values from class constant --- docs/styles.rst | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/docs/styles.rst b/docs/styles.rst index 03366427..8c5de7cb 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -29,6 +29,7 @@ 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. @@ -45,7 +46,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,7 +57,7 @@ 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 @@ -69,7 +70,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*. @@ -82,6 +83,7 @@ Available Paragraph style options: - ``spaceAfter``. Space after paragraph in *twip*. - ``spacing``. Space between lines. - ``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 +91,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,7 +101,7 @@ 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*. @@ -168,7 +170,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. From 9f28ece4e9b4d8c917135af05d58370c62a40287 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 16 Nov 2018 22:40:37 +0100 Subject: [PATCH 068/142] Fix path to test document --- tests/PhpWord/TemplateProcessorTest.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 2b3a9fd1..1513486e 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -279,8 +279,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase public function testMainPartNameDetection() { - $templatePath = 'templates/document22-xml.docx'; - $templateProcessor = new TemplateProcessor($templatePath); + $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); $variables = array('test'); From c51b6febc0feb8841202e38b817422ab0bcb09c5 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 16 Nov 2018 23:00:23 +0100 Subject: [PATCH 069/142] rename variable to comply with rules --- src/PhpWord/TemplateProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index ced3880e..b4102bcd 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -513,9 +513,9 @@ class TemplateProcessor $pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~'; - preg_match($pattern, $contentTypes, $m); + preg_match($pattern, $contentTypes, $matches); - return (array_key_exists(1, $m) ? $m[1] : 'word/document.xml'); + return (array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'); } /** From 925e9e091910bf90290dcfcfeb3a36fe94aa6855 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 16 Nov 2018 23:33:38 +0100 Subject: [PATCH 070/142] remove trailing spaces --- src/PhpWord/TemplateProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index b4102bcd..f9a8ceb6 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -514,7 +514,7 @@ class TemplateProcessor $pattern = '~PartName="\/(word\/document.*?\.xml)" ContentType="application\/vnd\.openxmlformats-officedocument\.wordprocessingml\.document\.main\+xml"~'; preg_match($pattern, $contentTypes, $matches); - + return (array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'); } From ea6edf95ccefca19ab3a87cd192d8e9ca54a9c61 Mon Sep 17 00:00:00 2001 From: Christopher ARZUR Date: Fri, 16 Nov 2018 23:35:57 +0000 Subject: [PATCH 071/142] Added PHP 7.3 support for travis (#1495) * Added PHP 7.3 support for travis * mark php 7.3 as failable --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db77ff05..6fcdad43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,19 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - php: 7.0 env: COVERAGE=1 + - php: 5.3 + env: COMPOSER_MEMORY_LIMIT=2G + exclude: + - php: 7.0 + - php: 5.3 + allow_failures: + - php: 7.3 cache: directories: @@ -32,7 +40,7 @@ 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 From 3cf07703765656dc78531504aadfe37d47485855 Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Mon, 19 Nov 2018 01:32:28 -0200 Subject: [PATCH 073/142] Remove unnecessary ternary expressions --- src/PhpWord/Metadata/DocInfo.php | 2 +- src/PhpWord/Reader/MsDoc.php | 2 +- src/PhpWord/Reader/Word2007/AbstractPart.php | 2 +- src/PhpWord/Shared/ZipArchive.php | 6 +++--- tests/PhpWord/_includes/XmlDocument.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) 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/Reader/MsDoc.php b/src/PhpWord/Reader/MsDoc.php index d4945229..c1f65809 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: diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 5e5eb1d6..966d3882 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -336,7 +336,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), diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index 2783e17e..0d26f9de 100644 --- a/src/PhpWord/Shared/ZipArchive.php +++ b/src/PhpWord/Shared/ZipArchive.php @@ -252,7 +252,7 @@ class ZipArchive unlink($this->tempDir . DIRECTORY_SEPARATOR . $localnameParts['basename']); } - return ($res == 0) ? false : true; + return $res != 0; } /** @@ -283,7 +283,7 @@ class ZipArchive // Remove temp file @unlink($this->tempDir . DIRECTORY_SEPARATOR . $filenameParts['basename']); - return ($res == 0) ? false : true; + return $res != 0; } /** @@ -303,7 +303,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/tests/PhpWord/_includes/XmlDocument.php b/tests/PhpWord/_includes/XmlDocument.php index 7062ebbf..f51eaad8 100644 --- a/tests/PhpWord/_includes/XmlDocument.php +++ b/tests/PhpWord/_includes/XmlDocument.php @@ -163,7 +163,7 @@ class XmlDocument { $nodeList = $this->getNodeList($path, $file); - return !($nodeList->length == 0); + return $nodeList->length != 0; } /** From 9b174e52c1600cb9f302a4dc40354eeb8fc6f01b Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Mon, 19 Nov 2018 01:35:03 -0200 Subject: [PATCH 074/142] Fix typo in the PR template --- docs/PULL_REQUEST_TEMPLATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 663fb036d003d2cd98c29639818ca3673dda22fb Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Mon, 19 Nov 2018 01:36:23 -0200 Subject: [PATCH 075/142] Use dedicated PHPUnit assertions --- tests/PhpWord/Writer/HTML/ElementTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index 7a6397ef..61aaf71c 100644 --- a/tests/PhpWord/Writer/HTML/ElementTest.php +++ b/tests/PhpWord/Writer/HTML/ElementTest.php @@ -73,8 +73,8 @@ class ElementTest extends \PHPUnit\Framework\TestCase $dom = $this->getAsHTML($phpWord); $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); } /** @@ -97,9 +97,9 @@ class ElementTest extends \PHPUnit\Framework\TestCase $dom = $this->getAsHTML($phpWord); $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); } /** @@ -126,9 +126,9 @@ class ElementTest extends \PHPUnit\Framework\TestCase $dom = $this->getAsHTML($phpWord); $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) @@ -155,6 +155,6 @@ class ElementTest extends \PHPUnit\Framework\TestCase $htmlWriter = new HTML($phpWord); $content = $htmlWriter->getContent(); - $this->assertTrue(strpos($content, $expected) !== false); + $this->assertContains($expected, $content); } } From d9d79c0666928ce564dbdf3d08dca9bcdc678863 Mon Sep 17 00:00:00 2001 From: Ralph02 Date: Thu, 25 Oct 2018 11:23:53 +0100 Subject: [PATCH 076/142] RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals --- src/PhpWord/Writer/RTF/Style/Section.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Writer/RTF/Style/Section.php b/src/PhpWord/Writer/RTF/Style/Section.php index 5c34fa86..ee6efcf3 100644 --- a/src/PhpWord/Writer/RTF/Style/Section.php +++ b/src/PhpWord/Writer/RTF/Style/Section.php @@ -43,8 +43,8 @@ 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()); From b5865b2fc2bb4773450add0c354e49166d32ea02 Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 20 Nov 2018 19:59:30 +0100 Subject: [PATCH 077/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf34834..7ce722c4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v0.16.0 (xx xxx 2018) ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 - HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 +- RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals @Patrick64 #1493 v0.15.0 (14 Jul 2018) ---------------------- From 1c20a4ed22c791e3cc574291c1f40e53b328568d Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 20 Nov 2018 21:22:50 +0100 Subject: [PATCH 078/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ce722c4..ce553f05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v0.16.0 (xx xxx 2018) - Fix regex in `cloneBlock` function @nicoder #1269 - HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 - RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals @Patrick64 #1493 +- Fix parsing of Office 365 documents @Timanx #1485 v0.15.0 (14 Jul 2018) ---------------------- From c12f98f69a201502f3f994d261c106c8926ef62b Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 20 Nov 2018 22:40:54 +0100 Subject: [PATCH 079/142] fix check style warning --- src/PhpWord/TemplateProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index f9a8ceb6..95468878 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -515,7 +515,7 @@ class TemplateProcessor preg_match($pattern, $contentTypes, $matches); - return (array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'); + return array_key_exists(1, $matches) ? $matches[1] : 'word/document.xml'; } /** From 5ccf985f9ad1c596c8b60d0730ff0bdaa2124804 Mon Sep 17 00:00:00 2001 From: Christopher ARZUR Date: Fri, 16 Nov 2018 23:35:57 +0000 Subject: [PATCH 080/142] Added PHP 7.3 support for travis (#1495) * Added PHP 7.3 support for travis * mark php 7.3 as failable --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db77ff05..6fcdad43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,19 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - php: 7.0 env: COVERAGE=1 + - php: 5.3 + env: COMPOSER_MEMORY_LIMIT=2G + exclude: + - php: 7.0 + - php: 5.3 + allow_failures: + - php: 7.3 cache: directories: @@ -32,7 +40,7 @@ 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 From a2a70736addbe2929c3edf41851bb83b75d00414 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 22 Nov 2018 23:05:43 +0100 Subject: [PATCH 081/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf34834..8c3a174d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v0.16.0 (xx xxx 2018) ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 - HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 +- Fix loading of Sharepoint document @Garrcomm #1498 v0.15.0 (14 Jul 2018) ---------------------- From 7aef21facaf78e78da3902d1c2dc7766bca52c30 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 28 Nov 2018 22:02:39 +0100 Subject: [PATCH 082/142] add test for parsing HTML containing entities --- tests/PhpWord/Shared/HtmlTest.php | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 51d92431..89292a20 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -86,6 +86,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 */ From 32fb85fc8e4ad5bc059574995344fd60dc950aaa Mon Sep 17 00:00:00 2001 From: Christopher ARZUR Date: Fri, 16 Nov 2018 23:35:57 +0000 Subject: [PATCH 083/142] Added PHP 7.3 support for travis (#1495) * Added PHP 7.3 support for travis * mark php 7.3 as failable --- .travis.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index db77ff05..6fcdad43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,11 +10,19 @@ php: - 7.0 - 7.1 - 7.2 + - 7.3 matrix: include: - php: 7.0 env: COVERAGE=1 + - php: 5.3 + env: COMPOSER_MEMORY_LIMIT=2G + exclude: + - php: 7.0 + - php: 5.3 + allow_failures: + - php: 7.3 cache: directories: @@ -32,7 +40,7 @@ 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 From b50de97a41f29987901b1370b74cbf08c643e741 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 28 Nov 2018 22:54:57 +0100 Subject: [PATCH 084/142] support `auto` table layout too --- CHANGELOG.md | 1 + src/PhpWord/Writer/HTML/Element/Table.php | 26 ++++++++++++++++++++--- tests/PhpWord/Writer/HTML/ElementTest.php | 11 +++++++--- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf34834..7e2325fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v0.16.0 (xx xxx 2018) ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 - HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 +- Adding table layout to the generated HTML @aarangara #1441 v0.15.0 (14 Jul 2018) ---------------------- diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index 50c5a777..a5143d2b 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -39,9 +39,8 @@ class Table extends AbstractElement $rows = $this->element->getRows(); $rowCount = count($rows); if ($rowCount > 0) { - $tableStyle = $this->element->getStyle(); - $tableLayout = $tableStyle === null ? '' : $tableStyle->getLayout(); - $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(); @@ -104,4 +103,25 @@ class Table extends AbstractElement return $content; } + + /** + * Translates Table style in CSS equivalent + * + * @param \PhpOffice\PhpWord\Style\Table|null $tableStyle + * @return string + */ + private function getTableStyle(\PhpOffice\PhpWord\Style\Table $tableStyle = null) + { + if ($tableStyle == null) { + return ''; + } + $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/tests/PhpWord/Writer/HTML/ElementTest.php b/tests/PhpWord/Writer/HTML/ElementTest.php index 1f286c5f..73c6ede9 100644 --- a/tests/PhpWord/Writer/HTML/ElementTest.php +++ b/tests/PhpWord/Writer/HTML/ElementTest.php @@ -166,14 +166,19 @@ class ElementTest extends \PHPUnit\Framework\TestCase $phpWord = new PhpWord(); $section = $phpWord->addSection(); $section->addTable(); - $table = $section->addTable(array('layout' => 'fixed')); - $row1 = $table->addRow(); + $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')->item(0)->attributes->getNamedItem('style')->textContent); + $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); } } From 6a7594630ce1a3adf7dbb8d9893f2f0f0e739faf Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 30 Nov 2018 23:01:05 +0100 Subject: [PATCH 085/142] add sonar config files --- .gitignore | 1 + phpunit.xml.dist | 1 + sonar-project.properties | 17 +++++++++++++++++ 3 files changed, 19 insertions(+) create mode 100644 sonar-project.properties diff --git a/.gitignore b/.gitignore index 2ac6e2b5..b2ec7e23 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ vendor /.settings phpword.ini /.buildpath +/.scannerwork /.project /nbproject /.php_cs.cache diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 17fcfa39..4a882446 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -23,5 +23,6 @@ + \ No newline at end of file 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 From a44aee8c34a78ed93d755224b8afd26d99737311 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 30 Nov 2018 22:59:21 +0100 Subject: [PATCH 086/142] fix some sonar warnings --- src/PhpWord/Reader/MsDoc.php | 2 ++ src/PhpWord/Shared/Html.php | 3 +-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Reader/MsDoc.php b/src/PhpWord/Reader/MsDoc.php index d4945229..187d5b17 100644 --- a/src/PhpWord/Reader/MsDoc.php +++ b/src/PhpWord/Reader/MsDoc.php @@ -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/Shared/Html.php b/src/PhpWord/Shared/Html.php index 873234de..60fd7e16 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -531,6 +531,7 @@ class Html $styles['bgColor'] = trim($cValue, '#'); break; case 'line-height': + $matches = array(); if (preg_match('/([0-9]+\.?[0-9]*[a-z]+)/', $cValue, $matches)) { $spacingLineRule = \PhpOffice\PhpWord\SimpleType\LineSpacingRule::EXACT; $spacing = Converter::cssToTwip($matches[1]) / \PhpOffice\PhpWord\Style\Paragraph::LINE_HEIGHT; @@ -743,8 +744,6 @@ class Html default: return Jc::START; } - - return null; } /** From fb60865b8dfa6b9e038d7798d126a0a717b09a2f Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 2 Dec 2018 15:39:25 +0100 Subject: [PATCH 087/142] test build php 7.3 --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6fcdad43..881decfe 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,9 +18,12 @@ matrix: env: COVERAGE=1 - php: 5.3 env: COMPOSER_MEMORY_LIMIT=2G + - php: 7.3 + env: DEPENDENCIES="--ignore-platform-reqs" exclude: - - php: 7.0 - php: 5.3 + - php: 7.0 + - php: 7.3 allow_failures: - php: 7.3 @@ -43,7 +46,7 @@ before_script: - 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/coverage From 5b688d50d82cf491b6114e94bc84de59dc96941d Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 2 Dec 2018 23:54:25 +0100 Subject: [PATCH 088/142] fix formatting --- src/PhpWord/Style/Chart.php | 26 +++++++++++++--------- src/PhpWord/Writer/Word2007/Part/Chart.php | 16 ++++++------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/PhpWord/Style/Chart.php b/src/PhpWord/Style/Chart.php index 5c96afd2..06b4829c 100644 --- a/src/PhpWord/Style/Chart.php +++ b/src/PhpWord/Style/Chart.php @@ -51,8 +51,8 @@ class Chart extends AbstractStyle * @var array */ private $colors = array(); - - /** + + /** * Chart title * * @var string @@ -111,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 @@ -234,7 +240,7 @@ class Chart extends AbstractStyle return $this; } - + /** * Get the chart title * @@ -248,7 +254,7 @@ class Chart extends AbstractStyle /** * Set the chart title * - * @param string $value + * @param string $value */ public function setTitle($value = null) { @@ -262,7 +268,7 @@ class Chart extends AbstractStyle * * @return bool */ - public function getShowLegend() + public function isShowLegend() { return $this->showLegend; } @@ -270,7 +276,7 @@ class Chart extends AbstractStyle /** * Set chart legend visibility * - * @param bool $value + * @param bool $value */ public function setShowLegend($value = false) { @@ -452,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) { @@ -461,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/Writer/Word2007/Part/Chart.php b/src/PhpWord/Writer/Word2007/Part/Chart.php index e14a708b..812d3bf1 100644 --- a/src/PhpWord/Writer/Word2007/Part/Chart.php +++ b/src/PhpWord/Writer/Word2007/Part/Chart.php @@ -128,12 +128,12 @@ class Chart extends AbstractPart $type = $this->element->getType(); $style = $this->element->getStyle(); $this->options = $this->types[$type]; - - $title = $style->getTitle(); - $showLegend = $style->getShowLegend(); + + $title = $style->getTitle(); + $showLegend = $style->isShowLegend(); //Chart title - if($title){ + if ($title) { $xmlWriter->startElement('c:title'); $xmlWriter->startElement('c:tx'); $xmlWriter->startElement('c:rich'); @@ -142,20 +142,18 @@ class Chart extends AbstractPart - '.$title.' + ' . $title . ' '); - $xmlWriter->endElement(); // c:rich $xmlWriter->endElement(); // c:tx $xmlWriter->endElement(); // c:title - - }else{ + } else { $xmlWriter->writeElementBlock('c:autoTitleDeleted', 'val', 1); } //Chart legend - if($showLegend){ + if ($showLegend) { $xmlWriter->writeRaw(''); } From 0c4bd1d02f3b175d3944d6577c377a800e27aace Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 2 Dec 2018 23:54:32 +0100 Subject: [PATCH 089/142] update documentation --- docs/styles.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/styles.rst b/docs/styles.rst index 8c5de7cb..f8d26a9b 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -192,6 +192,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*. From 9f684c745e3b7f41c304659a77c74967e662f3f1 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 2 Dec 2018 23:54:40 +0100 Subject: [PATCH 090/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1db7a5e..79ae2511 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). v0.16.0 (xx xxx 2018) ---------------------- ### Added +- Add setting Chart Title and Legend visibility @Tom-Magill #1433 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 From 7930f5d136839a24c885b8fdcdb1681a3b0a05a4 Mon Sep 17 00:00:00 2001 From: Eugene Bulbaka <45563143+bulbaka@users.noreply.github.com> Date: Mon, 3 Dec 2018 16:04:19 +0200 Subject: [PATCH 091/142] Update Language.php Added Language constants and values for Ukrainian (lines 71, 72): const UK_UA = 'uk-UA'; const UK_UA_ID = 1058; --- src/PhpWord/Style/Language.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 412a76a7..8174f6ee 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -68,6 +68,9 @@ final class Language extends AbstractStyle 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 * From af5a271e9e144815502d01e14f3d00a960933498 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 3 Dec 2018 16:09:20 +0100 Subject: [PATCH 092/142] Line spacing is wrong when using "exact" line spacing rule (#1509) * Only add 240 twips when in auto lineRule * don't add 1 line when using EXACT line spacing rule * fix style & scrutinizer warning --- CHANGELOG.md | 3 ++- README.md | 2 +- docs/styles.rst | 2 +- src/PhpWord/Shared/Html.php | 17 +++++++++--- src/PhpWord/Style/Font.php | 2 +- src/PhpWord/Style/Paragraph.php | 11 ++++---- src/PhpWord/Writer/Word2007/Style/Spacing.php | 4 +++ tests/PhpWord/Style/ParagraphTest.php | 2 -- .../Writer/Word2007/Style/ParagraphTest.php | 26 +++++++++++++++++++ 9 files changed, 53 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 79ae2511..a273326a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -v0.16.0 (xx xxx 2018) +v0.16.0 (xx dec 2018) ---------------------- ### Added - Add setting Chart Title and Legend visibility @Tom-Magill #1433 @@ -11,6 +11,7 @@ v0.16.0 (xx xxx 2018) ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 - HTML Title Writer loses text when Title contains a TextRun instead a string. @begnini #1436 +- 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 diff --git a/README.md b/README.md index 7531a6bc..0509bce2 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,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/docs/styles.rst b/docs/styles.rst index f8d26a9b..855eab79 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -81,7 +81,7 @@ 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*. diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 60fd7e16..3f07a058 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -23,6 +23,7 @@ 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 @@ -533,17 +534,25 @@ class Html case 'line-height': $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); diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php index e9f3c9d6..d60ebafe 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 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/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/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/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(); From 260bb75fc21660bc087824530a4c31100af10c62 Mon Sep 17 00:00:00 2001 From: "Yurii.sio2" Date: Sat, 8 Dec 2018 00:22:04 +0200 Subject: [PATCH 093/142] Fix TemplateProcessor :: fixBrokenMacros; (#1502) * Fix TemplateProcessor :: fixBrokenMacros; * add unit test for fixBrokenMacros --- CHANGELOG.md | 1 + src/PhpWord/TemplateProcessor.php | 10 ++---- tests/PhpWord/TemplateProcessorTest.php | 32 +++++++++++++++++++ .../_includes/TestableTemplateProcesor.php | 30 +++++++++++++++++ 4 files changed, 66 insertions(+), 7 deletions(-) create mode 100644 tests/PhpWord/_includes/TestableTemplateProcesor.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a273326a..735f6d4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v0.16.0 (xx dec 2018) ### 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 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 0d4bfdeb..86d0c07d 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -442,17 +442,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; } /** diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 1513486e..8839200d 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -277,6 +277,38 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase } } + /** + * 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); + } + public function testMainPartNameDetection() { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php new file mode 100644 index 00000000..f76da417 --- /dev/null +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -0,0 +1,30 @@ + Date: Sun, 9 Dec 2018 01:21:59 +0300 Subject: [PATCH 094/142] add support for hidden text (#1527) * added hidden text word 2007 * update changelog * update documentation * added unit test * docx reader * html reader/writer * odt writer * updated samples --- CHANGELOG.md | 1 + composer.json | 2 +- docs/styles.rst | 1 + samples/Sample_04_Textrun.php | 3 ++ samples/Sample_26_Html.php | 3 ++ src/PhpWord/Reader/Word2007/AbstractPart.php | 1 + src/PhpWord/Reader/Word2007/Styles.php | 1 + src/PhpWord/Shared/Html.php | 3 ++ src/PhpWord/Style/Font.php | 32 ++++++++++++++++++++ src/PhpWord/Writer/HTML/Style/Font.php | 1 + src/PhpWord/Writer/ODText/Style/Font.php | 3 ++ src/PhpWord/Writer/Word2007/Style/Font.php | 3 ++ tests/PhpWord/Reader/Word2007/StyleTest.php | 24 +++++++++++++++ tests/PhpWord/Style/FontTest.php | 2 ++ 14 files changed, 79 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 735f6d4d..d56a1cff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ v0.16.0 (xx dec 2018) ---------------------- ### Added - Add setting Chart Title and Legend visibility @Tom-Magill #1433 +- Add support for hidden text @Alexmg86 #1527 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 diff --git a/composer.json b/composer.json index 5d8a855b..24d8532b 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" diff --git a/docs/styles.rst b/docs/styles.rst index 855eab79..31d04a3b 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -61,6 +61,7 @@ Available Font style options: - ``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: 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_26_Html.php b/samples/Sample_26_Html.php index e1823c43..82a5cf6e 100644 --- a/samples/Sample_26_Html.php +++ b/samples/Sample_26_Html.php @@ -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/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index 5e5eb1d6..eada60ee 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -444,6 +444,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); diff --git a/src/PhpWord/Reader/Word2007/Styles.php b/src/PhpWord/Reader/Word2007/Styles.php index f343ad92..554f4565 100644 --- a/src/PhpWord/Reader/Word2007/Styles.php +++ b/src/PhpWord/Reader/Word2007/Styles.php @@ -68,6 +68,7 @@ class Styles extends AbstractPart if (is_null($name)) { $name = $xmlReader->getAttribute('w:val', $node, 'w:name'); } + $headingMatches = array(); preg_match('/Heading(\d)/', $name, $headingMatches); // $default = ($xmlReader->getAttribute('w:default', $node) == 1); switch ($type) { diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 3f07a058..7f4bf825 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -515,6 +515,9 @@ class Html case 'text-align': $styles['alignment'] = self::mapAlign($cValue); break; + case 'display': + $styles['hidden'] = $cValue === 'none'; + break; case 'direction': $styles['rtl'] = $cValue === 'rtl'; break; diff --git a/src/PhpWord/Style/Font.php b/src/PhpWord/Style/Font.php index d60ebafe..018604d3 100644 --- a/src/PhpWord/Style/Font.php +++ b/src/PhpWord/Style/Font.php @@ -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 = false; + /** * Vertically Raised or Lowered Text * @@ -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/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/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/Word2007/Style/Font.php b/src/PhpWord/Writer/Word2007/Style/Font.php index 58282d15..f299d8ef 100644 --- a/src/PhpWord/Writer/Word2007/Style/Font.php +++ b/src/PhpWord/Writer/Word2007/Style/Font.php @@ -120,6 +120,9 @@ class Font extends AbstractStyle $xmlWriter->writeElementIf($style->isSmallCaps(), 'w:smallCaps'); $xmlWriter->writeElementIf($style->isAllCaps(), 'w:caps'); + //Hidden text + $xmlWriter->writeElementIf($style->isHidden(), 'w:vanish'); + // Underline $xmlWriter->writeElementIf($style->getUnderline() != 'none', 'w:u', 'w:val', $style->getUnderline()); diff --git a/tests/PhpWord/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index d64079fa..364c79c2 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -145,4 +145,28 @@ class StyleTest extends AbstractTestReader $this->assertSame(TblWidth::TWIP, $tableStyle->getIndent()->getType()); $this->assertSame(2160, $tableStyle->getIndent()->getValue()); } + + 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()); + } } 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) { From cf3132acac1b6624612b4363caad9d6a18d8e8fd Mon Sep 17 00:00:00 2001 From: Nathan Dench Date: Sun, 9 Dec 2018 08:35:32 +1000 Subject: [PATCH 095/142] Add ability to pass a Style object to a Section element (#1416) * Add ability to pass a Style object to a Section * Fix typo * update changelog --- CHANGELOG.md | 1 + src/PhpWord/Element/Section.php | 8 ++++--- tests/PhpWord/Element/SectionTest.php | 22 +++++++++++++++++++ tests/PhpWord/Writer/Word2007/ElementTest.php | 15 +++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d56a1cff..28ffef72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ v0.16.0 (xx dec 2018) ---------------------- ### Added - 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 ### Fixed 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/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/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index 25c62ecc..dc75a335 100644 --- a/tests/PhpWord/Writer/Word2007/ElementTest.php +++ b/tests/PhpWord/Writer/Word2007/ElementTest.php @@ -492,4 +492,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); + } } From 8f85424fb7efc2dd43f391234fbe7fdaa3fcb35b Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 11 Dec 2018 21:30:18 +0100 Subject: [PATCH 096/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abf34834..646f4e97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). v0.16.0 (xx xxx 2018) ---------------------- ### Added +- Add getVariableCount method in TemplateProcessor. @nicoder #1272 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 From c0f8cae55c3aeb6f212e79e87c4691e47f414162 Mon Sep 17 00:00:00 2001 From: Samuel BF <36996277+Samuel-BF@users.noreply.github.com> Date: Tue, 18 Dec 2018 15:04:25 +0100 Subject: [PATCH 097/142] =?UTF-8?q?Fixing=20RTF=20writers=20:=20numbers=20?= =?UTF-8?q?should=20be=20printed=20as=20integers=20and=20not=20float.=20Th?= =?UTF-8?q?is=20is=20specified=20in=20the=20spec,=20for=20example=20here?= =?UTF-8?q?=20:=20http://www.biblioscape.com/rtf15=5Fspec.htm#Heading2=20?= =?UTF-8?q?=C2=AB=20The=20delimiter=20marks=20the=20end=20of=20an=20RTF=20?= =?UTF-8?q?control=20word,=20and=20can=20be=20one=20of=20the=20following?= =?UTF-8?q?=20:=20[...]=20*=20A=20digit=20or=20a=20hyphen=20(-),=20which?= =?UTF-8?q?=20indicates=20that=20a=20numeric=20parameter=20follows.=20The?= =?UTF-8?q?=20subsequent=20digital=20sequence=20is=20then=20delimited=20by?= =?UTF-8?q?=20a=20space=20or=20any=20character=20other=20than=20a=20letter?= =?UTF-8?q?=20or=20a=20digit.=20=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/PhpWord/Writer/RTF/Style/Border.php | 2 +- src/PhpWord/Writer/RTF/Style/Font.php | 2 +- src/PhpWord/Writer/RTF/Style/Indentation.php | 6 +++--- src/PhpWord/Writer/RTF/Style/Paragraph.php | 4 ++-- src/PhpWord/Writer/RTF/Style/Section.php | 14 +++++++------- src/PhpWord/Writer/RTF/Style/Tab.php | 2 +- 6 files changed, 15 insertions(+), 15 deletions(-) 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 index dd52230e..50e8ad99 100644 --- a/src/PhpWord/Writer/RTF/Style/Indentation.php +++ b/src/PhpWord/Writer/RTF/Style/Indentation.php @@ -36,9 +36,9 @@ class Indentation extends AbstractStyle return ''; } - $content = '\fi' . $style->getFirstLine(); - $content .= '\li' . $style->getLeft(); - $content .= '\ri' . $style->getRight(); + $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 cb50a31b..8ef3e146 100644 --- a/src/PhpWord/Writer/RTF/Style/Paragraph.php +++ b/src/PhpWord/Writer/RTF/Style/Paragraph.php @@ -65,8 +65,8 @@ class Paragraph extends AbstractStyle $content .= $alignments[$style->getAlignment()]; } $content .= $this->writeIndentation($style->getIndentation()); - $content .= $this->getValueIf($spaceBefore !== null, '\sb' . $spaceBefore); - $content .= $this->getValueIf($spaceAfter !== null, '\sa' . $spaceAfter); + $content .= $this->getValueIf($spaceBefore !== null, '\sb' . round($spaceBefore)); + $content .= $this->getValueIf($spaceAfter !== null, '\sa' . round($spaceAfter)); $styles = $style->getStyleValues(); $content .= $this->writeTabs($styles['tabs']); diff --git a/src/PhpWord/Writer/RTF/Style/Section.php b/src/PhpWord/Writer/RTF/Style/Section.php index ee6efcf3..190bb670 100644 --- a/src/PhpWord/Writer/RTF/Style/Section.php +++ b/src/PhpWord/Writer/RTF/Style/Section.php @@ -46,13 +46,13 @@ class Section extends AbstractStyle $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 index fe1f9363..a21b13d3 100644 --- a/src/PhpWord/Writer/RTF/Style/Tab.php +++ b/src/PhpWord/Writer/RTF/Style/Tab.php @@ -42,7 +42,7 @@ class Tab extends AbstractStyle if (isset($tabs[$style->getType()])) { $content .= $tabs[$style->getType()]; } - $content .= '\tx' . $style->getPosition(); + $content .= '\tx' . round($style->getPosition()); return $content; } From b1661af71a1c3336c1b3ddf33a8384136ba83735 Mon Sep 17 00:00:00 2001 From: Craig Blanchette Date: Wed, 19 Dec 2018 15:17:42 -0500 Subject: [PATCH 098/142] Still add text if link missing If link target doesn't exist, still add the text anyway. --- src/PhpWord/Reader/Word2007/AbstractPart.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index eada60ee..c3296801 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -297,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 */ From 0b99f8f3e26f34d61ca36c1f01e2129ce4f8552c Mon Sep 17 00:00:00 2001 From: Gautier Date: Wed, 26 Dec 2018 11:23:43 +0100 Subject: [PATCH 099/142] docs: ListItemRun --- docs/elements.rst | 11 +++++++++-- samples/Sample_14_ListItem.php | 2 +- 2 files changed, 10 insertions(+), 3 deletions(-) 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/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(); From d5da80b56e3d2defe0fc0b2c83f3ded4c6c5f16c Mon Sep 17 00:00:00 2001 From: Maxim Date: Wed, 26 Dec 2018 15:35:21 +0200 Subject: [PATCH 100/142] Support adding images in Templates (#1170) * setImageValue() + fix adding files via ZipArchive * fix phpdoc variable name * Changed logic that determines extension image file extension for document to depend on MIME type. This same logic is used in Element/Image.php * support tags with arguments * allow setup size of image into template variable * support of 'ratio' replace attribute + documentation --- .travis.yml | 2 - CHANGELOG.md | 1 + docs/templates-processing.rst | 18 + src/PhpWord/Shared/ZipArchive.php | 10 +- src/PhpWord/TemplateProcessor.php | 367 +++++++++++++++++- tests/PhpWord/TemplateProcessorTest.php | 79 +++- .../_files/templates/header-footer.docx | Bin 4495 -> 15730 bytes 7 files changed, 462 insertions(+), 15 deletions(-) diff --git a/.travis.yml b/.travis.yml index 881decfe..1d32cfda 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,8 +24,6 @@ matrix: - php: 5.3 - php: 7.0 - php: 7.3 - allow_failures: - - php: 7.3 cache: directories: diff --git a/CHANGELOG.md b/CHANGELOG.md index 220d2eeb..ae41bd4d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ v0.16.0 (xx dec 2018) - 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 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index af03b245..0cc5683a 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -7,14 +7,32 @@ You can create an OOXML document template with included search-patterns (macros) 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}``. +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:: doc + + ${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')); + $templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); + $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); + 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``). See ``Sample_07_TemplateCloneRow.php`` for example on how to create diff --git a/src/PhpWord/Shared/ZipArchive.php b/src/PhpWord/Shared/ZipArchive.php index 2783e17e..a9419165 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,7 +245,13 @@ 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 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 7996b7f8..4ec9e2cc 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -62,6 +62,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 * @@ -88,19 +109,33 @@ 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->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName()); + } + + /** + * @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)); } /** @@ -233,6 +268,274 @@ class TemplateProcessor $this->tempDocumentFooters = $this->setValueForPart($search, $replace, $this->tempDocumentFooters, $limit); } + 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 = self::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. * @@ -406,15 +709,17 @@ 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); 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.'); @@ -423,6 +728,19 @@ class TemplateProcessor 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. * @@ -542,6 +860,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], '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 +200,83 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertTrue($docFound); } + /** + * @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 diff --git a/tests/PhpWord/_files/templates/header-footer.docx b/tests/PhpWord/_files/templates/header-footer.docx index 647d52222d612ef33a0113e8ea3ad84ad3c96085..dc2e9e8beaabaefe8ddc10384cdd747ce335cc10 100644 GIT binary patch literal 15730 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 L)>RFzpP&90bK-2! literal 4495 zcmb_fdpy(s|8;NVe(UCbNt)Z3A(xR$#8^{F$R&51xy)^d+zq*n+(Ih3jpP?B%jZC_OO!sq_peM)YG-3c9* zCOEFJ<5AGc7lA=k?@pmSpCa(t#ijsteLf=a#D-N6~Gfi|w&!8cV|d%u{2Pxa|k1}#~tUZ`)#ICjU% zAYj)Gj1K8})PWqh^e*8=QR|x{s|9f5eSq11TJblO^J>!3(7^vAA_A0e1fYE!<^K;B zNAzVsl#?e$E)a$M0Z|XjQw2_a@)iB8W>~qH80;Yzkuby&H@A8f#hrF$b&>7ty@Fk zxya_#)anx5yNZ?9d5g*$Gd_xW%p|B@@>Oxx{z`O=MTD#4neV-`UlOh_EjvHeTjG(N z({k*=-BYnzoOjAo8Cca=1SUyq3DRq9-Q43=(3zR*VQP0CE5@|%i0-ad6|Z?5pJ{Ae zVy+sTkF)HCY5{}ksKB_Y%MSNZj-B#HU^xE<#?{H*(a8ryMI`AA%%fF-wRzEkp~mB5 zZ6e4d{Ze;tWa!(ZTVUETmis(aiQ8l~@5A*yo%;6!7w3$YmsZ^{5V}ToRM*#s5l(h| z!#A4o3)`R6!W^L2VlV>7n)c^I|T7Y5#F z-*072j~M4U?j<+zWOl~G(Pad(>CpPryx`mb+Pl&y#x^r-pPE$4xuxZ8flIEYRXb&( z`yKF2j-JQaIT9^SwIC2#6CqtX-#+e?Gg{Jz8frcvGpy?w4yv`wS6-p;X&<1}tZR#z zmWD>`k1%rm4dY&B5zfMrM-@1m$rgI&t18l??{ji}g%6d*o-erved3`WFBzsceN~3A zLGrjvS5Zq)U64$_uA+o?*64k=)HaKhCjwo7h_~-0(lga>BF$6pe!4!8=qQxUXQB=S z`lRG*$U$W`5UMRogAV+P7i^nw3r{lh8wM4iinz#?%66`RVvHZ*fyN;pTsW>a&c0tO!5hiC!d;G8Uu1=l#XST z077`k5`f8@`>kXgU-%!srDr?bJC#d29mep?OqX!JWim%u5oA-d&X)ywdwUM4k+5V> z!a3xd?TcMxy&s5;>(y&mKO^~y)21UmE>}>9Umdq2kzKVg+%kANHaaS{HG%IwL}1*y|~8nK?g9iMS{AMh5ZGNiBjg!)*TuzP;=0(VJqN^r zEiF|m6EkJxW1xKSUcNByT`G_3+;mChCv3nxWalsF;n3-5*-F~d&&+(P2hetyW##)d z9=V~__EV*OWwQ;cBNDChJ&z;vcsk1h*RQb4gYQ~|y-HyOowiX_;fvkZUM_AcEp38s znGp%`+kGX1E#xTO_bp$f0AO^Y<6GM+4yxFc5cjVE{;SwH{uY}rCJ5=|OSP<5=f_N^ z6*xmlRdHLF(uysP)0K6LrN9rM-f(!OyIn>(SP8}UZ*TZ{MxUhfw9r^E9yeLYR{HC8 zN87_R$T~%dDjFEp+XP!snt6IkFe>BouyAaTygH; zE*KQ{T95x5Bop4ET&%U5&|T{u<-{X6zOebtV{_ zH`!(JIR^rNFSP1#AsR~a)#t+%D( zri5{4NLR^;t@ky0Tf!v$=bnVoa5IPU~{lhKpx~wSK zMRD=Z3rN>GVVRctm|MiaT}dvw5w<*z;k;m;u*U|~sE}_-{!RN6k*ejDZ+KLXZ1?3U z?|xa9rxIORz3@f_Wuh3c{>d)5|0cRK+7olu-T~=ECwrw*i#vU8>lpu*izn}sNCgM}M#}0xkELa#AdDM^e9@b)) zPd>t#2g1xch_La17Di)HZl)kdem_kwbMUGfM-m>1UG6Mbg_n#9G?zKo6m9W0$^In^bRDALp z3GI`V)0_Ws>Qe2LGH~HOXfI!RdoM34IyX|xjOjs~ZHrfMDZwgMIs<7i={S{Lfuej} z#WAKdlg`9D+h2=W%3YGt6VK5?W@t0DcyTc9!qG+{1L?v**6IbRb!TCV00ielZtJIX z=^FH1gvP-tHX!(#b`cK?Z25_`%IQ?7wm%s25aZIGDH!nPE6ZPM*g);w?3?$RggUGk zrDC{6Dn8QTq0e5V?(S!sm0zD7&-eK25K44p=HT;{_lj(^Y94hab^88Y?X1qBzN94m zk2VBpY)7-u3U8?H)9kh$viZ_gS=Y4y{c@7S^8-{wv~iDihA8K?q1fYp-gBvY$Ulf& zM*DmxOlX=FI#T3d%pw`cN@j6=h}3iQH%ymA-A8^-8Ryy>O_S!f)7;vWMva%g5iD4o zZY@tStA_~^h$misq#KAT1k@sdcdj#HkKbKl#N{1m;`S@Qsjcqi`n1o-Eek38c5FC& zjm3H>zV`AUuj~6}`-AF@5e5%#i(?MRT^s|DlKkV7QWhPURV8NxR3`N$A3bSSfJAZ$ z;xh#BA6kcX;6|*<;KJrs@Xc=tC_L$q>Uuk9%OLn(k~(v&qez1(+mLgO5yWz`X@~c5 zuS@cj`Q9p5b#K^91?%dUz*+ijqloKd^M zTtUpKQziEHgpILh(#%IVd`TprEq(Gy^7aV@XchM^1NBBUX45RqDYudNM~?DPZp2EL zqF+kfuss{(<>YHarD-!BYSya2*+#yC>ja*Dt0)qc=CAD##qMO#-Xg&t-?yAScqO>*Dqj$8VxS*_XBM``ohNFHsw;5Z3BJfCy=t-R;5F(~(1ciSkvll6QF zH!5-G^yxv_RUFIeF(P3$sGp}w-P2DgwkWPG6NF#4{Jj#L?_CG zj*AM@VI&f2aSK>Z31>+>`Npf`K3a{K5A?F6v@{=cUBViso$=%q7?x;PuNt}0=mICc z-;MFqIRr?5wsnYoe$@T7lc}>-A(L1O(9=#qt22zW>P^33igg+%3Me~hbO|s`M~2|$ z*=N6fBa)ANQZm!@_rlR43FK(Ib&W!)u6xi^;e0^0tUTX9z`$qWC)Ws|t)ktAfa^mA z@wX73TIF* Date: Wed, 26 Dec 2018 17:20:15 +0100 Subject: [PATCH 101/142] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 220d2eeb..1e21db06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ v0.16.0 (xx dec 2018) - 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 v0.15.0 (14 Jul 2018) ---------------------- From 2a088fb45b646544c7e88433eddbd2b51c470cfd Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 26 Dec 2018 20:07:32 +0100 Subject: [PATCH 102/142] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae41bd4d..8c2c73ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,9 @@ v0.16.0 (xx dec 2018) - RTF writer: Round getPageSizeW and getPageSizeH to avoid decimals @Patrick64 #1493 - Fix parsing of Office 365 documents @Timanx #1485 +### Miscelaneous +- Get rid of duplicated code in TemplateProcessor @abcdmitry #1161 + v0.15.0 (14 Jul 2018) ---------------------- ### Added From 575c5531b8a6d67a0eb270d2dc1ecfb08d637df3 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 26 Dec 2018 20:07:53 +0100 Subject: [PATCH 103/142] replace self with static --- src/PhpWord/TemplateProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 71090357..599ca0cf 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -521,7 +521,7 @@ class TemplateProcessor $xmlImage = str_replace(array('{RID}', '{WIDTH}', '{HEIGHT}'), array($rid, $preparedImageAttrs['width'], $preparedImageAttrs['height']), $imgTpl); // replace variable - $varNameWithArgsFixed = self::ensureMacroCompleted($varNameWithArgs); + $varNameWithArgsFixed = static::ensureMacroCompleted($varNameWithArgs); $matches = array(); if (preg_match('/(<[^<]+>)([^<]*)(' . preg_quote($varNameWithArgsFixed) . ')([^>]*)(<[^>]+>)/Uu', $partContent, $matches)) { $wholeTag = $matches[0]; From a5ec49d99dd3783571b9e477ecc24583cd194bf9 Mon Sep 17 00:00:00 2001 From: Daniel Morris Date: Wed, 26 Dec 2018 21:45:32 +0200 Subject: [PATCH 104/142] Add plain text SDT type --- src/PhpWord/Element/SDT.php | 2 +- src/PhpWord/Writer/Word2007/Element/SDT.php | 12 ++++++++++++ tests/PhpWord/Element/SDTTest.php | 4 ++-- 3 files changed, 15 insertions(+), 3 deletions(-) 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/Writer/Word2007/Element/SDT.php b/src/PhpWord/Writer/Word2007/Element/SDT.php index 21020a0f..fc1540b9 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 + * @param \PhpOffice\PhpWord\Element\SDT $element + */ + private function writePlainText(XMLWriter $xmlWriter, SDTElement $element) + { + $xmlWriter->startElement("w:text"); + $xmlWriter->endElement(); // w:{$type} + } /** * Write combo box. * 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'; From 8ffaa1c8b736dfe3669cb2258dcb5c5aff1c9973 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 26 Dec 2018 22:27:17 +0100 Subject: [PATCH 105/142] add test to cover get TemplateProcessor::getVariables --- tests/PhpWord/TemplateProcessorTest.php | 20 +++++++++++++++++++ .../_includes/TestableTemplateProcesor.php | 13 ++++++++++++ 2 files changed, 33 insertions(+) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index fa84cf3b..e4789671 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -425,6 +425,9 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertEquals('$15,000.00. ${variable_name}', $fixed); } + /** + * @covers ::getMainPartName + */ public function testMainPartNameDetection() { $templateProcessor = new TemplateProcessor(__DIR__ . '/_files/templates/document22-xml.docx'); @@ -433,4 +436,21 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $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); + } } diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php index f76da417..65e83695 100644 --- a/tests/PhpWord/_includes/TestableTemplateProcesor.php +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -17,6 +17,12 @@ namespace PhpOffice\PhpWord; +/** + * This class is used to expose publicly methods that are otherwise private or protected. + * This makes testing those methods easier + * + * @author troosan + */ class TestableTemplateProcesor extends TemplateProcessor { public function __construct() @@ -27,4 +33,11 @@ class TestableTemplateProcesor extends TemplateProcessor { return parent::fixBrokenMacros($documentPart); } + + public function getVariablesForPart($documentPartXML) + { + $documentPartXML = parent::fixBrokenMacros($documentPartXML); + + return parent::getVariablesForPart($documentPartXML); + } } From d84da93a362ffb58aceb80c2b9f319a9c0b9315b Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 26 Dec 2018 22:50:19 +0100 Subject: [PATCH 106/142] Add test and fix warnings --- src/PhpWord/Writer/Word2007/Element/SDT.php | 8 ++++---- tests/PhpWord/Writer/Word2007/ElementTest.php | 3 +++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/PhpWord/Writer/Word2007/Element/SDT.php b/src/PhpWord/Writer/Word2007/Element/SDT.php index fc1540b9..edf89b53 100644 --- a/src/PhpWord/Writer/Word2007/Element/SDT.php +++ b/src/PhpWord/Writer/Word2007/Element/SDT.php @@ -78,13 +78,13 @@ class SDT extends Text * * @see http://www.datypic.com/sc/ooxml/t-w_CT_SdtText.html * @param \PhpOffice\Common\XMLWriter $xmlWriter - * @param \PhpOffice\PhpWord\Element\SDT $element */ - private function writePlainText(XMLWriter $xmlWriter, SDTElement $element) + private function writePlainText(XMLWriter $xmlWriter) { - $xmlWriter->startElement("w:text"); - $xmlWriter->endElement(); // w:{$type} + $xmlWriter->startElement('w:text'); + $xmlWriter->endElement(); // w:text } + /** * Write combo box. * diff --git a/tests/PhpWord/Writer/Word2007/ElementTest.php b/tests/PhpWord/Writer/Word2007/ElementTest.php index dc75a335..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')); } /** From dcf637df5a03235d86699b198399f1774a1f5167 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 26 Dec 2018 22:53:49 +0100 Subject: [PATCH 107/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c2c73ef..70d2f392 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ v0.16.0 (xx dec 2018) - 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 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 From 1717bd4978439c8fe782e5fd7ef756c5d2df7282 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 27 Dec 2018 00:24:45 +0100 Subject: [PATCH 108/142] add test for cloneBlock operation --- tests/PhpWord/TemplateProcessorTest.php | 30 +++++++++++++++++++ .../_includes/TestableTemplateProcesor.php | 8 ++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index e4789671..73b4910c 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -393,6 +393,36 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase } } + /** + * @covers ::cloneBlock + * @test + */ + public function testCloneBlock() + { + $mainPart = ' + + + + ${CLONEME} + + + + + This block will be cloned + + + + + ${/CLONEME} + + '; + + $templateProcessor = new TestableTemplateProcesor($mainPart); + $templateProcessor->cloneBlock('CLONEME', 3); + + $this->assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned')); + } + /** * Template macros can be fixed. * diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php index 65e83695..3b6f5b56 100644 --- a/tests/PhpWord/_includes/TestableTemplateProcesor.php +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -25,8 +25,9 @@ namespace PhpOffice\PhpWord; */ class TestableTemplateProcesor extends TemplateProcessor { - public function __construct() + public function __construct($mainPart = null) { + $this->tempDocumentMainPart = $mainPart; } public function fixBrokenMacros($documentPart) @@ -40,4 +41,9 @@ class TestableTemplateProcesor extends TemplateProcessor return parent::getVariablesForPart($documentPartXML); } + + public function getMainPart() + { + return $this->tempDocumentMainPart; + } } From 75620caf5149aa79aa347dda81272b74116adfb2 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 27 Dec 2018 01:44:37 +0100 Subject: [PATCH 109/142] add parameter to keep backward compatibility + add test --- CHANGELOG.md | 1 + src/PhpWord/TemplateProcessor.php | 19 +++++++++---- tests/PhpWord/TemplateProcessorTest.php | 36 +++++++++++++++++++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4a140bf4..d440989c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ v0.16.0 (xx dec 2018) - 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 ### Fixed - Fix regex in `cloneBlock` function @nicoder #1269 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 6d3a1348..727dd74c 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -629,12 +629,13 @@ class TemplateProcessor * 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, ...) * * @return string|null */ - public function cloneBlock($blockname, $clones = 1, $replace = true) + public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false) { $xmlBlock = null; preg_match( @@ -645,7 +646,14 @@ class TemplateProcessor if (isset($matches[3])) { $xmlBlock = $matches[3]; - $cloned = $this->indexClonedVariables($clones, $xmlBlock); + if ($indexVariables) { + $cloned = $this->indexClonedVariables($clones, $xmlBlock); + } else { + $cloned = array(); + for ($i = 1; $i <= $clones; $i++) { + $cloned[] = $xmlBlock; + } + } if ($replace) { $this->tempDocumentMainPart = str_replace( @@ -935,10 +943,10 @@ class TemplateProcessor } /** - * Replaces variable names in cloned + * Replaces variable names in cloned * rows/blocks with indexed names * - * @param integer $count + * @param int $count * @param string $xmlBlock * * @return string @@ -949,6 +957,7 @@ class TemplateProcessor for ($i = 1; $i <= $count; $i++) { $results[] = preg_replace('/\$\{(.*?)\}/', '\${\\1#' . $i . '}', $xmlBlock); } + return $results; } } diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 36432dc5..2c388299 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -409,7 +409,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase - This block will be cloned + This block will be cloned with ${variable} @@ -421,7 +421,39 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $templateProcessor = new TestableTemplateProcesor($mainPart); $templateProcessor->cloneBlock('CLONEME', 3); - $this->assertEquals(3, substr_count($templateProcessor->getMainPart(), 'This block will be cloned')); + $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()); } /** From 01209ddbd195506c09b2d3e21558d2a707e130e6 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 27 Dec 2018 21:05:56 +0100 Subject: [PATCH 110/142] improve template processor documentation --- docs/conf.py | 2 +- docs/containers.rst | 2 +- docs/templates-processing.rst | 143 +++++++++++++++++++++++++++++----- 3 files changed, 127 insertions(+), 20 deletions(-) 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 34b2a9a3..9ee58efc 100644 --- a/docs/containers.rst +++ b/docs/containers.rst @@ -107,8 +107,8 @@ You can pass an optional parameter to specify where the header/footer should be 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/templates-processing.rst b/docs/templates-processing.rst index 0cc5683a..6513cb24 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -4,24 +4,45 @@ 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 ${name}! + +The following will replace ``${name}`` with ``World``. The resulting document will now contain ``Hello World!`` + +.. code-block:: php + + $templateProcessor->setValue('name', 'World'); + +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}`` + - ``${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. + - [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:: doc +.. code-block:: clean - ${CompanyLogo} + ${CompanyLogo} ${UserLogo:50:50} ${Name} - ${City} - ${Street} .. code-block:: php @@ -30,14 +51,100 @@ Example: $templateProcessor->setValue('Name', 'John Doe'); $templateProcessor->setValue(array('City', 'Street'), array('Detroit', '12th Street')); - $templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); - $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); + $templateProcessor->setImageValue('CompanyLogo', 'path/to/company/logo.png'); + $templateProcessor->setImageValue('UserLogo', array('path' => 'path/to/logo.png', 'width' => 100, 'height' => 100, 'ratio' => false)); -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``). +cloneBlock +"""""""""" +Given a template containing +See ``Sample_23_TemplateBlock.php`` for an example. -See ``Sample_07_TemplateCloneRow.php`` for example on how to create -multirow from a single row in a template by using ``TemplateProcessor::cloneRow``. +.. code-block:: clean -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``. + ${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} + +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} | + ---------------------------------- + +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); From 7790b6a6b0f964131c025703b81da5e4c350b60c Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 27 Dec 2018 22:13:48 +0100 Subject: [PATCH 111/142] Pass values to replace macros with in cloneBlock --- CHANGELOG.md | 1 + docs/templates-processing.rst | 23 +++++++++++++++++ src/PhpWord/TemplateProcessor.php | 27 +++++++++++++++++++- tests/PhpWord/TemplateProcessorTest.php | 33 +++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d440989c..af226cf5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ v0.16.0 (xx dec 2018) - 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 diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index 6513cb24..095093b2 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -79,11 +79,34 @@ The result will be 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 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 727dd74c..704352d4 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -632,10 +632,11 @@ class TemplateProcessor * @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, $indexVariables = false) + public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null) { $xmlBlock = null; preg_match( @@ -648,6 +649,8 @@ class TemplateProcessor $xmlBlock = $matches[3]; 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++) { @@ -960,4 +963,26 @@ class TemplateProcessor 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; + } } diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 2c388299..2bca64ef 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -456,6 +456,39 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $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. * From d8d697c8482798aeedaaed4e06ae08ac1f88be1e Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 28 Dec 2018 18:04:36 +0100 Subject: [PATCH 112/142] added convertors with test --- src/PhpWord/Shared/Converter.php | 45 ++++++++++++++++++++++++++ tests/PhpWord/Shared/ConverterTest.php | 5 +++ 2 files changed, 50 insertions(+) diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index c53f0030..a0cde503 100644 --- a/src/PhpWord/Shared/Converter.php +++ b/src/PhpWord/Shared/Converter.php @@ -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 int $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/tests/PhpWord/Shared/ConverterTest.php b/tests/PhpWord/Shared/ConverterTest.php index fbe92c25..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 From ccf291234e2cd561d3be24cf7860eaca312fe039 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 28 Dec 2018 20:32:10 +0100 Subject: [PATCH 113/142] Avoid warning if variable is null --- src/PhpWord/Writer/ODText/Style/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Writer/ODText/Style/Table.php b/src/PhpWord/Writer/ODText/Style/Table.php index 5ddee25a..c64dee4f 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -47,7 +47,7 @@ class Table extends AbstractStyle $xmlWriter->endElement(); // style:style $cellWidths = $style->getColumnWidths(); - $countCellWidths = count($cellWidths); + $countCellWidths = $cellWidths === null ? 0 : count($cellWidths); for ($i = 0; $i < $countCellWidths; $i++) { $width = $cellWidths[$i]; From 9e5da1e025f4f6ff9f9644a1292f2b601351d36b Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 28 Dec 2018 22:26:30 +0100 Subject: [PATCH 114/142] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index af226cf5..a9ed5508 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ v0.16.0 (xx dec 2018) - 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 From 7c8cfb7b8af351cd400975d715d639c1b7fd26de Mon Sep 17 00:00:00 2001 From: Antonio Malatesta Date: Wed, 1 Apr 2015 16:53:56 +0200 Subject: [PATCH 115/142] Adding condition for document template saving using libreoffice When I save document template with libreoffice (in .docx format) the cloneRow with nested table doesn't work. The problem is that the regular expression, that used for search if row is no longer part of the spanned row, was wrong: became without space at the closure tag. Sorry for my English. --- src/PhpWord/TemplateProcessor.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 63c55e10..fe342bcc 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -612,7 +612,8 @@ 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) && + !preg_match('##', $tmpXmlRow)) { break; } // This row was a spanned row, update $rowEnd and search for the next row. From 5057617de74a11f49e93e4c1f1300c8efe8f7678 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 14:56:11 +0100 Subject: [PATCH 116/142] change regex instead of checking twice --- src/PhpWord/TemplateProcessor.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index fe342bcc..d230cdf9 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -612,8 +612,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)) { + !preg_match('##', $tmpXmlRow)) { break; } // This row was a spanned row, update $rowEnd and search for the next row. From 23407c99dd547796ebc0cdccec11dd948c9b1535 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 22:03:01 +0100 Subject: [PATCH 117/142] Add unit tests --- src/PhpWord/TemplateProcessor.php | 19 ++++++++-- tests/PhpWord/TemplateProcessorTest.php | 46 +++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index d230cdf9..daef625a 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -96,12 +96,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 @@ -122,6 +122,19 @@ class TemplateProcessor $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 * @@ -729,7 +742,7 @@ class TemplateProcessor // 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; diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 2bca64ef..a8a01403 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -24,10 +24,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 +55,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 +76,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 +84,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 +196,18 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertTrue($docFound); } + /** + * @expectedException Exception + * @test + */ + public function testCloneNotExistingRowShouldThrowException() + { + $mainPart = 'text'; + $templateProcessor = new TestableTemplateProcesor($mainPart); + + $templateProcessor->cloneRow('fake_search', 2); + } + /** * @covers ::setValue * @covers ::saveAs @@ -200,6 +230,22 @@ 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() + ); + } + /** * @covers ::setImageValue * @test From 54b94be65a8051ba702af02b0eafc6b0222a0778 Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 23:14:18 +0100 Subject: [PATCH 118/142] fix doc --- src/PhpWord/Shared/Converter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Shared/Converter.php b/src/PhpWord/Shared/Converter.php index a0cde503..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) @@ -220,7 +220,7 @@ class Converter /** * Convert point to cm * - * @param int $point + * @param float $point * @return float */ public static function pointToCm($point = 1) From 3390c907dc55ef57d46288fedfc5709e28550c2a Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 23:16:56 +0100 Subject: [PATCH 119/142] fix scrutiniser warning --- src/PhpWord/Style/Table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Style/Table.php b/src/PhpWord/Style/Table.php index 0f7bf7dc..caf2c580 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -759,7 +759,7 @@ class Table extends Border /** * Get the columnWidths * - * @return number[] + * @return null|int[] */ public function getColumnWidths() { From 067c4f680977b54f9a6c345b1f79201e68aa0cdd Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 23:17:17 +0100 Subject: [PATCH 120/142] update badges --- README.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0509bce2..49c9d6bf 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,22 @@ # ![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://poser.pugx.org/phpoffice/phpword/v/unstable.png)](https://packagist.org/packages/phpoffice/phpword#dev-master) + +[![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/). From 481f2c7310673e69b9bae1d89ced448e6783506c Mon Sep 17 00:00:00 2001 From: troosan Date: Sat, 29 Dec 2018 23:30:23 +0100 Subject: [PATCH 121/142] fix code indenting --- README.md | 6 ++---- tests/PhpWord/TemplateProcessorTest.php | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 49c9d6bf..f7d403f3 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,7 @@ # ![PHPWord](https://rawgit.com/PHPOffice/PHPWord/develop/docs/images/phpword.svg "PHPWord") -Master +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?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) @@ -10,9 +9,8 @@ Master [![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 +Develop: [![Latest Development Version](https://poser.pugx.org/phpoffice/phpword/v/unstable.png)](https://packagist.org/packages/phpoffice/phpword#dev-master) - [![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) diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index a8a01403..370fcdf1 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -197,7 +197,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase } /** - * @expectedException Exception + * @expectedException \Exception * @test */ public function testCloneNotExistingRowShouldThrowException() @@ -243,7 +243,7 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $this->assertEquals( array('tableHeader', 'userId', 'userLocation'), $templateProcessor->getVariables() - ); + ); } /** From 1c35c4871afc6299c6a8607b7be03b9cb5531c36 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 30 Dec 2018 00:41:58 +0100 Subject: [PATCH 122/142] update develop badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7d403f3..8f0a2a7c 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Master: [![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://poser.pugx.org/phpoffice/phpword/v/unstable.png)](https://packagist.org/packages/phpoffice/phpword#dev-master) +[![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) From 54e7c6dd5a8199c8408dcfec8246eae4b5f6a17d Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 30 Dec 2018 00:45:45 +0100 Subject: [PATCH 123/142] don't default styles to false this allows us to for instance make part of a Heading not bold, which would otherwise be the default. --- samples/resources/Sample_11_ReadWord2007.docx | Bin 63388 -> 63832 bytes src/PhpWord/Style/Font.php | 18 ++--- .../Writer/Word2007/Style/AbstractStyle.php | 15 ++++ src/PhpWord/Writer/Word2007/Style/Font.php | 20 ++--- tests/PhpWord/Reader/Word2007/PartTest.php | 72 ++++++++++++++++++ tests/PhpWord/Reader/Word2007Test.php | 2 +- 6 files changed, 107 insertions(+), 20 deletions(-) diff --git a/samples/resources/Sample_11_ReadWord2007.docx b/samples/resources/Sample_11_ReadWord2007.docx index 406cf1e1ebf296f563f7a05ef4c7c6aaaffd148b..f6526360fd363431b222687b47a6d57ecde007af 100644 GIT binary patch delta 11300 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 !== 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'); + $xmlWriter->writeElementIf($style->isHidden(), 'w:vanish', 'w:val', $this->writeOnOf($style->isHidden())); // Underline $xmlWriter->writeElementIf($style->getUnderline() != 'none', 'w:u', 'w:val', $style->getUnderline()); @@ -139,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/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/Word2007Test.php b/tests/PhpWord/Reader/Word2007Test.php index e4ea62de..6d660ce5 100644 --- a/tests/PhpWord/Reader/Word2007Test.php +++ b/tests/PhpWord/Reader/Word2007Test.php @@ -62,7 +62,7 @@ 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')); } /** From b375b8580f67bc6854702a08f48ba96993cce876 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 30 Dec 2018 01:13:54 +0100 Subject: [PATCH 124/142] fix broken samples --- samples/Sample_11_ReadWord97.php | 13 ++++--------- samples/Sample_23_TemplateBlock.php | 2 +- samples/resources/Sample_30_ReadHTML.html | 6 +++--- src/PhpWord/Writer/HTML/Element/Table.php | 18 +++++++++++------- 4 files changed, 19 insertions(+), 20 deletions(-) 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_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/resources/Sample_30_ReadHTML.html b/samples/resources/Sample_30_ReadHTML.html index b3d2fad2..b4af3812 100644 --- a/samples/resources/Sample_30_ReadHTML.html +++ b/samples/resources/Sample_30_ReadHTML.html @@ -17,9 +17,9 @@

Includes images

- - - + + + diff --git a/src/PhpWord/Writer/HTML/Element/Table.php b/src/PhpWord/Writer/HTML/Element/Table.php index a5143d2b..67b15d63 100644 --- a/src/PhpWord/Writer/HTML/Element/Table.php +++ b/src/PhpWord/Writer/HTML/Element/Table.php @@ -107,19 +107,23 @@ class Table extends AbstractElement /** * Translates Table style in CSS equivalent * - * @param \PhpOffice\PhpWord\Style\Table|null $tableStyle + * @param string|\PhpOffice\PhpWord\Style\Table|null $tableStyle * @return string */ - private function getTableStyle(\PhpOffice\PhpWord\Style\Table $tableStyle = null) + private function getTableStyle($tableStyle = null) { if ($tableStyle == null) { return ''; } - $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;'; + 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 . '"'; From 7b7d4e4936014544aa706f4c03d3ac925d74beb9 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 30 Dec 2018 01:35:03 +0100 Subject: [PATCH 125/142] release version 0.16.0 --- CHANGELOG.md | 2 +- README.md | 2 +- composer.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a9ed5508..2d07c7bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). -v0.16.0 (xx dec 2018) +v0.16.0 (30 dec 2018) ---------------------- ### Added - Add getVariableCount method in TemplateProcessor. @nicoder #1272 diff --git a/README.md b/README.md index 8f0a2a7c..f1d1d6ee 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ You can of course also manually edit your composer.json file ```json { "require": { - "phpoffice/phpword": "v0.14.*" + "phpoffice/phpword": "v0.16.*" } } ``` diff --git a/composer.json b/composer.json index 24d8532b..bd57d6e3 100644 --- a/composer.json +++ b/composer.json @@ -90,7 +90,7 @@ }, "extra": { "branch-alias": { - "dev-develop": "0.16-dev" + "dev-develop": "0.17-dev" } } } From c408ac5d504599f38e19111108a6719b3a5c4306 Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 30 Dec 2018 14:14:27 +0100 Subject: [PATCH 126/142] Use embedded http server to test loading of remote images --- CHANGELOG.md | 9 ++ tests/PhpWord/Element/CellTest.php | 6 +- tests/PhpWord/Element/FooterTest.php | 6 +- tests/PhpWord/Element/HeaderTest.php | 6 +- tests/PhpWord/Element/ImageTest.php | 9 +- tests/PhpWord/MediaTest.php | 8 +- tests/PhpWord/Shared/HtmlTest.php | 5 +- tests/PhpWord/Writer/HTMLTest.php | 5 +- tests/PhpWord/Writer/Word2007Test.php | 5 +- tests/PhpWord/_files/images/new-php-logo.png | Bin 0 -> 10298 bytes .../AbstractWebServerEmbeddedTest.php | 80 ++++++++++++++++++ 11 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 tests/PhpWord/_files/images/new-php-logo.png create mode 100644 tests/PhpWord/_includes/AbstractWebServerEmbeddedTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d07c7bd..735264f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,15 @@ Change Log All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +v0.17.0 (?? ??? 2019) +---------------------- +### Added + +### Fixed + +### Miscelaneous +- Use embedded http server to test loading of remote images @troosan # + v0.16.0 (30 dec 2018) ---------------------- ### Added 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/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/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 89292a20..2f9a4be4 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 @@ -487,7 +488,7 @@ class HtmlTest extends \PHPUnit\Framework\TestCase */ public function testParseRemoteImage() { - $src = 'https://phpword.readthedocs.io/en/latest/_images/phpword.png'; + $src = self::getRemoteImageUrl(); $phpWord = new \PhpOffice\PhpWord\PhpWord(); $section = $phpWord->addSection(); 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/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/images/new-php-logo.png b/tests/PhpWord/_files/images/new-php-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..6649079930716c425de2d675a0feebbe7d0a0d70 GIT binary patch literal 10298 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 M07*qoM6N<$f)aut$N&HU literal 0 HcmV?d00001 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', '>='); + } +} From f91863ed6431cb5e7eb6920385f51696951e4f9e Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 3 Jan 2019 11:33:56 +0100 Subject: [PATCH 127/142] Add RTL aligning of tables --- samples/Sample_36_RTL.php | 23 +++++++++++++ samples/index.php | 2 +- src/PhpWord/Reader/Word2007/AbstractPart.php | 1 + src/PhpWord/Style/Table.php | 32 +++++++++++++++++++ src/PhpWord/Writer/ODText/Style/Paragraph.php | 4 +++ src/PhpWord/Writer/ODText/Style/Table.php | 1 + src/PhpWord/Writer/Word2007/Style/Table.php | 3 ++ tests/PhpWord/Reader/Word2007/StyleTest.php | 18 +++++++++++ .../Writer/Word2007/Style/TableTest.php | 17 ++++++++++ 9 files changed, 100 insertions(+), 1 deletion(-) 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/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/src/PhpWord/Reader/Word2007/AbstractPart.php b/src/PhpWord/Reader/Word2007/AbstractPart.php index a7816b19..bb4a3a49 100644 --- a/src/PhpWord/Reader/Word2007/AbstractPart.php +++ b/src/PhpWord/Reader/Word2007/AbstractPart.php @@ -483,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/Style/Table.php b/src/PhpWord/Style/Table.php index caf2c580..f777ac67 100644 --- a/src/PhpWord/Style/Table.php +++ b/src/PhpWord/Style/Table.php @@ -170,6 +170,14 @@ class Table extends Border */ 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 * @@ -775,4 +783,28 @@ class Table extends Border { $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/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 c64dee4f..646f2e44 100644 --- a/src/PhpWord/Writer/ODText/Style/Table.php +++ b/src/PhpWord/Writer/ODText/Style/Table.php @@ -43,6 +43,7 @@ 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 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/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index 4a7add16..ad48dcba 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -147,6 +147,24 @@ class StyleTest extends AbstractTestReader $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 = ' 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')); + } } From 6aae8bdccb9aca8ba3b40aa0e5ab942fe7eddc10 Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 3 Jan 2019 11:36:28 +0100 Subject: [PATCH 128/142] update doc and release note --- CHANGELOG.md | 1 + docs/styles.rst | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 735264f1..c7030a5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ This project adheres to [Semantic Versioning](http://semver.org/). v0.17.0 (?? ??? 2019) ---------------------- ### Added +- Add RightToLeft table presentation. @troosan #1550 ### Fixed diff --git a/docs/styles.rst b/docs/styles.rst index 31d04a3b..2be6eb94 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -108,11 +108,12 @@ Available Table style options: - ``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: From 3c9fa2df13517c3ffa29aaaea7da6c3455a909e6 Mon Sep 17 00:00:00 2001 From: Stathis Papadopoulos Date: Mon, 28 Jan 2019 10:50:28 +0100 Subject: [PATCH 129/142] Language::validateLocale should pass with locale 'zxx'. --- src/PhpWord/Style/Language.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Style/Language.php b/src/PhpWord/Style/Language.php index 8174f6ee..dd3ed819 100644 --- a/src/PhpWord/Style/Language.php +++ b/src/PhpWord/Style/Language.php @@ -229,7 +229,7 @@ final class Language extends AbstractStyle return strtolower($locale) . '-' . strtoupper($locale); } - if ($locale !== null && strstr($locale, '-') === false) { + if ($locale !== null && $locale !== 'zxx' && strstr($locale, '-') === false) { throw new \InvalidArgumentException($locale . ' is not a valid language code'); } From 67f3bd369cb42cc547d832035d25e3912447071e Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 Jan 2019 01:26:19 +0100 Subject: [PATCH 130/142] Add methods to replace macro with ComplexType --- .gitignore | 1 - samples/Sample_40_TemplateSetComplexValue.php | 45 ++++ .../Sample_40_TemplateSetComplexValue.docx | Bin 0 -> 14735 bytes src/PhpWord/TemplateProcessor.php | 212 ++++++++++++++++++ tests/PhpWord/TemplateProcessorTest.php | 128 +++++++++++ .../_includes/TestableTemplateProcesor.php | 28 +++ 6 files changed, 413 insertions(+), 1 deletion(-) create mode 100644 samples/Sample_40_TemplateSetComplexValue.php create mode 100644 samples/resources/Sample_40_TemplateSetComplexValue.docx diff --git a/.gitignore b/.gitignore index b2ec7e23..dd858cea 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ composer.phar vendor /report /build -/samples/resources /samples/results /.settings phpword.ini 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/resources/Sample_40_TemplateSetComplexValue.docx b/samples/resources/Sample_40_TemplateSetComplexValue.docx new file mode 100644 index 0000000000000000000000000000000000000000..7265908e8c5c54842b11079507f3fea33af4b8e1 GIT binary patch literal 14735 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 DlaFJ( literal 0 HcmV?d00001 diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index fbfdd9dc..498cf701 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; @@ -249,6 +250,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 @@ -685,6 +726,7 @@ class TemplateProcessor public function cloneBlock($blockname, $clones = 1, $replace = true, $indexVariables = false, $variableReplacements = null) { $xmlBlock = null; + $matches = array(); preg_match( '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, @@ -724,6 +766,7 @@ class TemplateProcessor */ public function replaceBlock($blockname, $replacement) { + $matches = array(); preg_match( '/(<\?xml.*)(\${' . $blockname . '}<\/w:.*?p>)(.*)()/is', $this->tempDocumentMainPart, @@ -865,6 +908,7 @@ class TemplateProcessor */ protected function getVariablesForPart($documentPartXML) { + $matches = array(); preg_match_all('/\$\{(.*?)}/i', $documentPartXML, $matches); return $matches[1]; @@ -893,6 +937,7 @@ class TemplateProcessor $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'; @@ -1031,4 +1076,171 @@ class TemplateProcessor 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 (false !== $where) { + $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']); + } + + return $this; + } + + /** + * Find start and end of XML block containing the given macro + * e.g. ...${macro}... + * + * Note that only the first instance of the macro will be found + * + * @param string $macro Name of macro + * @param string $blockType XML tag for block + * @return bool|int[] FALSE if not found, otherwise array with start and end + */ + protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') + { + $macroPos = $this->findMacro($macro); + if (false === $macroPos) { + return false; + } + $start = $this->findXmlBlockStart($macroPos, $blockType); + if (0 > $start) { + return false; + } + $end = $this->findXmlBlockEnd($start, $blockType); + if (0 > $end) { + return false; + } + + return array('start' => $start, 'end' => $end); + } + + /** + * Find start and end of XML block containing the given block macro + * e.g. ...${macro}...${/macro}... + * + * Note that only the first instance of the macro will be found + * + * @param string $macro Name of macro + * @param string $blockType XML tag for block + * @return bool|int[] FALSE if not found, otherwise array with start and end + */ + protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p') + { + $macroStartPos = $this->findMacro($macro); + if (0 > $macroStartPos) { + return false; + } + $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos); + if (0 > $macroEndPos) { + return false; + } + $start = $this->findXmlBlockStart($macroStartPos, $blockType); + if (0 > $start) { + return false; + } + $end = $this->findXmlBlockEnd($macroEndPos, $blockType); + if (0 > $end) { + return false; + } + + return array('start' => $start, 'end' => $end); + } + + /** + * Find the position of (the start of) a macro + * + * Returns -1 if not found, otherwise position of opening $ + * + * Note that only the first instance of the macro will be found + * + * @param string $search Macro name + * @param string $offset Offset from which to start searching + * @return 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) + { + // first try XML tag with attributes + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + // 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 . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + } + + 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/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 286ffe97..4c9f2358 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 @@ -307,6 +310,59 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase ); } + 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 @@ -675,4 +731,76 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $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'])); + } } diff --git a/tests/PhpWord/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php index 3b6f5b56..44c0bb55 100644 --- a/tests/PhpWord/_includes/TestableTemplateProcesor.php +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -35,6 +35,16 @@ class TestableTemplateProcesor extends TemplateProcessor 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); @@ -42,6 +52,24 @@ class TestableTemplateProcesor extends TemplateProcessor 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; From d862b1f267ca86635593b3af3c637889248602ed Mon Sep 17 00:00:00 2001 From: troosan Date: Thu, 31 Jan 2019 01:32:00 +0100 Subject: [PATCH 131/142] update documentation --- docs/templates-processing.rst | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/docs/templates-processing.rst b/docs/templates-processing.rst index 325de8de..5b32aa18 100644 --- a/docs/templates-processing.rst +++ b/docs/templates-processing.rst @@ -215,3 +215,32 @@ Applies the XSL stylesheet passed to header part, footer part and main part $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); From bc448aed6c7df7782d9655d5df99e2789090daa6 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Feb 2019 21:53:19 +0100 Subject: [PATCH 132/142] improve code coverage --- src/PhpWord/TemplateProcessor.php | 42 ++++--------------------- tests/PhpWord/TemplateProcessorTest.php | 27 ++++++++++++++++ 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 498cf701..9e12028d 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -1108,7 +1108,7 @@ class TemplateProcessor protected function findContainingXmlBlockForMacro($macro, $blockType = 'w:p') { $macroPos = $this->findMacro($macro); - if (false === $macroPos) { + if (0 > $macroPos) { return false; } $start = $this->findXmlBlockStart($macroPos, $blockType); @@ -1116,39 +1116,8 @@ class TemplateProcessor return false; } $end = $this->findXmlBlockEnd($start, $blockType); - if (0 > $end) { - return false; - } - - return array('start' => $start, 'end' => $end); - } - - /** - * Find start and end of XML block containing the given block macro - * e.g. ...${macro}...${/macro}... - * - * Note that only the first instance of the macro will be found - * - * @param string $macro Name of macro - * @param string $blockType XML tag for block - * @return bool|int[] FALSE if not found, otherwise array with start and end - */ - protected function findContainingXmlBlockForBlockMacro($macro, $blockType = 'w:p') - { - $macroStartPos = $this->findMacro($macro); - if (0 > $macroStartPos) { - return false; - } - $macroEndPos = $this->findMacro('/' . $macro, $macroStartPos); - if (0 > $macroEndPos) { - return false; - } - $start = $this->findXmlBlockStart($macroStartPos, $blockType); - if (0 > $start) { - return false; - } - $end = $this->findXmlBlockEnd($macroEndPos, $blockType); - if (0 > $end) { + //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; } @@ -1183,12 +1152,13 @@ class TemplateProcessor */ protected function findXmlBlockStart($offset, $blockType) { + $reverseOffset = (strlen($this->tempDocumentMainPart) - $offset) * -1; // first try XML tag with attributes - $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . ' ', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + $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 . '>', ((strlen($this->tempDocumentMainPart) - $offset) * -1)); + $blockStart = strrpos($this->tempDocumentMainPart, '<' . $blockType . '>', $reverseOffset); } return ($blockStart === false) ? -1 : $blockStart; diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 4c9f2358..043ad1ff 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -803,4 +803,31 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $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); + } } From d2b0b317e025d6e42bd8389258b3e6ca9d4dfed1 Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Feb 2019 22:57:33 +0100 Subject: [PATCH 133/142] fix scrutinizer warnings --- src/PhpWord/TemplateProcessor.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 9e12028d..0a366617 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -1088,7 +1088,7 @@ class TemplateProcessor protected function replaceXmlBlock($macro, $block, $blockType = 'w:p') { $where = $this->findContainingXmlBlockForMacro($macro, $blockType); - if (false !== $where) { + if (is_array($where)) { $this->tempDocumentMainPart = $this->getSlice(0, $where['start']) . $block . $this->getSlice($where['end']); } @@ -1132,7 +1132,7 @@ class TemplateProcessor * Note that only the first instance of the macro will be found * * @param string $search Macro name - * @param string $offset Offset from which to start searching + * @param int $offset Offset from which to start searching * @return int -1 if macro not found */ protected function findMacro($search, $offset = 0) From 58a2849e38b566ea3bf267d0f8b5bb2729be0d6b Mon Sep 17 00:00:00 2001 From: troosan Date: Mon, 4 Feb 2019 23:59:37 +0100 Subject: [PATCH 134/142] Add reading of the settings part --- src/PhpWord/TemplateProcessor.php | 35 +++++++++++++++++++ tests/PhpWord/TemplateProcessorTest.php | 14 ++++++++ .../_includes/TestableTemplateProcesor.php | 11 +++++- 3 files changed, 59 insertions(+), 1 deletion(-) diff --git a/src/PhpWord/TemplateProcessor.php b/src/PhpWord/TemplateProcessor.php index 0a366617..0f685bc4 100644 --- a/src/PhpWord/TemplateProcessor.php +++ b/src/PhpWord/TemplateProcessor.php @@ -49,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 * @@ -120,6 +127,7 @@ class TemplateProcessor } $this->tempDocumentMainPart = $this->readPartWithRels($this->getMainPartName()); + $this->tempDocumentSettingsPart = $this->readPartWithRels($this->getSettingsPartName()); $this->tempDocumentContentTypes = $this->zipClass->getFromName($this->getDocumentContentTypesName()); } @@ -792,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. * @@ -806,6 +830,7 @@ class TemplateProcessor } $this->savePartWithRels($this->getMainPartName(), $this->tempDocumentMainPart); + $this->savePartWithRels($this->getSettingsPartName(), $this->tempDocumentSettingsPart); foreach ($this->tempDocumentFooters as $index => $xml) { $this->savePartWithRels($this->getFooterName($index), $xml); @@ -943,6 +968,16 @@ class TemplateProcessor 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'; + } + /** * Get the name of the footer file for $index. * diff --git a/tests/PhpWord/TemplateProcessorTest.php b/tests/PhpWord/TemplateProcessorTest.php index 043ad1ff..4caca77a 100644 --- a/tests/PhpWord/TemplateProcessorTest.php +++ b/tests/PhpWord/TemplateProcessorTest.php @@ -830,4 +830,18 @@ final class TemplateProcessorTest extends \PHPUnit\Framework\TestCase $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/_includes/TestableTemplateProcesor.php b/tests/PhpWord/_includes/TestableTemplateProcesor.php index 44c0bb55..80cc748f 100644 --- a/tests/PhpWord/_includes/TestableTemplateProcesor.php +++ b/tests/PhpWord/_includes/TestableTemplateProcesor.php @@ -25,9 +25,10 @@ namespace PhpOffice\PhpWord; */ class TestableTemplateProcesor extends TemplateProcessor { - public function __construct($mainPart = null) + public function __construct($mainPart = null, $settingsPart = null) { $this->tempDocumentMainPart = $mainPart; + $this->tempDocumentSettingsPart = $settingsPart; } public function fixBrokenMacros($documentPart) @@ -74,4 +75,12 @@ class TestableTemplateProcesor extends TemplateProcessor { return $this->tempDocumentMainPart; } + + /** + * @return string + */ + public function getSettingsPart() + { + return $this->tempDocumentSettingsPart; + } } From 235cc1205c199a0cace54f858982ddbae7acdacd Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Feb 2019 21:42:14 +0100 Subject: [PATCH 135/142] implement support for section vAlign --- CHANGELOG.md | 1 + docs/styles.rst | 2 ++ samples/Sample_03_Sections.php | 8 +++++ src/PhpWord/Reader/Word2007/Document.php | 1 + src/PhpWord/SimpleType/VerticalJc.php | 36 +++++++++++++++++++ src/PhpWord/Style/Cell.php | 15 ++++++-- src/PhpWord/Style/Section.php | 34 ++++++++++++++++++ src/PhpWord/Writer/Word2007/Style/Section.php | 4 +++ tests/PhpWord/Reader/Word2007/StyleTest.php | 13 +++++++ tests/PhpWord/Style/CellTest.php | 4 ++- tests/PhpWord/Style/SectionTest.php | 16 +++++++++ 11 files changed, 131 insertions(+), 3 deletions(-) create mode 100644 src/PhpWord/SimpleType/VerticalJc.php diff --git a/CHANGELOG.md b/CHANGELOG.md index c7030a5b..e5ce3c15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ v0.17.0 (?? ??? 2019) ---------------------- ### Added - Add RightToLeft table presentation. @troosan #1550 +- Add support for page vertical alignment. @troosan #672 #1569 ### Fixed diff --git a/docs/styles.rst b/docs/styles.rst index 2be6eb94..27f8ee66 100644 --- a/docs/styles.rst +++ b/docs/styles.rst @@ -32,6 +32,8 @@ Available Section style options: 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: diff --git a/samples/Sample_03_Sections.php b/samples/Sample_03_Sections.php index a7b5b13d..5bb9ecc2 100644 --- a/samples/Sample_03_Sections.php +++ b/samples/Sample_03_Sections.php @@ -1,4 +1,6 @@ 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/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/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/Section.php b/src/PhpWord/Style/Section.php index 162e08e0..3989a31e 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 \PhpOffice\PhpWord\SimpleType\VerticalJc + */ + 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/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/tests/PhpWord/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index ad48dcba..91e96c4a 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -22,6 +22,7 @@ use PhpOffice\PhpWord\SimpleType\TblWidth; use PhpOffice\PhpWord\Style; use PhpOffice\PhpWord\Style\Table; use PhpOffice\PhpWord\Style\TablePosition; +use PhpOffice\PhpWord\SimpleType\VerticalJc; /** * Test class for PhpOffice\PhpWord\Reader\Word2007\Styles @@ -213,4 +214,16 @@ class StyleTest extends AbstractTestReader $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/Style/CellTest.php b/tests/PhpWord/Style/CellTest.php index db789fdc..3c31a457 100644 --- a/tests/PhpWord/Style/CellTest.php +++ b/tests/PhpWord/Style/CellTest.php @@ -17,6 +17,8 @@ namespace PhpOffice\PhpWord\Style; +use PhpOffice\PhpWord\SimpleType\VerticalJc; + /** * Test class for PhpOffice\PhpWord\Style\Cell * @@ -33,7 +35,7 @@ class CellTest extends \PHPUnit\Framework\TestCase $object = new Cell(); $attributes = array( - 'valign' => Cell::VALIGN_TOP, + 'valign' => VerticalJc::TOP, 'textDirection' => Cell::TEXT_DIR_BTLR, 'bgColor' => 'FFFF00', 'borderTopSize' => 120, 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()); + } } From e3020c0db3cafc74371d088ee26e91bae5ce3c45 Mon Sep 17 00:00:00 2001 From: troosan Date: Tue, 5 Feb 2019 23:05:18 +0100 Subject: [PATCH 136/142] fix warnings --- src/PhpWord/Style/Section.php | 2 +- tests/PhpWord/Reader/Word2007/StyleTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/PhpWord/Style/Section.php b/src/PhpWord/Style/Section.php index 3989a31e..ff9b0be0 100644 --- a/src/PhpWord/Style/Section.php +++ b/src/PhpWord/Style/Section.php @@ -613,7 +613,7 @@ class Section extends Border /** * Get vertical alignment * - * @return \PhpOffice\PhpWord\SimpleType\VerticalJc + * @return string */ public function getVAlign() { diff --git a/tests/PhpWord/Reader/Word2007/StyleTest.php b/tests/PhpWord/Reader/Word2007/StyleTest.php index 91e96c4a..a7308f1a 100644 --- a/tests/PhpWord/Reader/Word2007/StyleTest.php +++ b/tests/PhpWord/Reader/Word2007/StyleTest.php @@ -19,10 +19,10 @@ 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; -use PhpOffice\PhpWord\SimpleType\VerticalJc; /** * Test class for PhpOffice\PhpWord\Reader\Word2007\Styles From 5206c7f6905e965b76ceb18d72b51e525223a51e Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 6 Feb 2019 18:19:01 +0100 Subject: [PATCH 137/142] fix parsing of border-color and add test --- CHANGELOG.md | 2 ++ samples/Sample_26_Html.php | 2 +- src/PhpWord/Shared/Html.php | 16 +++++++++++++++- tests/PhpWord/Shared/HtmlTest.php | 7 ++++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5ce3c15..9ec1deef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,9 +7,11 @@ 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 # diff --git a/samples/Sample_26_Html.php b/samples/Sample_26_Html.php index 82a5cf6e..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
'; diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 7f4bf825..66ddc9f5 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -581,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); @@ -738,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]] = $colors[$i]; + } + } + } + /** * Transforms a HTML/CSS alignment into a \PhpOffice\PhpWord\SimpleType\Jc * diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 2f9a4be4..43472324 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -298,7 +298,7 @@ class HtmlTest extends AbstractWebServerEmbeddedTest header a header b - header c + header c @@ -313,6 +313,11 @@ class HtmlTest extends AbstractWebServerEmbeddedTest $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('#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')); } /** From 3219950d5988fbc50c628a3930fff4bca294ad06 Mon Sep 17 00:00:00 2001 From: troosan Date: Wed, 6 Feb 2019 22:07:42 +0100 Subject: [PATCH 138/142] trim color codes and add tests --- src/PhpWord/Shared/Html.php | 4 +-- tests/PhpWord/Shared/HtmlTest.php | 47 +++++++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/src/PhpWord/Shared/Html.php b/src/PhpWord/Shared/Html.php index 66ddc9f5..89881822 100644 --- a/src/PhpWord/Shared/Html.php +++ b/src/PhpWord/Shared/Html.php @@ -516,7 +516,7 @@ class Html $styles['alignment'] = self::mapAlign($cValue); break; case 'display': - $styles['hidden'] = $cValue === 'none'; + $styles['hidden'] = $cValue === 'none' || $cValue === 'hidden'; break; case 'direction': $styles['rtl'] = $cValue === 'rtl'; @@ -747,7 +747,7 @@ class Html $colors = explode(' ', $cssBorderColor); $borders = array('borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor'); for ($i = 0; $i < min(4, $numColors, count($colors)); $i++) { - $styles[$borders[$i]] = $colors[$i]; + $styles[$borders[$i]] = trim($colors[$i], '#'); } } } diff --git a/tests/PhpWord/Shared/HtmlTest.php b/tests/PhpWord/Shared/HtmlTest.php index 43472324..5bc9e241 100644 --- a/tests/PhpWord/Shared/HtmlTest.php +++ b/tests/PhpWord/Shared/HtmlTest.php @@ -297,7 +297,7 @@ class HtmlTest extends AbstractWebServerEmbeddedTest header a - header b + header b header c @@ -313,11 +313,17 @@ class HtmlTest extends AbstractWebServerEmbeddedTest $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('#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')); + $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')); } /** @@ -595,4 +601,35 @@ class HtmlTest extends AbstractWebServerEmbeddedTest $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')); + } } From b3982ebb70d694080525adc860891094dddf14ac Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 22 Feb 2019 22:06:30 +0100 Subject: [PATCH 139/142] fix documentation --- src/PhpWord/Element/AbstractContainer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/PhpWord/Element/AbstractContainer.php b/src/PhpWord/Element/AbstractContainer.php index 204d4a73..5e058667 100644 --- a/src/PhpWord/Element/AbstractContainer.php +++ b/src/PhpWord/Element/AbstractContainer.php @@ -31,7 +31,7 @@ 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) From 9958a4825fbe623ebf0dc16e831dc9fe9d7e8ff9 Mon Sep 17 00:00:00 2001 From: troosan Date: Fri, 22 Feb 2019 22:06:54 +0100 Subject: [PATCH 140/142] allow other streams --- src/PhpWord/Writer/AbstractWriter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 81a1b2acffd55a47d94b372963404c425adc1eee Mon Sep 17 00:00:00 2001 From: Nick Winfield Date: Sat, 23 Feb 2019 23:24:49 +0000 Subject: [PATCH 141/142] TrackChange doesn't handle all return types of \DateTime::createFromFormat(...) (#1584) * Added boolean check before setting the date --- src/PhpWord/Element/TrackChange.php | 4 ++-- tests/PhpWord/Element/TrackChangeTest.php | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) 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/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()); + } } From 81af913a66d7c508ad2337b2763cbd0c05e106de Mon Sep 17 00:00:00 2001 From: troosan Date: Sun, 24 Feb 2019 21:23:59 +0100 Subject: [PATCH 142/142] Update CONTRIBUTING.md --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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!