Skip to content
Open
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
24 changes: 22 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ ifeq (,$(PLATFORM))
endif

PREFIX ?= /usr
UNAME_S := $(shell uname -s)
HOMEBREW_PREFIX ?= /opt/homebrew

WARNFLAGS := -pedantic-errors -Wall -Wextra
CXXFLAGS := -std=c++17 -O2
LDFLAGS := -lstdc++ -lSDL -lSDL_ttf -lSDL_image -lzip -lxml2 -lstdc++fs
CXXFLAGS := -std=c++17 -O2 -pthread
LDFLAGS := -pthread -lstdc++ -lSDL -lSDL_ttf -lSDL_image -lzip -lxml2 -lstdc++fs

ifeq ($(PLATFORM),miyoomini)
CXXFLAGS := $(CXXFLAGS) \
Expand All @@ -28,6 +30,24 @@ OBJ_DIR := $(BUILD)/objects
APP_DIR := $(BUILD)
INCLUDE := -Isrc -I${SYSROOT}${PREFIX}/include/libxml2

ifeq ($(UNAME_S),Darwin)
INCLUDE := -Icompat_include \
-I$(HOMEBREW_PREFIX)/opt/sdl12-compat/include \
-I$(HOMEBREW_PREFIX)/opt/sdl2/include \
-I$(HOMEBREW_PREFIX)/opt/sdl2/include/SDL2 \
-I$(HOMEBREW_PREFIX)/opt/sdl2_ttf/include \
-I$(HOMEBREW_PREFIX)/opt/sdl2_image/include \
-I$(HOMEBREW_PREFIX)/opt/libxml2/include/libxml2 \
-I$(HOMEBREW_PREFIX)/opt/libzip/include \
-Isrc
LDFLAGS := -L$(HOMEBREW_PREFIX)/opt/sdl12-compat/lib \
-L$(HOMEBREW_PREFIX)/opt/sdl2_ttf/lib \
-L$(HOMEBREW_PREFIX)/opt/sdl2_image/lib \
-L$(HOMEBREW_PREFIX)/opt/libxml2/lib \
-L$(HOMEBREW_PREFIX)/opt/libzip/lib \
-lstdc++ -lSDL -lSDL2_ttf -lSDL2_image -lzip -lxml2 -lstdc++fs
endif

