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

IOFactory::load destory formatting #882

Open
Ryuzakix3 opened this issue Sep 1, 2016 · 14 comments
Open

IOFactory::load destory formatting #882

Ryuzakix3 opened this issue Sep 1, 2016 · 14 comments

Comments

@Ryuzakix3
Copy link

Ryuzakix3 commented Sep 1, 2016

When i try to edit a existing word document with the IOFactory method, all formatting are destroyed after saving. Before i had table with 6 rows and 6 columns, after saving the table had 1 row with 1 big column. And the linebreaks are set random.


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

@armenmargaryan
Copy link

Hello, i have same issue. is there some news about this issue ?

@christopher-francisco
Copy link

Hello, I have the same issue

@tjvdberg
Copy link

tjvdberg commented Aug 3, 2017

Hey guys,

I have the same issue. Formatting is lost when I IOFactory::load a file and directly save it. It also loses the images. I spend quite a few hours troubleshooting but it is leading me nowhere.

I use PHPWord 0.13.0 (also tried dev-master) at PHP 7.0.4. Would be great if someone could point me in the right direction.

@tjvdberg
Copy link

tjvdberg commented Aug 5, 2017

I found some solutions. My problems where: 1.) losing table format, 2.) losing images.

1.) phpoffice\phpword\src\PhpWord\Reader\Word2007\AbstractPart.php line 391.
$borders = $margins + array('insideH', 'insideV');
I changed it to array_merge because it only kept $margins:
$borders = array_merge($borders, $margins);

That fixed losing the table borders inside a table.

I also could not copy the background color of a table row.
phpoffice\phpword\src\PhpWord\Reader\Word2007\AbstractPart.php line 429.
'bgColor' => array(self::READ_VALUE, 'w:shd/w:fill'),

phpoffice\phpword\src\PhpWord\Reader\Word2007\AbstractPart.php line 449 expects third parameter in the array to be the attribute.
'bgColor' => array(self::READ_VALUE, 'w:shd', 'w:fill'), did the trick.

