Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

## OpenDocument.core
build/
build-wasm/
cmake-build-*/
jni/target/
jni/.flattened-pom.xml
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ The release run heads these entries with the version and opens a fresh

## Unreleased

- New `Document::save(std::ostream &)` and `Document::save_to_memory()`, which
returns the saved document as an in-memory `File`. The path overloads are
unchanged.

- The wasm binding gained `edit(diff)`, `save()`, `isEditable()` and
`isSavable()`, so a browser can save an edit back.

- Memory saves in the other bindings: `save_to_memory()` returns `bytes` in
python, `saveToMemory()` `byte[]` in java, `-saveToMemoryWithError:` `NSData`
in Objective-C.

- Two entries of the same zip document can be read at once; nothing serialises
on a single lock any more.

Expand Down
7 changes: 7 additions & 0 deletions apple/include/OdrCoreObjC/ODRDocument.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ NS_SWIFT_NAME(Document)
password:(NSString *)password
error:(NSError **)error;

/// The saved document as bytes.
- (nullable NSData *)saveToMemoryWithError:(NSError **)error
NS_SWIFT_NAME(saveToMemory());
- (nullable NSData *)saveToMemoryWithPassword:(NSString *)password
error:(NSError **)error
NS_SWIFT_NAME(saveToMemory(password:));

/// The document's parts as a filesystem.
- (nullable ODRFilesystem *)filesystemWithError:(NSError **)error
NS_SWIFT_NAME(filesystem());
Expand Down
21 changes: 21 additions & 0 deletions apple/src/ODRDocument.mm
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <odr/document.hpp>

#include <optional>
#include <sstream>
#include <string>

using odr::apple::guarded;
using odr::apple::guarded_value;
Expand Down Expand Up @@ -63,6 +65,25 @@ - (BOOL)saveTo:(NSString *)path
});
}

- (nullable NSData *)saveToMemoryWithError:(NSError **)error {
return guarded(error, [&]() -> NSData * {
std::ostringstream out;
_handle->save(out);
const std::string bytes = out.str();
return [NSData dataWithBytes:bytes.data() length:bytes.size()];
});
}

- (nullable NSData *)saveToMemoryWithPassword:(NSString *)password
error:(NSError **)error {
return guarded(error, [&]() -> NSData * {
std::ostringstream out;
_handle->save(out, to_string(password));
const std::string bytes = out.str();
return [NSData dataWithBytes:bytes.data() length:bytes.size()];
});
}

