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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ jobs:
ci/baseline/libcustomnotebooktemplates.so
mods/custom-notebook-templates/libcustomnotebooktemplates.so
--tool-prefix arm-linux-gnueabihf-
--expected-symbol-delta ci/notebook-sleep-abi-additions.json
- name: Validate installer contents and binary identity
run: >-
python scripts/verify-release-package.py
Expand Down
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

All notable changes to KoboNotebookPlus will be documented in this file.

## [0.0.3-alpha] - 2026-07-17

### Added

- Add **Show notebook on sleep** to Kobo's native Energy saving and privacy
settings, with **Cover** and **Current page** image choices.
- Show a notebook image only when sleep or power-off starts from a visible
Advanced Notebook; books and all other views keep Kobo's stock cover flow.
- Composite current-page ink over the active built-in or custom paper
template while preserving the notebook's live zoom, pan, and canvas offset.

### Fixed

- Suppress the stale book reading-status panel while a notebook image is
substituted on the power view.
- Interpret MyScript export extents as left/top/right/bottom coordinates and
apply the live renderer transform, preventing ink from being centered,
shifted, stretched, or clipped by its content bounding box.
- Match Kobo's native two-column settings layout by placing the setting label
on the left and the checkbox's **On** control on the right.
- Fall back safely to the normal book cover whenever the notebook is hidden,
busy, invalid, or cannot be rendered.

### Verification

- Tested on Kobo Elipsa 2E (`condor`) firmware `4.38.23697` for notebook and
book sleep routing, current-page ink/template alignment, and native settings
alignment.
- Verified button sleep and power-off behavior with on-device notebook pages;
the release remains pinned to this exact device and firmware.

## [0.0.2-alpha] - 2026-07-17

### Fixed
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# KoboNotebookPlus

**Native layers, custom paper templates, writable covers, page tools, and a
configurable eraser for Kobo Advanced Notebooks.**
**Native layers, custom paper templates, writable covers, notebook sleep
screens, page tools, and a configurable eraser for Kobo Advanced Notebooks.**

