Skip to content

Writer WPS: basic support of Microsoft Works documents - #2929

Merged
Progi1984 merged 6 commits into
PHPOffice:masterfrom
businessarshgoyal:devin/1788015204-wps-writer
Sep 23, 2026
Merged

Progi1984 merged 6 commits into
PHPOffice:masterfrom
businessarshgoyal:devin/1788015204-wps-writer

Conversation

@businessarshgoyal

Copy link
Copy Markdown
Contributor

Description

Adds a WPS writer producing documents for the word processor of Microsoft Works 7 and 8, ie an OLE compound file whose CONTENTS stream holds the CHNKWKS chunks of the document, as described by the format notes linked in the issue and implemented by libwps (WPS8Parser).

$writer = IOFactory::createWriter($phpWord, 'WPS');
$writer->save(__DIR__ . '/sample.wps');

What is added:

  • Shared\OLEWrite: a small compound file (CFB v3, 512 bytes sectors) writer, counterpart of the existing Shared\OLERead, building the FAT, the mini FAT, the mini stream and the red-black directory from a set of named streams. No new dependency.
  • Writer\WPS and Writer\WPS\Part\Contents, following the structure of the RTF writer: the part writes the stream header, the chunk index and the TEXT and FONT chunks; the text is UTF-16LE, paragraphs are terminated by \r and page breaks by \f.
  • Registration of WPS in IOFactory::createWriter().

First iteration scope, as suggested in the issue: text and basic paragraph output. Text, text runs, titles, links, list items, text breaks, page breaks and the text of the cells of a table are written as paragraphs. Not supported yet (silently ignored, no error): character and paragraph styles, table and list structure, images, objects, headers, footers, footnotes, endnotes, comments, table of contents, document properties, form fields, and reading .wps files.

About #2769

I looked at #2769 first and did not build on it: it writes a ZIP package of ODF-like XML parts (content.xml, styles.xml, META-INF/manifest.xml), which is not the Microsoft Works binary format the issue asks for — such a file is not an OLE document and is not recognised by Works or by libwps-based readers (LibreOffice). This PR writes the CONTENTS/CHNKWKS stream instead, so the two approaches do not share code.

Validation

Besides the unit tests, the generated files were checked against the reference implementation, wps2text (libwps 0.4.12), which extracts the expected text, and against olefile/file(1), which validate the compound file structure.

Fixes #69

/claim #69

Checklist:

  • My CI is 🟢
  • I have covered by unit tests my new code (check build/coverage for coverage report)
  • I have updated the documentation to describe the changes
  • I have updated the changelog

devin-ai-integration Bot and others added 2 commits August 29, 2026 14:53
Add a WPS writer producing an OLE compound file whose CONTENTS stream
holds the CHNKWKS chunks of the document, with the text and the page
breaks of the elements.

Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
@coveralls

coveralls commented Aug 29, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 96.636% (+0.01%) from 96.622% — businessarshgoyal:devin/1788015204-wps-writer into PHPOffice:master

@Progi1984 Progi1984 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than adding file src/PhpWord/Shared/OLEWrite.php, could use PhpOffice/Common ?

@businessarshgoyal

Copy link
Copy Markdown
Contributor Author

Happy to, but PhpOffice/Common only has Common\Microsoft\OLERead on master (1.0.5), there is no compound file writer in it, and PHPWord dropped the phpoffice/common requirement back in 67b1598 (Jan 2021), so pulling it in for this would both add a dependency and still need the writer to be written somewhere.

Two ways I can go, tell me which you prefer:

  1. I open a PR on PhpOffice/Common adding Common\Microsoft\OLEWrite as the counterpart of OLERead, then this PR requires phpoffice/common and drops src/PhpWord/Shared/OLEWrite.php. Cleanest long term since PHPPresentation and PHPWord could share it, but it blocks this PR on a Common release
  2. It stays in PHPWord as Shared\OLEWrite, marked @internal, and moves to Common later without a breaking change for users

Default is 2 unless you say otherwise

Written by Devin

@Progi1984

Copy link
Copy Markdown
Member

Go to 1

@businessarshgoyal

Copy link
Copy Markdown
Contributor Author

Done: PHPOffice/Common#65 adds Common\Microsoft\OLEWrite (round-trip tested against OLERead; phpunit, phpstan level 7 and phpmd pass).

Once that lands and Common tags a release, I'll update this PR to require phpoffice/common and delete src/PhpWord/Shared/OLEWrite.php, pointing Writer\MsDoc at the Common class. I've left the copy in place here for now so this PR keeps building — say the word if you'd rather I switch it to dev-master immediately instead of waiting for the tag.

