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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ bytes ─▢ magic/open_strategy ─▢ DecodedFile ─▢ Document ─▢ Eleme
| `src/odr/internal/common/` | Reusable impls: `Path`/`AbsPath`, base `Document`, filesystem, `style`, table cursor/range, temp files. |
| `src/odr/internal/util/` | Helpers: `byte_stream_util`, `string_util`, `stream_util`, `document_util`, `xml_util`. |
| `src/odr/internal/magic.*`, `open_strategy.*` | File-type detection + open/dispatch. |
| `src/odr/internal/file_type_table.*` | **The** per-`FileType` table: extensions, MIME types, category, document type, `FileTypeCapabilities`. Every public lookup in `odr.hpp` is a thin forward into it β€” extend the table, not the lookups. |
| `src/odr/internal/html/` | Generic HTML renderer. |
| `src/odr/internal/cfb/`, `zip/` | Container formats (CFB, ZIP). |
| `src/odr/internal/odf/` | OpenDocument (odt/ods/odp/odg); see [`odf/AGENTS.md`](src/odr/internal/odf/AGENTS.md). |
Expand Down Expand Up @@ -138,8 +139,12 @@ cmake --build cmake-build-relwithdebinfo --target translate # CLI: file β†’ HTM

## Adding / extending a document format