KoboNotebookPlus is a [NickelHook](https://github.com/pgaskin/NickelHook)
plugin that extends the stock Advanced Notebook experience on the Kobo
Expand Down Expand Up @@ -70,6 +70,10 @@ Click any preview to view it at full size.
- **Custom paper templates** — drop in your own full-page PNG templates.
- **Writable covers** — custom notebook covers you can write on as a first
page.
- **Notebook sleep screens** — when sleep or power-off starts from a visible
Advanced Notebook, show either its cover (page zero) or its current page,
including the active built-in or custom paper template; books and every
non-notebook view retain Kobo's stock cover behavior.
- **Page operations** — duplicate and reorder notebook pages.
- **Clean library** — plugin support images stay out of Home/My Books
without touching the Kobo database.
Expand Down Expand Up @@ -186,6 +190,10 @@ If Nickel crash-loops or notebooks misbehave after install:
- Inside an Advanced Notebook, the notebook menu gains Covers, Layers, and
page operations. The eraser popup shows the stock mode choices plus the
size row.
- Under **Settings → Energy saving and privacy**, enable **Show notebook on
sleep**, then choose **Cover** or **Current page** under **Notebook sleep
image**. The selector is disabled until the toggle is enabled. Rendering
failure or a busy/hidden notebook falls back to Kobo's normal book cover.

Layer metadata and preview caches live under `.kobo/custom/layers/`; native
ink stays inside the `.nebo` document. Backups created by cover, page, and
Expand Down
42 changes: 42 additions & 0 deletions ci/notebook-sleep-abi-additions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"candidate_only_exports": [
{"kind": "T", "name": "_cnt_power_cover_loaded_hook"},
{"kind": "T", "name": "_cnt_power_set_info_panel_visible_hook"},
{"kind": "T", "name": "_cnt_power_update_cover_hook"},
{"kind": "T", "name": "_cnt_power_update_reading_status_hook"},
{"kind": "T", "name": "_cnt_settings_power_view_constructor_hook"},
{"kind": "T", "name": "_cnt_show_power_off_view_hook"},
{"kind": "T", "name": "_cnt_show_sleep_view_hook"}
],
"candidate_only_imports": [
{"kind": "U", "name": "_ZN10QBoxLayout9addWidgetEP7QWidgeti6QFlagsIN2Qt13AlignmentFlagEE"},
{"kind": "U", "name": "_ZN10QJsonValueC1E13QLatin1String"},
{"kind": "U", "name": "_ZN10QJsonValueC1Eb"},
{"kind": "U", "name": "_ZN10QTransformC1Edddddd"},
{"kind": "U", "name": "_ZN11QVBoxLayout16staticMetaObjectE"},
{"kind": "U", "name": "_ZN4QDir8tempPathEv"},
{"kind": "U", "name": "_ZN6QImage4fillEN2Qt11GlobalColorE"},
{"kind": "U", "name": "_ZN6QImageC1ERK5QSizeNS_6FormatE"},
{"kind": "U", "name": "_ZN7QLocaleC1Ev"},
{"kind": "U", "name": "_ZN7QLocaleD1Ev"},
{"kind": "U", "name": "_ZN7QObject7connectEPKS_PKcS1_S3_N2Qt14ConnectionTypeE"},
{"kind": "U", "name": "_ZN7QRegionD1Ev"},
{"kind": "U", "name": "_ZN7QWidget10setEnabledEb"},
{"kind": "U", "name": "_ZN8QPainter13setRenderHintENS_10RenderHintEb"},
{"kind": "U", "name": "_ZN8QPainter17setWorldTransformERK10QTransformb"},
{"kind": "U", "name": "_ZN8QPainter18setCompositionModeENS_15CompositionModeE"},
{"kind": "U", "name": "_ZN8QPainter9drawImageERK6QRectFRK6QImageS2_6QFlagsIN2Qt19ImageConversionFlagEE"},
{"kind": "U", "name": "_ZN8QVariantC1Ei"},
{"kind": "U", "name": "_ZNK10QJsonValue4typeEv"},
{"kind": "U", "name": "_ZNK10QJsonValue6toBoolEb"},
{"kind": "U", "name": "_ZNK10QTransform7mapRectERK6QRectF"},
{"kind": "U", "name": "_ZNK11QMetaObject4castEP7QObject"},
{"kind": "U", "name": "_ZNK6QImage4rectEv"},
{"kind": "U", "name": "_ZNK6QRectF10intersectsERKS_"},
{"kind": "U", "name": "_ZNK7QRegion7isEmptyEv"},
{"kind": "U", "name": "_ZNK7QWidget13visibleRegionEv"},
{"kind": "U", "name": "_ZNK7QWidget5mapToEPKS_RK6QPoint"},
{"kind": "U", "name": "_ZNK7QWidget6layoutEv"},
{"kind": "U", "name": "_ZNK7QWidget6windowEv"}
]
}
4 changes: 2 additions & 2 deletions mods/custom-notebook-templates/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ override LIBRARY := libcustomnotebooktemplates.so
override SOURCES += src/firmware_resolver.cc src/fs_util.cc src/settings.cc \
src/templates.cc src/covers.cc src/cover_cache.cc src/cover_editor.cc src/page_io.cc \
src/notebook_widget.cc src/notebook_hook_services.cc src/notebook_menu.cc src/pages.cc \
src/page_actions.cc src/visibility.cc src/eraser_menu.cc src/layers_state.cc \
src/page_actions.cc src/notebook_sleep.cc src/visibility.cc src/eraser_menu.cc src/layers_state.cc \
src/layers_service.cc src/layers_preview.cc src/layers_eraser.cc src/layers_menu.cc \
src/plugin_runtime.cc src/plugin_entry.cc src/hooks.cc src/visibility_hooks.cc
override MOCS += src/covermenureceiver.h
override MOCS += src/covermenureceiver.h src/notebooksleepsettingsreceiver.h
override PKGCONF += Qt5Widgets
override CXXFLAGS += -Wall -Wextra -Werror -Wno-missing-field-initializers \
-fvisibility=hidden -fvisibility-inlines-hidden
Expand Down
35 changes: 30 additions & 5 deletions mods/custom-notebook-templates/src/abi_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,15 +78,38 @@ typedef std::shared_ptr<ActiveBackendOpaque> SharedActiveBackend;

