feat(document): save to a stream or into memory, and bind edit/save in wasm - #778
Merged
Conversation
…n wasm `abstract::Document::save` takes a `std::ostream` rather than a path: no engine used the path for anything but opening an `ofstream`, and both engines that implement `save` already ended at `archive.save(ostream)`. The public `save(path)` overloads are unchanged, except that they check `is_savable` before opening the file so an unsavable format leaves none behind. `save_to_memory` returns the saved document as an in-memory `File`. That closes the wasm binding's editing round trip (#777): `edit(diff)`, `save()`, plus `isEditable()`/`isSavable()`, which answer for the open document where `capabilities()` answers for its format. The session now owns the one `Document` render, edit and save share — `DocumentFile::document()` decodes a fresh tree per call. python, jni and apple get the memory save too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GGQAJdFL17EKZZqsysu3jQ
andiwand
force-pushed
the
feat/document-save-stream-memory
branch
from
August 30, 2026 06:59
8d207a7 to
f48045e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🤖 Generated with Claude Code
Closes #777.
The core change
Document::savetook a path, and a browser has none. It now writes to astd::ostream.abstract::Document::savetakes the stream rather than aninternal::Path:no engine used the path for anything but opening an
ofstream. Only twoimplement
saveat all (odf::Document,ooxml::text::Document) and bothalready ended at
archive.save(ostream); the other eleven throwUnsupportedOperationand ignored the argument. So theofstreammoves up onelayer instead of being repeated per engine, and the two savables in the library
are now stated the same way as
Archive::save(std::ostream &).Two decisions worth stating:
save_to_memoryreturnsFile, notstd::string.Fileis thelibrary's own currency for "bytes with no path": the caller can take
memory_data(),pipethem, or hand the result straight back toDecodedFileto reopen and check. It mirrorsFile::from_memoryat theother end of the round trip, and costs one move.
save().save(const std::string &)is already thepath overload, so a
save(password)returningFilewould read as "save toa file named hunter2".
One behaviour change falls out of moving the
ofstreamup: the path overloadswould now create the file before the engine got to refuse the format, so they
check
is_savablefirst. Every engine'sis_savablealready agrees exactlywith whether its
savethrows, so this restores the old "an unsavable formatleaves no file" precisely, and it is the honest check anyway.
wasm — the round trip #777 asks for
edit(diff)applies what the rendered page'sodr.generateDiff()collected;save()answers with the document's bytes. The stream overload means no MEMFSdetour and no second copy of the document in the heap — the
ostringstreamis the buffer, and
to_uint8_arrayis the one copy that has to happen.The part the issue doesn't mention:
Sessionheld aDecodedFile, andhtml::translate(const DecodedFile &)callsdocument_file.document(), whichdecodes a fresh tree on every call. A
savethat opened its ownDocumentwould have written the document nobody edited. So the session now owns the one
Documentthat the render, the edit and the save all go through.decryptdrops it along with the service.
isEditable()andisSavable()are bound too — the issue's complaint thatcapabilities()reportsedit: true, save: truefor a binding that could doneither is now answerable per document rather than per format.
The other bindings
save_to_memory() -> bytessaveToMemory() -> byte[]-saveToMemoryWithError:→NSDataEach follows its own binding's stated convention for a stream-based API —
bytesin python (asArchive.save()already does),byte[]in JNI,NSDatain ObjC.
Test
Everything green, all five suites run locally:
.fodtbuilt from an inline string roundtrips through
save_to_memory(no test data, and it is the branch thatwrites xml rather than a zip),
save(ostream)writes whatsave_to_memoryholds, a package round trips, and an unsavable format leaves no file behind.
editable, take the
data-odr-pathout of the html the way the browser does,edit, re-render, save, reopen the saved bytes and see the edit; plus saving
without a render, a non-document refusing both, and the encrypted save.
memory round trip.
clang-tidyis clean on the changed files.