1. Detection: extend `magic`/`open_strategy` to map bytes β†’ `FileType`
(+ `DecoderEngine`) and construct your `DecodedFile`.
1. Detection: extend `magic`/`open_strategy` to map bytes β†’ `FileType` and
construct your `DecodedFile`, and add a row to
`internal/file_type_table.cpp` (extensions, MIME types, category, document
type, capabilities) β€” `odr_test` fails if a `FileType` has no row, if an
alias is claimed twice, or if the declared capabilities exceed what the
engines actually do.
2. For documents: subclass `internal::Document`; in its constructor build an
`ElementRegistry` and an `ElementAdapter` (pattern above).
3. Implement the per-element adapters you can populate; the **generic HTML
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ set(ODR_SOURCE_FILES
"src/odr/table_dimension.cpp"
"src/odr/table_position.cpp"

"src/odr/internal/file_type_table.cpp"
"${CMAKE_CURRENT_BINARY_DIR}/src/odr/internal/git_info.cpp"
"src/odr/internal/magic.cpp"
"src/odr/internal/open_strategy.cpp"
Expand Down
26 changes: 24 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,35 @@ C++ library to visualize files, especially documents, in HTML.
- [cfb](https://github.com/opendocument-app/OpenDocument.core/issues/110) (Microsoft Compound File Binary File Format)
- ttf / otf (font specimen pages)

Every accepted file extension and MIME type β€” including the template and
macro-enabled aliases (`docm`, `dotx`, `xltx`, `ppsx`, …) β€” is enumerable at
runtime via `file_extensions_by_file_type` / `mimetypes_by_file_type`, so a
consumer never has to maintain its own copy of these tables.

## Detected but not decoded

These are recognised by `list_file_types` / `mimetype` so a caller can report
them, but there is no decoder β€” opening one throws:
These are classified by extension and MIME type, and `rtf`/`wpd` are also
recognised from their bytes, so a caller can report them β€” but there is no
decoder, and opening one throws:

- rtf
- wpd (WordPerfect)
- md (Markdown)
- xlsb (Excel binary workbook β€” an OOXML package whose workbook parts are
binary rather than spreadsheetml)

## Asking what is supported

`capabilities_by_file_type(FileType)` answers, per format, whether the library
can detect, open, decrypt, render, edit, save or encrypt it β€” the decisions a
caller has to make *before* it holds a file, e.g. which MIME types to advertise
to a system file picker. It is a declared upper bound; `DecodedFile::capabilities()`
and `Document::is_editable` / `is_savable` give the precise answer for a
concrete file.

Editing and saving are currently limited to odt, odp, odg (`edit` + `save`),
ods (`save` only) and docx (`edit` + `save`); saving with a password is not
supported for any format.

## Unsupported files

Expand Down
1 change: 1 addition & 0 deletions jni/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ add_jar(odr_java
"java/app/opendocument/core/FileLocation.java"
"java/app/opendocument/core/FileMeta.java"
"java/app/opendocument/core/FileType.java"
"java/app/opendocument/core/FileTypeCapabilities.java"
"java/app/opendocument/core/FileWalker.java"
"java/app/opendocument/core/Filesystem.java"
"java/app/opendocument/core/FontFile.java"
Expand Down
11 changes: 11 additions & 0 deletions jni/java/app/opendocument/core/DecodedFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ public DecodedFile decrypt(String password) {
return new DecodedFile(decryptNative(handle(), password));
}

/**
* What can be done with this file. Refines {@link Odr#capabilitiesByFileType}; {@code edit},
* {@code save} and {@code encrypt} stay as declared for the format β€” ask {@link Document} for
* the precise answer.
*/
public FileTypeCapabilities capabilities() {
return capabilitiesNative(handle());
}

public boolean isDecodable() {
return isDecodableNative(handle());
}
Expand Down Expand Up @@ -129,6 +138,8 @@ public FontFile asFontFile() {

private native long decryptNative(long handle, String password);

private native FileTypeCapabilities capabilitiesNative(long handle);

private native boolean isDecodableNative(long handle);

private native boolean isTextFileNative(long handle);
Expand Down
2 changes: 1 addition & 1 deletion jni/java/app/opendocument/core/FileType.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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,
OFFICE_OPEN_XML_ENCRYPTED, EXCEL_BINARY_WORKBOOK, 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,
Expand Down
50 changes: 50 additions & 0 deletions jni/java/app/opendocument/core/FileTypeCapabilities.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package app.opendocument.core;

/**
* What this library can do with a file format. Mirrors {@code
* odr::FileTypeCapabilities}.
*
* <p>Declared, format-level support β€” an <em>upper bound</em>. A concrete file may still fail
* (corrupt, encrypted, an unsupported sub-variant); ask {@link DecodedFile} or {@link Document} for
* the precise answer. The point of the static query is the decisions a caller has to make
* <em>before</em> it holds a file, e.g. which MIME types to advertise to the system file picker.
*/
public final class FileTypeCapabilities {
/** Recognised from its bytes alone, without a file name or MIME type. */
public final boolean detectByContent;

/** A decoder exists; {@link Odr#open(String)} can decode it. */
public final boolean open;

/** Encrypted instances can be decrypted. */
public final boolean decrypt;

/** {@link Html#translate} produces output. */
public final boolean translateHtml;

/** {@link Document#isEditable} can be {@code true}. */
public final boolean edit;

/** {@link Document#save} is supported. */
public final boolean save;

/** {@link Document#save} with a password is supported. */
public final boolean encrypt;

FileTypeCapabilities(
boolean detectByContent,
boolean open,
boolean decrypt,
boolean translateHtml,
boolean edit,
boolean save,
boolean encrypt) {
this.detectByContent = detectByContent;
this.open = open;
this.decrypt = decrypt;
this.translateHtml = translateHtml;
this.edit = edit;
this.save = save;
this.encrypt = encrypt;
}
}
41 changes: 41 additions & 0 deletions jni/java/app/opendocument/core/Odr.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package app.opendocument.core;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/** Library-level functions. Mirrors the free functions in {@code odr/odr.hpp}. */
Expand All @@ -25,10 +26,29 @@ public final class Odr {
/** Whether the native library was built with the HTTP server. */
public static native boolean hasHttpServer();

/** Every file type this library knows about. */
public static List<FileType> allFileTypes() {
List<FileType> result = new ArrayList<>();
for (int code : allFileTypesNative()) {
result.add(FileType.fromNative(code));
}
return result;
}

public static FileType fileTypeByFileExtension(String extension) {
return FileType.fromNative(fileTypeByFileExtensionNative(extension));
}

/** Every file extension accepted for the file type; the canonical one first. */
public static List<String> fileExtensionsByFileType(FileType type) {
return Arrays.asList(fileExtensionsByFileTypeNative(type.toNative()));
}

/** Canonical file extension for the file type, without a leading dot. */
public static String fileExtensionByFileType(FileType type) {
return fileExtensionByFileTypeNative(type.toNative());
}

public static FileCategory fileCategoryByFileType(FileType type) {
return FileCategory.fromNative(fileCategoryByFileTypeNative(type.toNative()));
}
Expand All @@ -53,10 +73,21 @@ public static FileType fileTypeByMimetype(String mimetype) {
return FileType.fromNative(fileTypeByMimetypeNative(mimetype));
}

/** Canonical MIME type for the file type. */
public static String mimetypeByFileType(FileType type) {
return mimetypeByFileTypeNative(type.toNative());
}

/** Every MIME type accepted for the file type; the canonical one first. */
public static List<String> mimetypesByFileType(FileType type) {
return Arrays.asList(mimetypesByFileTypeNative(type.toNative()));
}

/** What this library can do with the file type. Format-level, i.e. an upper bound. */
public static FileTypeCapabilities capabilitiesByFileType(FileType type) {
return capabilitiesByFileTypeNative(type.toNative());
}

/** Determines the possible file types of a file. */
public static List<FileType> listFileTypes(String path) {
List<FileType> result = new ArrayList<>();
Expand Down Expand Up @@ -93,8 +124,14 @@ public static DecodedFile open(String path, DecodePreference preference) {
preference.fileTypePriorityNative()));
}

private static native int[] allFileTypesNative();

private static native int fileTypeByFileExtensionNative(String extension);

private static native String[] fileExtensionsByFileTypeNative(int type);

private static native String fileExtensionByFileTypeNative(int type);

private static native int fileCategoryByFileTypeNative(int type);

private static native int documentTypeByFileTypeNative(int type);
Expand All @@ -109,6 +146,10 @@ public static DecodedFile open(String path, DecodePreference preference) {

private static native String mimetypeByFileTypeNative(int type);

private static native String[] mimetypesByFileTypeNative(int type);

private static native FileTypeCapabilities capabilitiesByFileTypeNative(int type);

private static native int[] listFileTypesNative(String path);

private static native long openNative(String path);
Expand Down
2 changes: 2 additions & 0 deletions jni/src/jni_convert.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ jobject make_table_dimensions(JNIEnv *env,
const odr::TableDimensions &dimensions);
jobject make_table_position(JNIEnv *env, const odr::TablePosition &position);
jobject make_file_meta(JNIEnv *env, const odr::FileMeta &meta);
jobject make_file_type_capabilities(JNIEnv *env,
const odr::FileTypeCapabilities &);

jobject html_config_to_java(JNIEnv *env, const odr::HtmlConfig &config);
odr::HtmlConfig html_config_from_java(JNIEnv *env, jobject config);
Expand Down
73 changes: 73 additions & 0 deletions jni/src/jni_core.cpp
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#include "jni_convert.hpp"
#include "odr_jni.hpp"

#include <odr/file.hpp>
Expand All @@ -6,13 +7,16 @@
#include <odr/odr.hpp>
#include <odr/table_position.hpp>

#include <span>
#include <string>
#include <string_view>
#include <vector>

namespace {

using odr_jni::from_handle;
using odr_jni::guarded;
using odr_jni::make_file_type_capabilities;
using odr_jni::make_handle;
using odr_jni::to_jstring;
using odr_jni::to_string;
Expand All @@ -27,6 +31,26 @@ jintArray to_jint_array(JNIEnv *env, const std::vector<jint> &values) {
return result;
}

jobjectArray to_jstring_array(JNIEnv *env,
const std::span<const std::string_view> values) {
jclass cls = env->FindClass("java/lang/String");
if (cls == nullptr) {
return nullptr;
}
jobjectArray result =
env->NewObjectArray(static_cast<jsize>(values.size()), cls, nullptr);
env->DeleteLocalRef(cls);
if (result == nullptr) {
return nullptr;
}
for (std::size_t i = 0; i < values.size(); ++i) {
jstring value = to_jstring(env, values[i]);
env->SetObjectArrayElement(result, static_cast<jsize>(i), value);
env->DeleteLocalRef(value);
}
return result;
}

} // namespace

// app.opendocument.core.Odr
Expand Down Expand Up @@ -56,6 +80,17 @@ Java_app_opendocument_core_Odr_identify(JNIEnv *env, jclass) {
return guarded(env, [&] { return to_jstring(env, odr::identify()); });
}

extern "C" JNIEXPORT jintArray JNICALL
Java_app_opendocument_core_Odr_allFileTypesNative(JNIEnv *env, jclass) {
return guarded(env, [&] {
std::vector<jint> codes;
for (const odr::FileType type : odr::all_file_types()) {
codes.push_back(static_cast<jint>(type));
}
return to_jint_array(env, codes);
});
}

extern "C" JNIEXPORT jint JNICALL
Java_app_opendocument_core_Odr_fileTypeByFileExtensionNative(
JNIEnv *env, jclass, jstring extension) {
Expand All @@ -65,6 +100,26 @@ Java_app_opendocument_core_Odr_fileTypeByFileExtensionNative(
});
}

extern "C" JNIEXPORT jobjectArray JNICALL
Java_app_opendocument_core_Odr_fileExtensionsByFileTypeNative(JNIEnv *env,
jclass,
jint type) {
return guarded(env, [&] {
return to_jstring_array(env, odr::file_extensions_by_file_type(
static_cast<odr::FileType>(type)));
});
}

extern "C" JNIEXPORT jstring JNICALL
Java_app_opendocument_core_Odr_fileExtensionByFileTypeNative(JNIEnv *env,
jclass,
jint type) {
return guarded(env, [&] {
return to_jstring(env, odr::file_extension_by_file_type(
static_cast<odr::FileType>(type)));
});
}

extern "C" JNIEXPORT jint JNICALL
Java_app_opendocument_core_Odr_fileCategoryByFileTypeNative(JNIEnv *env, jclass,
jint type) {
Expand Down Expand Up @@ -128,6 +183,24 @@ Java_app_opendocument_core_Odr_mimetypeByFileTypeNative(JNIEnv *env, jclass,
});
}

extern "C" JNIEXPORT jobjectArray JNICALL
Java_app_opendocument_core_Odr_mimetypesByFileTypeNative(JNIEnv *env, jclass,
jint type) {
return guarded(env, [&] {
return to_jstring_array(
env, odr::mimetypes_by_file_type(static_cast<odr::FileType>(type)));
});
}

extern "C" JNIEXPORT jobject JNICALL
Java_app_opendocument_core_Odr_capabilitiesByFileTypeNative(JNIEnv *env, jclass,
jint type) {
return guarded(env, [&] {
return make_file_type_capabilities(
env, odr::capabilities_by_file_type(static_cast<odr::FileType>(type)));
});
}

extern "C" JNIEXPORT jintArray JNICALL
Java_app_opendocument_core_Odr_listFileTypesNative(JNIEnv *env, jclass,
jstring path) {
Expand Down
9 changes: 9 additions & 0 deletions jni/src/jni_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,15 @@ Java_app_opendocument_core_DecodedFile_isDecodableNative(JNIEnv *env, jobject,
});
}

extern "C" JNIEXPORT jobject JNICALL
Java_app_opendocument_core_DecodedFile_capabilitiesNative(JNIEnv *env, jobject,
jlong handle) {
return guarded(env, [&] {
return odr_jni::make_file_type_capabilities(env,
decoded(handle).capabilities());
});
}

extern "C" JNIEXPORT jboolean JNICALL
Java_app_opendocument_core_DecodedFile_isTextFileNative(JNIEnv *env, jobject,
jlong handle) {
Expand Down
Loading
Loading