Written by Devin

Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
@Progi1984

Copy link
Copy Markdown
Member

devin-ai-integration Bot and others added 2 commits September 13, 2026 05:44
…mmon 1.1

Drops the interim Shared\OLEWrite copy now that PHPOffice/Common 1.1.0 ships the writer.

Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
@businessarshgoyal

Copy link
Copy Markdown
Contributor Author

Thanks @Progi1984. Switched in e274d83e7: composer.json now requires phpoffice/common ^1.1, Writer\\WPS uses PhpOffice\\Common\\Microsoft\\OLEWrite, and src/PhpWord/Shared/OLEWrite.php is gone. Also merged current master. tests/PhpWordTests/Writer/WPSTest.php is 7 passed locally; phpstan and php-cs-fixer clean on the touched file.

Written by Devin

@Progi1984

Copy link
Copy Markdown
Member

@businessarshgoyal ⚠️ the CI is 🔴

…loadable via phpoffice/common

Co-Authored-By: business.arshgoyal@gmail.com <business.arshgoyal@gmail.com>
@businessarshgoyal

Copy link
Copy Markdown
Contributor Author

Thanks @Progi1984 — fixed in a84bae6. The static-analysis failure was a side effect of requiring phpoffice/common: it pulls in pclzip/pclzip, so PclZip becomes a known class to phpstan and the previously-baselined "unknown class PclZip" ignores in src/PhpWord/Shared/ZipArchive.php no longer matched. Added the 5 corresponding undefined method entries to phpstan-baseline.neon; phpstan is clean locally.

@Progi1984
Progi1984 merged commit 8811e5d into PHPOffice:master Sep 23, 2026
33 checks passed
@Progi1984

Copy link
Copy Markdown
Member

@businessarshgoyal It's merged for the writer. Thanks. Do you plan a reader too ?

k4it00 added a commit to k4it00/PHPWord that referenced this pull request Sep 23, 2026
- Rebase on current master (WPS Writer PHPOffice#2929 merged)
- Add Reader WPS entry to docs/changes/1.x/1.5.0.md
- Make internal helpers protected and non-static
- Align @SInCE with 1.5.0
Progi1984 pushed a commit that referenced this pull request Sep 24, 2026
* Reader WPS: add Microsoft Works (.wps) text reader

Implements PhpOffice\PhpWord\Reader\WPS per issue #69:

- Reuses Shared\OLERead to parse the OLE compound container and extract the
  'CONTENTS' stream (handles both regular and mini streams).
- Walks the WPS chunk list: CHNKWKS magic, 0x01F8 chunk tags, TEXT chunk
  lookup with linked-block traversal.
- Decodes UTF-16LE text, normalizes \r line breaks, and maps paragraphs to
  Text elements in a single section.
- canRead() validates the OLE signature + CHNKWKS magic (gracefully false
  for non-OLE/non-WPS files).
- Ships a generated CFB/WPS fixture (tests/_files/documents/reader.wps) with
  3 known paragraphs, plus unit tests: canRead true/false, full load with
  exact text assertions.

Verification: phpunit WPSTest 3/3 green; phpstan analyse clean; php-cs-fixer
0 fixable violations across the repo.

* fix: handle false returns from preg_split() and unpack() flagged by PHPStan

The CI static analysis config (full phpstan.neon.dist) is stricter than a
single-file level-6 run: preg_split() and unpack() both return array|false.
Throw a dedicated Exception on the impossible-by-construction failure paths
so the types are honest and the analysis passes on PHP 7.1-8.5.

* Reader WPS: address review feedback

- Rebase on current master (WPS Writer #2929 merged)
- Add Reader WPS entry to docs/changes/1.x/1.5.0.md
- Make internal helpers protected and non-static
- Align @SInCE with 1.5.0

* Reader WPS: add types to methods per review feedback

Apply the return type and native type hints requested in review on the
protected helpers (no inheritance constraints) and add return types on
canRead()/load().

Note on canRead()/load(): the parameter types from the review
suggestions (string $filename / string $docFile) cannot be applied
right now because AbstractReader and ReaderInterface declare canRead()
and load() without parameter types, and PHP requires overriding
parameter types to be contravariant (a typed narrower parameter in a
child is a fatal incompatibility). Full param typing needs the whole
Reader hierarchy typed in one sweep; happy to do that as a follow-up
maintained separately if preferred.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.

Implement WPS Writer & Reader

3 participants