ROTOZOOM_SRC := src/extern/rotozoom/SDL_rotozoom.c
COMMON_SRC := $(filter-out src/reader/main.cpp, $(wildcard src/filetypes/*.cpp src/filetypes/txt/*.cpp src/filetypes/epub/*.cpp src/reader/*.cpp src/reader/views/*.cpp src/reader/views/token_view/*.cpp src/sys/*.cpp src/util/*.cpp src/doc_api/*.cpp src/extern/hash-library/*.cpp))
READER_SRC := $(COMMON_SRC) src/reader/main.cpp
Expand Down
36 changes: 36 additions & 0 deletions NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Pixel Reader Notes

## Project

Add lightweight ebook cover support to Pixel Reader for Onion on the Miyoo Mini+.

## Goal

Improve browsing books without turning the app into a heavy bookshelf UI.

## Current Understanding

- The file browser is currently a text-only list of filenames.
- EPUB metadata parsing currently handles manifest, spine, and TOC, but not title/author/cover metadata.
- The reader already supports rendering images from inside EPUB content.
- A lightweight cover preview is more realistic than a full gallery view.

## Likely MVP

- Extract cover metadata from EPUB files.
- Load the selected book's cover when a file is focused.
- Show a small preview pane or modal for the highlighted book.
- Cache decoded cover images so browsing stays responsive.

## Constraints

- Keep the UI simple and readable on a small screen.
- Avoid heavy parsing on every scroll event.
- Keep the change realistic for upstream contribution.

## Next Steps

1. Inspect EPUB metadata flow and identify where cover references should be parsed.
2. Inspect file browser rendering path and choose the smallest UI change that can display a cover.
3. Decide whether to parse embedded covers directly or support sidecar/cached cover images first.
4. Build a minimal prototype.
4 changes: 3 additions & 1 deletion cross-compile/miyoo-mini/create_packages.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ if [ -z "$VERSION" ]; then
fi
echo "Version v${VERSION}"

MAKE_JOBS=${MAKE_JOBS:-1}

make clean
make -j
make -j"${MAKE_JOBS}"

stage_common() {
local STAGE_ROOT=$1
Expand Down
106 changes: 106 additions & 0 deletions src/filetypes/epub/epub_cover.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "./epub_cover.h"

#include "./epub_metadata.h"
#include "util/zip_utils.h"

#include <iostream>
#include <zip.h>

namespace
{

bool media_type_is_image(const std::string &media_type)
{
return media_type.rfind("image/", 0) == 0;
}

bool load_epub_info(
const std::filesystem::path &book_path,
EpubCover &out_cover,
bool include_cover_data
)
{
int err = 0;
zip_t *zip = zip_open(book_path.c_str(), ZIP_RDONLY, &err);
if (zip == nullptr)
{
std::cerr << "Failed to open epub " << book_path
<< " code: " << err
<< std::endl;
return false;
}

auto close_zip = [&zip]() {
if (zip)
{
zip_close(zip);
zip = nullptr;
}
};

auto container_xml = read_zip_file_str(zip, EPUB_CONTAINER_PATH);
if (container_xml.empty())
{
close_zip();
return false;
}

auto rootfile_path = epub_parse_rootfile_path(container_xml.data());
if (rootfile_path.empty())
{
close_zip();
return false;
}

auto package_xml = read_zip_file_str(zip, rootfile_path);
if (package_xml.empty())
{
close_zip();
return false;
}

PackageContents package;
if (!epub_parse_package_contents(rootfile_path, package_xml.data(), package))
{
close_zip();
return false;
}

out_cover.title = package.title;
out_cover.author = package.author;
out_cover.href_absolute = package.cover_href_absolute;
out_cover.media_type = package.cover_media_type;
out_cover.data.clear();
if (include_cover_data &&
!package.cover_href_absolute.empty() &&
media_type_is_image(package.cover_media_type))
{
out_cover.data = read_zip_file_str(zip, package.cover_href_absolute);
}

close_zip();

return true;
}

} // namespace

bool epub_load_preview_info(const std::filesystem::path &book_path, EpubCover &out_cover)
{
return load_epub_info(book_path, out_cover, true);
}

bool epub_load_book_metadata(const std::filesystem::path &book_path, EpubCover &out_cover)
{
return load_epub_info(book_path, out_cover, false);
}

bool epub_load_cover(const std::filesystem::path &book_path, EpubCover &out_cover)
{
if (!epub_load_preview_info(book_path, out_cover))
{
return false;
}

return !out_cover.data.empty();
}
21 changes: 21 additions & 0 deletions src/filetypes/epub/epub_cover.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#ifndef EPUB_COVER_H_
#define EPUB_COVER_H_

#include <filesystem>
#include <string>
#include <vector>

struct EpubCover
{
std::string title;
std::string author;
std::string href_absolute;
std::string media_type;
std::vector<char> data;
};

bool epub_load_preview_info(const std::filesystem::path &book_path, EpubCover &out_cover);
bool epub_load_book_metadata(const std::filesystem::path &book_path, EpubCover &out_cover);
bool epub_load_cover(const std::filesystem::path &book_path, EpubCover &out_cover);

#endif
147 changes: 147 additions & 0 deletions src/filetypes/epub/epub_metadata.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <libxml/parser.h>

#include <algorithm>
#include <cstring>
#include <filesystem>
#include <iostream>
Expand Down Expand Up @@ -75,6 +76,98 @@ std::string epub_parse_rootfile_path(const char *container_xml)
namespace parse_package
{

xmlNodePtr metadata_first_child(xmlNodePtr node)
{
node = elem_first_child(elem_first_by_name(node, BAD_CAST "package"));
return elem_first_child(elem_first_by_name(node, BAD_CAST "metadata"));
}

std::string parse_metadata_text(xmlNodePtr node, const xmlChar *name)
{
node = metadata_first_child(node);
node = elem_first_by_name(node, name);
while (node)
{
xmlChar *content = xmlNodeGetContent(node);
if (content)
{
auto text = strip_whitespace((const char *)content);
xmlFree(content);
if (!text.empty())
{
return text;
}
}

node = elem_next_by_name(node, name);
}

return {};
}

bool manifest_item_has_property(const ManifestItem &item, const std::string &property)
{
size_t start_pos = 0;
while (start_pos < item.properties.size())
{
size_t end_pos = item.properties.find(' ', start_pos);
if (end_pos == std::string::npos)
{
end_pos = item.properties.size();
}

if (item.properties.substr(start_pos, end_pos - start_pos) == property)
{
return true;
}

start_pos = end_pos + 1;
}

return false;
}

std::string parse_metadata_cover_id(xmlNodePtr node)
{
node = metadata_first_child(node);
node = elem_first_by_name(node, BAD_CAST "meta");

while (node)
{
const char *name = (const char *)xmlGetProp(node, BAD_CAST "name");
const char *content = (const char *)xmlGetProp(node, BAD_CAST "content");
if (name && content && strcmp(name, "cover") == 0 && content[0] != 0)
{
return content;
}

node = elem_next_by_name(node, BAD_CAST "meta");
}

return {};
}

std::string parse_guide_cover_href(const std::filesystem::path &base_path, xmlNodePtr node)
{
node = elem_first_child(elem_first_by_name(node, BAD_CAST "package"));
node = elem_first_child(elem_first_by_name(node, BAD_CAST "guide"));
node = elem_first_by_name(node, BAD_CAST "reference");

while (node)
{
const char *type = (const char *)xmlGetProp(node, BAD_CAST "type");
const char *href = (const char *)xmlGetProp(node, BAD_CAST "href");
if (type && href && strcmp(type, "cover") == 0 && href[0] != 0)
{
return (base_path / href).lexically_normal();
}

node = elem_next_by_name(node, BAD_CAST "reference");
}

return {};
}

std::unordered_map<std::string, ManifestItem> parse_package_manifest(const std::filesystem::path &base_path, xmlNodePtr node)
{
std::unordered_map<std::string, ManifestItem> manifest;
Expand Down Expand Up @@ -145,6 +238,10 @@ bool epub_parse_package_contents(const std::string &rootfile_path, const char *p

out_package.id_to_manifest_item = parse_package::parse_package_manifest(base_path, node);
out_package.spine_ids = parse_package::parse_package_spine(node);
out_package.title = parse_package::parse_metadata_text(node, BAD_CAST "title");
out_package.author = parse_package::parse_metadata_text(node, BAD_CAST "creator");
out_package.cover_href_absolute.clear();
out_package.cover_media_type.clear();

// get toc id from spine
{
Expand All @@ -162,6 +259,56 @@ bool epub_parse_package_contents(const std::string &rootfile_path, const char *p
}
}

// Look for a cover image using common EPUB 2/3 patterns.
{
auto cover_item = std::find_if(
out_package.id_to_manifest_item.begin(),
out_package.id_to_manifest_item.end(),
[](const auto &item) {
return parse_package::manifest_item_has_property(item.second, "cover-image");
}
);
if (cover_item != out_package.id_to_manifest_item.end())
{
out_package.cover_href_absolute = cover_item->second.href_absolute;
out_package.cover_media_type = cover_item->second.media_type;
}
}

if (out_package.cover_href_absolute.empty())
{
auto cover_id = parse_package::parse_metadata_cover_id(node);
if (!cover_id.empty())
{
auto item_it = out_package.id_to_manifest_item.find(cover_id);
if (item_it != out_package.id_to_manifest_item.end())
{
out_package.cover_href_absolute = item_it->second.href_absolute;
out_package.cover_media_type = item_it->second.media_type;
}
}
}

if (out_package.cover_href_absolute.empty())
{
auto guide_href = parse_package::parse_guide_cover_href(base_path, node);
if (!guide_href.empty())
{
auto item_it = std::find_if(
out_package.id_to_manifest_item.begin(),
out_package.id_to_manifest_item.end(),
[&guide_href](const auto &item) {
return item.second.href_absolute == guide_href;
}
);
if (item_it != out_package.id_to_manifest_item.end())
{
out_package.cover_href_absolute = item_it->second.href_absolute;
out_package.cover_media_type = item_it->second.media_type;
}
}
}

xmlFreeDoc(package_doc);

return true;
Expand Down
Loading