Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Modify existing document (Template Processor vs PHPWord) #902

Open
jojomartius opened this issue Sep 28, 2016 · 21 comments
Open

Modify existing document (Template Processor vs PHPWord) #902

jojomartius opened this issue Sep 28, 2016 · 21 comments

Comments

@jojomartius
Copy link

jojomartius commented Sep 28, 2016

Aloha,

just a simple one (i guess). I have an existing document, contains a table, an image, and some style. I can open it like this

$phpWord = new \PhpOffice\PhpWord\PhpWord();
$testWord = $phpWord->loadTemplate('sample.docx');
$testWord->saveAs('result.docx');

Than the document looks exactly the same, but i can't go through and add stuff like sections etc.

Or i do the same with the template processor like this

$phpWord2 = \PhpOffice\PhpWord\IOFactory::load('sample2.docx');
$phpWord2->save('result2.docx');

Here I am able to go through the whole document with ->getSections(), ->getElements and stuff and add or replace something, but the Look and feel is completely lost.

Pretty sure, that i just missed something quite basic. I just wanted to open an existing documents, replace / add some text (no variables) and save it again. Shouldn't that be possible?

Looking forward to get a better understanding for what i'm doing here.


Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

@christopher-francisco
Copy link

Did you manage to find a solution for this?

@jojomartius
Copy link
Author

unfortunately not... do you have the same issue?

@christopher-francisco
Copy link

Well, I'm trying to find a way to create a Word document with some variables that I can modify from PHP.

However, the template processor doesn't fit what I need because I need the values to show when an user open the document. For example:

  1. The document says the total price is $200.00
  2. The customer changes a certain section in the document using Word
  3. We re-evaluate and decide the prices is no longer $200.00, but $190.00, that value is changed from PHP.