// atk::core::Extent is a four-float homogeneous aggregate under ARM's
// hard-float ABI. PageController::exportToPNG passes it in s0-s3 to its one
// RendererListener callback.
// RendererListener callback. Despite the API name, the last two floats are
// the maximum coordinates, not width and height. BackendImageDrawer proves
// this by calculating its bitmap dimensions from (right - left) and
// (bottom - top).
struct ExtentOpaque {
float x;
float y;
float width;
float height;
float left;
float top;
float right;
float bottom;
};
static_assert(sizeof(ExtentOpaque) == 16, "unexpected Extent ABI");

// atk::core::Transform is returned by value through ARM's hidden sret
// pointer. RendererImpl::getViewTransform converts its six internal floats
// to these six doubles in row-major affine order:
//
// viewX = xx * pageX + xy * pageY + x0
// viewY = yx * pageX + yy * pageY + y0
//
// Keeping the exact 48-byte return shape is important: declaring this as a
// Qt transform (or as six floats) would shift the implicit `this` argument.
struct RendererViewTransformOpaque {
double xx;
double xy;
double x0;
double yx;
double yy;
double y0;
};
static_assert(sizeof(RendererViewTransformOpaque) == 48,
"unexpected renderer view-transform ABI");

// atk::core::Point is two floats. The PointerInfo vector remains opaque: it
// is passed by reference to Kobo's own Eraser::strokerPolygon implementation,
// so the plugin never needs to construct or inspect a PointerInfo.
Expand Down Expand Up @@ -219,6 +242,8 @@ typedef void (*DocumentLayoutGetLayer)(
typedef bool (*LayerIteratorIsAtEnd)(void const* iterator);
typedef void (*ManagedObjectDestructor)(void* object);
typedef void* (*RendererGetBackend)(void* renderer);
typedef RendererViewTransformOpaque (*RendererGetViewTransform)(
void const* renderer);
typedef void (*RendererRestrictToLayers)(
void* renderer,
std::vector<std::string> const& ids);
Expand Down
17 changes: 15 additions & 2 deletions mods/custom-notebook-templates/src/cover_cache.cc
Original file line number Diff line number Diff line change
Expand Up @@ -677,10 +677,23 @@ void cacheRenderedCoverImage(
if (!info.isFile() || info.size() <= 0)
return;

persistRenderedCoverImage(notebookPath, coverType, image);
QImage cachedImage = image;
if (coverType.isEmpty()
&& (cachedImage.width() != kBackgroundWidth
|| cachedImage.height() != kBackgroundHeight)) {
cachedImage = cachedImage.scaled(
kBackgroundWidth,
kBackgroundHeight,
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
if (cachedImage.isNull())
return;

persistRenderedCoverImage(notebookPath, coverType, cachedImage);

RenderedCoverEntry entry;
entry.image = image;
entry.image = cachedImage;
entry.notebookModifiedMs = info.lastModified().toMSecsSinceEpoch();
entry.notebookSize = info.size();
entry.coverType = coverType;
Expand Down
7 changes: 4 additions & 3 deletions mods/custom-notebook-templates/src/cover_cache.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,9 @@ struct CoverScanEntry {
bool hasCover;
};

// Kobo's own rendered first-page preview, including the selected cover and
// live ink. The parser callback populates this cache; the notebook-card hook
// may reuse it only while the source .nebo has the same mtime and size.
// Kobo's rendered first-page preview and, for plugin covers, the composed
// cover plus live ink. The parser callback populates this cache; notebook
// cards and the notebook sleep screen can both reuse it.
struct RenderedCoverEntry {
QImage image;
qint64 notebookModifiedMs;
Expand Down Expand Up @@ -71,6 +71,7 @@ struct State {
QMutex cacheMutex;
bool pickerPending = false;
bool titlePending = false;
bool parserHookReady = false;
bool hooksReady = false;
bool gridHookReady = false;
};
Expand Down
1 change: 1 addition & 0 deletions mods/custom-notebook-templates/src/firmware_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ struct FirmwareApi {
LayerIteratorIsAtEnd layerIteratorIsAtEnd;
ManagedObjectDestructor managedObjectDestructor;
RendererGetBackend rendererGetBackend;
RendererGetViewTransform rendererGetViewTransform;
RendererRestrictToLayers rendererRestrictToLayers;
PageControllerExportToPng pageControllerExportToPng;
BackendImageDrawerMakeShared backendImageDrawerMakeShared;
Expand Down
3 changes: 3 additions & 0 deletions mods/custom-notebook-templates/src/firmware_pins.h
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ static char const kManagedObjectDestructorSymbol[] =
"_ZN8myscript6engine13ManagedObjectD1Ev";
static char const kRendererGetBackendSymbol[] =
"_ZN8myscript4iink12RendererImpl10getBackendEv";
static char const kRendererGetViewTransformSymbol[] =
"_ZNK8myscript4iink12RendererImpl16getViewTransformEv";
static char const kRendererRestrictToLayersSymbol[] =
"_ZN3atk4core8Renderer16restrictToLayersERKSt6vectorISsSaISsEE";
static char const kPageControllerExportToPngSymbol[] =
Expand Down Expand Up @@ -191,6 +193,7 @@ static uintptr_t const kDocumentLayoutGetLayerVma = 0x4b3d00;
static uintptr_t const kLayerIteratorIsAtEndVma = 0x4b3e88;
static uintptr_t const kManagedObjectDestructorVma = 0x3e5390;
static uintptr_t const kRendererGetBackendVma = 0x49e318;
static uintptr_t const kRendererGetViewTransformVma = 0x49df8c;
static uintptr_t const kRendererRestrictToLayersVma = 0xa57714;
static uintptr_t const kPageControllerExportToPngVma = 0x5130c4;
static uintptr_t const kBackendImageDrawerMakeSharedVma = 0x443eac;
Expand Down
3 changes: 3 additions & 0 deletions mods/custom-notebook-templates/src/firmware_resolver.cc
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,9 @@ bool resolveFirmwareApis(void* iinknoteHandle) {
kManagedObjectDestructorVma, &firmwareApi().managedObjectDestructor)
&& resolvePinned(iinkHandle, kRendererGetBackendSymbol,
kRendererGetBackendVma, &firmwareApi().rendererGetBackend)
&& resolvePinned(iinkHandle, kRendererGetViewTransformSymbol,
kRendererGetViewTransformVma,
&firmwareApi().rendererGetViewTransform)
&& resolvePinned(iinkHandle, kRendererRestrictToLayersSymbol,
kRendererRestrictToLayersVma, &firmwareApi().rendererRestrictToLayers)
&& resolvePinned(iinkHandle, kPageControllerInputDispatcherSymbol,
Expand Down
8 changes: 7 additions & 1 deletion mods/custom-notebook-templates/src/hooks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "layers_state.h"
#include "notebook_hook_services.h"
#include "notebook_menu.h"
#include "notebook_sleep.h"
#include "notebook_widget.h"
#include "page_actions.h"
#include "plugin_runtime.h"
Expand Down Expand Up @@ -177,12 +178,17 @@ void _cnt_render_volume_hook(void* widget, void const* volume) {
// caller reaches this symbol through the PLT, so running after the original
// is the first hookable point where all three identities are coherent.
firmwareApi().renderVolumeOriginal(widget, volume);
if (!hookState().notebookLifecycleHooksReady || !widget)
if (!widget)
return;

QObject* const object = reinterpret_cast<QObject*>(widget);
if (!cnt::notebook_widget::isNotebookWidget(object))
return;
cnt::notebook_sleep::observeNotebookWidget(
pluginState().notebookSleep,
reinterpret_cast<QWidget*>(widget));
if (!hookState().notebookLifecycleHooksReady)
return;
if (eraserState().sizeApisReady) {
try {
applyConfiguredEraserSizeForWidget(object, "notebook-open");
Expand Down
Loading
Loading