- (nullable ODRElement *)rootElementWithError:(NSError **)error {
return guarded(error, [&]() -> ODRElement * {
return [ODRElement elementWithHandle:_handle->root_element() owner:self];
Expand Down
29 changes: 29 additions & 0 deletions apple/tests/OdrCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,35 @@ final class ElementTreeTests: XCTestCase {
}
}

final class DocumentSaveTests: XCTestCase {
private func document() throws -> Document {
try DecodedFile.decode(path: try Fixture.odt())
.asDocumentFile().document()
}

func testSaveToMemoryCarriesAnEdit() throws {
let document = try self.document()
XCTAssertTrue(document.isSavable)

let root = try XCTUnwrap(try document.rootElement())
let text = try XCTUnwrap(root.firstDescendant(ofType: Text.self))
try text.setContent("saved to memory")

let saved = try XCTUnwrap(try document.saveToMemory())
XCTAssertFalse(saved.isEmpty)

let path = URL(fileURLWithPath: try temporaryDirectory())
.appendingPathComponent("from-memory.odt")
try saved.write(to: path)

let reloaded = try DecodedFile.decode(path: path.path)
.asDocumentFile().document()
let reloadedRoot = try XCTUnwrap(try reloaded.rootElement())
XCTAssertTrue(
reloadedRoot.descendants(ofType: Text.self).contains { $0.content == "saved to memory" })
}
}

final class TableAddressTests: XCTestCase {
func testRoundTrips() throws {
XCTAssertEqual(TableAddress.columnNumber(from: "C"), 2)
Expand Down
13 changes: 13 additions & 0 deletions jni/java/app/opendocument/core/Document.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ public void save(String path, String password) {
saveEncryptedNative(handle(), path, password);
}

/** The saved document as bytes. */
public byte[] saveToMemory() {
return saveToMemoryNative(handle());
}

public byte[] saveToMemory(String password) {
return saveToMemoryEncryptedNative(handle(), password);
}

public FileType fileType() {
return FileType.fromNative(fileTypeNative(handle()));
}
Expand Down Expand Up @@ -60,6 +69,10 @@ void edit(String diff) {

private native void saveEncryptedNative(long handle, String path, String password);

private native byte[] saveToMemoryNative(long handle);

private native byte[] saveToMemoryEncryptedNative(long handle, String password);

private native int fileTypeNative(long handle);

private native int documentTypeNative(long handle);
Expand Down
22 changes: 22 additions & 0 deletions jni/src/jni_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include <odr/filesystem.hpp>
#include <odr/html.hpp>

#include <sstream>
#include <vector>

namespace {
Expand All @@ -16,6 +17,7 @@ using odr_jni::from_handle;
using odr_jni::guarded;
using odr_jni::HandleGuard;
using odr_jni::make_handle;
using odr_jni::to_jbytes;
using odr_jni::to_jstring;
using odr_jni::to_string;

Expand Down Expand Up @@ -105,6 +107,26 @@ Java_app_opendocument_core_Document_saveEncryptedNative(JNIEnv *env, jobject,
});
}

extern "C" JNIEXPORT jbyteArray JNICALL
Java_app_opendocument_core_Document_saveToMemoryNative(JNIEnv *env, jobject,
jlong handle) {
return guarded(env, [&] {
std::ostringstream out;
from_handle<odr::Document>(handle)->save(out);
return to_jbytes(env, out.str());
});
}

extern "C" JNIEXPORT jbyteArray JNICALL
Java_app_opendocument_core_Document_saveToMemoryEncryptedNative(
JNIEnv *env, jobject, jlong handle, jstring password) {
return guarded(env, [&] {
std::ostringstream out;
from_handle<odr::Document>(handle)->save(out, to_string(env, password));
return to_jbytes(env, out.str());
});
}

extern "C" JNIEXPORT jint JNICALL
Java_app_opendocument_core_Document_fileTypeNative(JNIEnv *env, jobject,
jlong handle) {
Expand Down
19 changes: 19 additions & 0 deletions jni/tests/app/opendocument/core/DocumentTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -103,4 +104,22 @@ void editAppliesADiff() throws IOException {

assertTrue(walkText(document.rootElement()).contains("edited by the diff"));
}

@Test
void saveToMemoryRoundTripsAnEdit() throws IOException {
Document document = openDocument();

Element paragraph = document.rootElement().firstChild();
DocumentPath text = paragraph.firstChild().documentPath();
Html.edit(document, "{\"modifiedText\":{\"" + text + "\":\"saved to memory\"}}");

byte[] saved = document.saveToMemory();
assertTrue(saved.length > 0);

Path reloadedPath = tempDir.resolve("from-memory.odt");
Files.write(reloadedPath, saved);
Document reloaded = Odr.open(reloadedPath.toString()).asDocumentFile().document();

assertTrue(walkText(reloaded.rootElement()).contains("saved to memory"));
}
}
24 changes: 24 additions & 0 deletions python/src/bind_document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <pybind11/stl.h>

#include <sstream>
#include <string>

namespace py = pybind11;
Expand Down Expand Up @@ -333,6 +334,29 @@ void odr_python::bind_document(py::module_ &m) {
&odr::Document::save, py::const_),
py::arg("path"), py::arg("password"),
py::call_guard<py::gil_scoped_release>())
.def(
"save_to_memory",
[](const odr::Document &document) {
std::ostringstream out;
{
py::gil_scoped_release release;
document.save(out);
}
return py::bytes(out.str());
},
"Save the document and return its bytes.")
.def(
"save_to_memory",
[](const odr::Document &document, const std::string &password) {
std::ostringstream out;
{
py::gil_scoped_release release;
document.save(out, password);
}
return py::bytes(out.str());
},
py::arg("password"),
"Save the document encrypted and return its bytes.")
.def("file_type", &odr::Document::file_type)
.def("document_type", &odr::Document::document_type)
.def("root_element", &odr::Document::root_element, keep_self_alive)
Expand Down
27 changes: 27 additions & 0 deletions python/tests/test_document.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,30 @@ def items(element):

assert [item.marker() for item in items(lists[1])] == ["1.", "2."]
assert [item.number() for item in items(lists[1])] == [1, 2]


def test_save_to_memory_round_trips(odt_path, tmp_path):
document = pyodr.open(str(odt_path)).as_document_file().document()
assert document.is_savable()

saved = document.save_to_memory()
assert isinstance(saved, bytes)
assert saved[:2] == b"PK"

path = tmp_path / "from_memory.odt"
path.write_bytes(saved)
reloaded = pyodr.open(str(path)).as_document_file().document()
assert walk_text(reloaded.root_element()) == walk_text(document.root_element())


def test_save_to_memory_carries_an_edit(odt_path, tmp_path):
document = pyodr.open(str(odt_path)).as_document_file().document()

diff = '{"modifiedText":{"/child:0/child:0":"edited in python"}}'
pyodr.html.edit(document, diff)

path = tmp_path / "edited.odt"
path.write_bytes(document.save_to_memory())
reloaded = pyodr.open(str(path)).as_document_file().document()

assert "edited in python" in walk_text(reloaded.root_element())
36 changes: 33 additions & 3 deletions src/odr/document.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@

#include <odr/internal/abstract/document.hpp>
#include <odr/internal/common/filesystem.hpp>
#include <odr/internal/common/path.hpp>
#include <odr/internal/util/file_util.hpp>

#include <fstream>
#include <memory>
#include <sstream>
#include <utility>

namespace odr {

Expand All @@ -26,13 +29,40 @@ bool Document::is_savable(const bool encrypted) const noexcept {
return m_impl->is_savable(encrypted);
}

// Checked here so an unsavable format leaves no empty file behind.
void Document::save(const std::string &path) const {
m_impl->save(internal::Path(path));
if (!m_impl->is_savable(false)) {
throw UnsupportedOperation();
}
std::ofstream out = internal::util::file::create(path);
m_impl->save(out);
}

void Document::save(const std::string &path,
const std::string &password) const {
m_impl->save(internal::Path(path), password.c_str());
if (!m_impl->is_savable(true)) {
throw UnsupportedOperation();
}
std::ofstream out = internal::util::file::create(path);
m_impl->save(out, password.c_str());
}

void Document::save(std::ostream &out) const { m_impl->save(out); }

void Document::save(std::ostream &out, const std::string &password) const {
m_impl->save(out, password.c_str());
}

File Document::save_to_memory() const {
std::ostringstream out;
m_impl->save(out);
return File::from_memory(std::move(out).str());
}

File Document::save_to_memory(const std::string &password) const {
std::ostringstream out;
m_impl->save(out, password.c_str());
return File::from_memory(std::move(out).str());
}

FileType Document::file_type() const noexcept { return m_impl->file_type(); }
Expand Down
9 changes: 9 additions & 0 deletions src/odr/document.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <iosfwd>
#include <memory>
#include <string>

Expand All @@ -12,6 +13,7 @@ enum class FileType;
enum class DocumentType;
class DocumentFile;
class Element;
class File;
class Filesystem;

/// @brief Represents a document.
Expand All @@ -25,6 +27,13 @@ class Document final {
void save(const std::string &path) const;
void save(const std::string &path, const std::string &password) const;

void save(std::ostream &out) const;
void save(std::ostream &out, const std::string &password) const;

/// @brief The saved document as a file in memory.
[[nodiscard]] File save_to_memory() const;
[[nodiscard]] File save_to_memory(const std::string &password) const;

[[nodiscard]] FileType file_type() const noexcept;
[[nodiscard]] DocumentType document_type() const noexcept;

Expand Down
9 changes: 3 additions & 6 deletions src/odr/internal/abstract/document.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <odr/quantity.hpp>

#include <cstdint>
#include <iosfwd>
#include <memory>
#include <optional>
#include <string>
Expand All @@ -29,10 +30,6 @@ struct ParagraphStyle;
struct GraphicStyle;
} // namespace odr

namespace odr::internal {
class Path;
} // namespace odr::internal

namespace odr::internal::abstract {
class ReadableFilesystem;
class ElementAdapter;
Expand Down Expand Up @@ -71,8 +68,8 @@ class Document {
/// Savable, @p encrypted to ask for an encrypted save.
[[nodiscard]] virtual bool is_savable(bool encrypted) const noexcept = 0;

virtual void save(const Path &path) const = 0;
virtual void save(const Path &path, const char *password) const = 0;
virtual void save(std::ostream &out) const = 0;
virtual void save(std::ostream &out, const char *password) const = 0;

[[nodiscard]] virtual FileType file_type() const noexcept = 0;
[[nodiscard]] virtual DocumentType document_type() const noexcept = 0;
Expand Down
Loading
Loading