diff --git a/.github/workflows/jni.yml b/.github/workflows/jni.yml new file mode 100644 index 00000000..d0ba5c69 --- /dev/null +++ b/.github/workflows/jni.yml @@ -0,0 +1,102 @@ +name: jni + +on: + push: + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +env: + CCACHE_DIR: ${{ github.workspace }}/.ccache + CCACHE_MAXSIZE: 1G + CCACHE_KEY_SUFFIX: r1 + CONAN_HOME: ${{ github.workspace }}/.conan2 + CONAN_KEY_SUFFIX: r1 + +jobs: + build-test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - { os: ubuntu-24.04, build_profile: ubuntu-24.04-clang-18, host_profile: ubuntu-24.04-clang-18 } + - { os: macos-26, build_profile: macos-26-armv8-clang-14, host_profile: macos-26-armv8-clang-14 } + steps: + - name: checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + - name: checkout conan-odr-index + run: git submodule update --init --depth 1 conan-odr-index + + - name: ubuntu install ccache + if: runner.os == 'Linux' + run: | + sudo apt install ccache + ccache -V + - name: macos install ccache + if: runner.os == 'macOS' + run: | + brew install ccache + ccache -V + + - name: setup java + uses: actions/setup-java@f4f1212c880fdec8162ea9a6493f4495191887b4 # v5 + with: + distribution: temurin + java-version: 21 + + - name: setup python 3.14 + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: 3.14 + - name: install python dependencies + run: pip install conan + + - name: cache conan + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + with: + path: ${{ env.CONAN_HOME }} + key: conan-jni-${{ matrix.host_profile }}-${{ env.CONAN_KEY_SUFFIX }} + restore-keys: | + conan-jni-${{ matrix.host_profile }}- + conan-${{ matrix.host_profile }}- + + - name: export conan-odr-index + run: python conan-odr-index/scripts/conan_export_all_packages.py --selection-config conan-odr-index/defaults.yaml + - name: conan config + run: conan config install .github/config/conan + + - name: cache ccache + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5 + with: + path: ${{ env.CCACHE_DIR }} + key: ccache-jni-${{ matrix.host_profile }}-${{ env.CCACHE_KEY_SUFFIX }} + restore-keys: | + ccache-jni-${{ matrix.host_profile }}- + ccache-${{ matrix.host_profile }}- + + - name: conan install + run: > + conan install . + -o '&:with_jni=True' + --profile:host '${{ matrix.host_profile }}' + --profile:build '${{ matrix.build_profile }}' + --build missing + + - name: cmake + run: > + cmake -B build -S . + -DCMAKE_TOOLCHAIN_FILE="conan_toolchain.cmake" + -DCMAKE_CXX_COMPILER_LAUNCHER=ccache + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_CXX_FLAGS="-Werror" + -DODR_JNI=ON + -DODR_CLI=OFF + -DODR_TEST=ON + + - name: build + run: cmake --build build --target odr_jni odr_java odr_java_tests --config Release + + - name: junit + run: ctest --test-dir build/jni --output-on-failure diff --git a/AGENTS.md b/AGENTS.md index 48f97a7b..aa5f014e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -64,6 +64,7 @@ bytes ─▶ magic/open_strategy ─▶ DecodedFile ─▶ Document ─▶ Eleme | `src/odr/internal/{csv,json,text,svm}/` | Smaller formats. | | `cli/src/` | CLI tools: `translate`, `back_translate`, `meta`, `server`. | | `python/` | Python bindings (`pyodr`, pybind11); see [`python/AGENTS.md`](python/AGENTS.md). | +| `jni/` | JNI bindings (Java package `app.opendocument.core`); see [`jni/AGENTS.md`](jni/AGENTS.md). | | `tools/pdf/` | Dev tooling (not built): PDF encoding-data generators, see `tools/pdf/README.md`. | | `test/src/` | GoogleTest suites; data in `test/data` (git submodules). | | `offline/documentation/MS-*/` | Vendored Microsoft spec text (see [Specs](#specs)). | diff --git a/CMakeLists.txt b/CMakeLists.txt index dde9fb2e..04bde5b7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -23,6 +23,7 @@ option(ODR_WITH_WVWARE "Build with wvWare" "${WITH_WVWARE}") option(ODR_WITH_LIBMAGIC "Build with libmagic" "${WITH_LIBMAGIC}") option(ODR_BUNDLE_ASSETS "Bundle assets during build and install" OFF) option(ODR_PYTHON "Build Python bindings" OFF) +option(ODR_JNI "Build JNI bindings" OFF) include(GNUInstallDirs) @@ -410,6 +411,10 @@ if (ODR_PYTHON) add_subdirectory("python") endif () +if (ODR_JNI) + add_subdirectory("jni") +endif () + if (ODR_TEST) add_subdirectory("test") endif () diff --git a/conanfile.py b/conanfile.py index 5285ab1d..1da40418 100644 --- a/conanfile.py +++ b/conanfile.py @@ -22,6 +22,7 @@ class OpenDocumentCoreConan(ConanFile): "with_libmagic": [True, False], "with_http_server": [True, False], "with_python": [True, False], + "with_jni": [True, False], "bundle_assets": [True, False], } default_options = { @@ -32,10 +33,11 @@ class OpenDocumentCoreConan(ConanFile): "with_libmagic": True, "with_http_server": True, "with_python": False, + "with_jni": False, "bundle_assets": False, } - exports_sources = ["cli/*", "cmake/*", "python/*", "resources/dist/*", "src/*", "CMakeLists.txt"] + exports_sources = ["cli/*", "cmake/*", "jni/*", "python/*", "resources/dist/*", "src/*", "CMakeLists.txt"] def config_options(self): if self.settings.os == "Windows": @@ -84,6 +86,7 @@ def generate(self): tc.variables["ODR_WITH_LIBMAGIC"] = self.options.get_safe("with_libmagic", False) tc.variables["ODR_WITH_HTTP_SERVER"] = self.options.get_safe("with_http_server", False) tc.variables["ODR_PYTHON"] = self.options.get_safe("with_python", False) + tc.variables["ODR_JNI"] = self.options.get_safe("with_jni", False) tc.variables["ODR_BUNDLE_ASSETS"] = self.options.get_safe("bundle_assets", False) # Get runenv info, exported by package_info() of dependencies diff --git a/jni/AGENTS.md b/jni/AGENTS.md new file mode 100644 index 00000000..0399b557 --- /dev/null +++ b/jni/AGENTS.md @@ -0,0 +1,55 @@ +# AGENTS.md — JNI bindings + +Hand-written JNI bindings for the public C++ API (`src/odr/*.hpp`), Java +package `app.opendocument.core`. Mirrors the surface of the python bindings +(`python/`); replaces the ad-hoc odr.droid `CoreWrapper`. + +## Layout + +| Path | What | +|------|------| +| `CMakeLists.txt` | Builds `libodr_jni` + `odr-core-java.jar`; included from the root build via `ODR_JNI`, or standalone against an installed `odrcore`. | +| `src/` | JNI sources, one `jni_*` unit per public-API area; `odr_jni.hpp` (strings, exceptions, handles) and `jni_convert.hpp` (struct/POJO marshalling) are the helpers. | +| `java/app/opendocument/core/` | Java API: enums, POJOs (styles, metas, `HtmlConfig`), and handle-backed wrappers extending `NativeResource`. | +| `tests/` | JUnit 5 suite, run via ctest (`odr_jni_junit`); inputs are generated inline (tmp files, zip-built minimal ODT) — no fixture files. | + +## Design + +- **Handle model**: a Java wrapper owns a heap-allocated copy of the C++ value + handle (`odr::DecodedFile`, `odr::Element`, ...) referenced by a `long`. + `NativeResource` frees it via `Cleaner`/`AutoCloseable`; each class has a + static `destroy(long)` native deleting the concrete C++ type. +- **Typed views are re-derived per call**: element/file handles always point to + the *base* type (`odr::Element`, `odr::DecodedFile`); natives of typed Java + classes call `as_paragraph()`/`as_text_file()` etc. on each call. Never store + a typed C++ subobject behind a base-typed handle (slicing/delete issues). +- **Keep-alive**: navigation results carry an `owner` reference + (`Element.owner()` → the `Document`) so the GC cannot free the root while + handles into it are alive — the JNI analogue of pyodr's `keep_alive`. +- **GC safety**: natives that use a handle are *instance* methods (the `this` + local reference keeps the wrapper — and its owner chain — reachable for the + duration of the call); only factories and `destroy` are static. +- **Enums cross as ordinals**: Java enum constant order must match the C++ + enum declaration; `-1` encodes an absent `std::optional`. +- **Strings**: use `odr_jni::to_string`/`to_jstring` (real UTF-8 ↔ UTF-16), + never JNI's modified-UTF-8 `GetStringUTFChars`. +- **Exceptions**: every native body runs inside `odr_jni::guarded`; C++ + exceptions map to `OdrException` subclasses (`odr_jni.cpp::throw_java`). +- Mirror the C++ names; drop the `Logger` parameters (bindings use the default + null logger). +- Stream-based C++ APIs (`write`, `save`, `pipe`) are bound as natives + returning `byte[]`/`String` via `std::ostringstream`. +- **Not bound**: `HtmlConfig::resource_locator` (function pointer across JNI); + the standard resource locator is always used. A `null` Java + `HtmlConfig.resourcePath` keeps the C++ default (the odr core data path) — + don't marshal it unconditionally. +- New public C++ API? Extend the matching `jni_*.cpp` + Java class and add a + JUnit test. +- C++ sources follow the repo clang-format; Java follows the + google-java-format style (2-space indent), no enforced formatter yet. +- Tests must stay hermetic: build inputs inline in `tests/.../TestFiles.java`; + HTML-rendering tests `assumeTrue(TestFiles.hasCoreData())` (skips when + assets are missing). Use `127.0.0.1`, not `localhost`, for the HTTP server + (the JVM prefers `::1`). +- Build/test loop: see `jni/README.md`; CI lives in + `.github/workflows/jni.yml`. diff --git a/jni/CMakeLists.txt b/jni/CMakeLists.txt new file mode 100644 index 00000000..f0652b54 --- /dev/null +++ b/jni/CMakeLists.txt @@ -0,0 +1,174 @@ +# JNI bindings for OpenDocument.core (Java package `app.opendocument.core`). +# +# Included from the top-level CMakeLists.txt when `ODR_JNI` is ON. Can also be +# configured standalone against an installed `odrcore` package. + +if (CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + cmake_minimum_required(VERSION 3.18) + project(odrjni LANGUAGES CXX) + set(CMAKE_CXX_STANDARD 20) + set(CMAKE_CXX_STANDARD_REQUIRED ON) + set(CMAKE_CXX_EXTENSIONS OFF) + + find_package(odrcore REQUIRED) + set(ODR_JNI_ODR_TARGET odrcore::odrcore) + + # Same option name as the root build; an installed odrcore ships + # `http_server.hpp` only when it was built with the HTTP server. + option(ODR_WITH_HTTP_SERVER "Build with cpp-httplib HTTP server" ON) +else () + set(ODR_JNI_ODR_TARGET odr) +endif () + +find_package(Java 11 REQUIRED COMPONENTS Development) +find_package(JNI REQUIRED) +include(UseJava) + +add_library(odr_jni SHARED + "src/odr_jni.cpp" + "src/jni_core.cpp" + "src/jni_document.cpp" + "src/jni_file.cpp" + "src/jni_html.cpp" + "src/jni_http_server.cpp" + "src/jni_style.cpp" +) +target_include_directories(odr_jni PRIVATE ${JNI_INCLUDE_DIRS}) +target_link_libraries(odr_jni PRIVATE ${ODR_JNI_ODR_TARGET}) +if (ODR_WITH_HTTP_SERVER) + target_compile_definitions(odr_jni PRIVATE ODR_WITH_HTTP_SERVER) +endif () + +set(CMAKE_JAVA_COMPILE_FLAGS --release 17 -Xlint:all) + +add_jar(odr_java + SOURCES + "java/app/opendocument/core/AnchorType.java" + "java/app/opendocument/core/Archive.java" + "java/app/opendocument/core/ArchiveFile.java" + "java/app/opendocument/core/Bookmark.java" + "java/app/opendocument/core/Circle.java" + "java/app/opendocument/core/Color.java" + "java/app/opendocument/core/CustomShape.java" + "java/app/opendocument/core/DecodePreference.java" + "java/app/opendocument/core/DecodedFile.java" + "java/app/opendocument/core/DecoderEngine.java" + "java/app/opendocument/core/DirectionalMeasure.java" + "java/app/opendocument/core/DirectionalString.java" + "java/app/opendocument/core/Document.java" + "java/app/opendocument/core/DocumentFile.java" + "java/app/opendocument/core/DocumentMeta.java" + "java/app/opendocument/core/DocumentPath.java" + "java/app/opendocument/core/DocumentType.java" + "java/app/opendocument/core/Element.java" + "java/app/opendocument/core/ElementType.java" + "java/app/opendocument/core/EncryptionState.java" + "java/app/opendocument/core/File.java" + "java/app/opendocument/core/FileCategory.java" + "java/app/opendocument/core/FileLocation.java" + "java/app/opendocument/core/FileMeta.java" + "java/app/opendocument/core/FileType.java" + "java/app/opendocument/core/FileWalker.java" + "java/app/opendocument/core/Filesystem.java" + "java/app/opendocument/core/FontFile.java" + "java/app/opendocument/core/FontPosition.java" + "java/app/opendocument/core/FontStyle.java" + "java/app/opendocument/core/FontWeight.java" + "java/app/opendocument/core/Frame.java" + "java/app/opendocument/core/GlobalParams.java" + "java/app/opendocument/core/GraphicStyle.java" + "java/app/opendocument/core/HorizontalAlign.java" + "java/app/opendocument/core/Html.java" + "java/app/opendocument/core/HtmlConfig.java" + "java/app/opendocument/core/HtmlPage.java" + "java/app/opendocument/core/HtmlResource.java" + "java/app/opendocument/core/HtmlResourceType.java" + "java/app/opendocument/core/HtmlService.java" + "java/app/opendocument/core/HtmlTableGridlines.java" + "java/app/opendocument/core/HtmlView.java" + "java/app/opendocument/core/HttpServer.java" + "java/app/opendocument/core/Image.java" + "java/app/opendocument/core/ImageFile.java" + "java/app/opendocument/core/Line.java" + "java/app/opendocument/core/LineBreak.java" + "java/app/opendocument/core/Link.java" + "java/app/opendocument/core/ListItem.java" + "java/app/opendocument/core/MasterPage.java" + "java/app/opendocument/core/Measure.java" + "java/app/opendocument/core/NativeLibrary.java" + "java/app/opendocument/core/NativeResource.java" + "java/app/opendocument/core/Odr.java" + "java/app/opendocument/core/OdrException.java" + "java/app/opendocument/core/Page.java" + "java/app/opendocument/core/PageLayout.java" + "java/app/opendocument/core/Paragraph.java" + "java/app/opendocument/core/ParagraphStyle.java" + "java/app/opendocument/core/PdfFile.java" + "java/app/opendocument/core/PdfTextMode.java" + "java/app/opendocument/core/PrintOrientation.java" + "java/app/opendocument/core/Rect.java" + "java/app/opendocument/core/Sheet.java" + "java/app/opendocument/core/SheetCell.java" + "java/app/opendocument/core/Slide.java" + "java/app/opendocument/core/Span.java" + "java/app/opendocument/core/Table.java" + "java/app/opendocument/core/TableCell.java" + "java/app/opendocument/core/TableCellStyle.java" + "java/app/opendocument/core/TableColumn.java" + "java/app/opendocument/core/TableColumnStyle.java" + "java/app/opendocument/core/TableDimensions.java" + "java/app/opendocument/core/TablePosition.java" + "java/app/opendocument/core/TableRow.java" + "java/app/opendocument/core/TableRowStyle.java" + "java/app/opendocument/core/TableStyle.java" + "java/app/opendocument/core/Text.java" + "java/app/opendocument/core/TextAlign.java" + "java/app/opendocument/core/TextFile.java" + "java/app/opendocument/core/TextRoot.java" + "java/app/opendocument/core/TextStyle.java" + "java/app/opendocument/core/TextWrap.java" + "java/app/opendocument/core/ValueType.java" + "java/app/opendocument/core/VerticalAlign.java" + OUTPUT_NAME odr-core-java +) + +install(TARGETS odr_jni LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT jni) +install_jar(odr_java DESTINATION "${CMAKE_INSTALL_DATADIR}/java" COMPONENT jni) + +if (ODR_TEST) + include(FetchContent) + FetchContent_Declare(junit_console + URL "https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/1.10.2/junit-platform-console-standalone-1.10.2.jar" + URL_HASH SHA256=a1de557821293ce903c213c694165fff532cf92081bac4238b9e05b35f04f43f + DOWNLOAD_NO_EXTRACT TRUE + ) + FetchContent_MakeAvailable(junit_console) + set(ODR_JNI_JUNIT_JAR + "${junit_console_SOURCE_DIR}/junit-platform-console-standalone-1.10.2.jar") + + add_jar(odr_java_tests + SOURCES + "tests/app/opendocument/core/DocumentTest.java" + "tests/app/opendocument/core/FileTest.java" + "tests/app/opendocument/core/HtmlTest.java" + "tests/app/opendocument/core/HttpServerTest.java" + "tests/app/opendocument/core/MetaTest.java" + "tests/app/opendocument/core/TestFiles.java" + INCLUDE_JARS odr_java "${ODR_JNI_JUNIT_JAR}" + OUTPUT_NAME odr-core-java-tests + ) + + enable_testing() + add_test(NAME odr_jni_junit + COMMAND "${Java_JAVA_EXECUTABLE}" + "-Djava.library.path=$" + -jar "${ODR_JNI_JUNIT_JAR}" + execute + --class-path "$" + --class-path "$" + --scan-class-path + --fail-if-no-tests + ) + set_tests_properties(odr_jni_junit PROPERTIES + ENVIRONMENT "ODR_CORE_DATA_PATH=${ODR_BUILD_ODR_DATA_PATH}") +endif () diff --git a/jni/README.md b/jni/README.md new file mode 100644 index 00000000..a8571f4b --- /dev/null +++ b/jni/README.md @@ -0,0 +1,57 @@ +# odr-core-java — JNI bindings for OpenDocument.core + +Decode office documents (ODF, OOXML, legacy MS binary, PDF, CSV, ...) and +render them to HTML from Java (package `app.opendocument.core`). + +```java +import app.opendocument.core.DecodedFile; +import app.opendocument.core.Html; +import app.opendocument.core.HtmlConfig; +import app.opendocument.core.HtmlService; +import app.opendocument.core.Odr; + +DecodedFile file = Odr.open("document.odt"); +HtmlService service = Html.translate(file, "cache-dir", new HtmlConfig()); +app.opendocument.core.Html html = service.bringOffline("output-dir"); +for (var page : html.pages()) { + System.out.println(page.name + " " + page.path); +} +``` + +The native library is loaded from `java.library.path` as `odr_jni` +(`libodr_jni.so`/`libodr_jni.dylib`); the system property +`app.opendocument.core.library` overrides it with an absolute path. + +## Building + +The bindings are part of the main CMake build, toggled by `ODR_JNI` (requires +a JDK, 11+): + +```bash +conan install . -o '&:with_jni=True' --build missing +cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake -DODR_JNI=ON -DODR_TEST=ON +cmake --build build --target odr_jni odr_java odr_java_tests +(cd build/jni && ctest) +``` + +This produces `build/jni/libodr_jni.dylib` (or `.so`) and +`build/jni/odr-core-java.jar`. `jni/CMakeLists.txt` can also be configured +standalone against an installed `odrcore` package. + +## Runtime data + +Rendering uses shipped assets (CSS/JS). Point the library at them via +`GlobalParams.setOdrCoreDataPath(...)` or the `ODR_CORE_DATA_PATH` environment +variable (the tests read it; for in-tree builds it is `build/data`). The +optional pdf2htmlEX/wvWare/libmagic backends have their own paths on +`GlobalParams`. + +## Notes + +- Handle-backed objects (`DecodedFile`, `Document`, `Element`, + `HtmlService`, ...) own native memory. They free it on garbage collection; + use `close()` (or try-with-resources) to release large objects eagerly. +- The C++ `HtmlConfig::resource_locator` callback is not exposed; the standard + resource locator is always used. +- `HttpServer` is available when the native library was built with the HTTP + server (`Odr.hasHttpServer()`). diff --git a/jni/java/app/opendocument/core/AnchorType.java b/jni/java/app/opendocument/core/AnchorType.java new file mode 100644 index 00000000..2c680060 --- /dev/null +++ b/jni/java/app/opendocument/core/AnchorType.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::AnchorType}; constant order must match the C++ declaration. */ +public enum AnchorType { + AS_CHAR, AT_CHAR, AT_FRAME, AT_PAGE, AT_PARAGRAPH; + + static AnchorType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/Archive.java b/jni/java/app/opendocument/core/Archive.java new file mode 100644 index 00000000..60329fcf --- /dev/null +++ b/jni/java/app/opendocument/core/Archive.java @@ -0,0 +1,23 @@ +package app.opendocument.core; + +/** An archive (e.g. ZIP contents). Mirrors {@code odr::Archive}. */ +public final class Archive extends NativeResource { + Archive(long handle) { + super(handle, null, Archive::destroy); + } + + public Filesystem asFilesystem() { + return new Filesystem(asFilesystemNative(handle()), null); + } + + /** Serializes the archive into a byte array. */ + public byte[] save() { + return saveNative(handle()); + } + + private static native void destroy(long handle); + + private native long asFilesystemNative(long handle); + + private native byte[] saveNative(long handle); +} diff --git a/jni/java/app/opendocument/core/ArchiveFile.java b/jni/java/app/opendocument/core/ArchiveFile.java new file mode 100644 index 00000000..3b792c65 --- /dev/null +++ b/jni/java/app/opendocument/core/ArchiveFile.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** A decoded archive file. Mirrors {@code odr::ArchiveFile}. */ +public final class ArchiveFile extends DecodedFile { + ArchiveFile(long handle) { + super(handle); + } + + public Archive archive() { + return new Archive(archiveNative(handle())); + } + + private native long archiveNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Bookmark.java b/jni/java/app/opendocument/core/Bookmark.java new file mode 100644 index 00000000..3e623645 --- /dev/null +++ b/jni/java/app/opendocument/core/Bookmark.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Bookmark element. Mirrors {@code odr::Bookmark}. */ +public final class Bookmark extends Element { + Bookmark(long handle, Object owner) { + super(handle, owner); + } + + public String name() { + return nameNative(handle()); + } + + private native String nameNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Circle.java b/jni/java/app/opendocument/core/Circle.java new file mode 100644 index 00000000..3b401cbd --- /dev/null +++ b/jni/java/app/opendocument/core/Circle.java @@ -0,0 +1,38 @@ +package app.opendocument.core; + +/** Circle element. Mirrors {@code odr::Circle}. */ +public final class Circle extends Element { + Circle(long handle, Object owner) { + super(handle, owner); + } + + public String x() { + return xNative(handle()); + } + + public String y() { + return yNative(handle()); + } + + public String width() { + return widthNative(handle()); + } + + public String height() { + return heightNative(handle()); + } + + public GraphicStyle style() { + return styleNative(handle()); + } + + private native String xNative(long handle); + + private native String yNative(long handle); + + private native String widthNative(long handle); + + private native String heightNative(long handle); + + private native GraphicStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Color.java b/jni/java/app/opendocument/core/Color.java new file mode 100644 index 00000000..8daf782a --- /dev/null +++ b/jni/java/app/opendocument/core/Color.java @@ -0,0 +1,47 @@ +package app.opendocument.core; + +/** An RGBA color. Mirrors {@code odr::Color}; channels are 0-255. */ +public final class Color { + public final int red; + public final int green; + public final int blue; + public final int alpha; + + public Color(int red, int green, int blue) { + this(red, green, blue, 255); + } + + public Color(int red, int green, int blue, int alpha) { + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; + } + + public static Color ofRgb(int rgb) { + return new Color((rgb >> 16) & 0xff, (rgb >> 8) & 0xff, rgb & 0xff); + } + + public int rgb() { + return (red << 16) | (green << 8) | blue; + } + + public int argb() { + return (alpha << 24) | rgb(); + } + + @Override + public boolean equals(Object other) { + return other instanceof Color color && argb() == color.argb(); + } + + @Override + public int hashCode() { + return argb(); + } + + @Override + public String toString() { + return String.format("Color(%d, %d, %d, %d)", red, green, blue, alpha); + } +} diff --git a/jni/java/app/opendocument/core/CustomShape.java b/jni/java/app/opendocument/core/CustomShape.java new file mode 100644 index 00000000..cce6368c --- /dev/null +++ b/jni/java/app/opendocument/core/CustomShape.java @@ -0,0 +1,38 @@ +package app.opendocument.core; + +/** Custom shape element. Mirrors {@code odr::CustomShape}; x/y may be {@code null}. */ +public final class CustomShape extends Element { + CustomShape(long handle, Object owner) { + super(handle, owner); + } + + public String x() { + return xNative(handle()); + } + + public String y() { + return yNative(handle()); + } + + public String width() { + return widthNative(handle()); + } + + public String height() { + return heightNative(handle()); + } + + public GraphicStyle style() { + return styleNative(handle()); + } + + private native String xNative(long handle); + + private native String yNative(long handle); + + private native String widthNative(long handle); + + private native String heightNative(long handle); + + private native GraphicStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/DecodePreference.java b/jni/java/app/opendocument/core/DecodePreference.java new file mode 100644 index 00000000..f9b89c32 --- /dev/null +++ b/jni/java/app/opendocument/core/DecodePreference.java @@ -0,0 +1,32 @@ +package app.opendocument.core; + +import java.util.ArrayList; +import java.util.List; + +/** Preference for decoding files. Mirrors {@code odr::DecodePreference}. */ +public final class DecodePreference { + /** Decode as this file type; {@code null} to detect. */ + public FileType asFileType; + /** Decode with this engine; {@code null} to choose automatically. */ + public DecoderEngine withEngine; + + public List fileTypePriority = new ArrayList<>(); + public List enginePriority = new ArrayList<>(); + + // Flattened for the native layer. + int asFileTypeNative() { + return asFileType == null ? -1 : asFileType.toNative(); + } + + int withEngineNative() { + return withEngine == null ? -1 : withEngine.toNative(); + } + + int[] fileTypePriorityNative() { + return fileTypePriority.stream().mapToInt(FileType::toNative).toArray(); + } + + int[] enginePriorityNative() { + return enginePriority.stream().mapToInt(DecoderEngine::toNative).toArray(); + } +} diff --git a/jni/java/app/opendocument/core/DecodedFile.java b/jni/java/app/opendocument/core/DecodedFile.java new file mode 100644 index 00000000..1e287370 --- /dev/null +++ b/jni/java/app/opendocument/core/DecodedFile.java @@ -0,0 +1,163 @@ +package app.opendocument.core; + +/** + * A decoded file. Mirrors {@code odr::DecodedFile}. Obtain via + * {@link Odr#open} or the constructors; {@code as*} accessors return typed + * views and throw when the file is not of that kind (check {@code is*} first). + */ +public class DecodedFile extends NativeResource { + static { + NativeLibrary.load(); + } + + DecodedFile(long handle) { + super(handle, null, DecodedFile::destroy); + } + + public DecodedFile(String path) { + this(create(path)); + } + + public DecodedFile(String path, FileType as) { + this(createAs(path, as.toNative())); + } + + public DecodedFile(File file) { + this(createFromFile(file.handle())); + } + + public File file() { + return new File(fileNative(handle())); + } + + public FileType fileType() { + return FileType.fromNative(fileTypeNative(handle())); + } + + public FileCategory fileCategory() { + return FileCategory.fromNative(fileCategoryNative(handle())); + } + + public FileMeta fileMeta() { + return fileMetaNative(handle()); + } + + public DecoderEngine decoderEngine() { + return DecoderEngine.fromNative(decoderEngineNative(handle())); + } + + public boolean passwordEncrypted() { + return passwordEncryptedNative(handle()); + } + + public EncryptionState encryptionState() { + return EncryptionState.fromNative(encryptionStateNative(handle())); + } + + /** Returns a decrypted copy of this file. */ + public DecodedFile decrypt(String password) { + return new DecodedFile(decryptNative(handle(), password)); + } + + public boolean isDecodable() { + return isDecodableNative(handle()); + } + + public boolean isTextFile() { + return isTextFileNative(handle()); + } + + public boolean isImageFile() { + return isImageFileNative(handle()); + } + + public boolean isArchiveFile() { + return isArchiveFileNative(handle()); + } + + public boolean isDocumentFile() { + return isDocumentFileNative(handle()); + } + + public boolean isPdfFile() { + return isPdfFileNative(handle()); + } + + public boolean isFontFile() { + return isFontFileNative(handle()); + } + + public TextFile asTextFile() { + return new TextFile(asTextFileNative(handle())); + } + + public ImageFile asImageFile() { + return new ImageFile(asImageFileNative(handle())); + } + + public ArchiveFile asArchiveFile() { + return new ArchiveFile(asArchiveFileNative(handle())); + } + + public DocumentFile asDocumentFile() { + return new DocumentFile(asDocumentFileNative(handle())); + } + + public PdfFile asPdfFile() { + return new PdfFile(asPdfFileNative(handle())); + } + + public FontFile asFontFile() { + return new FontFile(asFontFileNative(handle())); + } + + private static native long create(String path); + + private static native long createAs(String path, int as); + + private static native long createFromFile(long fileHandle); + + static native void destroy(long handle); + + private native long fileNative(long handle); + + private native int fileTypeNative(long handle); + + private native int fileCategoryNative(long handle); + + private native FileMeta fileMetaNative(long handle); + + private native int decoderEngineNative(long handle); + + private native boolean passwordEncryptedNative(long handle); + + private native int encryptionStateNative(long handle); + + private native long decryptNative(long handle, String password); + + private native boolean isDecodableNative(long handle); + + private native boolean isTextFileNative(long handle); + + private native boolean isImageFileNative(long handle); + + private native boolean isArchiveFileNative(long handle); + + private native boolean isDocumentFileNative(long handle); + + private native boolean isPdfFileNative(long handle); + + private native boolean isFontFileNative(long handle); + + private native long asTextFileNative(long handle); + + private native long asImageFileNative(long handle); + + private native long asArchiveFileNative(long handle); + + private native long asDocumentFileNative(long handle); + + private native long asPdfFileNative(long handle); + + private native long asFontFileNative(long handle); +} diff --git a/jni/java/app/opendocument/core/DecoderEngine.java b/jni/java/app/opendocument/core/DecoderEngine.java new file mode 100644 index 00000000..1b355ed7 --- /dev/null +++ b/jni/java/app/opendocument/core/DecoderEngine.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::DecoderEngine}; constant order must match the C++ declaration. */ +public enum DecoderEngine { + ODR, POPPLER, WVWARE; + + static DecoderEngine fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/DirectionalMeasure.java b/jni/java/app/opendocument/core/DirectionalMeasure.java new file mode 100644 index 00000000..6e7692c9 --- /dev/null +++ b/jni/java/app/opendocument/core/DirectionalMeasure.java @@ -0,0 +1,16 @@ +package app.opendocument.core; + +/** Per-direction measures (e.g. margins). Any side may be {@code null}. */ +public final class DirectionalMeasure { + public final Measure right; + public final Measure top; + public final Measure left; + public final Measure bottom; + + public DirectionalMeasure(Measure right, Measure top, Measure left, Measure bottom) { + this.right = right; + this.top = top; + this.left = left; + this.bottom = bottom; + } +} diff --git a/jni/java/app/opendocument/core/DirectionalString.java b/jni/java/app/opendocument/core/DirectionalString.java new file mode 100644 index 00000000..e886bb36 --- /dev/null +++ b/jni/java/app/opendocument/core/DirectionalString.java @@ -0,0 +1,16 @@ +package app.opendocument.core; + +/** Per-direction strings (e.g. borders). Any side may be {@code null}. */ +public final class DirectionalString { + public final String right; + public final String top; + public final String left; + public final String bottom; + + public DirectionalString(String right, String top, String left, String bottom) { + this.right = right; + this.top = top; + this.left = left; + this.bottom = bottom; + } +} diff --git a/jni/java/app/opendocument/core/Document.java b/jni/java/app/opendocument/core/Document.java new file mode 100644 index 00000000..b0569084 --- /dev/null +++ b/jni/java/app/opendocument/core/Document.java @@ -0,0 +1,63 @@ +package app.opendocument.core; + +/** A decoded document. Mirrors {@code odr::Document}. */ +public final class Document extends NativeResource { + Document(long handle) { + super(handle, null, Document::destroy); + } + + public boolean isEditable() { + return isEditableNative(handle()); + } + + public boolean isSavable() { + return isSavable(false); + } + + public boolean isSavable(boolean encrypted) { + return isSavableNative(handle(), encrypted); + } + + public void save(String path) { + saveNative(handle(), path); + } + + public void save(String path, String password) { + saveEncryptedNative(handle(), path, password); + } + + public FileType fileType() { + return FileType.fromNative(fileTypeNative(handle())); + } + + public DocumentType documentType() { + return DocumentType.fromNative(documentTypeNative(handle())); + } + + /** Root of the element tree; keeps this document reachable. */ + public Element rootElement() { + return new Element(rootElementNative(handle()), this); + } + + public Filesystem asFilesystem() { + return new Filesystem(asFilesystemNative(handle()), this); + } + + private static native void destroy(long handle); + + private native boolean isEditableNative(long handle); + + private native boolean isSavableNative(long handle, boolean encrypted); + + private native void saveNative(long handle, String path); + + private native void saveEncryptedNative(long handle, String path, String password); + + private native int fileTypeNative(long handle); + + private native int documentTypeNative(long handle); + + private native long rootElementNative(long handle); + + private native long asFilesystemNative(long handle); +} diff --git a/jni/java/app/opendocument/core/DocumentFile.java b/jni/java/app/opendocument/core/DocumentFile.java new file mode 100644 index 00000000..80331da2 --- /dev/null +++ b/jni/java/app/opendocument/core/DocumentFile.java @@ -0,0 +1,52 @@ +package app.opendocument.core; + +/** A decoded document file. Mirrors {@code odr::DocumentFile}. */ +public final class DocumentFile extends DecodedFile { + DocumentFile(long handle) { + super(handle); + } + + public DocumentFile(String path) { + this(create(path)); + } + + public static FileType typeByPath(String path) { + return FileType.fromNative(typeByPathNative(path)); + } + + public static FileMeta metaByPath(String path) { + return metaByPathNative(path); + } + + public DocumentType documentType() { + return DocumentType.fromNative(documentTypeNative(handle())); + } + + public DocumentMeta documentMeta() { + return documentMetaNative(handle()); + } + + /** Returns a decrypted copy of this file. */ + @Override + public DocumentFile decrypt(String password) { + return new DocumentFile(decryptDocumentFileNative(handle(), password)); + } + + public Document document() { + return new Document(documentNative(handle())); + } + + private static native long create(String path); + + private static native int typeByPathNative(String path); + + private static native FileMeta metaByPathNative(String path); + + private native int documentTypeNative(long handle); + + private native DocumentMeta documentMetaNative(long handle); + + private native long decryptDocumentFileNative(long handle, String password); + + private native long documentNative(long handle); +} diff --git a/jni/java/app/opendocument/core/DocumentMeta.java b/jni/java/app/opendocument/core/DocumentMeta.java new file mode 100644 index 00000000..6963397b --- /dev/null +++ b/jni/java/app/opendocument/core/DocumentMeta.java @@ -0,0 +1,39 @@ +package app.opendocument.core; + +/** Meta information about a document. Mirrors {@code odr::DocumentMeta}. */ +public final class DocumentMeta { + public final DocumentType documentType; + /** Number of entries (pages/slides/sheets); {@code null} if unknown. */ + public final Long entryCount; + public final String title; + public final String author; + public final String subject; + public final String keywords; + public final String creator; + public final String producer; + public final String creationDate; + public final String modificationDate; + + DocumentMeta( + int documentType, + Long entryCount, + String title, + String author, + String subject, + String keywords, + String creator, + String producer, + String creationDate, + String modificationDate) { + this.documentType = DocumentType.fromNative(documentType); + this.entryCount = entryCount; + this.title = title; + this.author = author; + this.subject = subject; + this.keywords = keywords; + this.creator = creator; + this.producer = producer; + this.creationDate = creationDate; + this.modificationDate = modificationDate; + } +} diff --git a/jni/java/app/opendocument/core/DocumentPath.java b/jni/java/app/opendocument/core/DocumentPath.java new file mode 100644 index 00000000..2bc49756 --- /dev/null +++ b/jni/java/app/opendocument/core/DocumentPath.java @@ -0,0 +1,55 @@ +package app.opendocument.core; + +/** A path to a specific element in a document. Mirrors {@code odr::DocumentPath}. */ +public final class DocumentPath extends NativeResource { + static { + NativeLibrary.load(); + } + + DocumentPath(long handle) { + super(handle, null, DocumentPath::destroy); + } + + public DocumentPath(String path) { + this(create(path)); + } + + public boolean empty() { + return emptyNative(handle()); + } + + public DocumentPath parent() { + return new DocumentPath(parentNative(handle())); + } + + public DocumentPath join(DocumentPath other) { + return new DocumentPath(joinNative(handle(), other.handle())); + } + + @Override + public boolean equals(Object other) { + return other instanceof DocumentPath path && toString().equals(path.toString()); + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + + @Override + public String toString() { + return toStringNative(handle()); + } + + private static native long create(String path); + + private static native void destroy(long handle); + + private native boolean emptyNative(long handle); + + private native long parentNative(long handle); + + private native long joinNative(long handle, long otherHandle); + + private native String toStringNative(long handle); +} diff --git a/jni/java/app/opendocument/core/DocumentType.java b/jni/java/app/opendocument/core/DocumentType.java new file mode 100644 index 00000000..4be7e876 --- /dev/null +++ b/jni/java/app/opendocument/core/DocumentType.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::DocumentType}; constant order must match the C++ declaration. */ +public enum DocumentType { + UNKNOWN, TEXT, PRESENTATION, SPREADSHEET, DRAWING; + + static DocumentType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/Element.java b/jni/java/app/opendocument/core/Element.java new file mode 100644 index 00000000..b5d6a924 --- /dev/null +++ b/jni/java/app/opendocument/core/Element.java @@ -0,0 +1,267 @@ +package app.opendocument.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * An element in a document. Mirrors {@code odr::Element}. Navigation returns + * {@code null} where the C++ API returns an empty element; {@code as*} + * accessors return {@code null} when the element is not of that type. + * + *