So I was trying to find a way to work with variables with Word (and I found DocProperty, but that's more of a workaround), but I don't think PhpWord supports DocProperty handling.

So now I'm looking for possible solutions.

PS: The variables I need might include a complete table, so it's just not a single value.

@jojomartius
Copy link
Author

jojomartius commented Dec 16, 2016 via email

@christopher-francisco
Copy link

sure, I will

@christopher-francisco
Copy link

@jojomartius well, so far no luck, the document loses the formatting :/

@MarcoSantana
Copy link

Ping?

@SalvatorePollaci
Copy link

up

@cbloss
Copy link

cbloss commented Oct 17, 2017

Ping!

@FBnil
Copy link

FBnil commented Oct 18, 2017

@chris-fa A way I would do it is using excel with locked cells except where data can be changed, then just read the fields. In LibreOffice, there is a form template, which can be used in design mode to fill in text fields, which you can then read back with phpword. Other than that, there is free format searching , for example:
#1152

Or combine what you have been doing. Make a template where the value is still ${value}, then read the updated document from the customer, find the place where the new value is, and create a new document from the template.

But your application actually could benefit from a paid library.

@Cholowao
Copy link

Cholowao commented May 6, 2018

Anyone

@g4rf
Copy link

g4rf commented Dec 11, 2019

For those that ends up here too, I post my solution for a my problem. It differs a bit, but it's closely related.

My challenge was to load a template, replace some variables and then add HTML at the end. PhpWord offers the HTML class for this. And here it begins: The HTML class can add elements only to a Section. This given, I tried lot around to manipulate a loaded docx but every time it killed the formatting (changed tables to normal text, added "X" to fields etc.).

At the end I came around with using the setComplexBlock of the TemplateProcessor and iterating through a temporary document. Therefor I used a block variable and cloned it.

${htmlblock}
${html}
${/htmlblock}

After doing variables magic with the TemplateProcessor these are the steps:

  1. create a temporary PhpWord and add a section
  2. add the HTML to the section
  3. get the elements of the section
  4. clone the block as many times as elements are in the section
  5. put every single element in its cloned variable

Here is a sample code for doing the HTML insertion in a loaded template

use \PhpOffice\PhpWord\TemplateProcessor,
    \PhpOffice\PhpWord\Shared\Html,
    \PhpOffice\PhpWord\PhpWord;

$doc = new TemplateProcessor($pathToTemplateFile);

// do some variable magic
$doc->setValue('key', $value);

// create temporary section
$section = (new PhpWord())->addSection();

// add html
Html::addHtml($section, $html, false, false);

// get elements in section
$containers = $section->getElements();

// clone the html block in the template
$doc->cloneBlock('htmlblock', count($containers), true, true);

// replace the variables with the elements
for($i = 0; $i < count($containers); $i++) {

    // be aware of using setComplexBlock
    // and the $i+1 as the cloned elements start with #1
    $doc->setComplexBlock('html#' . ($i+1), $containers[$i]);
}

// save final document
$doc->saveAs($pathToOutputFile);

As a caveat lists an tables won't work very well. If I find a solution I'll post it here.

@damienfa
Copy link

Hello,

We've arrived to the same conclusion. It works on my side. But the titles or links included in the template are just displayed like simple text. Do you see the same ?

I don't understand why setComplexBlock do not display Titles correctly ... 🤔
Any idea ?

@g4rf
Copy link

g4rf commented Jan 12, 2020

I don't understand why setComplexBlock do not display Titles correctly ...
Any idea ?

Yes, a very clear idea:

Remember: The HTML-Converter works on a document, not on a template. And for a document it adds the formatting of titles, tables and lists to the document as a stylesheet (in German its "Formatvorlage") in a special section. My above code won't add this formattings to the template, as the TemplateProcessor has no method for this.

A workaround in theory:
Add the stylesheet to your docx template in Word/Writer and name it, like the HTML-Converter do it (look in \PhpOffice\PhpWord\Shared\Html). I think for the titles it's heading1 and so on.

@g4rf
Copy link

g4rf commented Jan 12, 2020

As a caveat lists an tables won't work very well. If I find a solution I'll post it here.

I'll ended up rewriting the TemplateProcessor.php and the Html.php a bit to make lists partly work, see TemplateProcessor and Html.zip.

Lists:
I'll added a bullet list and a numbered list to my docx template below the htmlblock. Then I hided it with a white rectangle. As a next step I'll opened the docx template with a zip program, opened the file word/document.xml in it and search for the lists. They have an attribute <w:numId w:val="2"> which describes if the list is bulleted or numbered.
If you look in the document.xml in our final generated docx document, you'll see, that this values are empty and therefor the lists won't work.
And here we do our magic: After generating our docx we replace the empty values with the values for the bullets or the numbers.

// this is the block setting
for($i = 0; $i < count($containers); $i++) {
    $doc->setComplexBlock('html#' . ($i+1), $containers[$i]);
}

//-- here is our new function
$doc->repairListItems("2", "3");

// save final document
$doc->saveAs($pathToOutputFile);

I added the function repairListItems to the TemplateProcessor:

public function repairListItems($numIdBullets, $numIdNumbers, $bullets = false) {
    // TODO: insert a possibiltiy to divide ordered and unordered lists
    // for the moment we use either numbers or bullets for all list
    $replace = $numIdNumbers;
    if($bullets) $replace = $numIdBullets;
        
    $this->tempDocumentMainPart = str_replace('<w:numId w:val=""', '<w:numId w:val="' . $replace . '"', $this->tempDocumentMainPart);
}

The caveeat here as you see in my inline comment: We can give the whole document either bullet lists or numbered lists. Maybe someone has an idea, to find a workaround by rewriting the list-conversion in the Html.php and mark bullet lists and numbered lists in a different way.

In addition I commented out some lines in the Html::parseList() function, maybe it's relevant.

protected static function parseList($node, $element, &$styles, &$data)
{
    $isOrderedList = $node->nodeName === 'ol';
    if (isset($data['listdepth'])) {
        $data['listdepth']++;
    } else {
        $data['listdepth'] = 0;
        $styles['list'] = $isOrderedList ? 'ordered' : 'unordered'; //'listStyle_' . self::$listIndex++;
        //$element->getPhpWord()->addNumberingStyle($styles['list'], self::getListStyle($isOrderedList));
    }
    if ($node->parentNode->nodeName === 'li') {
        return $element->getParent();
    }
}

@g4rf
Copy link

g4rf commented Jan 12, 2020

As a caveat lists an tables won't work very well. If I find a solution I'll post it here.

And now my magic to make tables partly work:

I'll ended up rewriting the Html.php a bit, see TemplateProcessor and Html.zip.

I added default values to the converted tables, that it fit my needs, at least with LibreOffice. I altered the Html::parseTable() this way:

protected static function parseTable($node, $element, &$styles)
{
    // add standard styles
    $default = [
        'unit' => 'auto',
        'width' => '1',
        'borderColor' => '000000',
        'borderSize' => '1',
        'cellSpacing' => '57'
    ];
    foreach($default as $key => $value) {
        if(empty($styles['table'][$key])) $styles['table'][$key] = $value;
    }

    // this code remains:
    $elementStyles = self::parseInlineStyle($node, $styles['table']);
    $newElement = $element->addTable($elementStyles);
    return $newElement;
}

The same I did with the Html::parseCell() and added this code to the top:

// add standard styles
$default = [
    'valign' => 'top'
];
foreach($default as $key => $value) {
    if(empty($styles['cell'][$key])) $styles['cell'][$key] = $value;
}

The resulting tables are not the best, but worked for me.

@damienfa
Copy link

Ho, very good job ! 👍

But, I'm not sure that it's only about HTML-Converter and "styling".
Look at this issue 1803 for example, it does not use the HTML-Converter, and style "Heading1" appears in the generated XML, but no titles appear in the generated document.

I also notice the same with links (and there is for example the issue 471 about that. More of all, this sample doesn't work with the link item.

@devmaufh
Copy link

For those that ends up here too, I post my solution for a my problem. It differs a bit, but it's closely related.

My challenge was to load a template, replace some variables and then add HTML at the end. PhpWord offers the HTML class for this. And here it begins: The HTML class can add elements only to a Section. This given, I tried lot around to manipulate a loaded docx but every time it killed the formatting (changed tables to normal text, added "X" to fields etc.).

At the end I came around with using the setComplexBlock of the TemplateProcessor and iterating through a temporary document. Therefor I used a block variable and cloned it.

${htmlblock}
${html}
${/htmlblock}

After doing variables magic with the TemplateProcessor these are the steps:

  1. create a temporary PhpWord and add a section
  2. add the HTML to the section
  3. get the elements of the section
  4. clone the block as many times as elements are in the section
  5. put every single element in its cloned variable

Here is a sample code for doing the HTML insertion in a loaded template

use \PhpOffice\PhpWord\TemplateProcessor,
    \PhpOffice\PhpWord\Shared\Html,
    \PhpOffice\PhpWord\PhpWord;

$doc = new TemplateProcessor($pathToTemplateFile);

// do some variable magic
$doc->setValue('key', $value);

// create temporary section
$section = (new PhpWord())->addSection();

// add html
Html::addHtml($section, $html, false, false);

// get elements in section
$containers = $section->getElements();

// clone the html block in the template
$doc->cloneBlock('htmlblock', count($containers), true, true);

// replace the variables with the elements
for($i = 0; $i < count($containers); $i++) {

    // be aware of using setComplexBlock
    // and the $i+1 as the cloned elements start with #1
    $doc->setComplexBlock('html#' . ($i+1), $containers[$i]);
}

// save final document
$doc->saveAs($pathToOutputFile);

As a caveat lists an tables won't work very well. If I find a solution I'll post it here.

This works perfecly for me, thanks! 👍

@strtob
Copy link

strtob commented Feb 17, 2022

g4rf / that works great in MS Word and Libre! unfortunately, unorded list will be shown as ordered lists :-(

@g4rf
Copy link

g4rf commented Feb 17, 2022

@strtob Thanks a lot.

Yes, you are right. As mentioned here and in the inline documentation, there is at the moment no possibility to differentiate between an unordered or an ordered list. You can have all lists ordered or all lists unordered.

The caveeat here as you see in my inline comment: We can give the whole document either bullet lists or numbered lists. Maybe someone has an idea, to find a workaround by rewriting the list-conversion in the Html.php and mark bullet lists and numbered lists in a different way.

@strtob
Copy link

strtob commented Feb 17, 2022

crazy, I've found out if you made a unsorted list (in my case with ckeditor 4) which result in <ul> you get an ordered list - if you use a sorted list (<ol>) you'll get an unorded list in word :-) a quick win could be to swap the output before set in word

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests