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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ set(ODR_SOURCE_FILES
"src/odr/internal/font/sfnt_transform.cpp"
"src/odr/internal/font/font_file.cpp"

"src/odr/internal/svg/svg_util.cpp"

"src/odr/internal/svm/svm_file.cpp"
"src/odr/internal/svm/svm_format.cpp"
"src/odr/internal/svm/svm_to_svg.cpp"
Expand Down
10 changes: 10 additions & 0 deletions apple/include/OdrCoreObjC/ODRFile.h
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ typedef NS_ENUM(NSInteger, ODRFileType) {
ODRFileTypeThirdGenerationPartnershipVideo,
ODRFileTypeMatroskaVideo,
ODRFileTypeAudioVideoInterleave,

ODRFileTypeScalableVectorGraphics,
ODRFileTypeWindowsIcon,
ODRFileTypeJpegXl,
ODRFileTypeJpeg2000,
ODRFileTypePhotoshopDocument,
ODRFileTypeWindowsMetafile,
ODRFileTypeEnhancedMetafile,

ODRFileTypeXml,
} NS_SWIFT_NAME(FileType);

typedef NS_ENUM(NSInteger, ODRFileCategory) {
Expand Down
9 changes: 9 additions & 0 deletions apple/src/ODRFile.mm
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@
ODR_SAME_ENUM(ODRFileTypeMatroskaVideo, odr::FileType::matroska_video);
ODR_SAME_ENUM(ODRFileTypeAudioVideoInterleave,
odr::FileType::audio_video_interleave);
ODR_SAME_ENUM(ODRFileTypeScalableVectorGraphics,
odr::FileType::scalable_vector_graphics);
ODR_SAME_ENUM(ODRFileTypeWindowsIcon, odr::FileType::windows_icon);
ODR_SAME_ENUM(ODRFileTypeJpegXl, odr::FileType::jpeg_xl);
ODR_SAME_ENUM(ODRFileTypeJpeg2000, odr::FileType::jpeg_2000);
ODR_SAME_ENUM(ODRFileTypePhotoshopDocument, odr::FileType::photoshop_document);
ODR_SAME_ENUM(ODRFileTypeWindowsMetafile, odr::FileType::windows_metafile);
ODR_SAME_ENUM(ODRFileTypeEnhancedMetafile, odr::FileType::enhanced_metafile);
ODR_SAME_ENUM(ODRFileTypeXml, odr::FileType::xml);

ODR_SAME_ENUM(ODRFileCategoryUnknown, odr::FileCategory::unknown);
ODR_SAME_ENUM(ODRFileCategoryText, odr::FileCategory::text);
Expand Down
3 changes: 2 additions & 1 deletion jni/java/app/opendocument/core/FileType.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ public enum FileType {
HIGH_EFFICIENCY_IMAGE_FORMAT, AV1_IMAGE_FILE_FORMAT, MPEG_AUDIO, MPEG4_AUDIO,
OGG_AUDIO, WAVEFORM_AUDIO, FREE_LOSSLESS_AUDIO_CODEC, MPEG4_VIDEO,
QUICKTIME_VIDEO, THIRD_GENERATION_PARTNERSHIP_VIDEO, MATROSKA_VIDEO,
AUDIO_VIDEO_INTERLEAVE;
AUDIO_VIDEO_INTERLEAVE, SCALABLE_VECTOR_GRAPHICS, WINDOWS_ICON, JPEG_XL,
JPEG_2000, PHOTOSHOP_DOCUMENT, WINDOWS_METAFILE, ENHANCED_METAFILE, XML;

static FileType fromNative(int code) {
return code < 0 ? null : values()[code];
Expand Down
11 changes: 10 additions & 1 deletion python/src/bind_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ void odr_python::bind_file(py::module_ &m) {
.value("third_generation_partnership_video",
odr::FileType::third_generation_partnership_video)
.value("matroska_video", odr::FileType::matroska_video)
.value("audio_video_interleave", odr::FileType::audio_video_interleave);
.value("audio_video_interleave", odr::FileType::audio_video_interleave)
.value("scalable_vector_graphics",
odr::FileType::scalable_vector_graphics)
.value("windows_icon", odr::FileType::windows_icon)
.value("jpeg_xl", odr::FileType::jpeg_xl)
.value("jpeg_2000", odr::FileType::jpeg_2000)
.value("photoshop_document", odr::FileType::photoshop_document)
.value("windows_metafile", odr::FileType::windows_metafile)
.value("enhanced_metafile", odr::FileType::enhanced_metafile)
.value("xml", odr::FileType::xml);

py::enum_<odr::FileCategory>(m, "FileCategory")
.value("unknown", odr::FileCategory::unknown)
Expand Down
23 changes: 23 additions & 0 deletions src/odr/file.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,29 @@ enum class FileType {
matroska_video,
// https://en.wikipedia.org/wiki/Audio_Video_Interleave
audio_video_interleave,

// More images that arrive alongside documents, named the same way and for
// the same reason as the block above - nothing here is decoded either.
// https://en.wikipedia.org/wiki/SVG
scalable_vector_graphics,
// https://en.wikipedia.org/wiki/ICO_(file_format)
windows_icon,
// https://en.wikipedia.org/wiki/JPEG_XL
jpeg_xl,
// https://en.wikipedia.org/wiki/JPEG_2000
jpeg_2000,
// https://en.wikipedia.org/wiki/Adobe_Photoshop#File_format
photoshop_document,
// https://en.wikipedia.org/wiki/Windows_Metafile
windows_metafile,
// https://en.wikipedia.org/wiki/Windows_Metafile#Enhanced_Metafile
enhanced_metafile,

// Classification only for now - detection reports it under the formats built
// on it, e.g. an svg comes back as `[text_file, xml, scalable_vector_
// graphics]`, but there is no decoder of its own behind it yet.
// https://en.wikipedia.org/wiki/XML
xml,
};

/// @brief Collection of file categories.
Expand Down
7 changes: 6 additions & 1 deletion src/odr/internal/common/image_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ std::shared_ptr<abstract::File> ImageFile::file() const noexcept {

FileType ImageFile::file_type() const noexcept { return m_file_type; }

FileMeta ImageFile::file_meta() const noexcept { return {}; }
FileMeta ImageFile::file_meta() const noexcept {
FileMeta result;
result.type = file_type();
result.mimetype = mimetype();
return result;
}

std::string_view ImageFile::mimetype() const noexcept {
// not `mimetype_by_file_type` β€” that throws, and this is `noexcept`
Expand Down
94 changes: 94 additions & 0 deletions src/odr/internal/file_type_table.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,37 @@ constexpr std::array heif_mimetypes{"image/heic"sv, "image/heif"sv,
constexpr std::array avif_extensions{"avif"sv, "avifs"sv};
constexpr std::array avif_mimetypes{"image/avif"sv, "image/avif-sequence"sv};

constexpr std::array xml_extensions{"xml"sv};
constexpr std::array xml_mimetypes{"application/xml"sv, "text/xml"sv};

constexpr std::array svg_extensions{"svg"sv};
constexpr std::array svg_mimetypes{"image/svg+xml"sv};

// `.cur` is the same container with a different resource type, so it rides
// along here rather than becoming a type of its own
constexpr std::array ico_extensions{"ico"sv, "cur"sv};
constexpr std::array ico_mimetypes{"image/vnd.microsoft.icon"sv,
"image/x-icon"sv};

constexpr std::array jxl_extensions{"jxl"sv};
constexpr std::array jxl_mimetypes{"image/jxl"sv};

constexpr std::array jp2_extensions{"jp2"sv, "jpx"sv, "jpf"sv,
"j2k"sv, "jpc"sv, "j2c"sv};
constexpr std::array jp2_mimetypes{"image/jp2"sv, "image/jpx"sv};

// `.psb` is the large document variant and carries the same signature
constexpr std::array psd_extensions{"psd"sv, "psb"sv};
constexpr std::array psd_mimetypes{"image/vnd.adobe.photoshop"sv,
"application/x-photoshop"sv};

constexpr std::array wmf_extensions{"wmf"sv};
constexpr std::array wmf_mimetypes{"image/wmf"sv, "image/x-wmf"sv,
"application/x-msmetafile"sv};

constexpr std::array emf_extensions{"emf"sv};
constexpr std::array emf_mimetypes{"image/emf"sv, "image/x-emf"sv};

constexpr std::array mp3_extensions{"mp3"sv};
constexpr std::array mp3_mimetypes{"audio/mpeg"sv, "audio/mp3"sv,
"audio/x-mpeg"sv};
Expand Down Expand Up @@ -579,6 +610,69 @@ constexpr std::array table{
FileCategory::video,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},

// Named but not decoded, like the images above. `translate_html` says the
// image page is written and the data url is labelled with the type below,
// not that every browser paints it - that is already true of tiff and heif.
Row{FileType::scalable_vector_graphics,
"svg"sv,
svg_extensions,
svg_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::windows_icon,
"ico"sv,
ico_extensions,
ico_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::jpeg_xl,
"jxl"sv,
jxl_extensions,
jxl_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::jpeg_2000,
"jp2"sv,
jp2_extensions,
jp2_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::photoshop_document,
"psd"sv,
psd_extensions,
psd_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::windows_metafile,
"wmf"sv,
wmf_extensions,
wmf_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},
Row{FileType::enhanced_metafile,
"emf"sv,
emf_extensions,
emf_mimetypes,
FileCategory::image,
DocumentType::unknown,
{.detect_by_content = true, .open = true, .translate_html = true}},

// Detection reports it, nothing opens it yet - a plain xml file still
// decodes as text.
Row{FileType::xml,
"xml"sv,
xml_extensions,
xml_mimetypes,
FileCategory::text,
DocumentType::unknown,
{.detect_by_content = true}},
};

/// Finds the row whose list, selected by @p list, contains @p needle.
Expand Down
17 changes: 6 additions & 11 deletions src/odr/internal/html/image_file.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,16 +37,14 @@ void write_image_src(const ImageFile &image_file, std::ostream &out,
// TODO use stream
out << file_to_url(svg_out.str(), "image/svg+xml");
} catch (...) {
// else we guess that it is a usual image
// else it is a usual image and goes out as it came in
// TODO use stream
out << file_to_url(*image_file.stream(), mime_type);
}
}

/// The image page knows exactly which format it is holding, so it says so
/// rather than taking `translate_image_src`'s `image/jpg` - webp, heif and
/// avif arrive here now, and a browser that honours the data URL's type would
/// be left with nothing.
/// An image file knows exactly which format it is holding, so the data url
/// says so. The table's first mime type is the canonical one.
std::string image_mime_type(const ImageFile &image_file) {
const std::span<const std::string_view> mimetypes =
mimetypes_by_file_type(image_file.file_type());
Expand Down Expand Up @@ -147,19 +145,16 @@ void html::translate_image_src(const File &file, std::ostream &out,
try {
translate_image_src(DecodedFile(file).as_image_file(), out, config);
} catch (...) {
// TODO hacky - `image/jpg` works for all common image types in chrome
// nothing named it, so the label is a guess - browsers sniff `<img>` and
// `image/jpg` is what they have been handed here for years
// TODO use stream
out << file_to_url(*file.stream(), "image/jpg");
}
}

void html::translate_image_src(const ImageFile &image_file, std::ostream &out,
const HtmlConfig & /*config*/) {
// TODO hacky - `image/jpg` works for all common image types in chrome.
// An image inside a document keeps it: browsers sniff `<img>`, and naming
// the real type here would rewrite every reference output we have. The
// standalone image page does name it - see `image_mime_type`.
write_image_src(image_file, out, "image/jpg");
write_image_src(image_file, out, image_mime_type(image_file));
}

HtmlService html::create_image_service(const ImageFile &image_file,
Expand Down
53 changes: 43 additions & 10 deletions src/odr/internal/magic.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,24 @@
#include <odr/internal/open_strategy.hpp>
#include <odr/internal/util/string_util.hpp>

#include <array>
#include <istream>
#include <memory>
#include <string_view>
#include <vector>

namespace odr::internal {

namespace {

/// At most @p size bytes, cut back to what was actually read - a file shorter
/// than the longest signature must never be matched against what sat behind it.
std::string read_head(std::istream &in, const std::size_t size) {
std::string result(size, '\0');
in.read(result.data(), static_cast<std::streamsize>(size));
result.resize(static_cast<std::size_t>(in.gcount()));
return result;
}

bool match_magic(const std::string &head, const std::string &pattern) {
const auto bytes = util::string::split(pattern, " ");
if (bytes.size() > head.size()) {
Expand Down Expand Up @@ -161,20 +170,44 @@ FileType magic::file_type(const std::string &magic) {
return FileType::mpeg_audio;
}

if (match_magic(magic, "00 00 01 00") || // icon
match_magic(magic, "00 00 02 00")) { // cursor
return FileType::windows_icon;
}
if (match_magic(magic, "00 00 00 0C 6A 50 20 20 0D 0A 87 0A") || // 'jP ' box
match_magic(magic, "FF 4F FF 51")) { // bare codestream
return FileType::jpeg_2000;
}
if (match_magic(magic, "00 00 00 0C 4A 58 4C 20 0D 0A 87 0A")) { // 'JXL ' box
return FileType::jpeg_xl;
}
if (match_magic(magic, "38 42 50 53")) { // '8BPS'
return FileType::photoshop_document;
}
if (match_magic(magic, "D7 CD C6 9A") || // aldus placeable header
match_magic(magic, "01 00 09 00 00 03") || // memory metafile header
match_magic(magic, "02 00 09 00 00 03")) { // disk metafile header
return FileType::windows_metafile;
}
// the leading record type alone is far too weak, so the header signature at
// offset 40 has to agree
if (match_magic(magic, "01 00 00 00") && tag_at(magic, 40) == " EMF") {
return FileType::enhanced_metafile;
}
// two bytes and nothing more to check, so it goes after everything else
if (match_magic(magic, "FF 0A")) { // bare codestream
return FileType::jpeg_xl;
}

return FileType::unknown;
}

FileType magic::file_type(std::istream &in) {
static constexpr std::size_t max_head_size = 12;

// value initialized, and cut back to what was actually read: a file shorter
// than the longest signature would otherwise be matched against whatever the
// stack held behind it
std::array<char, max_head_size> head{};
in.read(head.data(), head.size());
// every signature is a prefix but one: an enhanced metafile names itself at
// offset 40, so the head has to reach 44
static constexpr std::size_t head_size = 64;

return file_type(
std::string(head.data(), static_cast<std::size_t>(in.gcount())));
return file_type(read_head(in, head_size));
}

FileType magic::file_type(const abstract::File &file) {
Expand Down
31 changes: 31 additions & 0 deletions src/odr/internal/open_strategy.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@
#include <odr/internal/oldms/oldms_file.hpp>
#include <odr/internal/ooxml/ooxml_file.hpp>
#include <odr/internal/pdf/pdf_file.hpp>
#include <odr/internal/svg/svg_util.hpp>
#include <odr/internal/svm/svm_file.hpp>
#include <odr/internal/util/xml_util.hpp>
#include <odr/internal/zip/zip_file.hpp>

#include <algorithm>
Expand Down Expand Up @@ -282,6 +284,24 @@ open_strategy::list_file_types(const std::shared_ptr<abstract::File> &file,
} catch (...) {
ODR_VERBOSE(logger, "failed to open as json");
}

// an svg has no signature to find it by - it is xml, and only the root
// element tells the two apart, so both are reported
try {
ODR_VERBOSE(logger, "try open as xml");
util::xml::check_xml_file(*file->stream());
result.push_back(FileType::xml);

try {
ODR_VERBOSE(logger, "try open as svg");
svg::check_svg_file(*file->stream());
result.push_back(FileType::scalable_vector_graphics);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as svg");
}
} catch (...) {
ODR_VERBOSE(logger, "failed to open as xml");
}
} catch (...) {
ODR_VERBOSE(logger, "failed to open as text");
}
Expand Down Expand Up @@ -393,6 +413,17 @@ open_strategy::open_file(const std::shared_ptr<abstract::File> &file,
ODR_VERBOSE(logger, "failed to open as json");
}

// see `list_file_types` - an svg is only recognised by parsing it, and
// a plain xml file has no decoder of its own, so it stays text
try {
ODR_VERBOSE(logger, "try open as svg");
svg::check_svg_file(*file->stream());
return std::make_unique<ImageFile>(file,
FileType::scalable_vector_graphics);
} catch (...) {
ODR_VERBOSE(logger, "failed to open as svg");
}

ODR_VERBOSE(logger, "open as text file");
// TODO looks dirty
return std::make_unique<text::TextFile>(file);
Expand Down
Loading
Loading