2.) Images use DrawingML ([http:https://officeopenxml.com/drwOverview.php]). That is not yet part of the phpword library. Images are now wrapped in <w:drawing>. Before it was in <w:pict>.

I fixed this by adding the following code to phpoffice\phpword\src\PhpWord\Reader\Word2007\AbstractPart.php line 224.

 } elseif ( $xmlReader->elementExists( 'w:drawing', $domNode ) ) {
				// find if it is inline or anchored
				if ( $xmlReader->elementExists( 'w:drawing/wp:inline', $domNode ) ) {
					$inlineOrAnchor = 'wp:inline';
				} else {
					$inlineOrAnchor = 'wp:anchor';
				}

				$rId = $xmlReader->getAttribute( 'r:embed', $domNode, 'w:drawing/' . $inlineOrAnchor . '/a:graphic/a:graphicData/pic:pic/pic:blipFill/a:blip' );

				$target = $this->getMediaTarget( $docPart, $rId );
				if ( ! is_null( $target ) ) {
					$imageSource = "zip:https://{$this->docFile}#{$target}";
					$parent->addImage( $imageSource );
				}

You need to register two new XML namespaces to the xml reader. Change the following code in phpoffice\common\src\Common\XMLReader.php 92:

        if ($this->xpath === null) {
            $this->xpath = new \DOMXpath($this->dom);
        }

to

if ($this->xpath === null) {
            $this->xpath = new \DOMXpath($this->dom);
		    $this->xpath->registerNamespace('a', 'http:https://schemas.openxmlformats.org/drawingml/2006/main');
		    $this->xpath->registerNamespace('pic', 'http:https://schemas.openxmlformats.org/drawingml/2006/picture');
        }

This will keep the image in your document although the styling will be lost for a part as the XMLWriter will put the image in a <w:pict> again and the reader did not extract the style yet. That is work in progress.

@cbloss
Copy link

cbloss commented Oct 17, 2017

Hey! I'm still having the same problem with 0.13.0 and PHP 7.1.7. I don't want to override a bunch of vendor folders because they get over written each time.

@troosan
Copy link
Contributor

troosan commented Jan 3, 2018

@tjvdberg I modified the XMLReader to be able to register additional namespaces, as soon as this is released there will be no need to override the classes from Common
Here is the PR PHPOffice/Common#17.
Ideally I'd like to correctly implement the DrawingML specs (http:https://www.datypic.com/sc/ooxml/e-w_drawing-1.html) but will be a bit more word :-)

@troosan
Copy link
Contributor

troosan commented Jan 3, 2018

@tjvdberg thanks for the fix on the table formatting. I have included your fix in the 0.14.0 release.

@jigneshpotenza
Copy link

@tjvdberg, thanks for sharing your code. Did you find any solution regarding image styling/formatting issue after update ( #882 (comment) )?

If yes, then please share your code OR changes.

Thanks in advance!

@nogenem
Copy link

nogenem commented Jun 27, 2018

I'm still having problems with images in the header being lost and cell border being wrong (in the original document the internal borders aren't showing, but after loading the file and saving again they are being shown)... I'm using v0.14.0...

@rahal
Copy link

rahal commented Feb 8, 2019

I'm too having a problem with this issue, I set up a docx document with a header and a footer, and some basics stylings for paragraph and headings ( as well a numbering rules or headings ) , the main idea was to open the document, and add some simple html in the first section so I can get a clean docx file ( with header, footer, and pagination and the correct styles ) .
Sadly, I lost the header, footer, font setup for default styles, obviously IOFactory::load doesn't load all those aspects and we won't be able to save it back correctly as expected ( and I don't know if it can save it if it was correctly loaded ) . I would have been very nice to be able to keep those aspects so we can be able to create easily ready to use docx documents.

Any idea how we could fix this? Thank you

@creatorness
Copy link

I can not use TemplateProcessor, because i want add Section to the Dokument.
Why is this library not working like PHPExcel, because it is too new ?

@rameshsomepallidrg
Copy link

rameshsomepallidrg commented Jun 18, 2019

Hi There,

I have implemented a custom functionality to download my content into ".docx ." All downloaded data displaying correctly expect Table data it is skipping some columns.

and also I want display "comment" in word.docx which we add the comments to text by using CKEditor.

Any help please ?
Please find the attached screen-shot for error reference.

Screenshot 2019-06-13 at 2 35 31 PM

Actually it Should come like below image

Screenshot 2019-06-19 at 4 15 43 PM

Thanks & Regards,
Ramesh S,
9866399969

@irisda
Copy link

irisda commented Apr 22, 2021

@rameshsomepallidrg can you share your solution ?

@davisriska
Copy link

I found myself having similar issues when trying to do a combination TemplateProcessor and IOFactory::load.

To get around the issue of losing formatting and images/relationships I made an extended template processor by copying various parts of PhPWord.


use PhpOffice\PhpWord\Element\AbstractContainer;
use PhpOffice\PhpWord\Element\Image;
use PhpOffice\PhpWord\Element\Link;
use PhpOffice\PhpWord\Exception\Exception;
use PhpOffice\PhpWord\Media;
use PhpOffice\PhpWord\PhpWord;
use PhpOffice\PhpWord\Settings;
use PhpOffice\PhpWord\Shared\XMLWriter;
use PhpOffice\PhpWord\Shared\ZipArchive;
use PhpOffice\PhpWord\TemplateProcessor;

class ExtendedTemplateProcessor extends TemplateProcessor {

    /**
     * @param PhpWord $phpWord
     *
     * @throws Exception
     */
    public function setupRelationships(PhpWord $phpWord): void {
        $originalRid = $this->getNextRelationsIndex($this->getMainPartName());
        $rid = $originalRid;

        $xmlWriter = new XMLWriter();

        $sections = $phpWord->getSections();
        foreach ($sections as $section) {
            $this->fixRelId($section->getElements(), $rid);
        }

        $sectionMedia = Media::getElements('section');
        if (!empty($sectionMedia)) {
            $this->addFilesToPackage($this->zip(), $sectionMedia, $originalRid);

            foreach ($sectionMedia as $element) {
                $this->writeMediaRel($xmlWriter, $element['rID'] - 1 + $originalRid, $element);
            }
        }

        $mediaXml = $xmlWriter->getData();

        // Add link as relationship in XML relationships.
        $mainPartName = $this->getMainPartName();
        $this->tempDocumentRelations[$mainPartName] = str_replace(
                                                          '</Relationships>',
                                                          $mediaXml,
                                                          $this->tempDocumentRelations[$mainPartName]
                                                      ) . '</Relationships>';
    }

    protected function fixRelId(array $elements, &$id): void {
        foreach ($elements as $element) {
            if (
                $element instanceof Link
                || $element instanceof Image
            ) {
                $element->setRelationId($id++ - 6);
            }

            if ($element instanceof AbstractContainer) {
                $this->fixRelId($element->getElements(), $id);
            }
        }
    }

    /**
     * Write media relationships.
     *
     * @param int   $relId
     * @param array $mediaRel
     *
     * @throws Exception
     */
    protected function writeMediaRel(XMLWriter $xmlWriter, $relId, $mediaRel): void {
        $typePrefix = 'officeDocument/2006/relationships/';
        $typeMapping = ['image' => 'image', 'object' => 'oleObject', 'link' => 'hyperlink'];
        $targetMapping = ['image' => 'media/', 'object' => 'embeddings/'];

        $mediaType = $mediaRel['type'];
        $type = $typeMapping[$mediaType] ?? $mediaType;
        $targetPrefix = $targetMapping[$mediaType] ?? '';
        $target = $mediaRel['target'];
        $targetMode = ($type == 'hyperlink') ? 'External' : '';

        $this->writeRel($xmlWriter, $relId, $typePrefix . $type, $targetPrefix . $target, $targetMode);
    }

    /**
     * Write individual rels entry.
     *
     * Format:
     * <Relationship Id="rId..." Type="http:https://..." Target="....xml" TargetMode="..." />
     *
     * @param int    $relId      Relationship ID
     * @param string $type       Relationship type
     * @param string $target     Relationship target
     * @param string $targetMode Relationship target mode
     *
     * @throws Exception
     */
    protected function writeRel(XMLWriter $xmlWriter, $relId, $type, $target, $targetMode = ''): void {
        if ($type != '' && $target != '') {
            if (strpos($relId, 'rId') === false) {
                $relId = 'rId' . $relId;
            }
            $xmlWriter->startElement('Relationship');
            $xmlWriter->writeAttribute('Id', $relId);
            $xmlWriter->writeAttribute('Type', 'http:https://schemas.openxmlformats.org/' . $type);
            $xmlWriter->writeAttribute('Target', $target);
            if ($targetMode != '') {
                $xmlWriter->writeAttribute('TargetMode', $targetMode);
            }
            $xmlWriter->endElement();
        } else {
            throw new Exception('Invalid parameters passed.');
        }
    }

    /**
     * Add files to package.
     *
     * @param mixed $elements
     *
     * @throws Exception
     */
    protected function addFilesToPackage(ZipArchive $zip, array $elements, $originalRid): void {
        $types = [];

        foreach ($elements as $element) {
            $type = $element['type']; // image|object|link

            if (!in_array($type, ['image', 'object'])) {
                continue;
            }

            $target = 'word/media/' . $element['target'];

            // Retrive GD image content or get local media
            if (isset($element['isMemImage']) && $element['isMemImage']) {
                $imageContents = $element['imageString'];
                $zip->addFromString($target, $imageContents);
            } else {
                $this->addFileToPackage($zip, $element['source'], $target);
            }

            if ($type === 'image') {
                $types[$element['imageExtension']] = $element['imageType'];
            }
        }

        $types = array_map(function ($value, $key) {
            return str_replace(['{ext}', '{type}'], [$key, $value], '<Default Extension="{ext}" ContentType="{type}"/>');
        }, $types, array_keys($types));

        $this->tempDocumentContentTypes = str_replace('</Types>', join("\n", $types), $this->tempDocumentContentTypes) . '</Types>';
    }

    /**
     * Add file to package.
     *
     * Get the actual source from an archive image.
     *
     * @param ZipArchive $zipPackage
     * @param string     $source
     * @param string     $target
     *
     * @throws Exception
     */
    protected function addFileToPackage(ZipArchive $zipPackage, string $source, string $target): void {
        $isArchive = strpos($source, 'zip:https://') !== false;
        $actualSource = null;

        if ($isArchive) {
            $source = substr($source, 6);
            [$zipFilename, $imageFilename] = explode('#', $source);

            $zip = new ZipArchive();

            if ($zip->open($zipFilename) !== false) {
                if ($zip->locateName($imageFilename)) {
                    $zip->extractTo(Settings::getTempDir(), $imageFilename);
                    $actualSource = Settings::getTempDir() . DIRECTORY_SEPARATOR . $imageFilename;
                }
            }

            $zip->close();
        } else {
            $actualSource = $source;
        }

        if (null !== $actualSource) {
            $zipPackage->addFile($actualSource, $target);
        }
    }

}

And this is how I use the extended template processor.

$templateProcessor = new ExtendedTemplateProcessor(resource_path('prints/form.docx'));

        $templateProcessor->setValue('date-now', $this->date(Carbon::now()));
        $templateProcessor->setValue('title', $title);

        $phpWord = IOFactory::load(resource_path('prints/form.docx'));
        $section = $phpWord->addSection();

        if ($prependTitle) {
            $section->addTitle($title, 0);
            $section->addTextBreak();
        }

        $this->formGroup($section, $config->children, $model, $model->toArray());

        $templateProcessor->setupRelationships($phpWord);
        $xmlWriter = new XMLWriter();
        (new Container($xmlWriter, $section, false))->write();
        $templateProcessor->replaceXmlBlock('content', $xmlWriter->getData());

        return $templateProcessor->save();

The important part is $templateProcessor->setupRelationships($phpWord); were I fix relIds and add media to the document.

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