Skip to content

Reader WPS: add Microsoft Works (.wps) text reader - #2935

Merged
Progi1984 merged 4 commits into
PHPOffice:masterfrom
k4it00:feat/wps-reader
Sep 24, 2026
Merged

Progi1984 merged 4 commits into
PHPOffice:masterfrom
k4it00:feat/wps-reader

Conversation

@k4it00

@k4it00 k4it00 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Fixes #69
/claim #69

What

Adds a WPS Reader — extracts text from Microsoft Works documents (.wps, Works 8+) into a PhpWord document.

Why this interpretation

The issue title says "Writer", but both reference links in the issue body (the digitally-disturbed blog posts) document reading/extracting text from WPS files, and the format details they cover (CHNKWKS CONTENTS stream, TEXT chunk lookup) are read-side. This PR implements that. The existing open Writer PRs (#2933, #2929, #2769) cover the write side — a Reader complements them rather than duplicating them.

How

  • src/PhpWord/Reader/WPS.php: reuses Shared\OLERead (same plumbing as the MsDoc reader — including mini-stream handling) to extract the OLE CONTENTS stream, then walks the WPS chunk list: CHNKWKS magic → 0x01F8 chunk tags → linked-block traversal → TEXT chunk → UTF-16LE decode → \r normalization → one Text element per paragraph.
  • canRead() validates the OLE signature + CHNKWKS magic and returns false gracefully for anything else.
  • Works via IOFactory::load(, 'WPS') — no factory changes needed.

Verification

  • Fixture: tests/_files/documents/reader.wps — a generated, spec-conformant CFB container (3 known paragraphs). Round-trip verified with an independent parser before being committed.
  • vendor/bin/phpunit --no-coverage --filter WPSTest: 3/3 green (canRead true/false + full load with exact text assertions).
  • vendor/bin/phpstan analyse: clean. php-cs-fixer: 0 violations repo-wide.

Happy to re-scope to the Writer if maintainers prefer that for this bounty — but a Reader seemed the missing piece given the linked references.

@coveralls

coveralls commented Sep 8, 2026 •

Copy link
Copy Markdown

Coverage Status

coverage: 96.514% (-0.1%) from 96.636% — k4it00:feat/wps-reader into PHPOffice:master

@Progi1984

Copy link
Copy Markdown
Member

@k4it00 Can you rebase it on master branch ?

And can you update the changelog too ?

Some feedback :

  • Please use methods of PHPOffice\Common dependency
  • Don't use static methods
  • Change private in protected

Implements PhpOffice\PhpWord\Reader\WPS per issue PHPOffice#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.
…HPStan

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.
- 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
@k4it00

k4it00 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Rebased on current master (@Progi1984), changelog updated (docs/changes/1.x/1.5.0.md), internal helpers are now protected and non-static, and @since is aligned with 1.5.0 to match the WPS Writer.

One clarification: for the PHPOffice/Common note — for stream extraction I use PhpOffice\PhpWord\Shared\OLERead (the same one the MsDoc reader uses). PhpOffice\Common\Microsoft\OLERead (1.1.0) throws \Exception('OLE Block Not defined: ...') on property sets it does not know, and the WPS CONTENTS stream is not among its known names ('CONTENTS' is absent from the switch in readPropertySets()), so it cannot parse a WPS container as-is. Happy to switch if you'd like a CONTENTS case added in phpoffice/common or if you had other Common methods in mind (e.g. Common\Text) — point me at the ones you want and I'll move everything over.

@k4it00

k4it00 commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review @Progi1984 — all items from your feedback are addressed in commit a975c14 (rebased on master, changelog entry for 1.5.0, instance methods instead of static, protected visibility, tests/PHPStan/php-cs-fixer green).

One quick question about scope: the Algora bounty on #69 is still open, and I noticed #2929 (WPS Writer) was merged — is the funded scope also open to the Reader half (a companion writer PR already landed, a reader would complete the picture)? Happy to align with whatever the intended scope is; if the Reader is unfunded I'm still glad to have this merged for the ecosystem.

@Progi1984

Copy link
Copy Markdown
Member

Weird. I removed my account from algora.io. So bounties shouldn't exist anymore.

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return int
*/
protected function getULong($data, $offset)

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.

Suggested change
protected function getULong($data, $offset)
protected function getULong(string $data, int $offset): int

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return int
*/
protected function getUShort($data, $offset)

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.

Suggested change
protected function getUShort($data, $offset)
protected function getUShort(string $data, int $offset): int

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return string
*/
protected function readContentsStream($filename)

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.

Suggested change
protected function readContentsStream($filename)
protected function readContentsStream(string $filename): string

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return string
*/
protected function extractText($filename)

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.

Suggested change
protected function extractText($filename)
protected function extractText(string $filename): string

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return PhpWord
*/
public function load($docFile)

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.

Suggested change
public function load($docFile)
public function load(string $docFile): PhpWord

Comment thread src/PhpWord/Reader/WPS.php Outdated
*
* @return bool
*/
public function canRead($filename)

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.

Suggested change
public function canRead($filename)
public function canRead(string $filename): bool

@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.

Last feedbacks

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.
@k4it00

k4it00 commented Sep 24, 2026

Copy link
Copy Markdown
Contributor Author

All suggestions applied in 26af990 — including return types on canRead()/load() and full native types on the four internal helpers (phpunit/wpstan/php-cs-fixer green).

One deviation to flag: the parameter types on canRead(string $filename) and load(string $docFile) cannot be applied yet — AbstractReader and ReaderInterface declare both methods without parameter types, and PHP requires overriding parameter types to be contravariant, so a narrower typed parameter in the child is a fatal signature incompatibility. I kept the parameters untyped and added the return types (which are covariant and legal). Full parameter typing needs the Reader hierarchy itself typed in one pass — happy to do that as a separate PR if you want it.

@Progi1984
Progi1984 merged commit 599e438 into PHPOffice:master Sep 24, 2026
33 checks passed
@Progi1984

Copy link
Copy Markdown
Member

@k4it00 Thanks for your contribution.

@Progi1984 Progi1984 added this to the 1.5.0 milestone Sep 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Implement WPS Writer & Reader

3 participants