Elements keep the {@link Document} they originate from reachable. + */ +public class Element extends NativeResource { + Element(long handle, Object owner) { + super(handle, owner, Element::destroy); + } + + public ElementType type() { + return ElementType.fromNative(typeNative(handle())); + } + + public Element parent() { + return wrap(parentNative(handle())); + } + + public Element firstChild() { + return wrap(firstChildNative(handle())); + } + + public Element previousSibling() { + return wrap(previousSiblingNative(handle())); + } + + public Element nextSibling() { + return wrap(nextSiblingNative(handle())); + } + + public boolean isUnique() { + return isUniqueNative(handle()); + } + + public boolean isSelfLocatable() { + return isSelfLocatableNative(handle()); + } + + public boolean isEditable() { + return isEditableNative(handle()); + } + + /** Whether this and {@code other} refer to the same element. */ + public boolean isSame(Element other) { + return isSameNative(handle(), other.handle()); + } + + public DocumentPath documentPath() { + return new DocumentPath(documentPathNative(handle())); + } + + public Element navigatePath(DocumentPath path) { + return wrap(navigatePathNative(handle(), path.handle())); + } + + public List children() { + List result = new ArrayList<>(); + for (Element child = firstChild(); child != null; child = child.nextSibling()) { + result.add(child); + } + return result; + } + + public TextRoot asTextRoot() { + long h = asTextRootNative(handle()); + return h == 0 ? null : new TextRoot(h, owner()); + } + + public Slide asSlide() { + long h = asSlideNative(handle()); + return h == 0 ? null : new Slide(h, owner()); + } + + public Sheet asSheet() { + long h = asSheetNative(handle()); + return h == 0 ? null : new Sheet(h, owner()); + } + + public Page asPage() { + long h = asPageNative(handle()); + return h == 0 ? null : new Page(h, owner()); + } + + public SheetCell asSheetCell() { + long h = asSheetCellNative(handle()); + return h == 0 ? null : new SheetCell(h, owner()); + } + + public MasterPage asMasterPage() { + long h = asMasterPageNative(handle()); + return h == 0 ? null : new MasterPage(h, owner()); + } + + public LineBreak asLineBreak() { + long h = asLineBreakNative(handle()); + return h == 0 ? null : new LineBreak(h, owner()); + } + + public Paragraph asParagraph() { + long h = asParagraphNative(handle()); + return h == 0 ? null : new Paragraph(h, owner()); + } + + public Span asSpan() { + long h = asSpanNative(handle()); + return h == 0 ? null : new Span(h, owner()); + } + + public Text asText() { + long h = asTextNative(handle()); + return h == 0 ? null : new Text(h, owner()); + } + + public Link asLink() { + long h = asLinkNative(handle()); + return h == 0 ? null : new Link(h, owner()); + } + + public Bookmark asBookmark() { + long h = asBookmarkNative(handle()); + return h == 0 ? null : new Bookmark(h, owner()); + } + + public ListItem asListItem() { + long h = asListItemNative(handle()); + return h == 0 ? null : new ListItem(h, owner()); + } + + public Table asTable() { + long h = asTableNative(handle()); + return h == 0 ? null : new Table(h, owner()); + } + + public TableColumn asTableColumn() { + long h = asTableColumnNative(handle()); + return h == 0 ? null : new TableColumn(h, owner()); + } + + public TableRow asTableRow() { + long h = asTableRowNative(handle()); + return h == 0 ? null : new TableRow(h, owner()); + } + + public TableCell asTableCell() { + long h = asTableCellNative(handle()); + return h == 0 ? null : new TableCell(h, owner()); + } + + public Frame asFrame() { + long h = asFrameNative(handle()); + return h == 0 ? null : new Frame(h, owner()); + } + + public Rect asRect() { + long h = asRectNative(handle()); + return h == 0 ? null : new Rect(h, owner()); + } + + public Line asLine() { + long h = asLineNative(handle()); + return h == 0 ? null : new Line(h, owner()); + } + + public Circle asCircle() { + long h = asCircleNative(handle()); + return h == 0 ? null : new Circle(h, owner()); + } + + public CustomShape asCustomShape() { + long h = asCustomShapeNative(handle()); + return h == 0 ? null : new CustomShape(h, owner()); + } + + public Image asImage() { + long h = asImageNative(handle()); + return h == 0 ? null : new Image(h, owner()); + } + + final Element wrap(long handle) { + return handle == 0 ? null : new Element(handle, owner()); + } + + final List wrapAll(long[] handles) { + List result = new ArrayList<>(handles.length); + for (long h : handles) { + result.add(new Element(h, owner())); + } + return result; + } + + static native void destroy(long handle); + + private native int typeNative(long handle); + + private native long parentNative(long handle); + + private native long firstChildNative(long handle); + + private native long previousSiblingNative(long handle); + + private native long nextSiblingNative(long handle); + + private native boolean isUniqueNative(long handle); + + private native boolean isSelfLocatableNative(long handle); + + private native boolean isEditableNative(long handle); + + private native boolean isSameNative(long handle, long otherHandle); + + private native long documentPathNative(long handle); + + private native long navigatePathNative(long handle, long pathHandle); + + private native long asTextRootNative(long handle); + + private native long asSlideNative(long handle); + + private native long asSheetNative(long handle); + + private native long asPageNative(long handle); + + private native long asSheetCellNative(long handle); + + private native long asMasterPageNative(long handle); + + private native long asLineBreakNative(long handle); + + private native long asParagraphNative(long handle); + + private native long asSpanNative(long handle); + + private native long asTextNative(long handle); + + private native long asLinkNative(long handle); + + private native long asBookmarkNative(long handle); + + private native long asListItemNative(long handle); + + private native long asTableNative(long handle); + + private native long asTableColumnNative(long handle); + + private native long asTableRowNative(long handle); + + private native long asTableCellNative(long handle); + + private native long asFrameNative(long handle); + + private native long asRectNative(long handle); + + private native long asLineNative(long handle); + + private native long asCircleNative(long handle); + + private native long asCustomShapeNative(long handle); + + private native long asImageNative(long handle); +} diff --git a/jni/java/app/opendocument/core/ElementType.java b/jni/java/app/opendocument/core/ElementType.java new file mode 100644 index 00000000..92271649 --- /dev/null +++ b/jni/java/app/opendocument/core/ElementType.java @@ -0,0 +1,17 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::ElementType}; constant order must match the C++ declaration. */ +public enum ElementType { + NONE, ROOT, SLIDE, SHEET, PAGE, MASTER_PAGE, SHEET_CELL, TEXT, LINE_BREAK, + PAGE_BREAK, PARAGRAPH, SPAN, LINK, BOOKMARK, LIST, LIST_ITEM, TABLE, + TABLE_COLUMN, TABLE_ROW, TABLE_CELL, FRAME, IMAGE, RECT, LINE, CIRCLE, + CUSTOM_SHAPE, GROUP; + + static ElementType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/EncryptionState.java b/jni/java/app/opendocument/core/EncryptionState.java new file mode 100644 index 00000000..e7b12377 --- /dev/null +++ b/jni/java/app/opendocument/core/EncryptionState.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::EncryptionState}; constant order must match the C++ declaration. */ +public enum EncryptionState { + UNKNOWN, NOT_ENCRYPTED, ENCRYPTED, DECRYPTED; + + static EncryptionState fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/File.java b/jni/java/app/opendocument/core/File.java new file mode 100644 index 00000000..3bf585f4 --- /dev/null +++ b/jni/java/app/opendocument/core/File.java @@ -0,0 +1,52 @@ +package app.opendocument.core; + +/** An undecoded file. Mirrors {@code odr::File}. */ +public final class File extends NativeResource { + static { + NativeLibrary.load(); + } + + File(long handle) { + super(handle, null, File::destroy); + } + + public File(String path) { + this(create(path)); + } + + public FileLocation location() { + return FileLocation.fromNative(locationNative(handle())); + } + + public long size() { + return sizeNative(handle()); + } + + /** Path on disk; {@code null} for in-memory files. */ + public String diskPath() { + return diskPathNative(handle()); + } + + /** Reads the whole file into a byte array. */ + public byte[] read() { + return readNative(handle()); + } + + public void copy(String path) { + copyNative(handle(), path); + } + + private static native long create(String path); + + private static native void destroy(long handle); + + private native int locationNative(long handle); + + private native long sizeNative(long handle); + + private native String diskPathNative(long handle); + + private native byte[] readNative(long handle); + + private native void copyNative(long handle, String path); +} diff --git a/jni/java/app/opendocument/core/FileCategory.java b/jni/java/app/opendocument/core/FileCategory.java new file mode 100644 index 00000000..975d9861 --- /dev/null +++ b/jni/java/app/opendocument/core/FileCategory.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FileCategory}; constant order must match the C++ declaration. */ +public enum FileCategory { + UNKNOWN, TEXT, IMAGE, ARCHIVE, DOCUMENT, FONT; + + static FileCategory fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/FileLocation.java b/jni/java/app/opendocument/core/FileLocation.java new file mode 100644 index 00000000..caf3f73d --- /dev/null +++ b/jni/java/app/opendocument/core/FileLocation.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FileLocation}; constant order must match the C++ declaration. */ +public enum FileLocation { + MEMORY, DISK; + + static FileLocation fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/FileMeta.java b/jni/java/app/opendocument/core/FileMeta.java new file mode 100644 index 00000000..6b460383 --- /dev/null +++ b/jni/java/app/opendocument/core/FileMeta.java @@ -0,0 +1,17 @@ +package app.opendocument.core; + +/** Meta information about a file. Mirrors {@code odr::FileMeta}. */ +public final class FileMeta { + public final FileType type; + public final String mimetype; + public final boolean passwordEncrypted; + /** {@code null} when the file is not a document. */ + public final DocumentMeta documentMeta; + + FileMeta(int type, String mimetype, boolean passwordEncrypted, DocumentMeta documentMeta) { + this.type = FileType.fromNative(type); + this.mimetype = mimetype; + this.passwordEncrypted = passwordEncrypted; + this.documentMeta = documentMeta; + } +} diff --git a/jni/java/app/opendocument/core/FileType.java b/jni/java/app/opendocument/core/FileType.java new file mode 100644 index 00000000..61e58e1c --- /dev/null +++ b/jni/java/app/opendocument/core/FileType.java @@ -0,0 +1,23 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FileType}; constant order must match the C++ declaration. */ +public enum FileType { + UNKNOWN, OPENDOCUMENT_TEXT, OPENDOCUMENT_PRESENTATION, + OPENDOCUMENT_SPREADSHEET, OPENDOCUMENT_GRAPHICS, OFFICE_OPEN_XML_DOCUMENT, + OFFICE_OPEN_XML_PRESENTATION, OFFICE_OPEN_XML_WORKBOOK, + OFFICE_OPEN_XML_ENCRYPTED, LEGACY_WORD_DOCUMENT, + LEGACY_POWERPOINT_PRESENTATION, LEGACY_EXCEL_WORKSHEETS, WORD_PERFECT, + RICH_TEXT_FORMAT, PORTABLE_DOCUMENT_FORMAT, TEXT_FILE, + COMMA_SEPARATED_VALUES, JAVASCRIPT_OBJECT_NOTATION, MARKDOWN, ZIP, + COMPOUND_FILE_BINARY_FORMAT, PORTABLE_NETWORK_GRAPHICS, + GRAPHICS_INTERCHANGE_FORMAT, JPEG, BITMAP_IMAGE_FILE, STARVIEW_METAFILE, + TRUETYPE_FONT, OPENTYPE_FONT; + + static FileType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/FileWalker.java b/jni/java/app/opendocument/core/FileWalker.java new file mode 100644 index 00000000..b373234a --- /dev/null +++ b/jni/java/app/opendocument/core/FileWalker.java @@ -0,0 +1,58 @@ +package app.opendocument.core; + +/** Walks the entries of a {@link Filesystem}. Mirrors {@code odr::FileWalker}. */ +public final class FileWalker extends NativeResource { + FileWalker(long handle, Object owner) { + super(handle, owner, FileWalker::destroy); + } + + public boolean end() { + return endNative(handle()); + } + + public int depth() { + return depthNative(handle()); + } + + public String path() { + return pathNative(handle()); + } + + public boolean isFile() { + return isFileNative(handle()); + } + + public boolean isDirectory() { + return isDirectoryNative(handle()); + } + + public void pop() { + popNative(handle()); + } + + public void next() { + nextNative(handle()); + } + + public void flatNext() { + flatNextNative(handle()); + } + + private static native void destroy(long handle); + + private native boolean endNative(long handle); + + private native int depthNative(long handle); + + private native String pathNative(long handle); + + private native boolean isFileNative(long handle); + + private native boolean isDirectoryNative(long handle); + + private native void popNative(long handle); + + private native void nextNative(long handle); + + private native void flatNextNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Filesystem.java b/jni/java/app/opendocument/core/Filesystem.java new file mode 100644 index 00000000..07579359 --- /dev/null +++ b/jni/java/app/opendocument/core/Filesystem.java @@ -0,0 +1,40 @@ +package app.opendocument.core; + +/** A readable filesystem. Mirrors {@code odr::Filesystem}. */ +public final class Filesystem extends NativeResource { + Filesystem(long handle, Object owner) { + super(handle, owner, Filesystem::destroy); + } + + public boolean exists(String path) { + return existsNative(handle(), path); + } + + public boolean isFile(String path) { + return isFileNative(handle(), path); + } + + public boolean isDirectory(String path) { + return isDirectoryNative(handle(), path); + } + + public FileWalker fileWalker(String path) { + return new FileWalker(fileWalkerNative(handle(), path), this); + } + + public File open(String path) { + return new File(openNative(handle(), path)); + } + + private static native void destroy(long handle); + + private native boolean existsNative(long handle, String path); + + private native boolean isFileNative(long handle, String path); + + private native boolean isDirectoryNative(long handle, String path); + + private native long fileWalkerNative(long handle, String path); + + private native long openNative(long handle, String path); +} diff --git a/jni/java/app/opendocument/core/FontFile.java b/jni/java/app/opendocument/core/FontFile.java new file mode 100644 index 00000000..f8557ebd --- /dev/null +++ b/jni/java/app/opendocument/core/FontFile.java @@ -0,0 +1,15 @@ +package app.opendocument.core; + +/** A decoded font file. Mirrors {@code odr::FontFile}. */ +public final class FontFile extends DecodedFile { + FontFile(long handle) { + super(handle); + } + + /** Reads the font data into a byte array. */ + public byte[] read() { + return readNative(handle()); + } + + private native byte[] readNative(long handle); +} diff --git a/jni/java/app/opendocument/core/FontPosition.java b/jni/java/app/opendocument/core/FontPosition.java new file mode 100644 index 00000000..cfe53e5a --- /dev/null +++ b/jni/java/app/opendocument/core/FontPosition.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FontPosition}; constant order must match the C++ declaration. */ +public enum FontPosition { + NORMAL, SUPER, SUB; + + static FontPosition fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/FontStyle.java b/jni/java/app/opendocument/core/FontStyle.java new file mode 100644 index 00000000..dae3b5d0 --- /dev/null +++ b/jni/java/app/opendocument/core/FontStyle.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FontStyle}; constant order must match the C++ declaration. */ +public enum FontStyle { + NORMAL, ITALIC; + + static FontStyle fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/FontWeight.java b/jni/java/app/opendocument/core/FontWeight.java new file mode 100644 index 00000000..58e46a4a --- /dev/null +++ b/jni/java/app/opendocument/core/FontWeight.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::FontWeight}; constant order must match the C++ declaration. */ +public enum FontWeight { + NORMAL, BOLD; + + static FontWeight fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/Frame.java b/jni/java/app/opendocument/core/Frame.java new file mode 100644 index 00000000..b0ba7789 --- /dev/null +++ b/jni/java/app/opendocument/core/Frame.java @@ -0,0 +1,50 @@ +package app.opendocument.core; + +/** Frame element. Mirrors {@code odr::Frame}; geometry values may be {@code null}. */ +public final class Frame extends Element { + Frame(long handle, Object owner) { + super(handle, owner); + } + + public AnchorType anchorType() { + return AnchorType.fromNative(anchorTypeNative(handle())); + } + + public String x() { + return xNative(handle()); + } + + public String y() { + return yNative(handle()); + } + + public String width() { + return widthNative(handle()); + } + + public String height() { + return heightNative(handle()); + } + + public String zIndex() { + return zIndexNative(handle()); + } + + public GraphicStyle style() { + return styleNative(handle()); + } + + private native int anchorTypeNative(long handle); + + private native String xNative(long handle); + + private native String yNative(long handle); + + private native String widthNative(long handle); + + private native String heightNative(long handle); + + private native String zIndexNative(long handle); + + private native GraphicStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/GlobalParams.java b/jni/java/app/opendocument/core/GlobalParams.java new file mode 100644 index 00000000..f449582a --- /dev/null +++ b/jni/java/app/opendocument/core/GlobalParams.java @@ -0,0 +1,30 @@ +package app.opendocument.core; + +/** Global resource paths of the library. Mirrors {@code odr::GlobalParams}. */ +public final class GlobalParams { + static { + NativeLibrary.load(); + } + + public static native String odrCoreDataPath(); + + public static native String fontconfigDataPath(); + + public static native String popplerDataPath(); + + public static native String pdf2htmlexDataPath(); + + public static native String libmagicDatabasePath(); + + public static native void setOdrCoreDataPath(String path); + + public static native void setFontconfigDataPath(String path); + + public static native void setPopplerDataPath(String path); + + public static native void setPdf2htmlexDataPath(String path); + + public static native void setLibmagicDatabasePath(String path); + + private GlobalParams() {} +} diff --git a/jni/java/app/opendocument/core/GraphicStyle.java b/jni/java/app/opendocument/core/GraphicStyle.java new file mode 100644 index 00000000..c186a1b3 --- /dev/null +++ b/jni/java/app/opendocument/core/GraphicStyle.java @@ -0,0 +1,19 @@ +package app.opendocument.core; + +/** Style of a graphic. Mirrors {@code odr::GraphicStyle}; fields may be {@code null}. */ +public final class GraphicStyle { + public final Measure strokeWidth; + public final Color strokeColor; + public final Color fillColor; + public final VerticalAlign verticalAlign; + public final TextWrap textWrap; + + GraphicStyle( + Measure strokeWidth, Color strokeColor, Color fillColor, int verticalAlign, int textWrap) { + this.strokeWidth = strokeWidth; + this.strokeColor = strokeColor; + this.fillColor = fillColor; + this.verticalAlign = VerticalAlign.fromNative(verticalAlign); + this.textWrap = TextWrap.fromNative(textWrap); + } +} diff --git a/jni/java/app/opendocument/core/HorizontalAlign.java b/jni/java/app/opendocument/core/HorizontalAlign.java new file mode 100644 index 00000000..6a4707be --- /dev/null +++ b/jni/java/app/opendocument/core/HorizontalAlign.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::HorizontalAlign}; constant order must match the C++ declaration. */ +public enum HorizontalAlign { + LEFT, CENTER, RIGHT; + + static HorizontalAlign fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/Html.java b/jni/java/app/opendocument/core/Html.java new file mode 100644 index 00000000..f6519088 --- /dev/null +++ b/jni/java/app/opendocument/core/Html.java @@ -0,0 +1,81 @@ +package app.opendocument.core; + +import java.util.List; + +/** + * Entry points for HTML translation (mirrors the {@code odr::html} namespace) + * and the offline output of {@link HtmlService#bringOffline}. + */ +public final class Html { + static { + NativeLibrary.load(); + } + + private final HtmlConfig config; + private final List pages; + + Html(HtmlConfig config, HtmlPage[] pages) { + this.config = config; + this.pages = List.of(pages); + } + + public HtmlConfig config() { + return config; + } + + public List pages() { + return pages; + } + + /** Rendered HTML of a single view plus the resources it references. */ + public static final class Content { + public final String html; + public final List resources; + + Content(String html, LocatedResource[] resources) { + this.html = html; + this.resources = List.of(resources); + } + } + + /** A resource together with its location; {@code location} may be {@code null}. */ + public static final class LocatedResource { + public final HtmlResource resource; + public final String location; + + LocatedResource(HtmlResource resource, String location) { + this.resource = resource; + this.location = location; + } + } + + /** Translates a decoded file to HTML. */ + public static HtmlService translate(DecodedFile file, String cachePath, HtmlConfig config) { + return new HtmlService(translateFile(file.handle(), cachePath, config), file); + } + + /** Translates a document to HTML. */ + public static HtmlService translate(Document document, String cachePath, HtmlConfig config) { + return new HtmlService(translateDocument(document.handle(), cachePath, config), document); + } + + /** Translates a filesystem to HTML. */ + public static HtmlService translate(Filesystem filesystem, String cachePath, HtmlConfig config) { + return new HtmlService(translateFilesystem(filesystem.handle(), cachePath, config), filesystem); + } + + /** Applies a diff (produced by the browser-side editor) to a document. */ + public static void edit(Document document, String diff) { + editDocument(document.handle(), diff); + } + + private static native long translateFile(long fileHandle, String cachePath, HtmlConfig config); + + private static native long translateDocument( + long documentHandle, String cachePath, HtmlConfig config); + + private static native long translateFilesystem( + long filesystemHandle, String cachePath, HtmlConfig config); + + private static native void editDocument(long documentHandle, String diff); +} diff --git a/jni/java/app/opendocument/core/HtmlConfig.java b/jni/java/app/opendocument/core/HtmlConfig.java new file mode 100644 index 00000000..fe926bc9 --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlConfig.java @@ -0,0 +1,54 @@ +package app.opendocument.core; + +/** + * HTML output configuration. Mirrors {@code odr::HtmlConfig}; field defaults + * match the C++ defaults. The C++ {@code resource_locator} is not exposed; the + * standard resource locator is always used. + */ +public final class HtmlConfig { + public String documentOutputFileName = "document.html"; + + public String slideOutputFileName = "slide{index}.html"; + public String sheetOutputFileName = "sheet{index}.html"; + public String pageOutputFileName = "page{index}.html"; + + public boolean embedImages = true; + public boolean embedShippedResources = true; + + /** {@code null} keeps the native default (the odr core data path). */ + public String resourcePath; + public boolean relativeResourcePaths = true; + + public boolean editable = false; + + public boolean textDocumentMargin = false; + + /** {@code null} disables the spreadsheet limit. */ + public TableDimensions spreadsheetLimit = new TableDimensions(10000, 500); + public boolean spreadsheetLimitByContent = true; + public HtmlTableGridlines spreadsheetGridlines = HtmlTableGridlines.SOFT; + + public boolean formatHtml = false; + public int htmlIndent = 1; + public String htmlIndentString = "\t"; + + public String backgroundImageFormat = "png"; + public double backgroundImageDpi = 144.0; + + public int pageRangeBegin = 0; + /** {@code null} renders to the end of the document. */ + public Integer pageRangeEnd; + + public PdfTextMode pdfTextMode = PdfTextMode.DUAL_LAYER; + public String[] pdfDualLayerFallbackFonts = { + "Arial", "Helvetica", "Liberation Sans", "DejaVu Sans", "Nimbus Sans" + }; + public double pdfDualLayerFallbackFontSizeAdjust = 0.5; + + public boolean noDrm = false; + + public boolean embedOutline = false; + + /** {@code null} keeps output in the cache directory. */ + public String outputPath; +} diff --git a/jni/java/app/opendocument/core/HtmlPage.java b/jni/java/app/opendocument/core/HtmlPage.java new file mode 100644 index 00000000..a9fb6751 --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlPage.java @@ -0,0 +1,17 @@ +package app.opendocument.core; + +/** Name and path of a rendered HTML page. Mirrors {@code odr::HtmlPage}. */ +public final class HtmlPage { + public final String name; + public final String path; + + HtmlPage(String name, String path) { + this.name = name; + this.path = path; + } + + @Override + public String toString() { + return "HtmlPage(name='" + name + "', path='" + path + "')"; + } +} diff --git a/jni/java/app/opendocument/core/HtmlResource.java b/jni/java/app/opendocument/core/HtmlResource.java new file mode 100644 index 00000000..866da28a --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlResource.java @@ -0,0 +1,67 @@ +package app.opendocument.core; + +/** A resource referenced by rendered HTML. Mirrors {@code odr::HtmlResource}. */ +public final class HtmlResource extends NativeResource { + HtmlResource(long handle) { + super(handle, null, HtmlResource::destroy); + } + + public HtmlResourceType type() { + return HtmlResourceType.fromNative(typeNative(handle())); + } + + public String mimeType() { + return mimeTypeNative(handle()); + } + + public String name() { + return nameNative(handle()); + } + + public String path() { + return pathNative(handle()); + } + + /** The backing file; {@code null} if not file-backed. */ + public File file() { + long h = fileNative(handle()); + return h == 0 ? null : new File(h); + } + + public boolean isShipped() { + return isShippedNative(handle()); + } + + public boolean isExternal() { + return isExternalNative(handle()); + } + + public boolean isAccessible() { + return isAccessibleNative(handle()); + } + + /** Reads the resource content into a byte array. */ + public byte[] read() { + return readNative(handle()); + } + + private static native void destroy(long handle); + + private native int typeNative(long handle); + + private native String mimeTypeNative(long handle); + + private native String nameNative(long handle); + + private native String pathNative(long handle); + + private native long fileNative(long handle); + + private native boolean isShippedNative(long handle); + + private native boolean isExternalNative(long handle); + + private native boolean isAccessibleNative(long handle); + + private native byte[] readNative(long handle); +} diff --git a/jni/java/app/opendocument/core/HtmlResourceType.java b/jni/java/app/opendocument/core/HtmlResourceType.java new file mode 100644 index 00000000..8a5dd3db --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlResourceType.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::HtmlResourceType}; constant order must match the C++ declaration. */ +public enum HtmlResourceType { + HTML_FRAGMENT, CSS, JS, IMAGE, FONT; + + static HtmlResourceType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/HtmlService.java b/jni/java/app/opendocument/core/HtmlService.java new file mode 100644 index 00000000..3b71b00a --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlService.java @@ -0,0 +1,84 @@ +package app.opendocument.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * Lazily renders a translated file; views materialize on access. Mirrors + * {@code odr::HtmlService}. Obtained via {@link Html#translate}. + */ +public final class HtmlService extends NativeResource { + HtmlService(long handle, Object owner) { + super(handle, owner, HtmlService::destroy); + } + + public HtmlConfig config() { + return configNative(handle()); + } + + public List listViews() { + long[] handles = listViewsNative(handle()); + List result = new ArrayList<>(handles.length); + for (long h : handles) { + result.add(new HtmlView(h, this)); + } + return result; + } + + /** Renders everything into the cache eagerly. */ + public void warmup() { + warmupNative(handle()); + } + + public boolean exists(String path) { + return existsNative(handle(), path); + } + + public String mimetype(String path) { + return mimetypeNative(handle(), path); + } + + /** Renders one view path into a byte array. */ + public byte[] write(String path) { + return writeNative(handle(), path); + } + + /** Renders one view path; returns the HTML and its resources. */ + public Html.Content writeHtml(String path) { + return writeHtmlNative(handle(), path); + } + + /** Renders all views into {@code outputPath} as self-contained files. */ + public Html bringOffline(String outputPath) { + return bringOfflineNative(handle(), outputPath); + } + + /** Renders the given views into {@code outputPath} as self-contained files. */ + public Html bringOffline(String outputPath, List views) { + long[] handles = new long[views.size()]; + for (int i = 0; i < handles.length; i++) { + handles[i] = views.get(i).handle(); + } + return bringOfflineViewsNative(handle(), outputPath, handles); + } + + private static native void destroy(long handle); + + private native HtmlConfig configNative(long handle); + + private native long[] listViewsNative(long handle); + + private native void warmupNative(long handle); + + private native boolean existsNative(long handle, String path); + + private native String mimetypeNative(long handle, String path); + + private native byte[] writeNative(long handle, String path); + + private native Html.Content writeHtmlNative(long handle, String path); + + private native Html bringOfflineNative(long handle, String outputPath); + + private native Html bringOfflineViewsNative(long handle, String outputPath, long[] viewHandles); +} diff --git a/jni/java/app/opendocument/core/HtmlTableGridlines.java b/jni/java/app/opendocument/core/HtmlTableGridlines.java new file mode 100644 index 00000000..fcadafa6 --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlTableGridlines.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::HtmlTableGridlines}; constant order must match the C++ declaration. */ +public enum HtmlTableGridlines { + NONE, SOFT, HARD; + + static HtmlTableGridlines fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/HtmlView.java b/jni/java/app/opendocument/core/HtmlView.java new file mode 100644 index 00000000..cd235ea4 --- /dev/null +++ b/jni/java/app/opendocument/core/HtmlView.java @@ -0,0 +1,48 @@ +package app.opendocument.core; + +/** One view (page/slide/sheet) of an {@link HtmlService}. Mirrors {@code odr::HtmlView}. */ +public final class HtmlView extends NativeResource { + HtmlView(long handle, Object owner) { + super(handle, owner, HtmlView::destroy); + } + + public String name() { + return nameNative(handle()); + } + + public long index() { + return indexNative(handle()); + } + + public String path() { + return pathNative(handle()); + } + + public HtmlConfig config() { + return configNative(handle()); + } + + /** Renders this view; returns the HTML and its resources. */ + public Html.Content writeHtml() { + return writeHtmlNative(handle()); + } + + /** Renders this view into {@code outputPath} as a self-contained file. */ + public Html bringOffline(String outputPath) { + return bringOfflineNative(handle(), outputPath); + } + + private static native void destroy(long handle); + + private native String nameNative(long handle); + + private native long indexNative(long handle); + + private native String pathNative(long handle); + + private native HtmlConfig configNative(long handle); + + private native Html.Content writeHtmlNative(long handle); + + private native Html bringOfflineNative(long handle, String outputPath); +} diff --git a/jni/java/app/opendocument/core/HttpServer.java b/jni/java/app/opendocument/core/HttpServer.java new file mode 100644 index 00000000..fad1ebd4 --- /dev/null +++ b/jni/java/app/opendocument/core/HttpServer.java @@ -0,0 +1,71 @@ +package app.opendocument.core; + +import java.util.ArrayList; +import java.util.List; + +/** + * Serves translated files over HTTP. Mirrors {@code odr::HttpServer}. Only + * available when the native library was built with the HTTP server + * ({@link Odr#hasHttpServer()}). + */ +public final class HttpServer extends NativeResource { + /** Mirrors {@code odr::HttpServer::Config}. */ + public static final class Config { + public String cachePath = "/tmp/odr"; + } + + public HttpServer(Config config) { + super(create(config.cachePath), null, HttpServer::destroy); + } + + public Config config() { + Config config = new Config(); + config.cachePath = cachePathNative(handle()); + return config; + } + + /** Hosts the service under {@code //}. */ + public void connectService(HtmlService service, String prefix) { + connectServiceNative(handle(), service.handle(), prefix); + } + + /** Translates a decoded file and hosts it under {@code /file//}. */ + public List serveFile(DecodedFile file, String prefix, HtmlConfig config) { + long[] handles = serveFileNative(handle(), file.handle(), prefix, config); + List result = new ArrayList<>(handles.length); + for (long h : handles) { + result.add(new HtmlView(h, this)); + } + return result; + } + + /** Blocks serving requests until {@link #stop()} is called from another thread. */ + public void listen(String host, int port) { + listenNative(handle(), host, port); + } + + public void clear() { + clearNative(handle()); + } + + public void stop() { + stopNative(handle()); + } + + private static native long create(String cachePath); + + private static native void destroy(long handle); + + private native String cachePathNative(long handle); + + private native void connectServiceNative(long handle, long serviceHandle, String prefix); + + private native long[] serveFileNative( + long handle, long fileHandle, String prefix, HtmlConfig config); + + private native void listenNative(long handle, String host, int port); + + private native void clearNative(long handle); + + private native void stopNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Image.java b/jni/java/app/opendocument/core/Image.java new file mode 100644 index 00000000..15c2459b --- /dev/null +++ b/jni/java/app/opendocument/core/Image.java @@ -0,0 +1,28 @@ +package app.opendocument.core; + +/** Image element. Mirrors {@code odr::Image}. */ +public final class Image extends Element { + Image(long handle, Object owner) { + super(handle, owner); + } + + public boolean isInternal() { + return isInternalNative(handle()); + } + + /** The image file for internal images; {@code null} otherwise. */ + public File file() { + long h = fileNative(handle()); + return h == 0 ? null : new File(h); + } + + public String href() { + return hrefNative(handle()); + } + + private native boolean isInternalNative(long handle); + + private native long fileNative(long handle); + + private native String hrefNative(long handle); +} diff --git a/jni/java/app/opendocument/core/ImageFile.java b/jni/java/app/opendocument/core/ImageFile.java new file mode 100644 index 00000000..8d029f26 --- /dev/null +++ b/jni/java/app/opendocument/core/ImageFile.java @@ -0,0 +1,15 @@ +package app.opendocument.core; + +/** A decoded image file. Mirrors {@code odr::ImageFile}. */ +public final class ImageFile extends DecodedFile { + ImageFile(long handle) { + super(handle); + } + + /** Reads the image data into a byte array. */ + public byte[] read() { + return readNative(handle()); + } + + private native byte[] readNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Line.java b/jni/java/app/opendocument/core/Line.java new file mode 100644 index 00000000..e8820c80 --- /dev/null +++ b/jni/java/app/opendocument/core/Line.java @@ -0,0 +1,38 @@ +package app.opendocument.core; + +/** Line element. Mirrors {@code odr::Line}. */ +public final class Line extends Element { + Line(long handle, Object owner) { + super(handle, owner); + } + + public String x1() { + return x1Native(handle()); + } + + public String y1() { + return y1Native(handle()); + } + + public String x2() { + return x2Native(handle()); + } + + public String y2() { + return y2Native(handle()); + } + + public GraphicStyle style() { + return styleNative(handle()); + } + + private native String x1Native(long handle); + + private native String y1Native(long handle); + + private native String x2Native(long handle); + + private native String y2Native(long handle); + + private native GraphicStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/LineBreak.java b/jni/java/app/opendocument/core/LineBreak.java new file mode 100644 index 00000000..501b895c --- /dev/null +++ b/jni/java/app/opendocument/core/LineBreak.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Line break element. Mirrors {@code odr::LineBreak}. */ +public final class LineBreak extends Element { + LineBreak(long handle, Object owner) { + super(handle, owner); + } + + public TextStyle style() { + return styleNative(handle()); + } + + private native TextStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Link.java b/jni/java/app/opendocument/core/Link.java new file mode 100644 index 00000000..e1377840 --- /dev/null +++ b/jni/java/app/opendocument/core/Link.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Link element. Mirrors {@code odr::Link}. */ +public final class Link extends Element { + Link(long handle, Object owner) { + super(handle, owner); + } + + public String href() { + return hrefNative(handle()); + } + + private native String hrefNative(long handle); +} diff --git a/jni/java/app/opendocument/core/ListItem.java b/jni/java/app/opendocument/core/ListItem.java new file mode 100644 index 00000000..5e8c0805 --- /dev/null +++ b/jni/java/app/opendocument/core/ListItem.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** List item element. Mirrors {@code odr::ListItem}. */ +public final class ListItem extends Element { + ListItem(long handle, Object owner) { + super(handle, owner); + } + + public TextStyle style() { + return styleNative(handle()); + } + + private native TextStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/MasterPage.java b/jni/java/app/opendocument/core/MasterPage.java new file mode 100644 index 00000000..9483f0bb --- /dev/null +++ b/jni/java/app/opendocument/core/MasterPage.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Master page element. Mirrors {@code odr::MasterPage}. */ +public final class MasterPage extends Element { + MasterPage(long handle, Object owner) { + super(handle, owner); + } + + public PageLayout pageLayout() { + return pageLayoutNative(handle()); + } + + private native PageLayout pageLayoutNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Measure.java b/jni/java/app/opendocument/core/Measure.java new file mode 100644 index 00000000..2cff05de --- /dev/null +++ b/jni/java/app/opendocument/core/Measure.java @@ -0,0 +1,31 @@ +package app.opendocument.core; + +import java.util.Objects; + +/** A quantity: a magnitude and a unit of measure. Mirrors {@code odr::Measure}. */ +public final class Measure { + public final double magnitude; + public final String unit; + + public Measure(double magnitude, String unit) { + this.magnitude = magnitude; + this.unit = Objects.requireNonNull(unit); + } + + @Override + public boolean equals(Object other) { + return other instanceof Measure measure + && magnitude == measure.magnitude + && unit.equals(measure.unit); + } + + @Override + public int hashCode() { + return Objects.hash(magnitude, unit); + } + + @Override + public String toString() { + return magnitude + unit; + } +} diff --git a/jni/java/app/opendocument/core/NativeLibrary.java b/jni/java/app/opendocument/core/NativeLibrary.java new file mode 100644 index 00000000..044d30a9 --- /dev/null +++ b/jni/java/app/opendocument/core/NativeLibrary.java @@ -0,0 +1,25 @@ +package app.opendocument.core; + +/** + * Loads the {@code odr_jni} native library once per JVM. The library is + * resolved from {@code java.library.path}; the system property + * {@code app.opendocument.core.library} overrides it with an absolute path. + */ +final class NativeLibrary { + private static boolean loaded; + + static synchronized void load() { + if (loaded) { + return; + } + String explicit = System.getProperty("app.opendocument.core.library"); + if (explicit != null) { + System.load(explicit); + } else { + System.loadLibrary("odr_jni"); + } + loaded = true; + } + + private NativeLibrary() {} +} diff --git a/jni/java/app/opendocument/core/NativeResource.java b/jni/java/app/opendocument/core/NativeResource.java new file mode 100644 index 00000000..67534519 --- /dev/null +++ b/jni/java/app/opendocument/core/NativeResource.java @@ -0,0 +1,54 @@ +package app.opendocument.core; + +import java.lang.ref.Cleaner; +import java.util.function.LongConsumer; + +/** + * Owns a handle to a heap-allocated native object. The native object is freed + * on {@link #close()} or, at the latest, when the garbage collector reclaims + * this wrapper (via {@link Cleaner}). + * + *

Navigation results (e.g. document elements) keep the object they + * originate from reachable through {@code owner}, so a root object is not + * collected while handles into it are alive. + */ +public abstract class NativeResource implements AutoCloseable { + private static final Cleaner CLEANER = Cleaner.create(); + + private final long handle; + private final Object owner; + private final Cleaner.Cleanable cleanable; + private volatile boolean closed; + + NativeResource(long handle, Object owner, LongConsumer destroyer) { + this.handle = handle; + this.owner = owner; + this.cleanable = CLEANER.register(this, new Destroyer(handle, destroyer)); + } + + /** The native handle; valid only while this object is open. */ + final long handle() { + if (closed) { + throw new IllegalStateException(getClass().getSimpleName() + " is closed"); + } + return handle; + } + + final Object owner() { + return owner; + } + + /** Frees the native object early. Idempotent. */ + @Override + public void close() { + closed = true; + cleanable.clean(); + } + + private record Destroyer(long handle, LongConsumer destroy) implements Runnable { + @Override + public void run() { + destroy.accept(handle); + } + } +} diff --git a/jni/java/app/opendocument/core/Odr.java b/jni/java/app/opendocument/core/Odr.java new file mode 100644 index 00000000..002b0cc5 --- /dev/null +++ b/jni/java/app/opendocument/core/Odr.java @@ -0,0 +1,142 @@ +package app.opendocument.core; + +import java.util.ArrayList; +import java.util.List; + +/** Library-level functions. Mirrors the free functions in {@code odr/odr.hpp}. */ +public final class Odr { + static { + NativeLibrary.load(); + } + + /** Version of the underlying odrcore library. */ + public static native String version(); + + /** Git commit hash of the underlying odrcore library. */ + public static native String commitHash(); + + public static native boolean isDirty(); + + public static native boolean isDebug(); + + /** Identification string of the underlying odrcore library. */ + public static native String identify(); + + /** Whether the native library was built with the HTTP server. */ + public static native boolean hasHttpServer(); + + public static FileType fileTypeByFileExtension(String extension) { + return FileType.fromNative(fileTypeByFileExtensionNative(extension)); + } + + public static FileCategory fileCategoryByFileType(FileType type) { + return FileCategory.fromNative(fileCategoryByFileTypeNative(type.toNative())); + } + + public static DocumentType documentTypeByFileType(FileType type) { + return DocumentType.fromNative(documentTypeByFileTypeNative(type.toNative())); + } + + public static String fileTypeToString(FileType type) { + return fileTypeToStringNative(type.toNative()); + } + + public static String fileCategoryToString(FileCategory category) { + return fileCategoryToStringNative(category.toNative()); + } + + public static String documentTypeToString(DocumentType type) { + return documentTypeToStringNative(type.toNative()); + } + + public static FileType fileTypeByMimetype(String mimetype) { + return FileType.fromNative(fileTypeByMimetypeNative(mimetype)); + } + + public static String mimetypeByFileType(FileType type) { + return mimetypeByFileTypeNative(type.toNative()); + } + + public static String decoderEngineToString(DecoderEngine engine) { + return decoderEngineToStringNative(engine.toNative()); + } + + public static DecoderEngine decoderEngineByName(String name) { + return DecoderEngine.fromNative(decoderEngineByNameNative(name)); + } + + /** Determines the possible file types of a file. */ + public static List listFileTypes(String path) { + List result = new ArrayList<>(); + for (int code : listFileTypesNative(path)) { + result.add(FileType.fromNative(code)); + } + return result; + } + + /** Determines the decoder engines for a file type. */ + public static List listDecoderEngines(FileType as) { + List result = new ArrayList<>(); + for (int code : listDecoderEnginesNative(as.toNative())) { + result.add(DecoderEngine.fromNative(code)); + } + return result; + } + + /** Determines the MIME type of a file. */ + public static native String mimetype(String path); + + /** Opens and decodes a file. */ + public static DecodedFile open(String path) { + return new DecodedFile(openNative(path)); + } + + /** Opens and decodes a file as the given file type. */ + public static DecodedFile open(String path, FileType as) { + return new DecodedFile(openAsNative(path, as.toNative())); + } + + /** Opens and decodes a file with a decode preference. */ + public static DecodedFile open(String path, DecodePreference preference) { + return new DecodedFile( + openWithPreferenceNative( + path, + preference.asFileTypeNative(), + preference.withEngineNative(), + preference.fileTypePriorityNative(), + preference.enginePriorityNative())); + } + + private static native int fileTypeByFileExtensionNative(String extension); + + private static native int fileCategoryByFileTypeNative(int type); + + private static native int documentTypeByFileTypeNative(int type); + + private static native String fileTypeToStringNative(int type); + + private static native String fileCategoryToStringNative(int category); + + private static native String documentTypeToStringNative(int type); + + private static native int fileTypeByMimetypeNative(String mimetype); + + private static native String mimetypeByFileTypeNative(int type); + + private static native String decoderEngineToStringNative(int engine); + + private static native int decoderEngineByNameNative(String name); + + private static native int[] listFileTypesNative(String path); + + private static native int[] listDecoderEnginesNative(int as); + + private static native long openNative(String path); + + private static native long openAsNative(String path, int as); + + private static native long openWithPreferenceNative( + String path, int asFileType, int withEngine, int[] fileTypePriority, int[] enginePriority); + + private Odr() {} +} diff --git a/jni/java/app/opendocument/core/OdrException.java b/jni/java/app/opendocument/core/OdrException.java new file mode 100644 index 00000000..54d2ed08 --- /dev/null +++ b/jni/java/app/opendocument/core/OdrException.java @@ -0,0 +1,142 @@ +package app.opendocument.core; + +/** + * Base class for exceptions thrown by the native library. The subclasses + * mirror the typed exceptions in {@code odr/exceptions.hpp}; native errors + * without a dedicated subclass are thrown as plain {@code OdrException}. + */ +public class OdrException extends RuntimeException { + private static final long serialVersionUID = 1L; + + public OdrException(String message) { + super(message); + } + + public static final class UnsupportedOperation extends OdrException { + private static final long serialVersionUID = 1L; + + public UnsupportedOperation(String message) { + super(message); + } + } + + public static final class FileNotFound extends OdrException { + private static final long serialVersionUID = 1L; + + public FileNotFound(String message) { + super(message); + } + } + + public static final class UnknownFileType extends OdrException { + private static final long serialVersionUID = 1L; + + public UnknownFileType(String message) { + super(message); + } + } + + public static final class UnsupportedFileType extends OdrException { + private static final long serialVersionUID = 1L; + + public UnsupportedFileType(String message) { + super(message); + } + } + + public static final class UnknownDecoderEngine extends OdrException { + private static final long serialVersionUID = 1L; + + public UnknownDecoderEngine(String message) { + super(message); + } + } + + public static final class UnsupportedDecoderEngine extends OdrException { + private static final long serialVersionUID = 1L; + + public UnsupportedDecoderEngine(String message) { + super(message); + } + } + + public static final class FileReadError extends OdrException { + private static final long serialVersionUID = 1L; + + public FileReadError(String message) { + super(message); + } + } + + public static final class FileWriteError extends OdrException { + private static final long serialVersionUID = 1L; + + public FileWriteError(String message) { + super(message); + } + } + + public static final class NoDocumentFile extends OdrException { + private static final long serialVersionUID = 1L; + + public NoDocumentFile(String message) { + super(message); + } + } + + public static final class UnknownDocumentType extends OdrException { + private static final long serialVersionUID = 1L; + + public UnknownDocumentType(String message) { + super(message); + } + } + + public static final class UnsupportedCryptoAlgorithm extends OdrException { + private static final long serialVersionUID = 1L; + + public UnsupportedCryptoAlgorithm(String message) { + super(message); + } + } + + public static final class WrongPassword extends OdrException { + private static final long serialVersionUID = 1L; + + public WrongPassword(String message) { + super(message); + } + } + + public static final class DecryptionFailed extends OdrException { + private static final long serialVersionUID = 1L; + + public DecryptionFailed(String message) { + super(message); + } + } + + public static final class NotEncrypted extends OdrException { + private static final long serialVersionUID = 1L; + + public NotEncrypted(String message) { + super(message); + } + } + + public static final class FileEncrypted extends OdrException { + private static final long serialVersionUID = 1L; + + public FileEncrypted(String message) { + super(message); + } + } + + public static final class DocumentCopyProtected extends OdrException { + private static final long serialVersionUID = 1L; + + public DocumentCopyProtected(String message) { + super(message); + } + } +} diff --git a/jni/java/app/opendocument/core/Page.java b/jni/java/app/opendocument/core/Page.java new file mode 100644 index 00000000..db5720b5 --- /dev/null +++ b/jni/java/app/opendocument/core/Page.java @@ -0,0 +1,27 @@ +package app.opendocument.core; + +/** Page element of a drawing. Mirrors {@code odr::Page}. */ +public final class Page extends Element { + Page(long handle, Object owner) { + super(handle, owner); + } + + public String name() { + return nameNative(handle()); + } + + public PageLayout pageLayout() { + return pageLayoutNative(handle()); + } + + public MasterPage masterPage() { + long h = masterPageNative(handle()); + return h == 0 ? null : new MasterPage(h, owner()); + } + + private native String nameNative(long handle); + + private native PageLayout pageLayoutNative(long handle); + + private native long masterPageNative(long handle); +} diff --git a/jni/java/app/opendocument/core/PageLayout.java b/jni/java/app/opendocument/core/PageLayout.java new file mode 100644 index 00000000..9a320b6c --- /dev/null +++ b/jni/java/app/opendocument/core/PageLayout.java @@ -0,0 +1,16 @@ +package app.opendocument.core; + +/** Layout of a page. Mirrors {@code odr::PageLayout}; fields may be {@code null}. */ +public final class PageLayout { + public final Measure width; + public final Measure height; + public final PrintOrientation printOrientation; + public final DirectionalMeasure margin; + + PageLayout(Measure width, Measure height, int printOrientation, DirectionalMeasure margin) { + this.width = width; + this.height = height; + this.printOrientation = PrintOrientation.fromNative(printOrientation); + this.margin = margin; + } +} diff --git a/jni/java/app/opendocument/core/Paragraph.java b/jni/java/app/opendocument/core/Paragraph.java new file mode 100644 index 00000000..dedeb607 --- /dev/null +++ b/jni/java/app/opendocument/core/Paragraph.java @@ -0,0 +1,20 @@ +package app.opendocument.core; + +/** Paragraph element. Mirrors {@code odr::Paragraph}. */ +public final class Paragraph extends Element { + Paragraph(long handle, Object owner) { + super(handle, owner); + } + + public ParagraphStyle style() { + return styleNative(handle()); + } + + public TextStyle textStyle() { + return textStyleNative(handle()); + } + + private native ParagraphStyle styleNative(long handle); + + private native TextStyle textStyleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/ParagraphStyle.java b/jni/java/app/opendocument/core/ParagraphStyle.java new file mode 100644 index 00000000..4ea4b68b --- /dev/null +++ b/jni/java/app/opendocument/core/ParagraphStyle.java @@ -0,0 +1,16 @@ +package app.opendocument.core; + +/** Style of a paragraph. Mirrors {@code odr::ParagraphStyle}; fields may be {@code null}. */ +public final class ParagraphStyle { + public final TextAlign textAlign; + public final DirectionalMeasure margin; + public final Measure lineHeight; + public final Measure textIndent; + + ParagraphStyle(int textAlign, DirectionalMeasure margin, Measure lineHeight, Measure textIndent) { + this.textAlign = TextAlign.fromNative(textAlign); + this.margin = margin; + this.lineHeight = lineHeight; + this.textIndent = textIndent; + } +} diff --git a/jni/java/app/opendocument/core/PdfFile.java b/jni/java/app/opendocument/core/PdfFile.java new file mode 100644 index 00000000..e06f3fab --- /dev/null +++ b/jni/java/app/opendocument/core/PdfFile.java @@ -0,0 +1,16 @@ +package app.opendocument.core; + +/** A decoded PDF file. Mirrors {@code odr::PdfFile}. */ +public final class PdfFile extends DecodedFile { + PdfFile(long handle) { + super(handle); + } + + /** Returns a decrypted copy of this file. */ + @Override + public PdfFile decrypt(String password) { + return new PdfFile(decryptPdfFileNative(handle(), password)); + } + + private native long decryptPdfFileNative(long handle, String password); +} diff --git a/jni/java/app/opendocument/core/PdfTextMode.java b/jni/java/app/opendocument/core/PdfTextMode.java new file mode 100644 index 00000000..96298e47 --- /dev/null +++ b/jni/java/app/opendocument/core/PdfTextMode.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::PdfTextMode}; constant order must match the C++ declaration. */ +public enum PdfTextMode { + DUAL_LAYER, SINGLE_LAYER; + + static PdfTextMode fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/PrintOrientation.java b/jni/java/app/opendocument/core/PrintOrientation.java new file mode 100644 index 00000000..f875f71e --- /dev/null +++ b/jni/java/app/opendocument/core/PrintOrientation.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::PrintOrientation}; constant order must match the C++ declaration. */ +public enum PrintOrientation { + PORTRAIT, LANDSCAPE; + + static PrintOrientation fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/Rect.java b/jni/java/app/opendocument/core/Rect.java new file mode 100644 index 00000000..b11214f6 --- /dev/null +++ b/jni/java/app/opendocument/core/Rect.java @@ -0,0 +1,38 @@ +package app.opendocument.core; + +/** Rectangle element. Mirrors {@code odr::Rect}. */ +public final class Rect extends Element { + Rect(long handle, Object owner) { + super(handle, owner); + } + + public String x() { + return xNative(handle()); + } + + public String y() { + return yNative(handle()); + } + + public String width() { + return widthNative(handle()); + } + + public String height() { + return heightNative(handle()); + } + + public GraphicStyle style() { + return styleNative(handle()); + } + + private native String xNative(long handle); + + private native String yNative(long handle); + + private native String widthNative(long handle); + + private native String heightNative(long handle); + + private native GraphicStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Sheet.java b/jni/java/app/opendocument/core/Sheet.java new file mode 100644 index 00000000..86047071 --- /dev/null +++ b/jni/java/app/opendocument/core/Sheet.java @@ -0,0 +1,69 @@ +package app.opendocument.core; + +import java.util.List; + +/** Sheet element of a spreadsheet. Mirrors {@code odr::Sheet}. */ +public final class Sheet extends Element { + Sheet(long handle, Object owner) { + super(handle, owner); + } + + public String name() { + return nameNative(handle()); + } + + public TableDimensions dimensions() { + return dimensionsNative(handle()); + } + + /** Dimensions of the content, optionally clamped to {@code range}. */ + public TableDimensions content(TableDimensions range) { + if (range == null) { + return contentNative(handle(), -1, -1); + } + return contentNative(handle(), range.rows, range.columns); + } + + public SheetCell cell(int column, int row) { + long h = cellNative(handle(), column, row); + return h == 0 ? null : new SheetCell(h, owner()); + } + + public List shapes() { + return wrapAll(shapesNative(handle())); + } + + public TableStyle style() { + return styleNative(handle()); + } + + public TableColumnStyle columnStyle(int column) { + return columnStyleNative(handle(), column); + } + + public TableRowStyle rowStyle(int row) { + return rowStyleNative(handle(), row); + } + + public TableCellStyle cellStyle(int column, int row) { + return cellStyleNative(handle(), column, row); + } + + private native String nameNative(long handle); + + private native TableDimensions dimensionsNative(long handle); + + private native TableDimensions contentNative(long handle, int rows, int columns); + + private native long cellNative(long handle, int column, int row); + + private native long[] shapesNative(long handle); + + private native TableStyle styleNative(long handle); + + private native TableColumnStyle columnStyleNative(long handle, int column); + + private native TableRowStyle rowStyleNative(long handle, int row); + + private native TableCellStyle cellStyleNative(long handle, int column, int row); +} diff --git a/jni/java/app/opendocument/core/SheetCell.java b/jni/java/app/opendocument/core/SheetCell.java new file mode 100644 index 00000000..9fb0fe1f --- /dev/null +++ b/jni/java/app/opendocument/core/SheetCell.java @@ -0,0 +1,32 @@ +package app.opendocument.core; + +/** Cell element of a sheet. Mirrors {@code odr::SheetCell}. */ +public final class SheetCell extends Element { + SheetCell(long handle, Object owner) { + super(handle, owner); + } + + public TablePosition position() { + return positionNative(handle()); + } + + public boolean isCovered() { + return isCoveredNative(handle()); + } + + public TableDimensions span() { + return spanNative(handle()); + } + + public ValueType valueType() { + return ValueType.fromNative(valueTypeNative(handle())); + } + + private native TablePosition positionNative(long handle); + + private native boolean isCoveredNative(long handle); + + private native TableDimensions spanNative(long handle); + + private native int valueTypeNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Slide.java b/jni/java/app/opendocument/core/Slide.java new file mode 100644 index 00000000..9abd4d00 --- /dev/null +++ b/jni/java/app/opendocument/core/Slide.java @@ -0,0 +1,27 @@ +package app.opendocument.core; + +/** Slide element of a presentation. Mirrors {@code odr::Slide}. */ +public final class Slide extends Element { + Slide(long handle, Object owner) { + super(handle, owner); + } + + public String name() { + return nameNative(handle()); + } + + public PageLayout pageLayout() { + return pageLayoutNative(handle()); + } + + public MasterPage masterPage() { + long h = masterPageNative(handle()); + return h == 0 ? null : new MasterPage(h, owner()); + } + + private native String nameNative(long handle); + + private native PageLayout pageLayoutNative(long handle); + + private native long masterPageNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Span.java b/jni/java/app/opendocument/core/Span.java new file mode 100644 index 00000000..234e6267 --- /dev/null +++ b/jni/java/app/opendocument/core/Span.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Span element. Mirrors {@code odr::Span}. */ +public final class Span extends Element { + Span(long handle, Object owner) { + super(handle, owner); + } + + public TextStyle style() { + return styleNative(handle()); + } + + private native TextStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/Table.java b/jni/java/app/opendocument/core/Table.java new file mode 100644 index 00000000..ffe00979 --- /dev/null +++ b/jni/java/app/opendocument/core/Table.java @@ -0,0 +1,48 @@ +package app.opendocument.core; + +import java.util.List; + +/** Table element. Mirrors {@code odr::Table}. */ +public final class Table extends Element { + Table(long handle, Object owner) { + super(handle, owner); + } + + public TableRow firstRow() { + long h = firstRowNative(handle()); + return h == 0 ? null : new TableRow(h, owner()); + } + + public TableColumn firstColumn() { + long h = firstColumnNative(handle()); + return h == 0 ? null : new TableColumn(h, owner()); + } + + public List columns() { + return wrapAll(columnsNative(handle())); + } + + public List rows() { + return wrapAll(rowsNative(handle())); + } + + public TableDimensions dimensions() { + return dimensionsNative(handle()); + } + + public TableStyle style() { + return styleNative(handle()); + } + + private native long firstRowNative(long handle); + + private native long firstColumnNative(long handle); + + private native long[] columnsNative(long handle); + + private native long[] rowsNative(long handle); + + private native TableDimensions dimensionsNative(long handle); + + private native TableStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TableCell.java b/jni/java/app/opendocument/core/TableCell.java new file mode 100644 index 00000000..956a5011 --- /dev/null +++ b/jni/java/app/opendocument/core/TableCell.java @@ -0,0 +1,32 @@ +package app.opendocument.core; + +/** Table cell element. Mirrors {@code odr::TableCell}. */ +public final class TableCell extends Element { + TableCell(long handle, Object owner) { + super(handle, owner); + } + + public boolean isCovered() { + return isCoveredNative(handle()); + } + + public TableDimensions span() { + return spanNative(handle()); + } + + public ValueType valueType() { + return ValueType.fromNative(valueTypeNative(handle())); + } + + public TableCellStyle style() { + return styleNative(handle()); + } + + private native boolean isCoveredNative(long handle); + + private native TableDimensions spanNative(long handle); + + private native int valueTypeNative(long handle); + + private native TableCellStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TableCellStyle.java b/jni/java/app/opendocument/core/TableCellStyle.java new file mode 100644 index 00000000..978fbdab --- /dev/null +++ b/jni/java/app/opendocument/core/TableCellStyle.java @@ -0,0 +1,26 @@ +package app.opendocument.core; + +/** Style of a table cell. Mirrors {@code odr::TableCellStyle}; fields may be {@code null}. */ +public final class TableCellStyle { + public final HorizontalAlign horizontalAlign; + public final VerticalAlign verticalAlign; + public final Color backgroundColor; + public final DirectionalMeasure padding; + public final DirectionalString border; + public final Double textRotation; + + TableCellStyle( + int horizontalAlign, + int verticalAlign, + Color backgroundColor, + DirectionalMeasure padding, + DirectionalString border, + Double textRotation) { + this.horizontalAlign = HorizontalAlign.fromNative(horizontalAlign); + this.verticalAlign = VerticalAlign.fromNative(verticalAlign); + this.backgroundColor = backgroundColor; + this.padding = padding; + this.border = border; + this.textRotation = textRotation; + } +} diff --git a/jni/java/app/opendocument/core/TableColumn.java b/jni/java/app/opendocument/core/TableColumn.java new file mode 100644 index 00000000..24447eb9 --- /dev/null +++ b/jni/java/app/opendocument/core/TableColumn.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Table column element. Mirrors {@code odr::TableColumn}. */ +public final class TableColumn extends Element { + TableColumn(long handle, Object owner) { + super(handle, owner); + } + + public TableColumnStyle style() { + return styleNative(handle()); + } + + private native TableColumnStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TableColumnStyle.java b/jni/java/app/opendocument/core/TableColumnStyle.java new file mode 100644 index 00000000..49a2fd3c --- /dev/null +++ b/jni/java/app/opendocument/core/TableColumnStyle.java @@ -0,0 +1,10 @@ +package app.opendocument.core; + +/** Style of a table column. Mirrors {@code odr::TableColumnStyle}; fields may be {@code null}. */ +public final class TableColumnStyle { + public final Measure width; + + TableColumnStyle(Measure width) { + this.width = width; + } +} diff --git a/jni/java/app/opendocument/core/TableDimensions.java b/jni/java/app/opendocument/core/TableDimensions.java new file mode 100644 index 00000000..06e80c4b --- /dev/null +++ b/jni/java/app/opendocument/core/TableDimensions.java @@ -0,0 +1,19 @@ +package app.opendocument.core; + +/** Dimensions of a table. Mirrors {@code odr::TableDimensions}. */ +public final class TableDimensions { + public int rows; + public int columns; + + public TableDimensions() {} + + public TableDimensions(int rows, int columns) { + this.rows = rows; + this.columns = columns; + } + + @Override + public String toString() { + return "TableDimensions(rows=" + rows + ", columns=" + columns + ")"; + } +} diff --git a/jni/java/app/opendocument/core/TablePosition.java b/jni/java/app/opendocument/core/TablePosition.java new file mode 100644 index 00000000..57c4f6c8 --- /dev/null +++ b/jni/java/app/opendocument/core/TablePosition.java @@ -0,0 +1,35 @@ +package app.opendocument.core; + +/** A cell position in a table. Mirrors {@code odr::TablePosition}. */ +public final class TablePosition { + static { + NativeLibrary.load(); + } + + public int column; + public int row; + + public TablePosition() {} + + public TablePosition(int column, int row) { + this.column = column; + this.row = row; + } + + /** Parses a column letter (e.g. {@code "A"}) to a 0-based column number. */ + public static native int toColumnNum(String string); + + /** Parses a row label (e.g. {@code "1"}) to a 0-based row number. */ + public static native int toRowNum(String string); + + /** Formats a 0-based column number as a column letter. */ + public static native String toColumnString(int column); + + /** Formats a 0-based row number as a row label. */ + public static native String toRowString(int row); + + @Override + public String toString() { + return toColumnString(column) + toRowString(row); + } +} diff --git a/jni/java/app/opendocument/core/TableRow.java b/jni/java/app/opendocument/core/TableRow.java new file mode 100644 index 00000000..29705bdb --- /dev/null +++ b/jni/java/app/opendocument/core/TableRow.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Table row element. Mirrors {@code odr::TableRow}. */ +public final class TableRow extends Element { + TableRow(long handle, Object owner) { + super(handle, owner); + } + + public TableRowStyle style() { + return styleNative(handle()); + } + + private native TableRowStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TableRowStyle.java b/jni/java/app/opendocument/core/TableRowStyle.java new file mode 100644 index 00000000..790642c5 --- /dev/null +++ b/jni/java/app/opendocument/core/TableRowStyle.java @@ -0,0 +1,10 @@ +package app.opendocument.core; + +/** Style of a table row. Mirrors {@code odr::TableRowStyle}; fields may be {@code null}. */ +public final class TableRowStyle { + public final Measure height; + + TableRowStyle(Measure height) { + this.height = height; + } +} diff --git a/jni/java/app/opendocument/core/TableStyle.java b/jni/java/app/opendocument/core/TableStyle.java new file mode 100644 index 00000000..9fe6efc5 --- /dev/null +++ b/jni/java/app/opendocument/core/TableStyle.java @@ -0,0 +1,10 @@ +package app.opendocument.core; + +/** Style of a table. Mirrors {@code odr::TableStyle}; fields may be {@code null}. */ +public final class TableStyle { + public final Measure width; + + TableStyle(Measure width) { + this.width = width; + } +} diff --git a/jni/java/app/opendocument/core/Text.java b/jni/java/app/opendocument/core/Text.java new file mode 100644 index 00000000..0c5fb55c --- /dev/null +++ b/jni/java/app/opendocument/core/Text.java @@ -0,0 +1,26 @@ +package app.opendocument.core; + +/** Text element. Mirrors {@code odr::Text}. */ +public final class Text extends Element { + Text(long handle, Object owner) { + super(handle, owner); + } + + public String content() { + return contentNative(handle()); + } + + public void setContent(String text) { + setContentNative(handle(), text); + } + + public TextStyle style() { + return styleNative(handle()); + } + + private native String contentNative(long handle); + + private native void setContentNative(long handle, String text); + + private native TextStyle styleNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TextAlign.java b/jni/java/app/opendocument/core/TextAlign.java new file mode 100644 index 00000000..74c58793 --- /dev/null +++ b/jni/java/app/opendocument/core/TextAlign.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::TextAlign}; constant order must match the C++ declaration. */ +public enum TextAlign { + LEFT, RIGHT, CENTER, JUSTIFY; + + static TextAlign fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/TextFile.java b/jni/java/app/opendocument/core/TextFile.java new file mode 100644 index 00000000..9b72fe0e --- /dev/null +++ b/jni/java/app/opendocument/core/TextFile.java @@ -0,0 +1,21 @@ +package app.opendocument.core; + +/** A decoded text file. Mirrors {@code odr::TextFile}. */ +public final class TextFile extends DecodedFile { + TextFile(long handle) { + super(handle); + } + + /** Detected character set; {@code null} if unknown. */ + public String charset() { + return charsetNative(handle()); + } + + public String text() { + return textNative(handle()); + } + + private native String charsetNative(long handle); + + private native String textNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TextRoot.java b/jni/java/app/opendocument/core/TextRoot.java new file mode 100644 index 00000000..1efb52d8 --- /dev/null +++ b/jni/java/app/opendocument/core/TextRoot.java @@ -0,0 +1,21 @@ +package app.opendocument.core; + +/** Root element of a text document. Mirrors {@code odr::TextRoot}. */ +public final class TextRoot extends Element { + TextRoot(long handle, Object owner) { + super(handle, owner); + } + + public PageLayout pageLayout() { + return pageLayoutNative(handle()); + } + + public MasterPage firstMasterPage() { + long h = firstMasterPageNative(handle()); + return h == 0 ? null : new MasterPage(h, owner()); + } + + private native PageLayout pageLayoutNative(long handle); + + private native long firstMasterPageNative(long handle); +} diff --git a/jni/java/app/opendocument/core/TextStyle.java b/jni/java/app/opendocument/core/TextStyle.java new file mode 100644 index 00000000..9e65182d --- /dev/null +++ b/jni/java/app/opendocument/core/TextStyle.java @@ -0,0 +1,38 @@ +package app.opendocument.core; + +/** Style of a text run. Mirrors {@code odr::TextStyle}; fields may be {@code null}. */ +public final class TextStyle { + public final String fontName; + public final Measure fontSize; + public final FontWeight fontWeight; + public final FontStyle fontStyle; + public final Boolean fontUnderline; + public final Boolean fontLineThrough; + public final String fontShadow; + public final Color fontColor; + public final Color backgroundColor; + public final FontPosition fontPosition; + + TextStyle( + String fontName, + Measure fontSize, + int fontWeight, + int fontStyle, + Boolean fontUnderline, + Boolean fontLineThrough, + String fontShadow, + Color fontColor, + Color backgroundColor, + int fontPosition) { + this.fontName = fontName; + this.fontSize = fontSize; + this.fontWeight = FontWeight.fromNative(fontWeight); + this.fontStyle = FontStyle.fromNative(fontStyle); + this.fontUnderline = fontUnderline; + this.fontLineThrough = fontLineThrough; + this.fontShadow = fontShadow; + this.fontColor = fontColor; + this.backgroundColor = backgroundColor; + this.fontPosition = FontPosition.fromNative(fontPosition); + } +} diff --git a/jni/java/app/opendocument/core/TextWrap.java b/jni/java/app/opendocument/core/TextWrap.java new file mode 100644 index 00000000..876bf1bb --- /dev/null +++ b/jni/java/app/opendocument/core/TextWrap.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::TextWrap}; constant order must match the C++ declaration. */ +public enum TextWrap { + NONE, BEFORE, AFTER, RUN_THROUGH; + + static TextWrap fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/ValueType.java b/jni/java/app/opendocument/core/ValueType.java new file mode 100644 index 00000000..63efa42c --- /dev/null +++ b/jni/java/app/opendocument/core/ValueType.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::ValueType}; constant order must match the C++ declaration. */ +public enum ValueType { + UNKNOWN, STRING, FLOAT_NUMBER; + + static ValueType fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/java/app/opendocument/core/VerticalAlign.java b/jni/java/app/opendocument/core/VerticalAlign.java new file mode 100644 index 00000000..6277aeab --- /dev/null +++ b/jni/java/app/opendocument/core/VerticalAlign.java @@ -0,0 +1,14 @@ +package app.opendocument.core; + +/** Mirrors {@code odr::VerticalAlign}; constant order must match the C++ declaration. */ +public enum VerticalAlign { + TOP, MIDDLE, BOTTOM; + + static VerticalAlign fromNative(int code) { + return code < 0 ? null : values()[code]; + } + + int toNative() { + return ordinal(); + } +} diff --git a/jni/src/jni_convert.hpp b/jni/src/jni_convert.hpp new file mode 100644 index 00000000..e50797d2 --- /dev/null +++ b/jni/src/jni_convert.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include + +#include +#include + +namespace odr_jni { + +// C++ to Java; optionals map to null. +jstring make_string_opt(JNIEnv *env, const std::optional &value); +jobject make_measure(JNIEnv *env, const std::optional &value); +jobject make_color(JNIEnv *env, const std::optional &value); +jobject make_directional_measure(JNIEnv *env, + const odr::DirectionalStyle &); +jobject make_directional_string(JNIEnv *env, + const odr::DirectionalStyle &); +jobject make_text_style(JNIEnv *env, const odr::TextStyle &style); +jobject make_paragraph_style(JNIEnv *env, const odr::ParagraphStyle &style); +jobject make_table_style(JNIEnv *env, const odr::TableStyle &style); +jobject make_table_column_style(JNIEnv *env, + const odr::TableColumnStyle &style); +jobject make_table_row_style(JNIEnv *env, const odr::TableRowStyle &style); +jobject make_table_cell_style(JNIEnv *env, const odr::TableCellStyle &style); +jobject make_graphic_style(JNIEnv *env, const odr::GraphicStyle &style); +jobject make_page_layout(JNIEnv *env, const odr::PageLayout &layout); +jobject make_table_dimensions(JNIEnv *env, + const odr::TableDimensions &dimensions); +jobject make_table_position(JNIEnv *env, const odr::TablePosition &position); +jobject make_document_meta(JNIEnv *env, const odr::DocumentMeta &meta); +jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta); + +jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config); +odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config); + +/// Optional enum to a Java-side code; -1 encodes absent. +template jint enum_code(const std::optional &value) { + return value.has_value() ? static_cast(*value) : jint{-1}; +} + +} // namespace odr_jni diff --git a/jni/src/jni_core.cpp b/jni/src/jni_core.cpp new file mode 100644 index 00000000..51ba91d2 --- /dev/null +++ b/jni/src/jni_core.cpp @@ -0,0 +1,343 @@ +#include "odr_jni.hpp" + +#include +#include +#include +#include + +#include +#include + +namespace { + +using odr_jni::from_handle; +using odr_jni::guarded; +using odr_jni::make_handle; +using odr_jni::to_jstring; +using odr_jni::to_string; + +jintArray to_jint_array(JNIEnv *env, const std::vector &values) { + jintArray result = env->NewIntArray(static_cast(values.size())); + if (result == nullptr) { + return nullptr; + } + env->SetIntArrayRegion(result, 0, static_cast(values.size()), + values.data()); + return result; +} + +} // namespace + +// app.opendocument.core.Odr + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_version(JNIEnv *env, jclass) { + return guarded(env, [&] { return to_jstring(env, odr::version()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_commitHash(JNIEnv *env, jclass) { + return guarded(env, [&] { return to_jstring(env, odr::commit_hash()); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Odr_isDirty(JNIEnv *env, jclass) { + return guarded(env, [&] { return static_cast(odr::is_dirty()); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Odr_isDebug(JNIEnv *env, jclass) { + return guarded(env, [&] { return static_cast(odr::is_debug()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_identify(JNIEnv *env, jclass) { + return guarded(env, [&] { return to_jstring(env, odr::identify()); }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Odr_fileTypeByFileExtensionNative( + JNIEnv *env, jclass, jstring extension) { + return guarded(env, [&] { + return static_cast( + odr::file_type_by_file_extension(to_string(env, extension))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Odr_fileCategoryByFileTypeNative(JNIEnv *env, jclass, + jint type) { + return guarded(env, [&] { + return static_cast( + odr::file_category_by_file_type(static_cast(type))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Odr_documentTypeByFileTypeNative(JNIEnv *env, jclass, + jint type) { + return guarded(env, [&] { + return static_cast( + odr::document_type_by_file_type(static_cast(type))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_fileTypeToStringNative(JNIEnv *env, jclass, + jint type) { + return guarded(env, [&] { + return to_jstring( + env, odr::file_type_to_string(static_cast(type))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_fileCategoryToStringNative(JNIEnv *env, jclass, + jint category) { + return guarded(env, [&] { + return to_jstring(env, odr::file_category_to_string( + static_cast(category))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_documentTypeToStringNative(JNIEnv *env, jclass, + jint type) { + return guarded(env, [&] { + return to_jstring(env, odr::document_type_to_string( + static_cast(type))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Odr_fileTypeByMimetypeNative(JNIEnv *env, jclass, + jstring mimetype) { + return guarded(env, [&] { + return static_cast( + odr::file_type_by_mimetype(to_string(env, mimetype))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_mimetypeByFileTypeNative(JNIEnv *env, jclass, + jint type) { + return guarded(env, [&] { + return to_jstring( + env, odr::mimetype_by_file_type(static_cast(type))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_decoderEngineToStringNative(JNIEnv *env, jclass, + jint engine) { + return guarded(env, [&] { + return to_jstring(env, odr::decoder_engine_to_string( + static_cast(engine))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Odr_decoderEngineByNameNative(JNIEnv *env, jclass, + jstring name) { + return guarded(env, [&] { + return static_cast(odr::decoder_engine_by_name(to_string(env, name))); + }); +} + +extern "C" JNIEXPORT jintArray JNICALL +Java_app_opendocument_core_Odr_listFileTypesNative(JNIEnv *env, jclass, + jstring path) { + return guarded(env, [&] { + std::vector codes; + for (const odr::FileType type : + odr::list_file_types(to_string(env, path))) { + codes.push_back(static_cast(type)); + } + return to_jint_array(env, codes); + }); +} + +extern "C" JNIEXPORT jintArray JNICALL +Java_app_opendocument_core_Odr_listDecoderEnginesNative(JNIEnv *env, jclass, + jint as) { + return guarded(env, [&] { + std::vector codes; + for (const odr::DecoderEngine engine : + odr::list_decoder_engines(static_cast(as))) { + codes.push_back(static_cast(engine)); + } + return to_jint_array(env, codes); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Odr_mimetype(JNIEnv *env, jclass, jstring path) { + return guarded(env, [&] { + return to_jstring(env, odr::mimetype(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Odr_openNative(JNIEnv *env, jclass, jstring path) { + return guarded(env, + [&] { return make_handle(odr::open(to_string(env, path))); }); +} + +extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_Odr_openAsNative( + JNIEnv *env, jclass, jstring path, jint as) { + return guarded(env, [&] { + return make_handle( + odr::open(to_string(env, path), static_cast(as))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Odr_openWithPreferenceNative( + JNIEnv *env, jclass, jstring path, jint as_file_type, jint with_engine, + jintArray file_type_priority, jintArray engine_priority) { + return guarded(env, [&] { + odr::DecodePreference preference; + if (as_file_type >= 0) { + preference.as_file_type = static_cast(as_file_type); + } + if (with_engine >= 0) { + preference.with_engine = static_cast(with_engine); + } + const auto append_codes = [&](jintArray array, auto &target, + auto transform) { + const jsize length = env->GetArrayLength(array); + jint *codes = env->GetIntArrayElements(array, nullptr); + for (jsize i = 0; i < length; ++i) { + target.push_back(transform(codes[i])); + } + env->ReleaseIntArrayElements(array, codes, JNI_ABORT); + }; + append_codes(file_type_priority, preference.file_type_priority, + [](jint code) { return static_cast(code); }); + append_codes(engine_priority, preference.engine_priority, [](jint code) { + return static_cast(code); + }); + return make_handle(odr::open(to_string(env, path), preference)); + }); +} + +// app.opendocument.core.GlobalParams + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_GlobalParams_odrCoreDataPath(JNIEnv *env, jclass) { + return guarded(env, [&] { + return to_jstring(env, odr::GlobalParams::odr_core_data_path()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_GlobalParams_fontconfigDataPath(JNIEnv *env, + jclass) { + return guarded(env, [&] { + return to_jstring(env, odr::GlobalParams::fontconfig_data_path()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_GlobalParams_popplerDataPath(JNIEnv *env, jclass) { + return guarded(env, [&] { + return to_jstring(env, odr::GlobalParams::poppler_data_path()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_GlobalParams_pdf2htmlexDataPath(JNIEnv *env, + jclass) { + return guarded(env, [&] { + return to_jstring(env, odr::GlobalParams::pdf2htmlex_data_path()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_GlobalParams_libmagicDatabasePath(JNIEnv *env, + jclass) { + return guarded(env, [&] { + return to_jstring(env, odr::GlobalParams::libmagic_database_path()); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_GlobalParams_setOdrCoreDataPath(JNIEnv *env, jclass, + jstring path) { + guarded(env, [&] { + odr::GlobalParams::set_odr_core_data_path(to_string(env, path)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_GlobalParams_setFontconfigDataPath(JNIEnv *env, + jclass, + jstring path) { + guarded(env, [&] { + odr::GlobalParams::set_fontconfig_data_path(to_string(env, path)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_GlobalParams_setPopplerDataPath(JNIEnv *env, jclass, + jstring path) { + guarded(env, [&] { + odr::GlobalParams::set_poppler_data_path(to_string(env, path)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_GlobalParams_setPdf2htmlexDataPath(JNIEnv *env, + jclass, + jstring path) { + guarded(env, [&] { + odr::GlobalParams::set_pdf2htmlex_data_path(to_string(env, path)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_GlobalParams_setLibmagicDatabasePath(JNIEnv *env, + jclass, + jstring path) { + guarded(env, [&] { + odr::GlobalParams::set_libmagic_database_path(to_string(env, path)); + }); +} + +// app.opendocument.core.TablePosition + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_TablePosition_toColumnNum(JNIEnv *env, jclass, + jstring string) { + return guarded(env, [&] { + return static_cast( + odr::TablePosition::to_column_num(to_string(env, string))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_TablePosition_toRowNum(JNIEnv *env, jclass, + jstring string) { + return guarded(env, [&] { + return static_cast( + odr::TablePosition::to_row_num(to_string(env, string))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_TablePosition_toColumnString(JNIEnv *env, jclass, + jint column) { + return guarded(env, [&] { + return to_jstring(env, odr::TablePosition::to_column_string( + static_cast(column))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_TablePosition_toRowString(JNIEnv *env, jclass, + jint row) { + return guarded(env, [&] { + return to_jstring(env, odr::TablePosition::to_row_string( + static_cast(row))); + }); +} diff --git a/jni/src/jni_document.cpp b/jni/src/jni_document.cpp new file mode 100644 index 00000000..6b81afb9 --- /dev/null +++ b/jni/src/jni_document.cpp @@ -0,0 +1,951 @@ +#include "jni_convert.hpp" +#include "odr_jni.hpp" + +#include +#include +#include +#include + +#include + +namespace { + +using odr_jni::destroy_handle; +using odr_jni::from_handle; +using odr_jni::guarded; +using odr_jni::make_handle; +using odr_jni::to_jstring; +using odr_jni::to_string; + +odr::Element &element(jlong handle) { + return *from_handle(handle); +} + +/// Elements cross JNI as plain `odr::Element` copies; typed Java views +/// re-derive the typed C++ view per call via `as_*`. +jlong wrap_element(odr::Element value) { + if (!value) { + return 0; + } + return make_handle(std::move(value)); +} + +jlongArray wrap_elements(JNIEnv *env, const odr::ElementRange &range) { + std::vector handles; + for (const odr::Element &value : range) { + handles.push_back(make_handle(odr::Element(value))); + } + jlongArray result = env->NewLongArray(static_cast(handles.size())); + if (result == nullptr) { + return nullptr; + } + env->SetLongArrayRegion(result, 0, static_cast(handles.size()), + handles.data()); + return result; +} + +} // namespace + +// app.opendocument.core.Document + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Document_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Document_isEditableNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_editable()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Document_isSavableNative(JNIEnv *env, jobject, + jlong handle, + jboolean encrypted) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_savable(encrypted != JNI_FALSE)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Document_saveNative(JNIEnv *env, jobject, + jlong handle, jstring path) { + guarded(env, [&] { + from_handle(handle)->save(to_string(env, path)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Document_saveEncryptedNative(JNIEnv *env, jobject, + jlong handle, + jstring path, + jstring password) { + guarded(env, [&] { + from_handle(handle)->save(to_string(env, path), + to_string(env, password)); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Document_fileTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(from_handle(handle)->file_type()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Document_documentTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->document_type()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Document_rootElementNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(from_handle(handle)->root_element()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Document_asFilesystemNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(from_handle(handle)->as_filesystem()); + }); +} + +// app.opendocument.core.DocumentPath + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentPath_create(JNIEnv *env, jclass, + jstring path) { + return guarded(env, [&] { + return make_handle(odr::DocumentPath(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_DocumentPath_destroy(JNIEnv *, jclass, + jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DocumentPath_emptyNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->empty()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentPath_parentNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(from_handle(handle)->parent()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentPath_joinNative(JNIEnv *env, jobject, + jlong handle, + jlong other_handle) { + return guarded(env, [&] { + return make_handle(from_handle(handle)->join( + *from_handle(other_handle))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_DocumentPath_toStringNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, from_handle(handle)->to_string()); + }); +} + +// app.opendocument.core.Element + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Element_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jint JNICALL Java_app_opendocument_core_Element_typeNative( + JNIEnv *env, jobject, jlong handle) { + return guarded(env, + [&] { return static_cast(element(handle).type()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_parentNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return wrap_element(element(handle).parent()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_firstChildNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return wrap_element(element(handle).first_child()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_previousSiblingNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return wrap_element(element(handle).previous_sibling()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_nextSiblingNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return wrap_element(element(handle).next_sibling()); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Element_isUniqueNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return static_cast(element(handle).is_unique()); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Element_isSelfLocatableNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).is_self_locatable()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Element_isEditableNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).is_editable()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Element_isSameNative(JNIEnv *env, jobject, + jlong handle, + jlong other_handle) { + return guarded(env, [&] { + return static_cast(element(handle) == element(other_handle)); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_documentPathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return make_handle(element(handle).document_path()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Element_navigatePathNative(JNIEnv *env, jobject, + jlong handle, + jlong path_handle) { + return guarded(env, [&] { + return wrap_element(element(handle).navigate_path( + *from_handle(path_handle))); + }); +} + +// The typed accessors return a fresh `odr::Element` copy when the element +// supports the typed view, 0 otherwise. +#define ODR_JNI_ELEMENT_AS(java_name, method) \ + extern "C" JNIEXPORT jlong JNICALL \ + Java_app_opendocument_core_Element_as##java_name##Native( \ + JNIEnv *env, jobject, jlong handle) { \ + return guarded(env, [&]() -> jlong { \ + if (!element(handle).method()) { \ + return 0; \ + } \ + return make_handle(odr::Element(element(handle))); \ + }); \ + } + +ODR_JNI_ELEMENT_AS(TextRoot, as_text_root) +ODR_JNI_ELEMENT_AS(Slide, as_slide) +ODR_JNI_ELEMENT_AS(Sheet, as_sheet) +ODR_JNI_ELEMENT_AS(Page, as_page) +ODR_JNI_ELEMENT_AS(SheetCell, as_sheet_cell) +ODR_JNI_ELEMENT_AS(MasterPage, as_master_page) +ODR_JNI_ELEMENT_AS(LineBreak, as_line_break) +ODR_JNI_ELEMENT_AS(Paragraph, as_paragraph) +ODR_JNI_ELEMENT_AS(Span, as_span) +ODR_JNI_ELEMENT_AS(Text, as_text) +ODR_JNI_ELEMENT_AS(Link, as_link) +ODR_JNI_ELEMENT_AS(Bookmark, as_bookmark) +ODR_JNI_ELEMENT_AS(ListItem, as_list_item) +ODR_JNI_ELEMENT_AS(Table, as_table) +ODR_JNI_ELEMENT_AS(TableColumn, as_table_column) +ODR_JNI_ELEMENT_AS(TableRow, as_table_row) +ODR_JNI_ELEMENT_AS(TableCell, as_table_cell) +ODR_JNI_ELEMENT_AS(Frame, as_frame) +ODR_JNI_ELEMENT_AS(Rect, as_rect) +ODR_JNI_ELEMENT_AS(Line, as_line) +ODR_JNI_ELEMENT_AS(Circle, as_circle) +ODR_JNI_ELEMENT_AS(CustomShape, as_custom_shape) +ODR_JNI_ELEMENT_AS(Image, as_image) + +#undef ODR_JNI_ELEMENT_AS + +// app.opendocument.core.TextRoot + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_TextRoot_pageLayoutNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_page_layout( + env, element(handle).as_text_root().page_layout()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_TextRoot_firstMasterPageNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(element(handle).as_text_root().first_master_page()); + }); +} + +// app.opendocument.core.Slide + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Slide_nameNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_slide().name()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Slide_pageLayoutNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_page_layout(env, + element(handle).as_slide().page_layout()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Slide_masterPageNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(element(handle).as_slide().master_page()); + }); +} + +// app.opendocument.core.Sheet + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Sheet_nameNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_sheet().name()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_dimensionsNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_dimensions( + env, element(handle).as_sheet().dimensions()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_contentNative(JNIEnv *env, jobject, + jlong handle, jint rows, + jint columns) { + return guarded(env, [&] { + std::optional range; + if (rows >= 0 && columns >= 0) { + range = odr::TableDimensions(static_cast(rows), + static_cast(columns)); + } + return odr_jni::make_table_dimensions( + env, element(handle).as_sheet().content(range)); + }); +} + +extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_Sheet_cellNative( + JNIEnv *env, jobject, jlong handle, jint column, jint row) { + return guarded(env, [&] { + return wrap_element(element(handle).as_sheet().cell( + static_cast(column), static_cast(row))); + }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_Sheet_shapesNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_elements(env, element(handle).as_sheet().shapes()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_style(env, element(handle).as_sheet().style()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_columnStyleNative(JNIEnv *env, jobject, + jlong handle, jint column) { + return guarded(env, [&] { + return odr_jni::make_table_column_style( + env, element(handle).as_sheet().column_style( + static_cast(column))); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_rowStyleNative(JNIEnv *env, jobject, + jlong handle, jint row) { + return guarded(env, [&] { + return odr_jni::make_table_row_style( + env, + element(handle).as_sheet().row_style(static_cast(row))); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Sheet_cellStyleNative(JNIEnv *env, jobject, + jlong handle, jint column, + jint row) { + return guarded(env, [&] { + return odr_jni::make_table_cell_style( + env, element(handle).as_sheet().cell_style( + static_cast(column), + static_cast(row))); + }); +} + +// app.opendocument.core.SheetCell + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_SheetCell_positionNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_position( + env, element(handle).as_sheet_cell().position()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_SheetCell_isCoveredNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_sheet_cell().is_covered()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_SheetCell_spanNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_dimensions( + env, element(handle).as_sheet_cell().span()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_SheetCell_valueTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_sheet_cell().value_type()); + }); +} + +// app.opendocument.core.Page + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Page_nameNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_page().name()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Page_pageLayoutNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_page_layout(env, + element(handle).as_page().page_layout()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Page_masterPageNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(element(handle).as_page().master_page()); + }); +} + +// app.opendocument.core.MasterPage + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_MasterPage_pageLayoutNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_page_layout( + env, element(handle).as_master_page().page_layout()); + }); +} + +// app.opendocument.core.LineBreak + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_LineBreak_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_text_style(env, + element(handle).as_line_break().style()); + }); +} + +// app.opendocument.core.Paragraph + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Paragraph_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_paragraph_style( + env, element(handle).as_paragraph().style()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Paragraph_textStyleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_text_style( + env, element(handle).as_paragraph().text_style()); + }); +} + +// app.opendocument.core.Span + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Span_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_text_style(env, element(handle).as_span().style()); + }); +} + +// app.opendocument.core.Text + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Text_contentNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_text().content()); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Text_setContentNative(JNIEnv *env, jobject, + jlong handle, jstring text) { + guarded(env, + [&] { element(handle).as_text().set_content(to_string(env, text)); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Text_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_text_style(env, element(handle).as_text().style()); + }); +} + +// app.opendocument.core.Link + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Link_hrefNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_link().href()); }); +} + +// app.opendocument.core.Bookmark + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Bookmark_nameNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_bookmark().name()); + }); +} + +// app.opendocument.core.ListItem + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_ListItem_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_text_style(env, + element(handle).as_list_item().style()); + }); +} + +// app.opendocument.core.Table + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Table_firstRowNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(element(handle).as_table().first_row()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Table_firstColumnNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_element(element(handle).as_table().first_column()); + }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_Table_columnsNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_elements(env, element(handle).as_table().columns()); + }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_Table_rowsNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return wrap_elements(env, element(handle).as_table().rows()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Table_dimensionsNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_dimensions( + env, element(handle).as_table().dimensions()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Table_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_style(env, element(handle).as_table().style()); + }); +} + +// app.opendocument.core.TableColumn + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_TableColumn_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_column_style( + env, element(handle).as_table_column().style()); + }); +} + +// app.opendocument.core.TableRow + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_TableRow_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_row_style( + env, element(handle).as_table_row().style()); + }); +} + +// app.opendocument.core.TableCell + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_TableCell_isCoveredNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_table_cell().is_covered()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_TableCell_spanNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_dimensions( + env, element(handle).as_table_cell().span()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_TableCell_valueTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_table_cell().value_type()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_TableCell_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_table_cell_style( + env, element(handle).as_table_cell().style()); + }); +} + +// app.opendocument.core.Frame + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_Frame_anchorTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_frame().anchor_type()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Frame_xNative(JNIEnv *env, jobject, jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_frame().x()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Frame_yNative(JNIEnv *env, jobject, jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_frame().y()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Frame_widthNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_frame().width()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Frame_heightNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_frame().height()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Frame_zIndexNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_frame().z_index()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Frame_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_graphic_style(env, element(handle).as_frame().style()); + }); +} + +// app.opendocument.core.Rect + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Rect_xNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_rect().x()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Rect_yNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_rect().y()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Rect_widthNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_rect().width()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Rect_heightNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_rect().height()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Rect_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_graphic_style(env, element(handle).as_rect().style()); + }); +} + +// app.opendocument.core.Line + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Line_x1Native(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_line().x1()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Line_y1Native(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_line().y1()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Line_x2Native(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_line().x2()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Line_y2Native(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_line().y2()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Line_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_graphic_style(env, element(handle).as_line().style()); + }); +} + +// app.opendocument.core.Circle + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Circle_xNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_circle().x()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Circle_yNative(JNIEnv *env, jobject, jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_circle().y()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Circle_widthNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_circle().width()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Circle_heightNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_circle().height()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_Circle_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_graphic_style(env, + element(handle).as_circle().style()); + }); +} + +// app.opendocument.core.CustomShape + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_CustomShape_xNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_custom_shape().x()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_CustomShape_yNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, element(handle).as_custom_shape().y()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_CustomShape_widthNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_custom_shape().width()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_CustomShape_heightNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, element(handle).as_custom_shape().height()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_CustomShape_styleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_graphic_style( + env, element(handle).as_custom_shape().style()); + }); +} + +// app.opendocument.core.Image + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Image_isInternalNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(element(handle).as_image().is_internal()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_Image_fileNative( + JNIEnv *env, jobject, jlong handle) { + return guarded(env, [&]() -> jlong { + const std::optional file = element(handle).as_image().file(); + if (!file.has_value()) { + return 0; + } + return make_handle(odr::File(*file)); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_Image_hrefNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, element(handle).as_image().href()); }); +} diff --git a/jni/src/jni_file.cpp b/jni/src/jni_file.cpp new file mode 100644 index 00000000..5b956d06 --- /dev/null +++ b/jni/src/jni_file.cpp @@ -0,0 +1,554 @@ +#include "jni_convert.hpp" +#include "odr_jni.hpp" + +#include +#include +#include +#include + +#include + +namespace { + +using odr_jni::destroy_handle; +using odr_jni::from_handle; +using odr_jni::guarded; +using odr_jni::make_handle; +using odr_jni::to_jbytes; +using odr_jni::to_jstring; +using odr_jni::to_string; + +odr::DecodedFile &decoded(jlong handle) { + return *from_handle(handle); +} + +} // namespace + +// app.opendocument.core.File + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_File_create(JNIEnv *env, jclass, jstring path) { + return guarded(env, + [&] { return make_handle(odr::File(to_string(env, path))); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_File_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_File_locationNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(from_handle(handle)->location()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_File_sizeNative(JNIEnv *env, jobject, jlong handle) { + return guarded(env, [&] { + return static_cast(from_handle(handle)->size()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_File_diskPathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt( + env, from_handle(handle)->disk_path()); + }); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_File_readNative(JNIEnv *env, jobject, jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + from_handle(handle)->pipe(out); + return to_jbytes(env, out.str()); + }); +} + +extern "C" JNIEXPORT void JNICALL Java_app_opendocument_core_File_copyNative( + JNIEnv *env, jobject, jlong handle, jstring path) { + guarded(env, + [&] { from_handle(handle)->copy(to_string(env, path)); }); +} + +// app.opendocument.core.DecodedFile +// +// Handles hold a heap `odr::DecodedFile` (the typed C++ subclasses are sliced +// away); typed accessors re-derive the typed view per call via `as_*`. + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_create(JNIEnv *env, jclass, + jstring path) { + return guarded( + env, [&] { return make_handle(odr::DecodedFile(to_string(env, path))); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_createAs(JNIEnv *env, jclass, + jstring path, jint as) { + return guarded(env, [&] { + return make_handle( + odr::DecodedFile(to_string(env, path), static_cast(as))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_createFromFile(JNIEnv *env, jclass, + jlong file_handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(*from_handle(file_handle))); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_DecodedFile_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_fileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return make_handle(decoded(handle).file()); }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DecodedFile_fileTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return static_cast(decoded(handle).file_type()); }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DecodedFile_fileCategoryNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return static_cast(decoded(handle).file_category()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_DecodedFile_fileMetaNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_file_meta(env, decoded(handle).file_meta()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DecodedFile_decoderEngineNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return static_cast(decoded(handle).decoder_engine()); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_passwordEncryptedNative(JNIEnv *env, + jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).password_encrypted()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DecodedFile_encryptionStateNative(JNIEnv *env, + jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).encryption_state()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_decryptNative(JNIEnv *env, jobject, + jlong handle, + jstring password) { + return guarded(env, [&] { + return make_handle(decoded(handle).decrypt(to_string(env, password))); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isDecodableNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_decodable()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isTextFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_text_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isImageFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_image_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isArchiveFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_archive_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isDocumentFileNative(JNIEnv *env, + jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_document_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isPdfFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_pdf_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_DecodedFile_isFontFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(decoded(handle).is_font_file()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asTextFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_text_file())); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asImageFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_image_file())); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asArchiveFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_archive_file())); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asDocumentFileNative(JNIEnv *env, + jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_document_file())); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asPdfFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_pdf_file())); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DecodedFile_asFontFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile(decoded(handle).as_font_file())); + }); +} + +// app.opendocument.core.TextFile + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_TextFile_charsetNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_string_opt(env, + decoded(handle).as_text_file().charset()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_TextFile_textNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, decoded(handle).as_text_file().text()); + }); +} + +// app.opendocument.core.ImageFile + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_ImageFile_readNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + out << decoded(handle).as_image_file().stream()->rdbuf(); + return to_jbytes(env, out.str()); + }); +} + +// app.opendocument.core.ArchiveFile + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_ArchiveFile_archiveNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(decoded(handle).as_archive_file().archive()); + }); +} + +// app.opendocument.core.DocumentFile + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentFile_create(JNIEnv *env, jclass, + jstring path) { + return guarded(env, [&] { + return make_handle( + odr::DecodedFile(odr::DocumentFile(to_string(env, path)))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DocumentFile_typeByPathNative(JNIEnv *env, jclass, + jstring path) { + return guarded(env, [&] { + return static_cast(odr::DocumentFile::type(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_DocumentFile_metaByPathNative(JNIEnv *env, jclass, + jstring path) { + return guarded(env, [&] { + return odr_jni::make_file_meta( + env, odr::DocumentFile::meta(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_DocumentFile_documentTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + decoded(handle).as_document_file().document_type()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_DocumentFile_documentMetaNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::make_document_meta( + env, decoded(handle).as_document_file().document_meta()); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentFile_decryptDocumentFileNative( + JNIEnv *env, jobject, jlong handle, jstring password) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile( + decoded(handle).as_document_file().decrypt(to_string(env, password)))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_DocumentFile_documentNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(decoded(handle).as_document_file().document()); + }); +} + +// app.opendocument.core.PdfFile + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_PdfFile_decryptPdfFileNative(JNIEnv *env, jobject, + jlong handle, + jstring password) { + return guarded(env, [&] { + return make_handle(odr::DecodedFile( + decoded(handle).as_pdf_file().decrypt(to_string(env, password)))); + }); +} + +// app.opendocument.core.FontFile + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_FontFile_readNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + out << decoded(handle).as_font_file().stream()->rdbuf(); + return to_jbytes(env, out.str()); + }); +} + +// app.opendocument.core.FileWalker + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_FileWalker_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_FileWalker_endNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(from_handle(handle)->end()); + }); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_FileWalker_depthNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(from_handle(handle)->depth()); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_FileWalker_pathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return to_jstring(env, from_handle(handle)->path()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_FileWalker_isFileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_file()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_FileWalker_isDirectoryNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_directory()); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_FileWalker_popNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { from_handle(handle)->pop(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_FileWalker_nextNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { from_handle(handle)->next(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_FileWalker_flatNextNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { from_handle(handle)->flat_next(); }); +} + +// app.opendocument.core.Filesystem + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Filesystem_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Filesystem_existsNative(JNIEnv *env, jobject, + jlong handle, jstring path) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->exists(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Filesystem_isFileNative(JNIEnv *env, jobject, + jlong handle, jstring path) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_file(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Filesystem_isDirectoryNative(JNIEnv *env, jobject, + jlong handle, + jstring path) { + return guarded(env, [&] { + return static_cast( + from_handle(handle)->is_directory( + to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Filesystem_fileWalkerNative(JNIEnv *env, jobject, + jlong handle, + jstring path) { + return guarded(env, [&] { + return make_handle(from_handle(handle)->file_walker( + to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Filesystem_openNative(JNIEnv *env, jobject, + jlong handle, jstring path) { + return guarded(env, [&] { + return make_handle( + from_handle(handle)->open(to_string(env, path))); + }); +} + +// app.opendocument.core.Archive + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_Archive_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Archive_asFilesystemNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return make_handle(from_handle(handle)->as_filesystem()); + }); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_Archive_saveNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + from_handle(handle)->save(out); + return to_jbytes(env, out.str()); + }); +} diff --git a/jni/src/jni_html.cpp b/jni/src/jni_html.cpp new file mode 100644 index 00000000..f2d3dbf8 --- /dev/null +++ b/jni/src/jni_html.cpp @@ -0,0 +1,408 @@ +#include "jni_convert.hpp" +#include "odr_jni.hpp" + +#include +#include +#include +#include + +#include +#include +#include + +namespace { + +using odr_jni::destroy_handle; +using odr_jni::from_handle; +using odr_jni::guarded; +using odr_jni::make_handle; +using odr_jni::to_jbytes; +using odr_jni::to_jstring; +using odr_jni::to_string; + +jlongArray to_jlong_array(JNIEnv *env, const std::vector &values) { + jlongArray result = env->NewLongArray(static_cast(values.size())); + if (result == nullptr) { + return nullptr; + } + env->SetLongArrayRegion(result, 0, static_cast(values.size()), + values.data()); + return result; +} + +jlongArray wrap_views(JNIEnv *env, const odr::HtmlViews &views) { + std::vector handles; + handles.reserve(views.size()); + for (const odr::HtmlView &view : views) { + handles.push_back(make_handle(odr::HtmlView(view))); + } + return to_jlong_array(env, handles); +} + +/// Builds an `app.opendocument.core.Html` from an offline result. +jobject make_html(JNIEnv *env, odr::Html html) { + jobject config = odr_jni::html_config_to_java(env, html.config()); + + jclass page_cls = env->FindClass("app/opendocument/core/HtmlPage"); + if (page_cls == nullptr) { + return nullptr; + } + jmethodID page_ctor = env->GetMethodID( + page_cls, "", "(Ljava/lang/String;Ljava/lang/String;)V"); + const std::vector &pages = html.pages(); + jobjectArray page_array = + env->NewObjectArray(static_cast(pages.size()), page_cls, nullptr); + for (jsize i = 0; i < static_cast(pages.size()); ++i) { + jobject page = + env->NewObject(page_cls, page_ctor, to_jstring(env, pages[i].name), + to_jstring(env, pages[i].path)); + env->SetObjectArrayElement(page_array, i, page); + env->DeleteLocalRef(page); + } + env->DeleteLocalRef(page_cls); + + jclass html_cls = env->FindClass("app/opendocument/core/Html"); + if (html_cls == nullptr) { + return nullptr; + } + jmethodID html_ctor = env->GetMethodID( + html_cls, "", + "(Lapp/opendocument/core/HtmlConfig;[Lapp/opendocument/core/HtmlPage;)V"); + jobject result = env->NewObject(html_cls, html_ctor, config, page_array); + env->DeleteLocalRef(html_cls); + return result; +} + +/// Builds an `app.opendocument.core.Html.Content` from rendered HTML + +/// resources. +jobject make_content(JNIEnv *env, const std::string &html, + const odr::HtmlResources &resources) { + jclass located_cls = + env->FindClass("app/opendocument/core/Html$LocatedResource"); + if (located_cls == nullptr) { + return nullptr; + } + jmethodID located_ctor = env->GetMethodID( + located_cls, "", + "(Lapp/opendocument/core/HtmlResource;Ljava/lang/String;)V"); + jclass resource_cls = env->FindClass("app/opendocument/core/HtmlResource"); + jmethodID resource_ctor = env->GetMethodID(resource_cls, "", "(J)V"); + + jobjectArray located_array = env->NewObjectArray( + static_cast(resources.size()), located_cls, nullptr); + for (jsize i = 0; i < static_cast(resources.size()); ++i) { + const auto &[resource, location] = resources[i]; + jobject resource_obj = env->NewObject( + resource_cls, resource_ctor, make_handle(odr::HtmlResource(resource))); + jobject located = env->NewObject( + located_cls, located_ctor, resource_obj, + location.has_value() ? to_jstring(env, *location) : nullptr); + env->SetObjectArrayElement(located_array, i, located); + env->DeleteLocalRef(located); + env->DeleteLocalRef(resource_obj); + } + env->DeleteLocalRef(resource_cls); + env->DeleteLocalRef(located_cls); + + jclass content_cls = env->FindClass("app/opendocument/core/Html$Content"); + if (content_cls == nullptr) { + return nullptr; + } + jmethodID content_ctor = env->GetMethodID( + content_cls, "", + "(Ljava/lang/String;[Lapp/opendocument/core/Html$LocatedResource;)V"); + jobject result = env->NewObject(content_cls, content_ctor, + to_jstring(env, html), located_array); + env->DeleteLocalRef(content_cls); + return result; +} + +odr::HtmlService &service(jlong handle) { + return *from_handle(handle); +} + +odr::HtmlView &view(jlong handle) { + return *from_handle(handle); +} + +odr::HtmlResource &resource(jlong handle) { + return *from_handle(handle); +} + +} // namespace + +// app.opendocument.core.Html (static entry points) + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Html_translateFile(JNIEnv *env, jclass, + jlong file_handle, + jstring cache_path, + jobject config) { + return guarded(env, [&] { + return make_handle(odr::html::translate( + *from_handle(file_handle), to_string(env, cache_path), + odr_jni::html_config_from_java(env, config))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Html_translateDocument(JNIEnv *env, jclass, + jlong document_handle, + jstring cache_path, + jobject config) { + return guarded(env, [&] { + return make_handle( + odr::html::translate(*from_handle(document_handle), + to_string(env, cache_path), + odr_jni::html_config_from_java(env, config))); + }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_Html_translateFilesystem(JNIEnv *env, jclass, + jlong filesystem_handle, + jstring cache_path, + jobject config) { + return guarded(env, [&] { + return make_handle( + odr::html::translate(*from_handle(filesystem_handle), + to_string(env, cache_path), + odr_jni::html_config_from_java(env, config))); + }); +} + +extern "C" JNIEXPORT void JNICALL Java_app_opendocument_core_Html_editDocument( + JNIEnv *env, jclass, jlong document_handle, jstring diff) { + guarded(env, [&] { + odr::html::edit(*from_handle(document_handle), + to_string(env, diff).c_str()); + }); +} + +// app.opendocument.core.HtmlService + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HtmlService_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlService_configNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::html_config_to_java(env, service(handle).config()); + }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_HtmlService_listViewsNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return wrap_views(env, service(handle).list_views()); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HtmlService_warmupNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { service(handle).warmup(); }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_HtmlService_existsNative(JNIEnv *env, jobject, + jlong handle, + jstring path) { + return guarded(env, [&] { + return static_cast(service(handle).exists(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlService_mimetypeNative(JNIEnv *env, jobject, + jlong handle, + jstring path) { + return guarded(env, [&] { + return to_jstring(env, service(handle).mimetype(to_string(env, path))); + }); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_HtmlService_writeNative(JNIEnv *env, jobject, + jlong handle, jstring path) { + return guarded(env, [&] { + std::ostringstream out; + service(handle).write(to_string(env, path), out); + return to_jbytes(env, out.str()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlService_writeHtmlNative(JNIEnv *env, jobject, + jlong handle, + jstring path) { + return guarded(env, [&] { + std::ostringstream out; + odr::HtmlResources resources = + service(handle).write_html(to_string(env, path), out); + return make_content(env, out.str(), resources); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlService_bringOfflineNative(JNIEnv *env, jobject, + jlong handle, + jstring output_path) { + return guarded(env, [&] { + return make_html( + env, service(handle).bring_offline(to_string(env, output_path))); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlService_bringOfflineViewsNative( + JNIEnv *env, jobject, jlong handle, jstring output_path, + jlongArray view_handles) { + return guarded(env, [&] { + std::vector views; + const jsize length = env->GetArrayLength(view_handles); + jlong *handles = env->GetLongArrayElements(view_handles, nullptr); + for (jsize i = 0; i < length; ++i) { + views.push_back(*from_handle(handles[i])); + } + env->ReleaseLongArrayElements(view_handles, handles, JNI_ABORT); + return make_html( + env, service(handle).bring_offline(to_string(env, output_path), views)); + }); +} + +// app.opendocument.core.HtmlView + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HtmlView_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlView_nameNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return to_jstring(env, view(handle).name()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_HtmlView_indexNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return static_cast(view(handle).index()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlView_pathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return to_jstring(env, view(handle).path()); }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlView_configNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return odr_jni::html_config_to_java(env, view(handle).config()); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlView_writeHtmlNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + odr::HtmlResources resources = view(handle).write_html(out); + return make_content(env, out.str(), resources); + }); +} + +extern "C" JNIEXPORT jobject JNICALL +Java_app_opendocument_core_HtmlView_bringOfflineNative(JNIEnv *env, jobject, + jlong handle, + jstring output_path) { + return guarded(env, [&] { + return make_html(env, + view(handle).bring_offline(to_string(env, output_path))); + }); +} + +// app.opendocument.core.HtmlResource + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HtmlResource_destroy(JNIEnv *, jclass, + jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jint JNICALL +Java_app_opendocument_core_HtmlResource_typeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return static_cast(resource(handle).type()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlResource_mimeTypeNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, + [&] { return to_jstring(env, resource(handle).mime_type()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlResource_nameNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return to_jstring(env, resource(handle).name()); }); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HtmlResource_pathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { return to_jstring(env, resource(handle).path()); }); +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_HtmlResource_fileNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&]() -> jlong { + const std::optional &file = resource(handle).file(); + if (!file.has_value()) { + return 0; + } + return make_handle(odr::File(*file)); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_HtmlResource_isShippedNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(resource(handle).is_shipped()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_HtmlResource_isExternalNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(resource(handle).is_external()); + }); +} + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_HtmlResource_isAccessibleNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + return static_cast(resource(handle).is_accessible()); + }); +} + +extern "C" JNIEXPORT jbyteArray JNICALL +Java_app_opendocument_core_HtmlResource_readNative(JNIEnv *env, jobject, + jlong handle) { + return guarded(env, [&] { + std::ostringstream out; + resource(handle).write_resource(out); + return to_jbytes(env, out.str()); + }); +} diff --git a/jni/src/jni_http_server.cpp b/jni/src/jni_http_server.cpp new file mode 100644 index 00000000..f437ff1d --- /dev/null +++ b/jni/src/jni_http_server.cpp @@ -0,0 +1,173 @@ +#include "odr_jni.hpp" + +#ifdef ODR_WITH_HTTP_SERVER + +#include "jni_convert.hpp" + +#include +#include +#include + +#include + +namespace { + +using odr_jni::destroy_handle; +using odr_jni::from_handle; +using odr_jni::guarded; +using odr_jni::make_handle; +using odr_jni::to_jstring; +using odr_jni::to_string; + +odr::HttpServer &server(jlong handle) { + return *from_handle(handle); +} + +} // namespace + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Odr_hasHttpServer(JNIEnv *, jclass) { + return JNI_TRUE; +} + +extern "C" JNIEXPORT jlong JNICALL Java_app_opendocument_core_HttpServer_create( + JNIEnv *env, jclass, jstring cache_path) { + return guarded(env, [&] { + odr::HttpServer::Config config; + config.cache_path = to_string(env, cache_path); + return make_handle(odr::HttpServer(config)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_destroy(JNIEnv *, jclass, jlong handle) { + destroy_handle(handle); +} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HttpServer_cachePathNative(JNIEnv *env, jobject, + jlong handle) { + return guarded( + env, [&] { return to_jstring(env, server(handle).config().cache_path); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_connectServiceNative(JNIEnv *env, jobject, + jlong handle, + jlong service_handle, + jstring prefix) { + guarded(env, [&] { + server(handle).connect_service( + *from_handle(service_handle), to_string(env, prefix)); + }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_HttpServer_serveFileNative(JNIEnv *env, jobject, + jlong handle, + jlong file_handle, + jstring prefix, + jobject config) { + return guarded(env, [&] { + const odr::HtmlViews views = server(handle).serve_file( + *from_handle(file_handle), to_string(env, prefix), + odr_jni::html_config_from_java(env, config)); + std::vector handles; + handles.reserve(views.size()); + for (const odr::HtmlView &view : views) { + handles.push_back(make_handle(odr::HtmlView(view))); + } + jlongArray result = env->NewLongArray(static_cast(handles.size())); + if (result == nullptr) { + return jlongArray{}; + } + env->SetLongArrayRegion(result, 0, static_cast(handles.size()), + handles.data()); + return result; + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_listenNative(JNIEnv *env, jobject, + jlong handle, jstring host, + jint port) { + guarded(env, [&] { + server(handle).listen(to_string(env, host), + static_cast(port)); + }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_clearNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { server(handle).clear(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_stopNative(JNIEnv *env, jobject, + jlong handle) { + guarded(env, [&] { server(handle).stop(); }); +} + +#else + +#include + +namespace { + +[[noreturn]] void unsupported() { + throw odr::UnsupportedOperation("built without HTTP server"); +} + +} // namespace + +extern "C" JNIEXPORT jboolean JNICALL +Java_app_opendocument_core_Odr_hasHttpServer(JNIEnv *, jclass) { + return JNI_FALSE; +} + +extern "C" JNIEXPORT jlong JNICALL +Java_app_opendocument_core_HttpServer_create(JNIEnv *env, jclass, jstring) { + return odr_jni::guarded(env, [&]() -> jlong { unsupported(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_destroy(JNIEnv *, jclass, jlong) {} + +extern "C" JNIEXPORT jstring JNICALL +Java_app_opendocument_core_HttpServer_cachePathNative(JNIEnv *env, jobject, + jlong) { + return odr_jni::guarded(env, [&]() -> jstring { unsupported(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_connectServiceNative(JNIEnv *env, jobject, + jlong, jlong, + jstring) { + odr_jni::guarded(env, [&] { unsupported(); }); +} + +extern "C" JNIEXPORT jlongArray JNICALL +Java_app_opendocument_core_HttpServer_serveFileNative(JNIEnv *env, jobject, + jlong, jlong, jstring, + jobject) { + return odr_jni::guarded(env, [&]() -> jlongArray { unsupported(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_listenNative(JNIEnv *env, jobject, jlong, + jstring, jint) { + odr_jni::guarded(env, [&] { unsupported(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_clearNative(JNIEnv *env, jobject, jlong) { + odr_jni::guarded(env, [&] { unsupported(); }); +} + +extern "C" JNIEXPORT void JNICALL +Java_app_opendocument_core_HttpServer_stopNative(JNIEnv *env, jobject, jlong) { + odr_jni::guarded(env, [&] { unsupported(); }); +} + +#endif diff --git a/jni/src/jni_style.cpp b/jni/src/jni_style.cpp new file mode 100644 index 00000000..f8b5072d --- /dev/null +++ b/jni/src/jni_style.cpp @@ -0,0 +1,489 @@ +#include "jni_convert.hpp" +#include "odr_jni.hpp" + +#include +#include + +namespace odr_jni { + +namespace { + +jobject new_object(JNIEnv *env, const char *class_name, const char *signature, + ...) { + jclass cls = env->FindClass(class_name); + if (cls == nullptr) { + return nullptr; + } + jmethodID ctor = env->GetMethodID(cls, "", signature); + if (ctor == nullptr) { + env->DeleteLocalRef(cls); + return nullptr; + } + va_list args; + va_start(args, signature); + jobject result = env->NewObjectV(cls, ctor, args); + va_end(args); + env->DeleteLocalRef(cls); + return result; +} + +jobject call_static_object(JNIEnv *env, const char *class_name, + const char *method, const char *signature, ...) { + jclass cls = env->FindClass(class_name); + if (cls == nullptr) { + return nullptr; + } + jmethodID mid = env->GetStaticMethodID(cls, method, signature); + if (mid == nullptr) { + env->DeleteLocalRef(cls); + return nullptr; + } + va_list args; + va_start(args, signature); + jobject result = env->CallStaticObjectMethodV(cls, mid, args); + va_end(args); + env->DeleteLocalRef(cls); + return result; +} + +jobject box_boolean(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return call_static_object(env, "java/lang/Boolean", "valueOf", + "(Z)Ljava/lang/Boolean;", + static_cast(*value)); +} + +jobject box_double(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return call_static_object(env, "java/lang/Double", "valueOf", + "(D)Ljava/lang/Double;", *value); +} + +jobject box_long(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return call_static_object(env, "java/lang/Long", "valueOf", + "(J)Ljava/lang/Long;", static_cast(*value)); +} + +jobject box_integer(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return call_static_object(env, "java/lang/Integer", "valueOf", + "(I)Ljava/lang/Integer;", + static_cast(*value)); +} + +/// Looks up an enum constant by its C++ code (= Java ordinal). +jobject enum_from_code(JNIEnv *env, const char *class_name, const jint code) { + if (code < 0) { + return nullptr; + } + jclass cls = env->FindClass(class_name); + if (cls == nullptr) { + return nullptr; + } + const std::string signature = std::string("()[L") + class_name + ";"; + jmethodID values = env->GetStaticMethodID(cls, "values", signature.c_str()); + if (values == nullptr) { + env->DeleteLocalRef(cls); + return nullptr; + } + auto array = + static_cast(env->CallStaticObjectMethod(cls, values)); + jobject result = env->GetObjectArrayElement(array, code); + env->DeleteLocalRef(array); + env->DeleteLocalRef(cls); + return result; +} + +jint enum_ordinal(JNIEnv *env, jobject value) { + if (value == nullptr) { + return -1; + } + jclass cls = env->FindClass("java/lang/Enum"); + jmethodID ordinal = env->GetMethodID(cls, "ordinal", "()I"); + const jint result = env->CallIntMethod(value, ordinal); + env->DeleteLocalRef(cls); + return result; +} + +} // namespace + +jstring make_string_opt(JNIEnv *env, const std::optional &value) { + return value.has_value() ? to_jstring(env, *value) : nullptr; +} + +jobject make_measure(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return new_object(env, "app/opendocument/core/Measure", + "(DLjava/lang/String;)V", value->magnitude(), + to_jstring(env, value->unit().to_string())); +} + +jobject make_color(JNIEnv *env, const std::optional &value) { + if (!value.has_value()) { + return nullptr; + } + return new_object( + env, "app/opendocument/core/Color", "(IIII)V", + static_cast(value->red), static_cast(value->green), + static_cast(value->blue), static_cast(value->alpha)); +} + +jobject +make_directional_measure(JNIEnv *env, + const odr::DirectionalStyle &style) { + return new_object( + env, "app/opendocument/core/DirectionalMeasure", + "(Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;" + "Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;)V", + make_measure(env, style.right), make_measure(env, style.top), + make_measure(env, style.left), make_measure(env, style.bottom)); +} + +jobject +make_directional_string(JNIEnv *env, + const odr::DirectionalStyle &style) { + return new_object( + env, "app/opendocument/core/DirectionalString", + "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)V", + make_string_opt(env, style.right), make_string_opt(env, style.top), + make_string_opt(env, style.left), make_string_opt(env, style.bottom)); +} + +jobject make_text_style(JNIEnv *env, const odr::TextStyle &style) { + return new_object( + env, "app/opendocument/core/TextStyle", + "(Ljava/lang/String;Lapp/opendocument/core/Measure;II" + "Ljava/lang/Boolean;Ljava/lang/Boolean;Ljava/lang/String;" + "Lapp/opendocument/core/Color;Lapp/opendocument/core/Color;I)V", + style.font_name == nullptr ? nullptr : to_jstring(env, style.font_name), + make_measure(env, style.font_size), enum_code(style.font_weight), + enum_code(style.font_style), box_boolean(env, style.font_underline), + box_boolean(env, style.font_line_through), + make_string_opt(env, style.font_shadow), + make_color(env, style.font_color), + make_color(env, style.background_color), enum_code(style.font_position)); +} + +jobject make_paragraph_style(JNIEnv *env, const odr::ParagraphStyle &style) { + return new_object( + env, "app/opendocument/core/ParagraphStyle", + "(ILapp/opendocument/core/DirectionalMeasure;" + "Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;)V", + enum_code(style.text_align), make_directional_measure(env, style.margin), + make_measure(env, style.line_height), + make_measure(env, style.text_indent)); +} + +jobject make_table_style(JNIEnv *env, const odr::TableStyle &style) { + return new_object(env, "app/opendocument/core/TableStyle", + "(Lapp/opendocument/core/Measure;)V", + make_measure(env, style.width)); +} + +jobject make_table_column_style(JNIEnv *env, + const odr::TableColumnStyle &style) { + return new_object(env, "app/opendocument/core/TableColumnStyle", + "(Lapp/opendocument/core/Measure;)V", + make_measure(env, style.width)); +} + +jobject make_table_row_style(JNIEnv *env, const odr::TableRowStyle &style) { + return new_object(env, "app/opendocument/core/TableRowStyle", + "(Lapp/opendocument/core/Measure;)V", + make_measure(env, style.height)); +} + +jobject make_table_cell_style(JNIEnv *env, const odr::TableCellStyle &style) { + return new_object( + env, "app/opendocument/core/TableCellStyle", + "(IILapp/opendocument/core/Color;" + "Lapp/opendocument/core/DirectionalMeasure;" + "Lapp/opendocument/core/DirectionalString;Ljava/lang/Double;)V", + enum_code(style.horizontal_align), enum_code(style.vertical_align), + make_color(env, style.background_color), + make_directional_measure(env, style.padding), + make_directional_string(env, style.border), + box_double(env, style.text_rotation)); +} + +jobject make_graphic_style(JNIEnv *env, const odr::GraphicStyle &style) { + return new_object( + env, "app/opendocument/core/GraphicStyle", + "(Lapp/opendocument/core/Measure;Lapp/opendocument/core/Color;" + "Lapp/opendocument/core/Color;II)V", + make_measure(env, style.stroke_width), + make_color(env, style.stroke_color), make_color(env, style.fill_color), + enum_code(style.vertical_align), enum_code(style.text_wrap)); +} + +jobject make_page_layout(JNIEnv *env, const odr::PageLayout &layout) { + return new_object( + env, "app/opendocument/core/PageLayout", + "(Lapp/opendocument/core/Measure;Lapp/opendocument/core/Measure;I" + "Lapp/opendocument/core/DirectionalMeasure;)V", + make_measure(env, layout.width), make_measure(env, layout.height), + enum_code(layout.print_orientation), + make_directional_measure(env, layout.margin)); +} + +jobject make_table_dimensions(JNIEnv *env, + const odr::TableDimensions &dimensions) { + return new_object(env, "app/opendocument/core/TableDimensions", "(II)V", + static_cast(dimensions.rows), + static_cast(dimensions.columns)); +} + +jobject make_table_position(JNIEnv *env, const odr::TablePosition &position) { + return new_object(env, "app/opendocument/core/TablePosition", "(II)V", + static_cast(position.column), + static_cast(position.row)); +} + +jobject make_document_meta(JNIEnv *env, const odr::DocumentMeta &meta) { + return new_object( + env, "app/opendocument/core/DocumentMeta", + "(ILjava/lang/Long;Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V", + static_cast(meta.document_type), box_long(env, meta.entry_count), + make_string_opt(env, meta.title), make_string_opt(env, meta.author), + make_string_opt(env, meta.subject), make_string_opt(env, meta.keywords), + make_string_opt(env, meta.creator), make_string_opt(env, meta.producer), + make_string_opt(env, meta.creation_date), + make_string_opt(env, meta.modification_date)); +} + +jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta) { + jobject document_meta = meta.document_meta.has_value() + ? make_document_meta(env, *meta.document_meta) + : nullptr; + return new_object( + env, "app/opendocument/core/FileMeta", + "(ILjava/lang/String;ZLapp/opendocument/core/DocumentMeta;)V", + static_cast(meta.type), to_jstring(env, meta.mimetype), + static_cast(meta.password_encrypted), document_meta); +} + +jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config) { + jclass cls = env->FindClass("app/opendocument/core/HtmlConfig"); + if (cls == nullptr) { + return nullptr; + } + jmethodID ctor = env->GetMethodID(cls, "", "()V"); + jobject result = env->NewObject(cls, ctor); + + const auto set_string = [&](const char *name, const std::string &value) { + env->SetObjectField(result, + env->GetFieldID(cls, name, "Ljava/lang/String;"), + to_jstring(env, value)); + }; + const auto set_boolean = [&](const char *name, const bool value) { + env->SetBooleanField(result, env->GetFieldID(cls, name, "Z"), + static_cast(value)); + }; + const auto set_int = [&](const char *name, const jint value) { + env->SetIntField(result, env->GetFieldID(cls, name, "I"), value); + }; + const auto set_double = [&](const char *name, const double value) { + env->SetDoubleField(result, env->GetFieldID(cls, name, "D"), value); + }; + const auto set_object = [&](const char *name, const char *signature, + jobject value) { + env->SetObjectField(result, env->GetFieldID(cls, name, signature), value); + }; + + set_string("documentOutputFileName", config.document_output_file_name); + set_string("slideOutputFileName", config.slide_output_file_name); + set_string("sheetOutputFileName", config.sheet_output_file_name); + set_string("pageOutputFileName", config.page_output_file_name); + set_boolean("embedImages", config.embed_images); + set_boolean("embedShippedResources", config.embed_shipped_resources); + set_string("resourcePath", config.resource_path); + set_boolean("relativeResourcePaths", config.relative_resource_paths); + set_boolean("editable", config.editable); + set_boolean("textDocumentMargin", config.text_document_margin); + set_object("spreadsheetLimit", "Lapp/opendocument/core/TableDimensions;", + config.spreadsheet_limit.has_value() + ? make_table_dimensions(env, *config.spreadsheet_limit) + : nullptr); + set_boolean("spreadsheetLimitByContent", config.spreadsheet_limit_by_content); + set_object("spreadsheetGridlines", + "Lapp/opendocument/core/HtmlTableGridlines;", + enum_from_code(env, "app/opendocument/core/HtmlTableGridlines", + static_cast(config.spreadsheet_gridlines))); + set_boolean("formatHtml", config.format_html); + set_int("htmlIndent", config.html_indent); + set_string("htmlIndentString", config.html_indent_string); + set_string("backgroundImageFormat", config.background_image_format); + set_double("backgroundImageDpi", config.background_image_dpi); + set_int("pageRangeBegin", static_cast(config.page_range_begin)); + set_object("pageRangeEnd", "Ljava/lang/Integer;", + box_integer(env, config.page_range_end)); + set_object("pdfTextMode", "Lapp/opendocument/core/PdfTextMode;", + enum_from_code(env, "app/opendocument/core/PdfTextMode", + static_cast(config.pdf_text_mode))); + { + jclass string_cls = env->FindClass("java/lang/String"); + jobjectArray fonts = env->NewObjectArray( + static_cast(config.pdf_dual_layer_fallback_fonts.size()), + string_cls, nullptr); + for (jsize i = 0; + i < static_cast(config.pdf_dual_layer_fallback_fonts.size()); + ++i) { + env->SetObjectArrayElement( + fonts, i, to_jstring(env, config.pdf_dual_layer_fallback_fonts[i])); + } + set_object("pdfDualLayerFallbackFonts", "[Ljava/lang/String;", fonts); + env->DeleteLocalRef(string_cls); + } + set_double("pdfDualLayerFallbackFontSizeAdjust", + config.pdf_dual_layer_fallback_font_size_adjust); + set_boolean("noDrm", config.no_drm); + set_boolean("embedOutline", config.embed_outline); + set_object("outputPath", "Ljava/lang/String;", + make_string_opt(env, config.output_path)); + + env->DeleteLocalRef(cls); + return result; +} + +odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config) { + // Start from the C++ defaults so unexposed members (resource_locator) keep + // their standard values. + odr::HtmlConfig result; + if (config == nullptr) { + return result; + } + + jclass cls = env->GetObjectClass(config); + + const auto get_string = [&](const char *name) { + auto value = static_cast(env->GetObjectField( + config, env->GetFieldID(cls, name, "Ljava/lang/String;"))); + return to_string(env, value); + }; + const auto get_string_opt = + [&](const char *name) -> std::optional { + auto value = static_cast(env->GetObjectField( + config, env->GetFieldID(cls, name, "Ljava/lang/String;"))); + if (value == nullptr) { + return std::nullopt; + } + return to_string(env, value); + }; + const auto get_boolean = [&](const char *name) { + return env->GetBooleanField(config, env->GetFieldID(cls, name, "Z")) != + JNI_FALSE; + }; + const auto get_int = [&](const char *name) { + return env->GetIntField(config, env->GetFieldID(cls, name, "I")); + }; + const auto get_double = [&](const char *name) { + return env->GetDoubleField(config, env->GetFieldID(cls, name, "D")); + }; + const auto get_object = [&](const char *name, const char *signature) { + return env->GetObjectField(config, env->GetFieldID(cls, name, signature)); + }; + + result.document_output_file_name = get_string("documentOutputFileName"); + result.slide_output_file_name = get_string("slideOutputFileName"); + result.sheet_output_file_name = get_string("sheetOutputFileName"); + result.page_output_file_name = get_string("pageOutputFileName"); + result.embed_images = get_boolean("embedImages"); + result.embed_shipped_resources = get_boolean("embedShippedResources"); + if (const auto resource_path = get_string_opt("resourcePath"); + resource_path.has_value()) { + result.resource_path = *resource_path; + } + result.relative_resource_paths = get_boolean("relativeResourcePaths"); + result.editable = get_boolean("editable"); + result.text_document_margin = get_boolean("textDocumentMargin"); + { + jobject limit = get_object("spreadsheetLimit", + "Lapp/opendocument/core/TableDimensions;"); + if (limit == nullptr) { + result.spreadsheet_limit = std::nullopt; + } else { + jclass dimensions_cls = env->GetObjectClass(limit); + const jint rows = + env->GetIntField(limit, env->GetFieldID(dimensions_cls, "rows", "I")); + const jint columns = env->GetIntField( + limit, env->GetFieldID(dimensions_cls, "columns", "I")); + result.spreadsheet_limit = + odr::TableDimensions(static_cast(rows), + static_cast(columns)); + env->DeleteLocalRef(dimensions_cls); + } + } + result.spreadsheet_limit_by_content = + get_boolean("spreadsheetLimitByContent"); + { + const jint code = enum_ordinal( + env, get_object("spreadsheetGridlines", + "Lapp/opendocument/core/HtmlTableGridlines;")); + if (code >= 0) { + result.spreadsheet_gridlines = static_cast(code); + } + } + result.format_html = get_boolean("formatHtml"); + result.html_indent = static_cast(get_int("htmlIndent")); + result.html_indent_string = get_string("htmlIndentString"); + result.background_image_format = get_string("backgroundImageFormat"); + result.background_image_dpi = get_double("backgroundImageDpi"); + result.page_range_begin = + static_cast(get_int("pageRangeBegin")); + { + jobject end = get_object("pageRangeEnd", "Ljava/lang/Integer;"); + if (end == nullptr) { + result.page_range_end = std::nullopt; + } else { + jclass integer_cls = env->GetObjectClass(end); + jmethodID int_value = env->GetMethodID(integer_cls, "intValue", "()I"); + result.page_range_end = + static_cast(env->CallIntMethod(end, int_value)); + env->DeleteLocalRef(integer_cls); + } + } + { + const jint code = enum_ordinal( + env, get_object("pdfTextMode", "Lapp/opendocument/core/PdfTextMode;")); + if (code >= 0) { + result.pdf_text_mode = static_cast(code); + } + } + { + auto fonts = static_cast( + get_object("pdfDualLayerFallbackFonts", "[Ljava/lang/String;")); + if (fonts != nullptr) { + const jsize length = env->GetArrayLength(fonts); + result.pdf_dual_layer_fallback_fonts.clear(); + for (jsize i = 0; i < length; ++i) { + auto font = static_cast(env->GetObjectArrayElement(fonts, i)); + result.pdf_dual_layer_fallback_fonts.push_back(to_string(env, font)); + env->DeleteLocalRef(font); + } + } + } + result.pdf_dual_layer_fallback_font_size_adjust = + get_double("pdfDualLayerFallbackFontSizeAdjust"); + result.no_drm = get_boolean("noDrm"); + result.embed_outline = get_boolean("embedOutline"); + result.output_path = get_string_opt("outputPath"); + + env->DeleteLocalRef(cls); + return result; +} + +} // namespace odr_jni diff --git a/jni/src/odr_jni.cpp b/jni/src/odr_jni.cpp new file mode 100644 index 00000000..aa1b04ca --- /dev/null +++ b/jni/src/odr_jni.cpp @@ -0,0 +1,181 @@ +#include "odr_jni.hpp" + +#include + +#include +#include + +namespace odr_jni { + +namespace { + +void throw_new(JNIEnv *env, const char *class_name, const char *message) { + jclass cls = env->FindClass(class_name); + if (cls == nullptr) { + return; // a NoClassDefFoundError is pending instead + } + env->ThrowNew(cls, message); + env->DeleteLocalRef(cls); +} + +void append_utf8(std::string &out, const std::uint32_t code_point) { + if (code_point < 0x80) { + out.push_back(static_cast(code_point)); + } else if (code_point < 0x800) { + out.push_back(static_cast(0xc0 | (code_point >> 6))); + out.push_back(static_cast(0x80 | (code_point & 0x3f))); + } else if (code_point < 0x10000) { + out.push_back(static_cast(0xe0 | (code_point >> 12))); + out.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3f))); + out.push_back(static_cast(0x80 | (code_point & 0x3f))); + } else { + out.push_back(static_cast(0xf0 | (code_point >> 18))); + out.push_back(static_cast(0x80 | ((code_point >> 12) & 0x3f))); + out.push_back(static_cast(0x80 | ((code_point >> 6) & 0x3f))); + out.push_back(static_cast(0x80 | (code_point & 0x3f))); + } +} + +} // namespace + +std::string to_string(JNIEnv *env, jstring string) { + if (string == nullptr) { + return {}; + } + const jsize length = env->GetStringLength(string); + const jchar *chars = env->GetStringChars(string, nullptr); + if (chars == nullptr) { + return {}; + } + std::string result; + result.reserve(static_cast(length)); + for (jsize i = 0; i < length; ++i) { + std::uint32_t code_point = chars[i]; + if (code_point >= 0xd800 && code_point <= 0xdbff && i + 1 < length && + chars[i + 1] >= 0xdc00 && chars[i + 1] <= 0xdfff) { + code_point = + 0x10000 + ((code_point - 0xd800) << 10) + (chars[i + 1] - 0xdc00); + ++i; + } + append_utf8(result, code_point); + } + env->ReleaseStringChars(string, chars); + return result; +} + +jstring to_jstring(JNIEnv *env, const std::string_view string) { + std::vector units; + units.reserve(string.size()); + for (std::size_t i = 0; i < string.size();) { + const auto byte = static_cast(string[i]); + std::uint32_t code_point = 0xfffd; + std::size_t sequence_length = 1; + if (byte < 0x80) { + code_point = byte; + } else if ((byte >> 5) == 0x6) { + code_point = byte & 0x1f; + sequence_length = 2; + } else if ((byte >> 4) == 0xe) { + code_point = byte & 0x0f; + sequence_length = 3; + } else if ((byte >> 3) == 0x1e) { + code_point = byte & 0x07; + sequence_length = 4; + } + if (i + sequence_length > string.size()) { + code_point = 0xfffd; + sequence_length = 1; + } else { + for (std::size_t j = 1; j < sequence_length; ++j) { + const auto continuation = static_cast(string[i + j]); + if ((continuation >> 6) != 0x2) { + code_point = 0xfffd; + sequence_length = 1; + break; + } + code_point = (code_point << 6) | (continuation & 0x3f); + } + } + i += sequence_length; + if (code_point >= 0x10000) { + const std::uint32_t offset = code_point - 0x10000; + units.push_back(static_cast(0xd800 + (offset >> 10))); + units.push_back(static_cast(0xdc00 + (offset & 0x3ff))); + } else { + units.push_back(static_cast(code_point)); + } + } + return env->NewString(units.data(), static_cast(units.size())); +} + +jbyteArray to_jbytes(JNIEnv *env, const std::string_view bytes) { + const auto length = static_cast(bytes.size()); + jbyteArray result = env->NewByteArray(length); + if (result == nullptr) { + return nullptr; + } + env->SetByteArrayRegion(result, 0, length, + reinterpret_cast(bytes.data())); + return result; +} + +void throw_java(JNIEnv *env) { + constexpr auto base = "app/opendocument/core/OdrException"; + try { + throw; + } catch (const odr::UnsupportedOperation &e) { + throw_new(env, "app/opendocument/core/OdrException$UnsupportedOperation", + e.what()); + } catch (const odr::FileNotFound &e) { + throw_new(env, "app/opendocument/core/OdrException$FileNotFound", e.what()); + } catch (const odr::UnknownFileType &e) { + throw_new(env, "app/opendocument/core/OdrException$UnknownFileType", + e.what()); + } catch (const odr::UnsupportedFileType &e) { + throw_new(env, "app/opendocument/core/OdrException$UnsupportedFileType", + e.what()); + } catch (const odr::UnknownDecoderEngine &e) { + throw_new(env, "app/opendocument/core/OdrException$UnknownDecoderEngine", + e.what()); + } catch (const odr::UnsupportedDecoderEngine &e) { + throw_new(env, + "app/opendocument/core/OdrException$UnsupportedDecoderEngine", + e.what()); + } catch (const odr::FileReadError &e) { + throw_new(env, "app/opendocument/core/OdrException$FileReadError", + e.what()); + } catch (const odr::FileWriteError &e) { + throw_new(env, "app/opendocument/core/OdrException$FileWriteError", + e.what()); + } catch (const odr::NoDocumentFile &e) { + throw_new(env, "app/opendocument/core/OdrException$NoDocumentFile", + e.what()); + } catch (const odr::UnknownDocumentType &e) { + throw_new(env, "app/opendocument/core/OdrException$UnknownDocumentType", + e.what()); + } catch (const odr::UnsupportedCryptoAlgorithm &e) { + throw_new(env, + "app/opendocument/core/OdrException$UnsupportedCryptoAlgorithm", + e.what()); + } catch (const odr::WrongPasswordError &e) { + throw_new(env, "app/opendocument/core/OdrException$WrongPassword", + e.what()); + } catch (const odr::DecryptionFailed &e) { + throw_new(env, "app/opendocument/core/OdrException$DecryptionFailed", + e.what()); + } catch (const odr::NotEncryptedError &e) { + throw_new(env, "app/opendocument/core/OdrException$NotEncrypted", e.what()); + } catch (const odr::FileEncryptedError &e) { + throw_new(env, "app/opendocument/core/OdrException$FileEncrypted", + e.what()); + } catch (const odr::DocumentCopyProtectedException &e) { + throw_new(env, "app/opendocument/core/OdrException$DocumentCopyProtected", + e.what()); + } catch (const std::exception &e) { + throw_new(env, base, e.what()); + } catch (...) { + throw_new(env, base, "unknown native error"); + } +} + +} // namespace odr_jni diff --git a/jni/src/odr_jni.hpp b/jni/src/odr_jni.hpp new file mode 100644 index 00000000..f7e78717 --- /dev/null +++ b/jni/src/odr_jni.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include + +#include +#include +#include +#include + +namespace odr_jni { + +/// Java string (UTF-16) to UTF-8. Handles nullptr and supplementary planes +/// (unlike JNI's modified UTF-8 accessors). +std::string to_string(JNIEnv *env, jstring string); +/// UTF-8 to Java string (UTF-16). +jstring to_jstring(JNIEnv *env, std::string_view string); + +jbyteArray to_jbytes(JNIEnv *env, std::string_view bytes); + +/// Rethrows the pending C++ exception as the matching Java exception +/// (`app.opendocument.core.OdrException` and subclasses). +void throw_java(JNIEnv *env); + +/// Runs `f`, converting any C++ exception into a pending Java exception and +/// returning a value-initialized result (ignored by the JVM in that case). +template auto guarded(JNIEnv *env, F &&f) -> decltype(f()) { + using Result = decltype(f()); + try { + return std::forward(f)(); + } catch (...) { + throw_java(env); + } + if constexpr (!std::is_void_v) { + return Result{}; + } +} + +template T *from_handle(jlong handle) { + return reinterpret_cast(handle); +} + +/// Moves `value` to the heap and returns the pointer as a Java handle. +template jlong make_handle(T value) { + return reinterpret_cast(new T(std::move(value))); +} + +template void destroy_handle(jlong handle) { + delete from_handle(handle); +} + +} // namespace odr_jni diff --git a/jni/tests/app/opendocument/core/DocumentTest.java b/jni/tests/app/opendocument/core/DocumentTest.java new file mode 100644 index 00000000..503b785b --- /dev/null +++ b/jni/tests/app/opendocument/core/DocumentTest.java @@ -0,0 +1,89 @@ +package app.opendocument.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class DocumentTest { + @TempDir Path tempDir; + + private Document openDocument() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + return Odr.open(odt.toString()).asDocumentFile().document(); + } + + private static List walkText(Element element) { + List parts = new ArrayList<>(); + if (element.type() == ElementType.TEXT) { + parts.add(element.asText().content()); + } + for (Element child : element.children()) { + parts.addAll(walkText(child)); + } + return parts; + } + + @Test + void elementTree() throws IOException { + Document document = openDocument(); + assertEquals(DocumentType.TEXT, document.documentType()); + assertEquals(FileType.OPENDOCUMENT_TEXT, document.fileType()); + + Element root = document.rootElement(); + assertEquals(ElementType.ROOT, root.type()); + + List paragraphs = + root.children().stream().filter(child -> child.type() == ElementType.PARAGRAPH).toList(); + assertEquals(2, paragraphs.size()); + assertNotNull(paragraphs.get(0).asParagraph()); + + List text = walkText(root); + assertTrue(text.contains(TestFiles.ODT_FIRST_PARAGRAPH)); + // Exercises non-BMP characters across the JNI string conversion. + assertTrue(text.contains(TestFiles.ODT_SECOND_PARAGRAPH)); + } + + @Test + void elementNavigation() throws IOException { + Document document = openDocument(); + Element root = document.rootElement(); + + Element first = root.firstChild(); + assertNotNull(first); + assertTrue(first.parent().isSame(root)); + Element second = first.nextSibling(); + assertNotNull(second); + assertTrue(second.previousSibling().isSame(first)); + } + + @Test + void textRoot() throws IOException { + Document document = openDocument(); + TextRoot root = document.rootElement().asTextRoot(); + assertNotNull(root); + assertNotNull(root.pageLayout()); + } + + @Test + void documentFilesystem() throws IOException { + Document document = openDocument(); + Filesystem filesystem = document.asFilesystem(); + assertTrue(filesystem.isFile("/content.xml")); + } + + @Test + void documentPath() throws IOException { + Document document = openDocument(); + Element first = document.rootElement().firstChild(); + DocumentPath path = first.documentPath(); + assertNotNull(path.toString()); + assertEquals(path, first.documentPath()); + } +} diff --git a/jni/tests/app/opendocument/core/FileTest.java b/jni/tests/app/opendocument/core/FileTest.java new file mode 100644 index 00000000..20b447f7 --- /dev/null +++ b/jni/tests/app/opendocument/core/FileTest.java @@ -0,0 +1,93 @@ +package app.opendocument.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.nio.file.Path; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class FileTest { + @TempDir Path tempDir; + + @Test + void openOdt() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + try (DecodedFile file = Odr.open(odt.toString())) { + assertEquals(FileType.OPENDOCUMENT_TEXT, file.fileType()); + assertEquals(FileCategory.DOCUMENT, file.fileCategory()); + assertTrue(file.isDocumentFile()); + assertFalse(file.passwordEncrypted()); + + DocumentFile documentFile = file.asDocumentFile(); + assertEquals(DocumentType.TEXT, documentFile.documentType()); + DocumentMeta meta = documentFile.documentMeta(); + assertEquals(DocumentType.TEXT, meta.documentType); + } + } + + @Test + void openText() throws IOException { + Path txt = TestFiles.txtFile(tempDir); + try (DecodedFile file = Odr.open(txt.toString())) { + assertEquals(FileType.TEXT_FILE, file.fileType()); + assertTrue(file.isTextFile()); + assertTrue(file.asTextFile().text().contains("hello text file")); + } + } + + @Test + void openCsv() throws IOException { + Path csv = TestFiles.csvFile(tempDir); + try (DecodedFile file = Odr.open(csv.toString())) { + assertEquals(FileType.COMMA_SEPARATED_VALUES, file.fileType()); + } + } + + @Test + void fileReadMatchesSize() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + try (File file = new File(odt.toString())) { + assertEquals(FileLocation.DISK, file.location()); + assertNotNull(file.diskPath()); + assertTrue(file.size() > 0); + assertEquals(file.size(), file.read().length); + } + } + + @Test + void fileMeta() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + try (DecodedFile file = Odr.open(odt.toString())) { + FileMeta meta = file.fileMeta(); + assertEquals(FileType.OPENDOCUMENT_TEXT, meta.type); + assertFalse(meta.passwordEncrypted); + assertNotNull(meta.documentMeta); + } + } + + @Test + void openMissingFileThrows() { + assertThrows( + OdrException.FileNotFound.class, + () -> Odr.open(tempDir.resolve("missing.odt").toString())); + } + + @Test + void listFileTypes() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + assertTrue(Odr.listFileTypes(odt.toString()).contains(FileType.OPENDOCUMENT_TEXT)); + } + + @Test + void documentFileByPath() throws IOException { + Path odt = TestFiles.odtFile(tempDir); + assertEquals(FileType.OPENDOCUMENT_TEXT, DocumentFile.typeByPath(odt.toString())); + FileMeta meta = DocumentFile.metaByPath(odt.toString()); + assertEquals(FileType.OPENDOCUMENT_TEXT, meta.type); + } +} diff --git a/jni/tests/app/opendocument/core/HtmlTest.java b/jni/tests/app/opendocument/core/HtmlTest.java new file mode 100644 index 00000000..5dad7837 --- /dev/null +++ b/jni/tests/app/opendocument/core/HtmlTest.java @@ -0,0 +1,75 @@ +package app.opendocument.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HtmlTest { + @TempDir Path tempDir; + + private Html translateOffline(Path input) throws IOException { + Path cache = Files.createDirectories(tempDir.resolve("cache")); + Path output = Files.createDirectories(tempDir.resolve("output")); + DecodedFile file = Odr.open(input.toString()); + HtmlService service = Html.translate(file, cache.toString(), new HtmlConfig()); + return service.bringOffline(output.toString()); + } + + @Test + void htmlConfigDefaults() { + HtmlConfig config = new HtmlConfig(); + assertTrue(config.embedImages); + assertTrue(!config.editable); + assertEquals(HtmlTableGridlines.SOFT, config.spreadsheetGridlines); + } + + @Test + void translateText() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + Html html = translateOffline(TestFiles.txtFile(tempDir)); + List pages = html.pages(); + assertEquals(1, pages.size()); + String content = Files.readString(Path.of(pages.get(0).path)); + assertTrue(content.contains("hello text file")); + } + + @Test + void translateCsv() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + Html html = translateOffline(TestFiles.csvFile(tempDir)); + assertEquals(1, html.pages().size()); + String content = Files.readString(Path.of(html.pages().get(0).path)); + assertTrue(content.contains("alpha")); + } + + @Test + void translateDocument() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + Html html = translateOffline(TestFiles.odtFile(tempDir)); + assertEquals(1, html.pages().size()); + String content = Files.readString(Path.of(html.pages().get(0).path)); + assertTrue(content.contains(TestFiles.ODT_FIRST_PARAGRAPH)); + } + + @Test + void htmlServiceViews() throws IOException { + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + Path cache = Files.createDirectories(tempDir.resolve("cache")); + DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString()); + HtmlService service = Html.translate(file, cache.toString(), new HtmlConfig()); + + List views = service.listViews(); + assertEquals(1, views.size()); + assertTrue(service.exists(views.get(0).path())); + + Html.Content content = views.get(0).writeHtml(); + assertTrue(content.html.contains(TestFiles.ODT_FIRST_PARAGRAPH)); + } +} diff --git a/jni/tests/app/opendocument/core/HttpServerTest.java b/jni/tests/app/opendocument/core/HttpServerTest.java new file mode 100644 index 00000000..559884fa --- /dev/null +++ b/jni/tests/app/opendocument/core/HttpServerTest.java @@ -0,0 +1,106 @@ +package app.opendocument.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.ServerSocket; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class HttpServerTest { + @TempDir Path tempDir; + + private static int freePort() throws IOException { + try (ServerSocket socket = new ServerSocket(0)) { + return socket.getLocalPort(); + } + } + + private record Response(int status, String body) {} + + // HttpURLConnection with "Connection: close" so no keep-alive connection + // outlives the request — cpp-httplib's listen() only returns after its + // workers finish, and an open keep-alive connection delays that by the + // keep-alive timeout, racing the thread join below. + private static Response fetch(String url) throws Exception { + long deadline = System.nanoTime() + Duration.ofSeconds(5).toNanos(); + while (true) { + HttpURLConnection connection = + (HttpURLConnection) URI.create(url).toURL().openConnection(); + connection.setConnectTimeout(1000); + connection.setReadTimeout(1000); + connection.setRequestProperty("Connection", "close"); + try { + int status = connection.getResponseCode(); + try (InputStream body = connection.getInputStream()) { + return new Response( + status, new String(body.readAllBytes(), StandardCharsets.UTF_8)); + } + } catch (IOException e) { + if (System.nanoTime() > deadline) { + throw e; + } + Thread.sleep(50); + } finally { + connection.disconnect(); + } + } + } + + @Test + void serveFile() throws Exception { + assumeTrue(Odr.hasHttpServer(), "built without the HTTP server"); + assumeTrue(TestFiles.hasCoreData(), "odr core data path not available"); + + HttpServer.Config config = new HttpServer.Config(); + config.cachePath = tempDir.resolve("server-cache").toString(); + HttpServer server = new HttpServer(config); + + DecodedFile file = Odr.open(TestFiles.odtFile(tempDir).toString()); + HtmlConfig htmlConfig = new HtmlConfig(); + htmlConfig.embedImages = false; + htmlConfig.relativeResourcePaths = false; + List views = server.serveFile(file, "doc", htmlConfig); + assertEquals(1, views.size()); + + int port = freePort(); + AtomicReference listenError = new AtomicReference<>(); + Thread thread = + new Thread( + () -> { + try { + server.listen("127.0.0.1", port); + } catch (Throwable t) { + listenError.set(t); + } + }); + thread.setDaemon(true); + thread.start(); + try { + Response response = + fetch("http://127.0.0.1:" + port + "/file/doc/" + views.get(0).path()); + assertEquals(200, response.status()); + assertTrue(response.body().contains(TestFiles.ODT_FIRST_PARAGRAPH)); + } catch (Exception e) { + if (listenError.get() != null) { + throw new AssertionError("listen failed", listenError.get()); + } + throw e; + } finally { + server.stop(); + thread.join(Duration.ofSeconds(5).toMillis()); + } + assertFalse(thread.isAlive()); + } +} diff --git a/jni/tests/app/opendocument/core/MetaTest.java b/jni/tests/app/opendocument/core/MetaTest.java new file mode 100644 index 00000000..7987bc70 --- /dev/null +++ b/jni/tests/app/opendocument/core/MetaTest.java @@ -0,0 +1,53 @@ +package app.opendocument.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class MetaTest { + @Test + void version() { + // The version is injected by the packaging build and empty in dev builds. + assertNotNull(Odr.version()); + assertNotNull(Odr.commitHash()); + assertFalse(Odr.identify().isEmpty()); + } + + @Test + void fileTypeByFileExtension() { + assertEquals(FileType.OPENDOCUMENT_TEXT, Odr.fileTypeByFileExtension("odt")); + assertEquals(FileType.PORTABLE_DOCUMENT_FORMAT, Odr.fileTypeByFileExtension("pdf")); + } + + @Test + void fileTypeStrings() { + String string = Odr.fileTypeToString(FileType.OPENDOCUMENT_TEXT); + assertFalse(string.isEmpty()); + assertEquals( + FileCategory.DOCUMENT, Odr.fileCategoryByFileType(FileType.OPENDOCUMENT_TEXT)); + assertEquals(DocumentType.TEXT, Odr.documentTypeByFileType(FileType.OPENDOCUMENT_TEXT)); + } + + @Test + void mimetypeRoundTrip() { + String mimetype = Odr.mimetypeByFileType(FileType.OPENDOCUMENT_TEXT); + assertFalse(mimetype.isEmpty()); + assertEquals(FileType.OPENDOCUMENT_TEXT, Odr.fileTypeByMimetype(mimetype)); + } + + @Test + void tablePosition() { + assertEquals(0, TablePosition.toColumnNum("A")); + assertEquals("A", TablePosition.toColumnString(0)); + assertEquals("A1", new TablePosition(0, 0).toString()); + } + + @Test + void listDecoderEngines() { + assertTrue( + Odr.listDecoderEngines(FileType.OPENDOCUMENT_TEXT).contains(DecoderEngine.ODR)); + } +} diff --git a/jni/tests/app/opendocument/core/TestFiles.java b/jni/tests/app/opendocument/core/TestFiles.java new file mode 100644 index 00000000..9530a873 --- /dev/null +++ b/jni/tests/app/opendocument/core/TestFiles.java @@ -0,0 +1,117 @@ +package app.opendocument.core; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.zip.CRC32; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** Builds minimal test input files from inline content (mirrors python/tests/conftest.py). */ +final class TestFiles { + static final String ODT_FIRST_PARAGRAPH = "Hello from odr-core-java!"; + static final String ODT_SECOND_PARAGRAPH = "Second paragraph äöü 😀"; + + private static final String ODT_CONTENT_XML = + """ + + + + + %s + %s + + + + """ + .formatted(ODT_FIRST_PARAGRAPH, ODT_SECOND_PARAGRAPH); + + private static final String ODT_STYLES_XML = + """ + + + + + + + """; + + private static final String ODT_MANIFEST_XML = + """ + + + + + + + """; + + static { + // Mirror the python conftest: pick up the shipped assets for rendering. + String dataPath = System.getenv("ODR_CORE_DATA_PATH"); + if (dataPath != null && !dataPath.isEmpty()) { + GlobalParams.setOdrCoreDataPath(dataPath); + } + } + + /** Whether the odr core data (CSS/JS assets) is available for rendering. */ + static boolean hasCoreData() { + String path = GlobalParams.odrCoreDataPath(); + return path != null && !path.isEmpty() && Files.isDirectory(Path.of(path)); + } + + /** A minimal OpenDocument text file built from inline XML. */ + static Path odtFile(Path directory) throws IOException { + Path path = directory.resolve("minimal.odt"); + try (ZipOutputStream zip = new ZipOutputStream(Files.newOutputStream(path))) { + byte[] mimetype = + "application/vnd.oasis.opendocument.text".getBytes(StandardCharsets.UTF_8); + ZipEntry entry = new ZipEntry("mimetype"); + entry.setMethod(ZipEntry.STORED); + entry.setSize(mimetype.length); + CRC32 crc = new CRC32(); + crc.update(mimetype); + entry.setCrc(crc.getValue()); + zip.putNextEntry(entry); + zip.write(mimetype); + zip.closeEntry(); + + writeEntry(zip, "content.xml", ODT_CONTENT_XML); + writeEntry(zip, "styles.xml", ODT_STYLES_XML); + writeEntry(zip, "META-INF/manifest.xml", ODT_MANIFEST_XML); + } + return path; + } + + static Path csvFile(Path directory) throws IOException { + Path path = directory.resolve("table.csv"); + Files.writeString(path, "name,value\nalpha,1\nbeta,2\n"); + return path; + } + + static Path txtFile(Path directory) throws IOException { + Path path = directory.resolve("note.txt"); + Files.writeString(path, "hello text file\nsecond line\n"); + return path; + } + + private static void writeEntry(ZipOutputStream zip, String name, String content) + throws IOException { + zip.putNextEntry(new ZipEntry(name)); + zip.write(content.getBytes(StandardCharsets.UTF_8)); + zip.closeEntry(); + } + + private TestFiles() {} +} diff --git a/scripts/format b/scripts/format index 5a60bd72..2a2bb5ba 100755 --- a/scripts/format +++ b/scripts/format @@ -21,3 +21,4 @@ format_folder src format_folder cli format_folder test/src format_folder python/src +format_folder jni/src