General fixes (#76)
* Added print functionality to PDF viewer * feat(pdf): add interactive button support and visibility reveal heuristic * Moved search state management to MainViewModel and improved search UI interaction. * Updated `pdfiumandroid` to version 2.0.0 and handled resulting API changes, including nullable page/text page returns and revised native pointer access. increased compileSdk to 36. * Fixed Table of Contents (TOC) truncation bug and improved the TOC UI in `PdfViewerScreen`. - Implemented `getFixedTableOfContents` using reflection to bypass a library issue where sibling nodes were incorrectly truncated during traversal. - Enhanced the TOC drawer with a nested, expandable tree structure using the new `PdfTocTreeItem` component. - Added a custom `VerticalScrollbar` with draggable support for better navigation within long TOC lists. - Integrated `animateColorAsState` and `animateFloatAsState` for smoother UI transitions in the TOC and scrollbar. - Optimized TOC loading by flattening the tree structure and managing expansion states with `rememberSaveable`. * Improved zoom pivot calculation and interaction handling in PdfVerticalReader * Fixed high-res PDF tile bleeding by implementing clipRect in PdfBitmapLayer * fix(pdf): resolve zoom stuttering, in pagination mode, by removing eager scale snapping
This commit is contained in:
parent
dece09fec0
commit
1884ace646
12 changed files with 1207 additions and 653 deletions
|
|
@ -19,7 +19,7 @@ if (localPropertiesFile.exists()) {
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.aryan.reader"
|
namespace = "com.aryan.reader"
|
||||||
compileSdk = 35
|
compileSdk = 36
|
||||||
ndkVersion = "29.0.14206865"
|
ndkVersion = "29.0.14206865"
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
|
|
@ -211,7 +211,7 @@ dependencies {
|
||||||
implementation("androidx.documentfile:documentfile:1.0.1")
|
implementation("androidx.documentfile:documentfile:1.0.1")
|
||||||
implementation("androidx.browser:browser:1.8.0")
|
implementation("androidx.browser:browser:1.8.0")
|
||||||
|
|
||||||
implementation("io.legere:pdfiumandroid:1.0.35")
|
implementation("io.legere:pdfiumandroid:2.0.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
spotless {
|
spotless {
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <mutex>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
#define LOG_TAG "PdfiumAnnotation"
|
#define LOG_TAG "PdfiumAnnotation"
|
||||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
@ -29,7 +31,21 @@ typedef int (*FPDFBitmap_GetStride_t)(void* bitmap);
|
||||||
typedef void* (*FPDFBitmap_GetBuffer_t)(void* bitmap);
|
typedef void* (*FPDFBitmap_GetBuffer_t)(void* bitmap);
|
||||||
typedef void (*FPDFBitmap_Destroy_t)(void* bitmap);
|
typedef void (*FPDFBitmap_Destroy_t)(void* bitmap);
|
||||||
typedef int (*FPDFPageObj_GetBounds_t)(void* page_object, float* left, float* bottom, float* right, float* top);
|
typedef int (*FPDFPageObj_GetBounds_t)(void* page_object, float* left, float* bottom, float* right, float* top);
|
||||||
|
typedef int (*FPDF_DoAnnotAction_t)(void* annot, int action_type);
|
||||||
|
typedef void* (*FPDFAnnot_GetWidgetAtPoint_t)(void* page, double page_x, double page_y);
|
||||||
|
typedef void* (*FPDFLink_GetAction_t)(void* link);
|
||||||
|
typedef unsigned long (*FPDFAction_GetType_t)(void* action);
|
||||||
|
typedef void* (*FPDFLink_GetAnnot_t)(void* link);
|
||||||
|
typedef int (*FPDFAnnot_GetFlags_t)(void* annot);
|
||||||
|
typedef int (*FPDFAnnot_SetFlags_t)(void* annot, int flags);
|
||||||
|
typedef unsigned long (*FPDFAnnot_GetFormFieldName_t)(void* hFPDFTextPage, void* annot, void* buffer, unsigned long buflen);
|
||||||
|
|
||||||
|
static std::mutex g_pdfium_mutex;
|
||||||
|
static FPDFLink_GetAnnot_t get_link_annot_func = nullptr;
|
||||||
|
static FPDFLink_GetAction_t get_link_action_func = nullptr;
|
||||||
|
static FPDFAction_GetType_t get_action_type_func = nullptr;
|
||||||
|
static FPDF_DoAnnotAction_t do_annot_action_func = nullptr;
|
||||||
|
static FPDFAnnot_GetWidgetAtPoint_t get_widget_at_point_func = nullptr;
|
||||||
static FPDFPage_CountObjects_t count_objects_func = nullptr;
|
static FPDFPage_CountObjects_t count_objects_func = nullptr;
|
||||||
static FPDFPage_GetObject_t get_object_func = nullptr;
|
static FPDFPage_GetObject_t get_object_func = nullptr;
|
||||||
static FPDFPageObj_GetType_t get_object_type_func = nullptr;
|
static FPDFPageObj_GetType_t get_object_type_func = nullptr;
|
||||||
|
|
@ -51,6 +67,9 @@ static FPDFText_GetFontSize_t get_font_size_func = nullptr;
|
||||||
static FPDFText_GetFontWeight_t get_font_weight_func = nullptr;
|
static FPDFText_GetFontWeight_t get_font_weight_func = nullptr;
|
||||||
static FPDFText_GetFontInfo_t get_font_info_func = nullptr;
|
static FPDFText_GetFontInfo_t get_font_info_func = nullptr;
|
||||||
static FPDFText_GetCharBox_t get_char_box_func = nullptr;
|
static FPDFText_GetCharBox_t get_char_box_func = nullptr;
|
||||||
|
static FPDFAnnot_GetFlags_t get_annot_flags_func = nullptr;
|
||||||
|
static FPDFAnnot_SetFlags_t set_annot_flags_func = nullptr;
|
||||||
|
static FPDFAnnot_GetFormFieldName_t get_form_field_name_func = nullptr;
|
||||||
|
|
||||||
typedef void* (*FPDFAnnot_GetLinkedAnnot_t)(void* annot, const char* key);
|
typedef void* (*FPDFAnnot_GetLinkedAnnot_t)(void* annot, const char* key);
|
||||||
typedef void (*FPDFPage_CloseAnnot_t)(void* annot);
|
typedef void (*FPDFPage_CloseAnnot_t)(void* annot);
|
||||||
|
|
@ -67,11 +86,13 @@ static bool init_pdfium() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Text Functions ---
|
||||||
get_font_size_func = (FPDFText_GetFontSize_t) dlsym(pdfium_handle, "FPDFText_GetFontSize");
|
get_font_size_func = (FPDFText_GetFontSize_t) dlsym(pdfium_handle, "FPDFText_GetFontSize");
|
||||||
get_font_weight_func = (FPDFText_GetFontWeight_t) dlsym(pdfium_handle, "FPDFText_GetFontWeight");
|
get_font_weight_func = (FPDFText_GetFontWeight_t) dlsym(pdfium_handle, "FPDFText_GetFontWeight");
|
||||||
get_font_info_func = (FPDFText_GetFontInfo_t) dlsym(pdfium_handle, "FPDFText_GetFontInfo");
|
get_font_info_func = (FPDFText_GetFontInfo_t) dlsym(pdfium_handle, "FPDFText_GetFontInfo");
|
||||||
get_char_box_func = (FPDFText_GetCharBox_t) dlsym(pdfium_handle, "FPDFText_GetCharBox");
|
get_char_box_func = (FPDFText_GetCharBox_t) dlsym(pdfium_handle, "FPDFText_GetCharBox");
|
||||||
|
|
||||||
|
// --- Annotation Functions ---
|
||||||
get_annot_count_func = (FPDFPage_GetAnnotCount_t) dlsym(pdfium_handle, "FPDFPage_GetAnnotCount");
|
get_annot_count_func = (FPDFPage_GetAnnotCount_t) dlsym(pdfium_handle, "FPDFPage_GetAnnotCount");
|
||||||
get_annot_func = (FPDFPage_GetAnnot_t) dlsym(pdfium_handle, "FPDFPage_GetAnnot");
|
get_annot_func = (FPDFPage_GetAnnot_t) dlsym(pdfium_handle, "FPDFPage_GetAnnot");
|
||||||
get_annot_subtype_func = (FPDFAnnot_GetSubtype_t) dlsym(pdfium_handle, "FPDFAnnot_GetSubtype");
|
get_annot_subtype_func = (FPDFAnnot_GetSubtype_t) dlsym(pdfium_handle, "FPDFAnnot_GetSubtype");
|
||||||
|
|
@ -80,28 +101,51 @@ static bool init_pdfium() {
|
||||||
get_annot_color_func = (FPDFAnnot_GetColor_t) dlsym(pdfium_handle, "FPDFAnnot_GetColor");
|
get_annot_color_func = (FPDFAnnot_GetColor_t) dlsym(pdfium_handle, "FPDFAnnot_GetColor");
|
||||||
get_linked_annot_func = (FPDFAnnot_GetLinkedAnnot_t) dlsym(pdfium_handle, "FPDFAnnot_GetLinkedAnnot");
|
get_linked_annot_func = (FPDFAnnot_GetLinkedAnnot_t) dlsym(pdfium_handle, "FPDFAnnot_GetLinkedAnnot");
|
||||||
close_annot_func = (FPDFPage_CloseAnnot_t) dlsym(pdfium_handle, "FPDFPage_CloseAnnot");
|
close_annot_func = (FPDFPage_CloseAnnot_t) dlsym(pdfium_handle, "FPDFPage_CloseAnnot");
|
||||||
|
get_annot_flags_func = (FPDFAnnot_GetFlags_t) dlsym(pdfium_handle, "FPDFAnnot_GetFlags");
|
||||||
|
set_annot_flags_func = (FPDFAnnot_SetFlags_t) dlsym(pdfium_handle, "FPDFAnnot_SetFlags");
|
||||||
|
|
||||||
|
// --- Object & Bitmap Functions ---
|
||||||
count_objects_func = (FPDFPage_CountObjects_t) dlsym(pdfium_handle, "FPDFPage_CountObjects");
|
count_objects_func = (FPDFPage_CountObjects_t) dlsym(pdfium_handle, "FPDFPage_CountObjects");
|
||||||
get_object_func = (FPDFPage_GetObject_t) dlsym(pdfium_handle, "FPDFPage_GetObject");
|
get_object_func = (FPDFPage_GetObject_t) dlsym(pdfium_handle, "FPDFPage_GetObject");
|
||||||
get_object_type_func = (FPDFPageObj_GetType_t) dlsym(pdfium_handle, "FPDFPageObj_GetType");
|
get_object_type_func = (FPDFPageObj_GetType_t) dlsym(pdfium_handle, "FPDFPageObj_GetType");
|
||||||
|
get_object_bounds_func = (FPDFPageObj_GetBounds_t) dlsym(pdfium_handle, "FPDFPageObj_GetBounds");
|
||||||
get_image_bitmap_func = (FPDFImageObj_GetBitmap_t) dlsym(pdfium_handle, "FPDFImageObj_GetBitmap");
|
get_image_bitmap_func = (FPDFImageObj_GetBitmap_t) dlsym(pdfium_handle, "FPDFImageObj_GetBitmap");
|
||||||
bitmap_get_width_func = (FPDFBitmap_GetWidth_t) dlsym(pdfium_handle, "FPDFBitmap_GetWidth");
|
bitmap_get_width_func = (FPDFBitmap_GetWidth_t) dlsym(pdfium_handle, "FPDFBitmap_GetWidth");
|
||||||
bitmap_get_height_func = (FPDFBitmap_GetHeight_t) dlsym(pdfium_handle, "FPDFBitmap_GetHeight");
|
bitmap_get_height_func = (FPDFBitmap_GetHeight_t) dlsym(pdfium_handle, "FPDFBitmap_GetHeight");
|
||||||
bitmap_get_stride_func = (FPDFBitmap_GetStride_t) dlsym(pdfium_handle, "FPDFBitmap_GetStride");
|
bitmap_get_stride_func = (FPDFBitmap_GetStride_t) dlsym(pdfium_handle, "FPDFBitmap_GetStride");
|
||||||
bitmap_get_buffer_func = (FPDFBitmap_GetBuffer_t) dlsym(pdfium_handle, "FPDFBitmap_GetBuffer");
|
bitmap_get_buffer_func = (FPDFBitmap_GetBuffer_t) dlsym(pdfium_handle, "FPDFBitmap_GetBuffer");
|
||||||
bitmap_destroy_func = (FPDFBitmap_Destroy_t) dlsym(pdfium_handle, "FPDFBitmap_Destroy");
|
bitmap_destroy_func = (FPDFBitmap_Destroy_t) dlsym(pdfium_handle, "FPDFBitmap_Destroy");
|
||||||
get_object_bounds_func = (FPDFPageObj_GetBounds_t) dlsym(pdfium_handle, "FPDFPageObj_GetBounds");
|
|
||||||
|
|
||||||
|
// --- Interaction, Links & Form Functions ---
|
||||||
|
do_annot_action_func = (FPDF_DoAnnotAction_t) dlsym(pdfium_handle, "FPDF_DoAnnotAction");
|
||||||
|
get_widget_at_point_func = (FPDFAnnot_GetWidgetAtPoint_t) dlsym(pdfium_handle, "FPDFAnnot_GetWidgetAtPoint");
|
||||||
|
get_link_action_func = (FPDFLink_GetAction_t) dlsym(pdfium_handle, "FPDFLink_GetAction");
|
||||||
|
get_action_type_func = (FPDFAction_GetType_t) dlsym(pdfium_handle, "FPDFAction_GetType");
|
||||||
|
get_link_annot_func = (FPDFLink_GetAnnot_t) dlsym(pdfium_handle, "FPDFLink_GetAnnot");
|
||||||
|
get_form_field_name_func = (FPDFAnnot_GetFormFieldName_t) dlsym(pdfium_handle, "FPDFAnnot_GetFormFieldName");
|
||||||
|
|
||||||
|
// --- Validation & Logging ---
|
||||||
bool success = get_annot_count_func && get_annot_func && get_annot_subtype_func &&
|
bool success = get_annot_count_func && get_annot_func && get_annot_subtype_func &&
|
||||||
get_annot_rect_func && get_annot_string_func;
|
get_annot_rect_func && get_annot_string_func;
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
LOGE("Failed to find one or more annotation functions in libpdfium.so");
|
LOGE("Failed to find one or more core annotation functions in libpdfium.so");
|
||||||
} else {
|
} else {
|
||||||
LOGI("Pdfium Annotation Bridge initialized successfully.");
|
LOGI("Pdfium Annotation Bridge initialized successfully.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return success;
|
LOGD("PdfInteraction: Flags -> Get:%p Set:%p, FormField -> %p",
|
||||||
|
get_annot_flags_func, set_annot_flags_func, get_form_field_name_func);
|
||||||
|
|
||||||
|
if (!get_link_action_func || !do_annot_action_func || !get_widget_at_point_func) {
|
||||||
|
LOGE("PdfInteraction: Missing one or more action/widget functions. LinkAction=%p, DoAction=%p, GetWidget=%p",
|
||||||
|
get_link_action_func, do_annot_action_func, get_widget_at_point_func);
|
||||||
|
} else {
|
||||||
|
LOGI("PdfInteraction: Initialization complete. Summary: LinkAction=%p, DoAction=%p, GetWidget=%p",
|
||||||
|
get_link_action_func, do_annot_action_func, get_widget_at_point_func);
|
||||||
|
}
|
||||||
|
|
||||||
|
return get_annot_count_func != nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT jdouble JNICALL
|
extern "C" JNIEXPORT jdouble JNICALL
|
||||||
|
|
@ -182,42 +226,19 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_getPageCharBoxes(JNIEnv *env, jclas
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
extern "C" JNIEXPORT jint JNICALL
|
|
||||||
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotCount(JNIEnv *env, jclass clazz, jlong pagePtr) {
|
|
||||||
if (!init_pdfium() || !get_annot_count_func) return 0;
|
|
||||||
return get_annot_count_func(reinterpret_cast<void*>(pagePtr));
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jint JNICALL
|
|
||||||
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotSubtype(JNIEnv *env, jclass clazz, jlong pagePtr, jint index) {
|
|
||||||
if (!init_pdfium() || !get_annot_func || !get_annot_subtype_func) return 0;
|
|
||||||
void* annot = get_annot_func(reinterpret_cast<void*>(pagePtr), index);
|
|
||||||
return annot ? get_annot_subtype_func(annot) : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jfloatArray JNICALL
|
|
||||||
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotRect(JNIEnv *env, jclass clazz, jlong pagePtr, jint index) {
|
|
||||||
if (!init_pdfium() || !get_annot_func || !get_annot_rect_func) return nullptr;
|
|
||||||
void* annot = get_annot_func(reinterpret_cast<void*>(pagePtr), index);
|
|
||||||
if (!annot) return nullptr;
|
|
||||||
|
|
||||||
float rect[4];
|
|
||||||
if (!get_annot_rect_func(annot, rect)) return nullptr;
|
|
||||||
|
|
||||||
jfloatArray result = env->NewFloatArray(4);
|
|
||||||
env->SetFloatArrayRegion(result, 0, 4, rect);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" JNIEXPORT jstring JNICALL
|
extern "C" JNIEXPORT jstring JNICALL
|
||||||
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotString(JNIEnv *env, jclass clazz, jlong pagePtr, jint index, jstring key) {
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotString(JNIEnv *env, jclass clazz, jlong pagePtr, jint index, jstring key) {
|
||||||
if (!init_pdfium() || !get_annot_func || !get_annot_string_func) return nullptr;
|
std::lock_guard<std::mutex> lock(g_pdfium_mutex);
|
||||||
void* annot = get_annot_func(reinterpret_cast<void*>(pagePtr), index);
|
if (!init_pdfium() || !get_annot_func || !get_annot_string_func || pagePtr == 0) return nullptr;
|
||||||
|
|
||||||
|
void* page = reinterpret_cast<void*>(pagePtr);
|
||||||
|
void* annot = get_annot_func(page, index);
|
||||||
if (!annot) return nullptr;
|
if (!annot) return nullptr;
|
||||||
|
|
||||||
const char* nativeKey = env->GetStringUTFChars(key, nullptr);
|
const char* nativeKey = env->GetStringUTFChars(key, nullptr);
|
||||||
|
|
||||||
if (strcmp(nativeKey, "IRT") == 0 && get_linked_annot_func && close_annot_func) {
|
if (strcmp(nativeKey, "IRT") == 0) {
|
||||||
|
if (get_linked_annot_func && close_annot_func) {
|
||||||
void* parentAnnot = get_linked_annot_func(annot, "IRT");
|
void* parentAnnot = get_linked_annot_func(annot, "IRT");
|
||||||
if (parentAnnot) {
|
if (parentAnnot) {
|
||||||
unsigned long len = get_annot_string_func(parentAnnot, "NM", nullptr, 0);
|
unsigned long len = get_annot_string_func(parentAnnot, "NM", nullptr, 0);
|
||||||
|
|
@ -232,6 +253,9 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotString(JNIEnv *env, jclass
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
env->ReleaseStringUTFChars(key, nativeKey);
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
unsigned long len = get_annot_string_func(annot, nativeKey, nullptr, 0);
|
unsigned long len = get_annot_string_func(annot, nativeKey, nullptr, 0);
|
||||||
|
|
||||||
|
|
@ -327,3 +351,144 @@ Java_com_aryan_reader_pdf_NativePdfiumBridge_extractImagePixels(JNIEnv *env, jcl
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_checkActionSupport(JNIEnv *env, jclass clazz) {
|
||||||
|
init_pdfium();
|
||||||
|
// Return true if we have ANY way to handle actions
|
||||||
|
return (do_annot_action_func || get_link_action_func) ? JNI_TRUE : JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotSubtypeAtPoint(JNIEnv *env, jclass clazz, jlong pagePtr, jdouble x, jdouble y) {
|
||||||
|
if (!init_pdfium() || !get_annot_count_func || pagePtr == 0) return -1;
|
||||||
|
|
||||||
|
void* page = reinterpret_cast<void*>(pagePtr);
|
||||||
|
int count = get_annot_count_func(page);
|
||||||
|
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
void* annot = get_annot_func(page, i);
|
||||||
|
float r[4]; // L, B, R, T
|
||||||
|
if (get_annot_rect_func(annot, r)) {
|
||||||
|
// FIX: Use min/max to handle inverted PDF rectangles
|
||||||
|
float minX = fmin(r[0], r[2]);
|
||||||
|
float maxX = fmax(r[0], r[2]);
|
||||||
|
float minY = fmin(r[1], r[3]);
|
||||||
|
float maxY = fmax(r[1], r[3]);
|
||||||
|
|
||||||
|
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||||
|
LOGI("PdfInteraction: MATCH FOUND! Index=%d, Type=%d", i, get_annot_subtype_func(annot));
|
||||||
|
return get_annot_subtype_func(annot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jfloatArray JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotRectAtPoint(JNIEnv *env, jclass clazz, jlong pagePtr, jdouble x, jdouble y) {
|
||||||
|
if (!init_pdfium() || !get_annot_count_func || !get_annot_func || !get_annot_rect_func || pagePtr == 0) return nullptr;
|
||||||
|
|
||||||
|
void* page = reinterpret_cast<void*>(pagePtr);
|
||||||
|
int count = get_annot_count_func(page);
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
void* annot = get_annot_func(page, i);
|
||||||
|
float rect[4];
|
||||||
|
if (get_annot_rect_func(annot, rect)) {
|
||||||
|
if (x >= rect[0] && x <= rect[2] && y >= rect[1] && y <= rect[3]) {
|
||||||
|
jfloatArray result = env->NewFloatArray(4);
|
||||||
|
env->SetFloatArrayRegion(result, 0, 4, rect);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotCount(JNIEnv *env, jclass clazz, jlong pagePtr) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_pdfium_mutex);
|
||||||
|
if (!init_pdfium() || !get_annot_count_func || pagePtr == 0) return 0;
|
||||||
|
return get_annot_count_func(reinterpret_cast<void*>(pagePtr));
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jint JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotSubtype(JNIEnv *env, jclass clazz, jlong pagePtr, jint index) {
|
||||||
|
if (!init_pdfium() || !get_annot_func || !get_annot_subtype_func || pagePtr == 0) return 0;
|
||||||
|
void* annot = get_annot_func(reinterpret_cast<void*>(pagePtr), index);
|
||||||
|
return annot ? get_annot_subtype_func(annot) : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jfloatArray JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_getAnnotRect(JNIEnv *env, jclass clazz, jlong pagePtr, jint index) {
|
||||||
|
if (!init_pdfium() || !get_annot_func || !get_annot_rect_func || pagePtr == 0) return nullptr;
|
||||||
|
void* annot = get_annot_func(reinterpret_cast<void*>(pagePtr), index);
|
||||||
|
if (!annot) return nullptr;
|
||||||
|
|
||||||
|
float rect[4];
|
||||||
|
if (!get_annot_rect_func(annot, rect)) return nullptr;
|
||||||
|
|
||||||
|
jfloatArray result = env->NewFloatArray(4);
|
||||||
|
env->SetFloatArrayRegion(result, 0, 4, rect);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" JNIEXPORT jboolean JNICALL
|
||||||
|
Java_com_aryan_reader_pdf_NativePdfiumBridge_performClick(JNIEnv *env, jclass clazz, jlong pagePtr, jdouble x, jdouble y) {
|
||||||
|
std::lock_guard<std::mutex> lock(g_pdfium_mutex);
|
||||||
|
if (!init_pdfium() || pagePtr == 0) return JNI_FALSE;
|
||||||
|
|
||||||
|
void* page = reinterpret_cast<void*>(pagePtr);
|
||||||
|
int count = get_annot_count_func(page);
|
||||||
|
void* hitAnnot = nullptr;
|
||||||
|
|
||||||
|
// 1. Find which annotation was clicked
|
||||||
|
for (int i = 0; i < count; i++) {
|
||||||
|
void* annot = get_annot_func(page, i);
|
||||||
|
if (!annot) continue;
|
||||||
|
|
||||||
|
float r[4];
|
||||||
|
if (get_annot_rect_func(annot, r)) {
|
||||||
|
float minX = fminf(r[0], r[2]);
|
||||||
|
float maxX = fmaxf(r[0], r[2]);
|
||||||
|
float minY = fminf(r[1], r[3]);
|
||||||
|
float maxY = fmaxf(r[1], r[3]);
|
||||||
|
|
||||||
|
if (x >= minX && x <= maxX && y >= minY && y <= maxY) {
|
||||||
|
hitAnnot = annot;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hitAnnot) {
|
||||||
|
int subtype = get_annot_subtype_func(hitAnnot);
|
||||||
|
|
||||||
|
if (subtype == 19 || subtype == 20) {
|
||||||
|
LOGI("PdfInteraction: Button clicked (Subtype %d). Performing Blanket Reveal.", subtype);
|
||||||
|
bool anyChanged = false;
|
||||||
|
|
||||||
|
for (int j = 0; j < count; j++) {
|
||||||
|
void* target = get_annot_func(page, j);
|
||||||
|
if (!target || !get_annot_flags_func || !set_annot_flags_func) continue;
|
||||||
|
|
||||||
|
int flags = get_annot_flags_func(target);
|
||||||
|
|
||||||
|
// We check for: Invisible (1), Hidden (2), or NoView (32)
|
||||||
|
if (flags & (1 | 2 | 32)) {
|
||||||
|
LOGD("PdfInteraction: Unhiding element at index %d (Flags were 0x%X)", j, flags);
|
||||||
|
|
||||||
|
// Clear bits 1, 2, and 6 (1 + 2 + 32 = 35)
|
||||||
|
set_annot_flags_func(target, flags & ~35);
|
||||||
|
anyChanged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyChanged) {
|
||||||
|
return JNI_TRUE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return JNI_FALSE;
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,6 @@
|
||||||
// LibraryScreen.kt
|
// LibraryScreen.kt
|
||||||
package com.aryan.reader
|
package com.aryan.reader
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
|
@ -64,7 +62,6 @@ import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExtendedFloatingActionButton
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
import androidx.compose.material3.FloatingActionButton
|
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
|
|
@ -99,8 +96,6 @@ import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
|
@ -146,7 +141,7 @@ fun LibraryScreen(
|
||||||
|
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var isSearchActive by remember { mutableStateOf(false) }
|
val isSearchActive = uiState.isSearchActive
|
||||||
val searchQuery = uiState.searchQuery
|
val searchQuery = uiState.searchQuery
|
||||||
|
|
||||||
val pickFolderLauncher = rememberLauncherForActivityResult(
|
val pickFolderLauncher = rememberLauncherForActivityResult(
|
||||||
|
|
@ -222,8 +217,7 @@ fun LibraryScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
BackHandler(enabled = isSearchActive) {
|
BackHandler(enabled = isSearchActive) {
|
||||||
isSearchActive = false
|
viewModel.setSearchActive(false)
|
||||||
viewModel.onSearchQueryChange("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
|
@ -238,10 +232,7 @@ fun LibraryScreen(
|
||||||
searchQuery = searchQuery,
|
searchQuery = searchQuery,
|
||||||
isSearchActive = isSearchActive,
|
isSearchActive = isSearchActive,
|
||||||
onSearchQueryChange = viewModel::onSearchQueryChange,
|
onSearchQueryChange = viewModel::onSearchQueryChange,
|
||||||
onSearchActiveChange = { active ->
|
onSearchActiveChange = viewModel::setSearchActive,
|
||||||
isSearchActive = active
|
|
||||||
if (!active) viewModel.onSearchQueryChange("")
|
|
||||||
},
|
|
||||||
onSortOrderChange = viewModel::setSortOrder,
|
onSortOrderChange = viewModel::setSortOrder,
|
||||||
onClearSelection = { viewModel.clearContextualAction() },
|
onClearSelection = { viewModel.clearContextualAction() },
|
||||||
onItemClick = viewModel::onRecentFileClicked,
|
onItemClick = viewModel::onRecentFileClicked,
|
||||||
|
|
@ -474,6 +465,19 @@ fun LibraryScreenContent(
|
||||||
val tabTitles = listOf("All Books", "Shelves", "Folders")
|
val tabTitles = listOf("All Books", "Shelves", "Folders")
|
||||||
val searchFocusRequester = remember { FocusRequester() }
|
val searchFocusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
var textFieldValue by remember(isSearchActive) {
|
||||||
|
mutableStateOf(TextFieldValue(searchQuery, TextRange(searchQuery.length)))
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(searchQuery) {
|
||||||
|
if (textFieldValue.text != searchQuery) {
|
||||||
|
textFieldValue = textFieldValue.copy(
|
||||||
|
text = searchQuery,
|
||||||
|
selection = TextRange(searchQuery.length)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(isSearchActive) {
|
LaunchedEffect(isSearchActive) {
|
||||||
if (isSearchActive) {
|
if (isSearchActive) {
|
||||||
searchFocusRequester.requestFocus()
|
searchFocusRequester.requestFocus()
|
||||||
|
|
@ -501,7 +505,7 @@ fun LibraryScreenContent(
|
||||||
} else if (isSearchActive) {
|
} else if (isSearchActive) {
|
||||||
Surface(
|
Surface(
|
||||||
shadowElevation = 4.dp,
|
shadowElevation = 4.dp,
|
||||||
modifier = Modifier.fillMaxWidth().statusBarsPadding()
|
modifier = Modifier.fillMaxWidth()
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -513,8 +517,11 @@ fun LibraryScreenContent(
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search")
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Close search")
|
||||||
}
|
}
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = searchQuery,
|
value = textFieldValue,
|
||||||
onValueChange = onSearchQueryChange,
|
onValueChange = {
|
||||||
|
textFieldValue = it
|
||||||
|
onSearchQueryChange(it.text)
|
||||||
|
},
|
||||||
placeholder = { Text("Search title or author...") },
|
placeholder = { Text("Search title or author...") },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
|
@ -866,9 +873,11 @@ private fun ShelfDetailScreen(
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
if (shelf.name != "Unshelved" && !isContextualModeActive) {
|
if (shelf.name != "Unshelved" && !isContextualModeActive) {
|
||||||
FloatingActionButton(onClick = onAddBooksClick) {
|
ExtendedFloatingActionButton(
|
||||||
Icon(Icons.Default.Add, contentDescription = "Add books")
|
onClick = onAddBooksClick,
|
||||||
}
|
icon = { Icon(Icons.Default.Add, contentDescription = null) },
|
||||||
|
text = { Text("Add books") }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
|
|
@ -1362,25 +1371,6 @@ private fun DeleteShelvesConfirmationDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayPathFromUri(context: Context, uriString: String): String {
|
|
||||||
val uri = uriString.toUri()
|
|
||||||
val fallbackName = DocumentFile.fromTreeUri(context, uri)?.name ?: "Unknown Folder"
|
|
||||||
|
|
||||||
if (DocumentsContract.isTreeUri(uri) && DocumentsContract.getTreeDocumentId(uri).isNotEmpty()) {
|
|
||||||
val documentId = DocumentsContract.getTreeDocumentId(uri)
|
|
||||||
val split = documentId.split(":")
|
|
||||||
if (split.size > 1) {
|
|
||||||
val type = split[0]
|
|
||||||
val path = split[1]
|
|
||||||
return when (type) {
|
|
||||||
"primary" -> "Internal Storage ▸ $path"
|
|
||||||
else -> path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fallbackName
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun FolderSyncScreen(
|
private fun FolderSyncScreen(
|
||||||
syncedFolders: List<SyncedFolder>,
|
syncedFolders: List<SyncedFolder>,
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,7 @@ data class ReaderScreenState(
|
||||||
val lastFolderScanTime: Long? = null,
|
val lastFolderScanTime: Long? = null,
|
||||||
val hasUnreadFeedback: Boolean = false,
|
val hasUnreadFeedback: Boolean = false,
|
||||||
val searchQuery: String = "",
|
val searchQuery: String = "",
|
||||||
|
val isSearchActive: Boolean = false,
|
||||||
val showFolderMigrationDialog: Boolean = false,
|
val showFolderMigrationDialog: Boolean = false,
|
||||||
val isRefreshing: Boolean = false,
|
val isRefreshing: Boolean = false,
|
||||||
val reflowProgress: Float? = null,
|
val reflowProgress: Float? = null,
|
||||||
|
|
@ -369,7 +370,23 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
|
||||||
)
|
)
|
||||||
|
|
||||||
fun onSearchQueryChange(newQuery: String) {
|
fun onSearchQueryChange(newQuery: String) {
|
||||||
_internalState.update { it.copy(searchQuery = newQuery) }
|
_internalState.update {
|
||||||
|
if (it.isSearchActive) {
|
||||||
|
it.copy(searchQuery = newQuery)
|
||||||
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchActive(active: Boolean) {
|
||||||
|
_internalState.update {
|
||||||
|
if (active) {
|
||||||
|
it.copy(isSearchActive = true)
|
||||||
|
} else {
|
||||||
|
it.copy(isSearchActive = false, searchQuery = "")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val _reviewRequestEvent = Channel<Unit>(Channel.BUFFERED)
|
private val _reviewRequestEvent = Channel<Unit>(Channel.BUFFERED)
|
||||||
|
|
|
||||||
|
|
@ -24,8 +24,15 @@ object NativePdfiumBridge {
|
||||||
@JvmStatic external fun getPageObjectBoundingBox(pagePtr: Long, index: Int, outRect: FloatArray): Boolean
|
@JvmStatic external fun getPageObjectBoundingBox(pagePtr: Long, index: Int, outRect: FloatArray): Boolean
|
||||||
@JvmStatic external fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray?
|
@JvmStatic external fun extractImagePixels(pagePtr: Long, index: Int, dimens: IntArray): IntArray?
|
||||||
|
|
||||||
|
@JvmStatic external fun performClick(pagePtr: Long, x: Double, y: Double): Boolean
|
||||||
|
|
||||||
|
@JvmStatic external fun getAnnotSubtypeAtPoint(pagePtr: Long, x: Double, y: Double): Int
|
||||||
|
@JvmStatic external fun getAnnotRectAtPoint(pagePtr: Long, x: Double, y: Double): FloatArray?
|
||||||
|
@JvmStatic external fun checkActionSupport(): Boolean
|
||||||
|
|
||||||
const val ANNOT_TEXT = 1 // Sticky Note
|
const val ANNOT_TEXT = 1 // Sticky Note
|
||||||
const val ANNOT_LINK = 2 // Link
|
const val ANNOT_LINK = 2 // Link
|
||||||
const val ANNOT_HIGHLIGHT = 8 // Highlight
|
const val ANNOT_HIGHLIGHT = 8 // Highlight
|
||||||
const val ANNOT_INK = 12 // Freehand drawing
|
const val ANNOT_INK = 12 // Freehand drawing
|
||||||
|
const val ANNOT_WIDGET = 19
|
||||||
}
|
}
|
||||||
|
|
@ -55,7 +55,7 @@ class PdfCoverGenerator(context: Context) {
|
||||||
Timber.w("PDF has no pages, cannot generate cover: $pdfUri")
|
Timber.w("PDF has no pages, cannot generate cover: $pdfUri")
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
doc.openPage(0).use { page ->
|
doc.openPage(0)?.use { page ->
|
||||||
val originalWidth = page.getPageWidthPoint()
|
val originalWidth = page.getPageWidthPoint()
|
||||||
val originalHeight = page.getPageHeightPoint()
|
val originalHeight = page.getPageHeightPoint()
|
||||||
if (originalWidth <= 0 || originalHeight <= 0) {
|
if (originalWidth <= 0 || originalHeight <= 0) {
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,7 @@ import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.StrokeJoin
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.graphics.drawscope.clipRect
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
import androidx.compose.ui.graphics.drawscope.rotate
|
||||||
import androidx.compose.ui.graphics.drawscope.translate
|
import androidx.compose.ui.graphics.drawscope.translate
|
||||||
|
|
@ -438,7 +439,6 @@ internal fun PdfPageComposable(
|
||||||
onHighlightDelete: (String) -> Unit = {},
|
onHighlightDelete: (String) -> Unit = {},
|
||||||
onTts: (Int, Int) -> Unit = { _, _ -> },
|
onTts: (Int, Int) -> Unit = { _, _ -> },
|
||||||
) {
|
) {
|
||||||
SideEffect { Timber.tag("PdfDrawPerf").v("PdfPageComposable Recompose: Page $pageIndex") }
|
|
||||||
val pdfDocumentItem = pdfDocument.item
|
val pdfDocumentItem = pdfDocument.item
|
||||||
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
var bitmapState by remember { mutableStateOf(PdfThumbnailCache.get(pageIndex)) }
|
||||||
var currentRenderedPageId by remember { mutableStateOf<String?>(null) }
|
var currentRenderedPageId by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
@ -677,7 +677,7 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pdfDocumentItem.openPage(pdfPageIndex).use { page ->
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||||
val mapped = userHighlights.map { highlight ->
|
val mapped = userHighlights.map { highlight ->
|
||||||
val screenRects = highlight.bounds.mapNotNull { pdfRectF ->
|
val screenRects = highlight.bounds.mapNotNull { pdfRectF ->
|
||||||
page.mapRectToDevice(
|
page.mapRectToDevice(
|
||||||
|
|
@ -750,7 +750,7 @@ internal fun PdfPageComposable(
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
tempPage = pdfDocumentItem.openPage(pdfPageIndex)
|
tempPage = pdfDocumentItem.openPage(pdfPageIndex)
|
||||||
tempPage.openTextPage().use { textPage ->
|
tempPage?.openTextPage()?.use { textPage ->
|
||||||
val charCount = textPage.textPageCountChars()
|
val charCount = textPage.textPageCountChars()
|
||||||
if (charCount > 0) {
|
if (charCount > 0) {
|
||||||
val pdfRectsF =
|
val pdfRectsF =
|
||||||
|
|
@ -760,14 +760,14 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
if (pdfRectsF.isNotEmpty()) {
|
if (pdfRectsF.isNotEmpty()) {
|
||||||
val mappedScreenRects = pdfRectsF.mapNotNull { pdfRectF ->
|
val mappedScreenRects = pdfRectsF.mapNotNull { pdfRectF ->
|
||||||
tempPage.mapRectToDevice(
|
tempPage?.mapRectToDevice(
|
||||||
startX = 0,
|
startX = 0,
|
||||||
startY = 0,
|
startY = 0,
|
||||||
sizeX = actualBitmapWidthPx,
|
sizeX = actualBitmapWidthPx,
|
||||||
sizeY = actualBitmapHeightPx,
|
sizeY = actualBitmapHeightPx,
|
||||||
rotate = currentPageRotation,
|
rotate = currentPageRotation,
|
||||||
coords = pdfRectF
|
coords = pdfRectF
|
||||||
).takeIf { it.width() > 0 && it.height() > 0 }
|
)?.takeIf { it.width() > 0 && it.height() > 0 }
|
||||||
}
|
}
|
||||||
if (mappedScreenRects.isNotEmpty()) {
|
if (mappedScreenRects.isNotEmpty()) {
|
||||||
rects = mappedScreenRects
|
rects = mappedScreenRects
|
||||||
|
|
@ -841,7 +841,7 @@ internal fun PdfPageComposable(
|
||||||
val annotLink = 2
|
val annotLink = 2
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pdfDocumentItem.openPage(pdfPageIndex).use { pageWrapper ->
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { pageWrapper ->
|
||||||
|
|
||||||
// 1. Extract Links (Method 1: Annotations)
|
// 1. Extract Links (Method 1: Annotations)
|
||||||
try {
|
try {
|
||||||
|
|
@ -876,7 +876,7 @@ internal fun PdfPageComposable(
|
||||||
// 2. Extract Links (Method 2: Text)
|
// 2. Extract Links (Method 2: Text)
|
||||||
try {
|
try {
|
||||||
pageWrapper.openTextPage().use { textPage ->
|
pageWrapper.openTextPage().use { textPage ->
|
||||||
textPage.loadWebLink().use { webLinks ->
|
textPage.loadWebLink()?.use { webLinks ->
|
||||||
val webLinkCount = webLinks.countWebLinks()
|
val webLinkCount = webLinks.countWebLinks()
|
||||||
for (linkIndex in 0 until webLinkCount) {
|
for (linkIndex in 0 until webLinkCount) {
|
||||||
val rawUrl = webLinks.getURL(linkIndex, 2048)
|
val rawUrl = webLinks.getURL(linkIndex, 2048)
|
||||||
|
|
@ -907,17 +907,12 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
// 3. Extract Embedded Annotations
|
// 3. Extract Embedded Annotations
|
||||||
try {
|
try {
|
||||||
val unlockedPage = pageWrapper.page
|
val pagePtr = getNativePointer(pageWrapper)
|
||||||
val pagePtr = try {
|
|
||||||
val field = unlockedPage.javaClass.getDeclaredField("pagePtr")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.get(unlockedPage) as Long
|
|
||||||
} catch (e: Exception) {
|
|
||||||
val field = unlockedPage.javaClass.getDeclaredField("mNativePage")
|
|
||||||
field.isAccessible = true
|
|
||||||
field.get(unlockedPage) as Long
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (pagePtr != 0L) {
|
||||||
|
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
|
||||||
|
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
|
||||||
|
if (count > 0) {
|
||||||
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
|
val count = NativePdfiumBridge.getAnnotCount(pagePtr)
|
||||||
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
|
Timber.tag("PdfCommentDebug").d("Page $pageIndex: Total Annotations found = $count")
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
|
@ -989,6 +984,10 @@ internal fun PdfPageComposable(
|
||||||
annot to screenRect
|
annot to screenRect
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Timber.tag("PdfCommentDebug").w("Page $pageIndex: Failed to resolve native page pointer.")
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag("PdfCommentDebug").e(e, "Error extracting annotations")
|
Timber.tag("PdfCommentDebug").e(e, "Error extracting annotations")
|
||||||
}
|
}
|
||||||
|
|
@ -1007,141 +1006,6 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(pageIndex, pdfDocumentItem, actualBitmapWidthPx, actualBitmapHeightPx, virtualPage) {
|
|
||||||
if (!isPdfPage) {
|
|
||||||
if (pageLinks.isNotEmpty()) pageLinks = emptyList()
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
|
|
||||||
if (actualBitmapWidthPx == 0 || actualBitmapHeightPx == 0) {
|
|
||||||
if (pageLinks.isNotEmpty()) pageLinks = emptyList()
|
|
||||||
return@LaunchedEffect
|
|
||||||
}
|
|
||||||
Timber.d(
|
|
||||||
"LaunchedEffect: Starting link fetch for page $pageIndex. Bitmap size: ${actualBitmapWidthPx}x${actualBitmapHeightPx}"
|
|
||||||
)
|
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
val allLinks = mutableListOf<PageLink>()
|
|
||||||
try {
|
|
||||||
pdfDocumentItem.openPage(pdfPageIndex).use { page ->
|
|
||||||
try {
|
|
||||||
val annotationLinks = page.getPageLinks()
|
|
||||||
Timber.d("Method 1 (getPageLinks) returned ${annotationLinks.size} links.")
|
|
||||||
if (annotationLinks.isNotEmpty()) {
|
|
||||||
val mappedAnnotationLinks = annotationLinks.mapNotNull { link ->
|
|
||||||
val uri = link.uri
|
|
||||||
val destPageIdx = link.destPageIdx
|
|
||||||
val bounds = link.bounds
|
|
||||||
|
|
||||||
if (uri != null || (destPageIdx != null && destPageIdx >= 0)) {
|
|
||||||
val deviceRect = page.mapRectToDevice(
|
|
||||||
startX = 0,
|
|
||||||
startY = 0,
|
|
||||||
sizeX = actualBitmapWidthPx,
|
|
||||||
sizeY = actualBitmapHeightPx,
|
|
||||||
rotate = currentPageRotation,
|
|
||||||
coords = bounds
|
|
||||||
)
|
|
||||||
|
|
||||||
Timber.d(
|
|
||||||
"[Method 1] Link '$uri' | PDF Bounds: $bounds | Mapped Device Rect: $deviceRect"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (deviceRect.width() > 0 && deviceRect.height() > 0) {
|
|
||||||
val tapRect = Rect(
|
|
||||||
deviceRect.left,
|
|
||||||
deviceRect.top - linkVerticalPaddingPx,
|
|
||||||
deviceRect.right,
|
|
||||||
deviceRect.bottom + linkVerticalPaddingPx
|
|
||||||
)
|
|
||||||
Timber.d(
|
|
||||||
"[Method 1] Padded Tap Rect: $tapRect (Padding: $linkVerticalPaddingPx)"
|
|
||||||
)
|
|
||||||
PageLink(
|
|
||||||
highlightBounds = deviceRect,
|
|
||||||
tapBounds = tapRect,
|
|
||||||
url = uri,
|
|
||||||
destPageIdx = destPageIdx,
|
|
||||||
source = LinkSource.ANNOTATION
|
|
||||||
)
|
|
||||||
} else null
|
|
||||||
} else null
|
|
||||||
}
|
|
||||||
allLinks.addAll(mappedAnnotationLinks)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error fetching annotation links")
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- METHOD 2: Get links detected within the text content
|
|
||||||
// ---
|
|
||||||
try {
|
|
||||||
page.openTextPage().use { textPage ->
|
|
||||||
textPage.loadWebLink().use { webLinks ->
|
|
||||||
val webLinkCount = webLinks.countWebLinks()
|
|
||||||
Timber.d("Method 2 (loadWebLink) returned $webLinkCount links.")
|
|
||||||
for (linkIndex in 0 until webLinkCount) {
|
|
||||||
val rawUrl = webLinks.getURL(linkIndex, 2048)
|
|
||||||
val url = rawUrl?.substringBefore('\u0000')
|
|
||||||
|
|
||||||
if (url.isNullOrBlank()) continue
|
|
||||||
|
|
||||||
val rectCount = webLinks.countRects(linkIndex)
|
|
||||||
for (rectIndex in 0 until rectCount) {
|
|
||||||
val pdfRect = webLinks.getRect(linkIndex, rectIndex)
|
|
||||||
val deviceRect = page.mapRectToDevice(
|
|
||||||
startX = 0,
|
|
||||||
startY = 0,
|
|
||||||
sizeX = actualBitmapWidthPx,
|
|
||||||
sizeY = actualBitmapHeightPx,
|
|
||||||
rotate = currentPageRotation,
|
|
||||||
coords = pdfRect
|
|
||||||
)
|
|
||||||
Timber.d(
|
|
||||||
"[Method 2] Link '$url' | PDF Rect: $pdfRect | Mapped Device Rect: $deviceRect"
|
|
||||||
)
|
|
||||||
|
|
||||||
if (deviceRect.width() > 0 && deviceRect.height() > 0) {
|
|
||||||
val tapRect = Rect(
|
|
||||||
deviceRect.left,
|
|
||||||
deviceRect.top - linkVerticalPaddingPx,
|
|
||||||
deviceRect.right,
|
|
||||||
deviceRect.bottom + linkVerticalPaddingPx
|
|
||||||
)
|
|
||||||
Timber.d(
|
|
||||||
"[Method 2] Padded Tap Rect: $tapRect (Padding: $linkVerticalPaddingPx)"
|
|
||||||
)
|
|
||||||
allLinks.add(
|
|
||||||
PageLink(
|
|
||||||
highlightBounds = deviceRect,
|
|
||||||
tapBounds = tapRect,
|
|
||||||
url = url,
|
|
||||||
destPageIdx = null,
|
|
||||||
source = LinkSource.TEXT_CONTENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Error fetching web links from text page")
|
|
||||||
}
|
|
||||||
|
|
||||||
pageLinks = allLinks
|
|
||||||
Timber.d(
|
|
||||||
"Finished fetching. Stored a total of ${allLinks.size} links in state."
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.e(e, "Failed to open page $pdfPageIndex for link fetching")
|
|
||||||
pageLinks = emptyList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(isVisible, pageIndex) {
|
LaunchedEffect(isVisible, pageIndex) {
|
||||||
if (!isVisible && !isVerticalScroll) {
|
if (!isVisible && !isVerticalScroll) {
|
||||||
if (bitmapState != null) {
|
if (bitmapState != null) {
|
||||||
|
|
@ -1153,6 +1017,21 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SideEffect {
|
||||||
|
if (effectiveScale > 1f) {
|
||||||
|
Timber.tag("PdfZoomDiagnostics").v(
|
||||||
|
"""
|
||||||
|
Page $pageIndex Stats:
|
||||||
|
- Internal Scale: $scale | Effective Scale: $effectiveScale
|
||||||
|
- Offset: $offset
|
||||||
|
- Bitmap Dims: ${actualBitmapWidthPx}x${actualBitmapHeightPx}
|
||||||
|
- Canvas Dims: ${canvasWidthPx.floatValue}x${canvasHeightPx.floatValue}
|
||||||
|
- Centering: X=$centeringOffsetX, Y=$centeringOffsetY
|
||||||
|
""".trimIndent()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
effectiveScale,
|
effectiveScale,
|
||||||
effectiveOffset,
|
effectiveOffset,
|
||||||
|
|
@ -1309,7 +1188,7 @@ internal fun PdfPageComposable(
|
||||||
val tileRenderX = (col * tileSizePx * effectiveScale).toInt()
|
val tileRenderX = (col * tileSizePx * effectiveScale).toInt()
|
||||||
val tileRenderY = (row * tileSizePx * effectiveScale).toInt()
|
val tileRenderY = (row * tileSizePx * effectiveScale).toInt()
|
||||||
|
|
||||||
page.renderPageBitmap(
|
page?.renderPageBitmap(
|
||||||
bitmap = tileBitmap,
|
bitmap = tileBitmap,
|
||||||
startX = -tileRenderX,
|
startX = -tileRenderX,
|
||||||
startY = -tileRenderY,
|
startY = -tileRenderY,
|
||||||
|
|
@ -1384,7 +1263,7 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
pdfDocumentItem.openPage(pdfPageIndex).use { page ->
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val charCount = textPage.textPageCountChars()
|
val charCount = textPage.textPageCountChars()
|
||||||
if (charCount > 0) {
|
if (charCount > 0) {
|
||||||
|
|
@ -1559,7 +1438,15 @@ internal fun PdfPageComposable(
|
||||||
textPageToUse = providedTextPage
|
textPageToUse = providedTextPage
|
||||||
} else {
|
} else {
|
||||||
localPage = doc.openPage(pageIdx)
|
localPage = doc.openPage(pageIdx)
|
||||||
localTextPage = localPage.openTextPage()
|
localTextPage = localPage?.openTextPage()
|
||||||
|
if (localPage == null || localTextPage == null) {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
selectedWordScreenRects = emptyList()
|
||||||
|
startHandleContentPosition.value = null
|
||||||
|
endHandleContentPosition.value = null
|
||||||
|
}
|
||||||
|
return@withContext
|
||||||
|
}
|
||||||
pageToUse = localPage
|
pageToUse = localPage
|
||||||
textPageToUse = localTextPage
|
textPageToUse = localTextPage
|
||||||
}
|
}
|
||||||
|
|
@ -1788,7 +1675,7 @@ internal fun PdfPageComposable(
|
||||||
pageForDrag = pdfDocumentItem.openPage(
|
pageForDrag = pdfDocumentItem.openPage(
|
||||||
pdfPageIndex
|
pdfPageIndex
|
||||||
)
|
)
|
||||||
textPageForDrag = pageForDrag.openTextPage()
|
textPageForDrag = pageForDrag?.openTextPage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1886,13 +1773,15 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// PDFIUM Logic
|
// PDFIUM Logic
|
||||||
if (pageForDrag != null && textPageForDrag != null) {
|
val pDrag = pageForDrag
|
||||||
|
val tDrag = textPageForDrag
|
||||||
|
if (pDrag != null && tDrag != null) {
|
||||||
val touchInContentCoords =
|
val touchInContentCoords =
|
||||||
screenToContentCoordinates(
|
screenToContentCoordinates(
|
||||||
dragPosition
|
dragPosition
|
||||||
)
|
)
|
||||||
|
|
||||||
val pdfCoords = pageForDrag.mapDeviceCoordsToPage(
|
val pdfCoords = pDrag.mapDeviceCoordsToPage(
|
||||||
startX = 0,
|
startX = 0,
|
||||||
startY = 0,
|
startY = 0,
|
||||||
sizeX = actualBitmapWidthPx,
|
sizeX = actualBitmapWidthPx,
|
||||||
|
|
@ -1904,7 +1793,7 @@ internal fun PdfPageComposable(
|
||||||
val charTolerance = 10.0
|
val charTolerance = 10.0
|
||||||
|
|
||||||
var charIndexForUpdate =
|
var charIndexForUpdate =
|
||||||
textPageForDrag.textPageGetCharIndexAtPos(
|
tDrag.textPageGetCharIndexAtPos(
|
||||||
x = pdfCoords.x.toDouble(),
|
x = pdfCoords.x.toDouble(),
|
||||||
y = pdfCoords.y.toDouble(),
|
y = pdfCoords.y.toDouble(),
|
||||||
xTolerance = charTolerance,
|
xTolerance = charTolerance,
|
||||||
|
|
@ -1913,7 +1802,7 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
if (charIndexForUpdate == -1 && activeDraggingHandle != null) {
|
if (charIndexForUpdate == -1 && activeDraggingHandle != null) {
|
||||||
val pageWidthPdfUnits =
|
val pageWidthPdfUnits =
|
||||||
pageForDrag.getPageWidthPoint()
|
pDrag.getPageWidthPoint()
|
||||||
val wideSearchXTolerance =
|
val wideSearchXTolerance =
|
||||||
pageWidthPdfUnits.toDouble()
|
pageWidthPdfUnits.toDouble()
|
||||||
var ySearchCoordinate = pdfCoords.y.toDouble()
|
var ySearchCoordinate = pdfCoords.y.toDouble()
|
||||||
|
|
@ -1921,7 +1810,7 @@ internal fun PdfPageComposable(
|
||||||
val currentRange = selectionCharRange.value
|
val currentRange = selectionCharRange.value
|
||||||
if (currentRange != null) {
|
if (currentRange != null) {
|
||||||
val pageTotalChars =
|
val pageTotalChars =
|
||||||
textPageForDrag.textPageCountChars()
|
tDrag.textPageCountChars()
|
||||||
if (pageTotalChars > 0) {
|
if (pageTotalChars > 0) {
|
||||||
val anchorCharIndex =
|
val anchorCharIndex =
|
||||||
if (activeDraggingHandle == Handle.START) {
|
if (activeDraggingHandle == Handle.START) {
|
||||||
|
|
@ -1933,7 +1822,7 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
if (anchorCharIndex in 0..<pageTotalChars) {
|
if (anchorCharIndex in 0..<pageTotalChars) {
|
||||||
val anchorCharBox =
|
val anchorCharBox =
|
||||||
textPageForDrag.textPageGetCharBox(
|
tDrag.textPageGetCharBox(
|
||||||
anchorCharIndex
|
anchorCharIndex
|
||||||
)
|
)
|
||||||
if (anchorCharBox != null) {
|
if (anchorCharBox != null) {
|
||||||
|
|
@ -1947,7 +1836,7 @@ internal fun PdfPageComposable(
|
||||||
val wideSearchYTolerance = charTolerance * 1.5
|
val wideSearchYTolerance = charTolerance * 1.5
|
||||||
if (wideSearchXTolerance > 0) {
|
if (wideSearchXTolerance > 0) {
|
||||||
charIndexForUpdate =
|
charIndexForUpdate =
|
||||||
textPageForDrag.textPageGetCharIndexAtPos(
|
tDrag.textPageGetCharIndexAtPos(
|
||||||
x = pdfCoords.x.toDouble(),
|
x = pdfCoords.x.toDouble(),
|
||||||
y = ySearchCoordinate,
|
y = ySearchCoordinate,
|
||||||
xTolerance = wideSearchXTolerance,
|
xTolerance = wideSearchXTolerance,
|
||||||
|
|
@ -1958,7 +1847,7 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
if (charIndexForUpdate != -1) {
|
if (charIndexForUpdate != -1) {
|
||||||
val pageCharCount =
|
val pageCharCount =
|
||||||
textPageForDrag.textPageCountChars()
|
tDrag.textPageCountChars()
|
||||||
|
|
||||||
val currentRange = selectionCharRange.value
|
val currentRange = selectionCharRange.value
|
||||||
if (currentRange != null) {
|
if (currentRange != null) {
|
||||||
|
|
@ -2120,8 +2009,8 @@ internal fun PdfPageComposable(
|
||||||
try {
|
try {
|
||||||
val text = withContext(Dispatchers.IO) {
|
val text = withContext(Dispatchers.IO) {
|
||||||
pageForMenu = pdfDocumentItem.openPage(pdfPageIndex)
|
pageForMenu = pdfDocumentItem.openPage(pdfPageIndex)
|
||||||
textPageForMenu = pageForMenu.openTextPage()
|
textPageForMenu = pageForMenu?.openTextPage()
|
||||||
textPageForMenu.textPageGetText(
|
textPageForMenu?.textPageGetText(
|
||||||
currentRange.first,
|
currentRange.first,
|
||||||
currentRange.second - currentRange.first
|
currentRange.second - currentRange.first
|
||||||
)
|
)
|
||||||
|
|
@ -2244,9 +2133,12 @@ internal fun PdfPageComposable(
|
||||||
var pdfiumSelectionSuccessful = false
|
var pdfiumSelectionSuccessful = false
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
tempPage = pdfDocumentItem.openPage(pdfPageIndex)
|
tempPage = pdfDocumentItem.openPage(pdfPageIndex)
|
||||||
tempTextPage = tempPage.openTextPage()
|
tempTextPage = tempPage?.openTextPage()
|
||||||
|
|
||||||
val pdfCoords = tempPage.mapDeviceCoordsToPage(
|
val tPage = tempPage
|
||||||
|
val tTextPage = tempTextPage
|
||||||
|
if (tPage != null && tTextPage != null) {
|
||||||
|
val pdfCoords = tPage.mapDeviceCoordsToPage(
|
||||||
startX = 0,
|
startX = 0,
|
||||||
startY = 0,
|
startY = 0,
|
||||||
sizeX = actualBitmapWidthPx,
|
sizeX = actualBitmapWidthPx,
|
||||||
|
|
@ -2256,7 +2148,7 @@ internal fun PdfPageComposable(
|
||||||
deviceY = touchInContentCoords.y.toInt()
|
deviceY = touchInContentCoords.y.toInt()
|
||||||
)
|
)
|
||||||
val charTolerance = 5.0
|
val charTolerance = 5.0
|
||||||
val charIndex = tempTextPage.textPageGetCharIndexAtPos(
|
val charIndex = tTextPage.textPageGetCharIndexAtPos(
|
||||||
x = pdfCoords.x.toDouble(),
|
x = pdfCoords.x.toDouble(),
|
||||||
y = pdfCoords.y.toDouble(),
|
y = pdfCoords.y.toDouble(),
|
||||||
xTolerance = charTolerance,
|
xTolerance = charTolerance,
|
||||||
|
|
@ -2264,9 +2156,9 @@ internal fun PdfPageComposable(
|
||||||
)
|
)
|
||||||
|
|
||||||
if (charIndex != -1) {
|
if (charIndex != -1) {
|
||||||
val pageCharCount = tempTextPage.textPageCountChars()
|
val pageCharCount = tTextPage.textPageCountChars()
|
||||||
val wordBoundaries = findWordBoundaries(
|
val wordBoundaries = findWordBoundaries(
|
||||||
tempTextPage, charIndex, pageCharCount
|
tTextPage, charIndex, pageCharCount
|
||||||
)
|
)
|
||||||
|
|
||||||
if (wordBoundaries != null) {
|
if (wordBoundaries != null) {
|
||||||
|
|
@ -2281,14 +2173,14 @@ internal fun PdfPageComposable(
|
||||||
actualBitmapWidthPx,
|
actualBitmapWidthPx,
|
||||||
actualBitmapHeightPx,
|
actualBitmapHeightPx,
|
||||||
currentPageRotation,
|
currentPageRotation,
|
||||||
providedPage = tempPage,
|
providedPage = tPage,
|
||||||
providedTextPage = tempTextPage
|
providedTextPage = tTextPage
|
||||||
)
|
)
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (selectionCharRange.value != null && selectedWordScreenRects.isNotEmpty()) {
|
if (selectionCharRange.value != null && selectedWordScreenRects.isNotEmpty()) {
|
||||||
val currentRange = selectionCharRange.value!!
|
val currentRange = selectionCharRange.value!!
|
||||||
val text = withContext(Dispatchers.IO) {
|
val text = withContext(Dispatchers.IO) {
|
||||||
tempTextPage.textPageGetText(
|
tTextPage.textPageGetText(
|
||||||
currentRange.first,
|
currentRange.first,
|
||||||
currentRange.second - currentRange.first
|
currentRange.second - currentRange.first
|
||||||
)
|
)
|
||||||
|
|
@ -2312,6 +2204,7 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!pdfiumSelectionSuccessful && bitmapState != null) {
|
if (!pdfiumSelectionSuccessful && bitmapState != null) {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
|
|
@ -2511,6 +2404,40 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
detectTapGestures(onTap = { tapOffset ->
|
detectTapGestures(onTap = { tapOffset ->
|
||||||
val tapInContentCoords = screenToContentCoordinates(tapOffset)
|
val tapInContentCoords = screenToContentCoordinates(tapOffset)
|
||||||
|
|
||||||
|
coroutineScope.launch {
|
||||||
|
val wasHandled = withContext(Dispatchers.IO) {
|
||||||
|
try {
|
||||||
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||||
|
val pagePtr = getNativePointer(page)
|
||||||
|
|
||||||
|
if (pagePtr == 0L) {
|
||||||
|
Timber.tag("PdfInteraction").e("Could not find native pointer for page $pdfPageIndex")
|
||||||
|
return@withContext false
|
||||||
|
}
|
||||||
|
|
||||||
|
val pdfCoords = page.mapDeviceCoordsToPage(
|
||||||
|
0, 0, actualBitmapWidthPx, actualBitmapHeightPx,
|
||||||
|
currentPageRotation, tapInContentCoords.x.toInt(), tapInContentCoords.y.toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
NativePdfiumBridge.performClick(pagePtr, pdfCoords.x.toDouble(), pdfCoords.y.toDouble())
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("PdfInteraction").e(e, "Interaction error")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasHandled) {
|
||||||
|
Timber.tag("PdfInteraction").i("Action detected. Refreshing page.")
|
||||||
|
tiles = emptyList()
|
||||||
|
bitmapState = null
|
||||||
|
isLoadingPage = true
|
||||||
|
currentRenderedPageId = "ACTION_${System.currentTimeMillis()}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val tapXInBitmap = tapInContentCoords.x
|
val tapXInBitmap = tapInContentCoords.x
|
||||||
val tapYInBitmap = tapInContentCoords.y
|
val tapYInBitmap = tapInContentCoords.y
|
||||||
|
|
||||||
|
|
@ -2636,6 +2563,7 @@ internal fun PdfPageComposable(
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
val startScale = scale
|
val startScale = scale
|
||||||
val targetScale = if (startScale > 1.1f) 1f else 2.5f
|
val targetScale = if (startScale > 1.1f) 1f else 2.5f
|
||||||
|
Timber.tag("PdfZoomDebug").i("DoubleTap Triggered: CurrentScale=$startScale, Target=$targetScale")
|
||||||
val startOffset = offset
|
val startOffset = offset
|
||||||
val targetOffsetUnbounded = if (targetScale <= 1.1f) {
|
val targetOffsetUnbounded = if (targetScale <= 1.1f) {
|
||||||
Offset.Zero
|
Offset.Zero
|
||||||
|
|
@ -2734,6 +2662,7 @@ internal fun PdfPageComposable(
|
||||||
accumulatedPan += panChange
|
accumulatedPan += panChange
|
||||||
if (accumulatedPan.getDistance() > touchSlop) {
|
if (accumulatedPan.getDistance() > touchSlop) {
|
||||||
mode = 1
|
mode = 1
|
||||||
|
Timber.tag("PdfZoomDebug").d("Mode Change: PAN (Single Pointer)")
|
||||||
}
|
}
|
||||||
} else if (pointerCount > 1) {
|
} else if (pointerCount > 1) {
|
||||||
accumulatedZoom *= zoomChange
|
accumulatedZoom *= zoomChange
|
||||||
|
|
@ -2744,8 +2673,10 @@ internal fun PdfPageComposable(
|
||||||
|
|
||||||
if (zoomDiff > 0.05f) {
|
if (zoomDiff > 0.05f) {
|
||||||
mode = 2
|
mode = 2
|
||||||
|
Timber.tag("PdfZoomDebug").d("Mode Change: ZOOM (Multi Pointer)")
|
||||||
} else if (panDist > touchSlop) {
|
} else if (panDist > touchSlop) {
|
||||||
mode = 1
|
mode = 1
|
||||||
|
Timber.tag("PdfZoomDebug").d("Mode Change: PAN (Multi Pointer)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2764,6 +2695,7 @@ internal fun PdfPageComposable(
|
||||||
val newY = (offset.y + panChange.y).coerceIn(
|
val newY = (offset.y + panChange.y).coerceIn(
|
||||||
-maxOffsetY, maxOffsetY
|
-maxOffsetY, maxOffsetY
|
||||||
)
|
)
|
||||||
|
Timber.tag("PdfZoomDebug").v("Panning: Offset $offset -> $newX, $newY (Max: $maxOffsetX, $maxOffsetY)")
|
||||||
offset = Offset(newX, newY)
|
offset = Offset(newX, newY)
|
||||||
|
|
||||||
event.changes.forEach {
|
event.changes.forEach {
|
||||||
|
|
@ -2773,40 +2705,25 @@ internal fun PdfPageComposable(
|
||||||
val oldScale = scale
|
val oldScale = scale
|
||||||
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
|
val newScale = (scale * zoomChange).coerceIn(1f, 4f)
|
||||||
|
|
||||||
val previousCentroid = event.calculateCentroid(
|
val previousCentroid = event.calculateCentroid(useCurrent = false)
|
||||||
useCurrent = false
|
|
||||||
)
|
|
||||||
if (previousCentroid != Offset.Unspecified) {
|
if (previousCentroid != Offset.Unspecified) {
|
||||||
val ratio = newScale / oldScale
|
val ratio = newScale / oldScale
|
||||||
val screenCenter = Offset(
|
val screenCenter = Offset(size.width / 2f, size.height / 2f)
|
||||||
size.width / 2f, size.height / 2f
|
val newOffset = offset * ratio + (previousCentroid - screenCenter) * (1 - ratio) + panChange
|
||||||
)
|
|
||||||
val newOffset =
|
|
||||||
offset * ratio + (previousCentroid - screenCenter) * (1 - ratio) + panChange
|
|
||||||
|
|
||||||
val contentWidth = actualBitmapWidthPx * newScale
|
val contentWidth = actualBitmapWidthPx * newScale
|
||||||
val contentHeight = actualBitmapHeightPx * newScale
|
val contentHeight = actualBitmapHeightPx * newScale
|
||||||
val maxOffsetX =
|
val maxOffsetX = (contentWidth - size.width).coerceAtLeast(0f) / 2f
|
||||||
(contentWidth - size.width).coerceAtLeast(0f) / 2f
|
val maxOffsetY = (contentHeight - size.height).coerceAtLeast(0f) / 2f
|
||||||
val maxOffsetY =
|
|
||||||
(contentHeight - size.height).coerceAtLeast(0f) / 2f
|
Timber.tag("PdfZoomDebug").v("Scaling: RawZoom=$zoomChange, Scale=$oldScale->$newScale, Offset=$offset->$newOffset (Clamped Max: $maxOffsetX, $maxOffsetY)")
|
||||||
|
|
||||||
offset = Offset(
|
offset = Offset(
|
||||||
x = newOffset.x.coerceIn(
|
x = newOffset.x.coerceIn(-maxOffsetX, maxOffsetX),
|
||||||
-maxOffsetX, maxOffsetX
|
y = newOffset.y.coerceIn(-maxOffsetY, maxOffsetY)
|
||||||
), y = newOffset.y.coerceIn(
|
|
||||||
-maxOffsetY, maxOffsetY
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
scale = newScale
|
scale = newScale
|
||||||
|
|
||||||
if (scale < 1.05f) {
|
|
||||||
scale = 1f
|
|
||||||
offset = Offset.Zero
|
|
||||||
onScaleChanged(scale)
|
onScaleChanged(scale)
|
||||||
} else {
|
|
||||||
onScaleChanged(scale)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
event.changes.forEach {
|
event.changes.forEach {
|
||||||
if (it.positionChanged()) it.consume()
|
if (it.positionChanged()) it.consume()
|
||||||
|
|
@ -2881,7 +2798,17 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
} while (!canceled && event.changes.any { it.pressed })
|
} while (!canceled && event.changes.any { it.pressed })
|
||||||
|
|
||||||
if (mode == 1 && scale > 1f) {
|
if (scale > 1f && scale < 1.05f) {
|
||||||
|
coroutineScope.launch {
|
||||||
|
val startScale = scale
|
||||||
|
val startOffset = offset
|
||||||
|
Animatable(0f).animateTo(1f) {
|
||||||
|
scale = lerp(startScale, 1f, value)
|
||||||
|
offset = lerp(startOffset, Offset.Zero, value)
|
||||||
|
onScaleChanged(scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (mode == 1 && scale > 1f) {
|
||||||
val velocity = velocityTracker.calculateVelocity()
|
val velocity = velocityTracker.calculateVelocity()
|
||||||
val contentWidth = actualBitmapWidthPx * scale
|
val contentWidth = actualBitmapWidthPx * scale
|
||||||
val contentHeight = actualBitmapHeightPx * scale
|
val contentHeight = actualBitmapHeightPx * scale
|
||||||
|
|
@ -3052,6 +2979,13 @@ internal fun PdfPageComposable(
|
||||||
}
|
}
|
||||||
}, contentAlignment = Alignment.Center
|
}, contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
SideEffect {
|
||||||
|
if (effectiveScale > 1f) {
|
||||||
|
Timber.tag("PdfZoomDiagnostics").d(
|
||||||
|
"BoxWithConstraints Page $pageIndex: MaxW=$maxWidth, MaxH=$maxHeight"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
val imeInsets = WindowInsets.ime
|
val imeInsets = WindowInsets.ime
|
||||||
val screenHeight = constraints.maxHeight.toFloat()
|
val screenHeight = constraints.maxHeight.toFloat()
|
||||||
|
|
||||||
|
|
@ -3189,7 +3123,7 @@ internal fun PdfPageComposable(
|
||||||
"Highlighting (Pdfium): page $pageIndex, index: ${ttsHighlightData.startIndex}, len: ${ttsHighlightData.length}"
|
"Highlighting (Pdfium): page $pageIndex, index: ${ttsHighlightData.startIndex}, len: ${ttsHighlightData.length}"
|
||||||
)
|
)
|
||||||
rects = withContext(Dispatchers.IO) {
|
rects = withContext(Dispatchers.IO) {
|
||||||
pdfDocumentItem.openPage(pdfPageIndex).use { page ->
|
pdfDocumentItem.openPage(pdfPageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val pdfRectsF = textPage.textPageGetRectsForRanges(
|
val pdfRectsF = textPage.textPageGetRectsForRanges(
|
||||||
intArrayOf(
|
intArrayOf(
|
||||||
|
|
@ -3214,7 +3148,7 @@ internal fun PdfPageComposable(
|
||||||
emptyList()
|
emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} ?: emptyList()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3290,7 +3224,8 @@ internal fun PdfPageComposable(
|
||||||
currentContainerMaxHeight,
|
currentContainerMaxHeight,
|
||||||
density,
|
density,
|
||||||
virtualPage,
|
virtualPage,
|
||||||
isVisible
|
isVisible,
|
||||||
|
currentRenderedPageId
|
||||||
) {
|
) {
|
||||||
if (!isVisible && !isVerticalScroll) return@LaunchedEffect
|
if (!isVisible && !isVerticalScroll) return@LaunchedEffect
|
||||||
|
|
||||||
|
|
@ -3381,7 +3316,7 @@ internal fun PdfPageComposable(
|
||||||
)
|
)
|
||||||
return@withContext null
|
return@withContext null
|
||||||
}
|
}
|
||||||
val page = pdfDocumentItem.openPage(pdfPageIndex)
|
val page = pdfDocumentItem.openPage(pdfPageIndex) ?: return@withContext null
|
||||||
val rotation = page.getPageRotation()
|
val rotation = page.getPageRotation()
|
||||||
val screenDpi = (density.density * 160).roundToInt()
|
val screenDpi = (density.density * 160).roundToInt()
|
||||||
val originalWidthPdfUnits = page.getPageWidth(screenDpi)
|
val originalWidthPdfUnits = page.getPageWidth(screenDpi)
|
||||||
|
|
@ -3730,12 +3665,14 @@ internal fun PdfPageComposable(
|
||||||
try {
|
try {
|
||||||
val charCount = withContext(Dispatchers.IO) {
|
val charCount = withContext(Dispatchers.IO) {
|
||||||
page = pdfDocumentItem.openPage(pdfPageIndex)
|
page = pdfDocumentItem.openPage(pdfPageIndex)
|
||||||
textPage = page.openTextPage()
|
textPage = page?.openTextPage()
|
||||||
textPage.textPageCountChars()
|
textPage?.textPageCountChars() ?: 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (charCount > 0) {
|
if (charCount > 0) {
|
||||||
selectionCharRange.value = Pair(0, charCount)
|
selectionCharRange.value = Pair(0, charCount)
|
||||||
|
val tPage = page
|
||||||
|
val tTextPage = textPage
|
||||||
updateSelectionVisuals(
|
updateSelectionVisuals(
|
||||||
pdfDocumentItem,
|
pdfDocumentItem,
|
||||||
pdfPageIndex,
|
pdfPageIndex,
|
||||||
|
|
@ -3743,12 +3680,12 @@ internal fun PdfPageComposable(
|
||||||
actualBitmapWidthPx,
|
actualBitmapWidthPx,
|
||||||
actualBitmapHeightPx,
|
actualBitmapHeightPx,
|
||||||
currentPageRotation,
|
currentPageRotation,
|
||||||
providedPage = page,
|
providedPage = tPage,
|
||||||
providedTextPage = textPage
|
providedTextPage = tTextPage
|
||||||
)
|
)
|
||||||
if (selectedWordScreenRects.isNotEmpty()) {
|
if (selectedWordScreenRects.isNotEmpty()) {
|
||||||
val fullText = withContext(Dispatchers.IO) {
|
val fullText = withContext(Dispatchers.IO) {
|
||||||
textPage!!.textPageGetText(0, charCount)
|
tTextPage?.textPageGetText(0, charCount)
|
||||||
}
|
}
|
||||||
if (!fullText.isNullOrBlank()) {
|
if (!fullText.isNullOrBlank()) {
|
||||||
val combinedRect = Rect(selectedWordScreenRects.first())
|
val combinedRect = Rect(selectedWordScreenRects.first())
|
||||||
|
|
@ -3904,23 +3841,19 @@ private fun PdfBitmapLayer(
|
||||||
colorFilter: ColorFilter? = null,
|
colorFilter: ColorFilter? = null,
|
||||||
isDarkMode: Boolean = false
|
isDarkMode: Boolean = false
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
|
||||||
Timber.tag("PdfDrawPerf")
|
|
||||||
.v("BITMAP LAYER: SideEffect (Scale: $effectiveScale, Tiles: ${tiles.size})")
|
|
||||||
}
|
|
||||||
Canvas(modifier = Modifier
|
Canvas(modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer()) {
|
.graphicsLayer()) {
|
||||||
val drawStart = System.nanoTime()
|
|
||||||
Timber.tag("PdfDrawPerf").v("BITMAP LAYER: Canvas Draw Start (Tiles: ${tiles.size})")
|
|
||||||
translate(left = centeringOffsetX, top = centeringOffsetY) {
|
translate(left = centeringOffsetX, top = centeringOffsetY) {
|
||||||
|
// THIS is the fix: Hard clip to the target bounds so edge tiles can't bleed out.
|
||||||
|
clipRect(left = 0f, top = 0f, right = targetWidth.toFloat(), bottom = targetHeight.toFloat()) {
|
||||||
if (bitmapState != null && !bitmapState.isRecycled) {
|
if (bitmapState != null && !bitmapState.isRecycled) {
|
||||||
val dstW = if (targetWidth > 0) targetWidth else bitmapState.width
|
val dstW = if (targetWidth > 0) targetWidth else bitmapState.width
|
||||||
val dstH = if (targetHeight > 0) targetHeight else bitmapState.height
|
val dstH = if (targetHeight > 0) targetHeight else bitmapState.height
|
||||||
val srcSize = IntSize(bitmapState.width, bitmapState.height)
|
val srcSize = IntSize(bitmapState.width, bitmapState.height)
|
||||||
val dstSize = IntSize(dstW, dstH)
|
val dstSize = IntSize(dstW, dstH)
|
||||||
|
|
||||||
// 1. Draw Base Bitmap (with Dark Mode filter if active)
|
// 1. Draw Base Bitmap
|
||||||
drawImage(
|
drawImage(
|
||||||
image = bitmapState.asImageBitmap(),
|
image = bitmapState.asImageBitmap(),
|
||||||
srcOffset = IntOffset.Zero,
|
srcOffset = IntOffset.Zero,
|
||||||
|
|
@ -3930,7 +3863,7 @@ private fun PdfBitmapLayer(
|
||||||
colorFilter = colorFilter
|
colorFilter = colorFilter
|
||||||
)
|
)
|
||||||
|
|
||||||
// 3. Draw Tiles
|
// 2. Draw High-Res Tiles
|
||||||
if (effectiveScale > 1f) {
|
if (effectiveScale > 1f) {
|
||||||
tiles.forEach { tile ->
|
tiles.forEach { tile ->
|
||||||
if (!tile.bitmap.isRecycled) {
|
if (!tile.bitmap.isRecycled) {
|
||||||
|
|
@ -3949,8 +3882,7 @@ private fun PdfBitmapLayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val drawTime = (System.nanoTime() - drawStart) / 1_000_000f
|
}
|
||||||
Timber.tag("PdfDrawPerf").v("BITMAP LAYER: Canvas draw finished in ${drawTime}ms")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4337,6 +4269,7 @@ internal object PdfAnnotationRenderHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("SameParameterValue")
|
||||||
@Composable
|
@Composable
|
||||||
private fun PdfAnnotationLayer(
|
private fun PdfAnnotationLayer(
|
||||||
actualBitmapWidthPx: Int,
|
actualBitmapWidthPx: Int,
|
||||||
|
|
@ -4454,13 +4387,6 @@ private fun PdfAnnotationLayer(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PdfPageStaticLayer(data: PageStaticData) {
|
private fun PdfPageStaticLayer(data: PageStaticData) {
|
||||||
SideEffect {
|
|
||||||
Timber.tag("PdfDrawPerf").v(
|
|
||||||
"STATIC LAYER: Composition (Should not happen during draw). DataHash: ${data.hashCode()}"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Timber.tag("PdfDrawPerf").v("STATIC LAYER: Recomposing")
|
|
||||||
|
|
||||||
PdfBitmapLayer(
|
PdfBitmapLayer(
|
||||||
bitmapState = data.bitmap.item,
|
bitmapState = data.bitmap.item,
|
||||||
tiles = data.tiles.item,
|
tiles = data.tiles.item,
|
||||||
|
|
@ -4591,15 +4517,11 @@ private fun PdfPageRenderer(
|
||||||
onHighlightDelete: (String) -> Unit,
|
onHighlightDelete: (String) -> Unit,
|
||||||
onTts: (Int, Int) -> Unit,
|
onTts: (Int, Int) -> Unit,
|
||||||
) {
|
) {
|
||||||
SideEffect {
|
|
||||||
Timber.tag("PdfPerf").v("PAGE_RENDERER: Recomposing Page ${selectionData.pageIndex}. DraggingHandle=${activeDraggingHandle != null}")
|
|
||||||
}
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.graphicsLayer {
|
.graphicsLayer {
|
||||||
Timber.tag("PdfPerf").v("GraphicsLayer Update: Scale=$scale, Offset=$offset")
|
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
translationX = offset.x
|
translationX = offset.x
|
||||||
|
|
@ -4607,9 +4529,7 @@ private fun PdfPageRenderer(
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
// Layer 1: The Heavy Bitmap
|
// Layer 1: The Heavy Bitmap
|
||||||
Box(modifier = Modifier
|
Box(modifier = Modifier.fillMaxSize().graphicsLayer()) {
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer()) {
|
|
||||||
PdfPageStaticLayer(data = staticData)
|
PdfPageStaticLayer(data = staticData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -4636,11 +4556,9 @@ private fun PdfPageRenderer(
|
||||||
selectionHighlightColor = selectionData.selectionHighlightColor
|
selectionHighlightColor = selectionData.selectionHighlightColor
|
||||||
)
|
)
|
||||||
|
|
||||||
// Layer 3: Annotations (Drawing)
|
// Layer 3: Annotations & Text
|
||||||
if (staticData.targetWidth > 0 && staticData.targetHeight > 0) {
|
if (staticData.targetWidth > 0 && staticData.targetHeight > 0) {
|
||||||
Box(modifier = Modifier
|
Box(modifier = Modifier.fillMaxSize().graphicsLayer()) {
|
||||||
.fillMaxSize()
|
|
||||||
.graphicsLayer()) {
|
|
||||||
PdfAnnotationLayer(
|
PdfAnnotationLayer(
|
||||||
actualBitmapWidthPx = staticData.targetWidth,
|
actualBitmapWidthPx = staticData.targetWidth,
|
||||||
actualBitmapHeightPx = staticData.targetHeight,
|
actualBitmapHeightPx = staticData.targetHeight,
|
||||||
|
|
@ -4653,19 +4571,6 @@ private fun PdfPageRenderer(
|
||||||
}
|
}
|
||||||
|
|
||||||
if (richTextController != null) {
|
if (richTextController != null) {
|
||||||
val density = LocalDensity.current
|
|
||||||
val textMeasurer = androidx.compose.ui.text.rememberTextMeasurer()
|
|
||||||
val targetW = staticData.targetWidth.toFloat()
|
|
||||||
val targetH = staticData.targetHeight.toFloat()
|
|
||||||
|
|
||||||
LaunchedEffect(targetW, targetH, density) {
|
|
||||||
if (targetW > 0 && targetH > 0) {
|
|
||||||
richTextController.updateLayoutConfig(
|
|
||||||
targetW, targetH, density, textMeasurer
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val isEditable = isEditMode && selectedTool == InkType.TEXT
|
val isEditable = isEditMode && selectedTool == InkType.TEXT
|
||||||
val hasContent = richTextController.pageLayouts.any {
|
val hasContent = richTextController.pageLayouts.any {
|
||||||
it.pageIndex == selectionData.pageIndex
|
it.pageIndex == selectionData.pageIndex
|
||||||
|
|
@ -4686,7 +4591,6 @@ private fun PdfPageRenderer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text Boxes
|
|
||||||
textBoxes.forEach { box ->
|
textBoxes.forEach { box ->
|
||||||
val isDraggingThisBox = (box.id == draggingBoxId)
|
val isDraggingThisBox = (box.id == draggingBoxId)
|
||||||
val boxAlpha = if (isDraggingThisBox) 0f else 1f
|
val boxAlpha = if (isDraggingThisBox) 0f else 1f
|
||||||
|
|
@ -4753,6 +4657,7 @@ private fun PdfPageRenderer(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Layer 4: Page Number Indicator
|
// Layer 4: Page Number Indicator
|
||||||
if (totalPages > 0) {
|
if (totalPages > 0) {
|
||||||
|
|
@ -4794,7 +4699,6 @@ private fun PdfPageRenderer(
|
||||||
onCanvasSizeChanged(size.width, size.height)
|
onCanvasSizeChanged(size.width, size.height)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
val teardropPainter = painterResource(id = R.drawable.teardrop)
|
val teardropPainter = painterResource(id = R.drawable.teardrop)
|
||||||
|
|
||||||
|
|
@ -4932,7 +4836,6 @@ private fun PdfPageRenderer(
|
||||||
): IntOffset {
|
): IntOffset {
|
||||||
val coords = layoutCoordinates ?: return IntOffset.Zero
|
val coords = layoutCoordinates ?: return IntOffset.Zero
|
||||||
|
|
||||||
// Map the bitmap-space anchor (the icon) to window-space
|
|
||||||
val topLeftLocal = contentToScreenCoordinates(Offset(
|
val topLeftLocal = contentToScreenCoordinates(Offset(
|
||||||
menuState.anchorRect.left.toFloat(),
|
menuState.anchorRect.left.toFloat(),
|
||||||
menuState.anchorRect.top.toFloat()))
|
menuState.anchorRect.top.toFloat()))
|
||||||
|
|
@ -4944,14 +4847,12 @@ private fun PdfPageRenderer(
|
||||||
val bottomRightWindow = coords.localToWindow(bottomRightLocal)
|
val bottomRightWindow = coords.localToWindow(bottomRightLocal)
|
||||||
|
|
||||||
val windowCenterX = (topLeftWindow.x + bottomRightWindow.x) / 2
|
val windowCenterX = (topLeftWindow.x + bottomRightWindow.x) / 2
|
||||||
val gapPx = with(density) { 16.dp.toPx() } // Increased gap
|
val gapPx = with(density) { 16.dp.toPx() }
|
||||||
|
|
||||||
// Try placing ABOVE the icon first
|
|
||||||
var yInWindow = (topLeftWindow.y - popupContentSize.height - gapPx).toInt()
|
var yInWindow = (topLeftWindow.y - popupContentSize.height - gapPx).toInt()
|
||||||
|
|
||||||
if (yInWindow < 0) {
|
if (yInWindow < 0) {
|
||||||
yInWindow = (bottomRightWindow.y + gapPx).toInt()
|
yInWindow = (bottomRightWindow.y + gapPx).toInt()
|
||||||
// Ensure it doesn't get pushed out of the bottom boundary either
|
|
||||||
if (yInWindow + popupContentSize.height > windowSize.height) {
|
if (yInWindow + popupContentSize.height > windowSize.height) {
|
||||||
yInWindow = windowSize.height - popupContentSize.height - gapPx.toInt()
|
yInWindow = windowSize.height - popupContentSize.height - gapPx.toInt()
|
||||||
}
|
}
|
||||||
|
|
@ -4975,11 +4876,9 @@ private fun PdfPageRenderer(
|
||||||
onSearch = onSearch,
|
onSearch = onSearch,
|
||||||
onSelectAll = onSelectAll,
|
onSelectAll = onSelectAll,
|
||||||
onColorSelected = { color ->
|
onColorSelected = { color ->
|
||||||
Timber.tag("PdfHighlightDebug").d("PdfSelectionMenuPopup onColorSelected: $color, isExisting=${menuState.isExistingHighlight}")
|
|
||||||
if (menuState.isExistingHighlight && menuState.highlightId != null) {
|
if (menuState.isExistingHighlight && menuState.highlightId != null) {
|
||||||
onHighlightUpdate(menuState.highlightId, color)
|
onHighlightUpdate(menuState.highlightId, color)
|
||||||
} else {
|
} else {
|
||||||
Timber.tag("PdfHighlightDebug").d("Calling onHighlightAdd for page ${selectionData.pageIndex}")
|
|
||||||
onHighlightAdd(
|
onHighlightAdd(
|
||||||
selectionData.pageIndex, menuState.charRange, menuState.selectedText,
|
selectionData.pageIndex, menuState.charRange, menuState.selectedText,
|
||||||
color
|
color
|
||||||
|
|
@ -5190,3 +5089,32 @@ fun PdfRichTextLayer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getNativePointer(obj: Any): Long {
|
||||||
|
val priorityFields = listOf("pagePtr", "mNativePage", "page")
|
||||||
|
|
||||||
|
for (name in priorityFields) {
|
||||||
|
try {
|
||||||
|
val field = obj.javaClass.getDeclaredField(name)
|
||||||
|
field.isAccessible = true
|
||||||
|
val value = field.get(obj)
|
||||||
|
if (value is Long && value != 0L) return value
|
||||||
|
if (value != null && value !is Long) {
|
||||||
|
val nestedPtr = getNativePointer(value)
|
||||||
|
if (nestedPtr != 0L) return nestedPtr
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (field in obj.javaClass.declaredFields) {
|
||||||
|
if (field.type == Long::class.java || field.type == Long::class.javaPrimitiveType) {
|
||||||
|
field.isAccessible = true
|
||||||
|
val value = field.get(obj) as Long
|
||||||
|
if (value > 0xFFFFFFFFL) return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
|
|
@ -131,13 +131,12 @@ object PdfToHtmlGenerator {
|
||||||
headerFooterStrings: Set<String>
|
headerFooterStrings: Set<String>
|
||||||
): String {
|
): String {
|
||||||
return try {
|
return try {
|
||||||
doc.openPage(pageIdx).use { page ->
|
doc.openPage(pageIdx)?.use { page ->
|
||||||
if (page == null) return buildEmptyPageSection(pageNumber)
|
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val charCount = textPage.textPageCountChars()
|
val charCount = textPage.textPageCountChars()
|
||||||
|
|
||||||
val pagePtr = page.page.pagePtr
|
val pagePtr = getNativePointer(page)
|
||||||
val textPagePtr = textPage.page.pagePtr
|
val textPagePtr = getNativePointer(textPage)
|
||||||
|
|
||||||
val imageElements = mutableListOf<ImageElement>()
|
val imageElements = mutableListOf<ImageElement>()
|
||||||
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
|
val objCount = NativePdfiumBridge.getPageObjectCount(pagePtr)
|
||||||
|
|
@ -178,7 +177,7 @@ object PdfToHtmlGenerator {
|
||||||
val flags: IntArray?
|
val flags: IntArray?
|
||||||
val charBoxes: FloatArray?
|
val charBoxes: FloatArray?
|
||||||
|
|
||||||
synchronized(PdfiumCore.lock) {
|
synchronized(NativePdfiumBridge::class.java) {
|
||||||
sizes = NativePdfiumBridge.getPageFontSizes(textPagePtr, actualCount)
|
sizes = NativePdfiumBridge.getPageFontSizes(textPagePtr, actualCount)
|
||||||
weights = NativePdfiumBridge.getPageFontWeights(textPagePtr, actualCount)
|
weights = NativePdfiumBridge.getPageFontWeights(textPagePtr, actualCount)
|
||||||
flags = NativePdfiumBridge.getPageFontFlags(textPagePtr, actualCount)
|
flags = NativePdfiumBridge.getPageFontFlags(textPagePtr, actualCount)
|
||||||
|
|
@ -293,8 +292,8 @@ object PdfToHtmlGenerator {
|
||||||
}
|
}
|
||||||
|
|
||||||
buildPageHtml(pageNumber, finalElements, headerFooterStrings)
|
buildPageHtml(pageNumber, finalElements, headerFooterStrings)
|
||||||
}
|
} ?: buildEmptyPageSection(pageNumber)
|
||||||
}
|
} ?: buildEmptyPageSection(pageNumber)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.tag(TAG).w(e, "Error extracting page $pageIdx")
|
Timber.tag(TAG).w(e, "Error extracting page $pageIdx")
|
||||||
buildEmptyPageSection(pageNumber)
|
buildEmptyPageSection(pageNumber)
|
||||||
|
|
@ -483,7 +482,7 @@ object PdfToHtmlGenerator {
|
||||||
|
|
||||||
for (pageIdx in samplePages) {
|
for (pageIdx in samplePages) {
|
||||||
try {
|
try {
|
||||||
doc.openPage(pageIdx).use { page ->
|
doc.openPage(pageIdx)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val charCount = textPage.textPageCountChars()
|
val charCount = textPage.textPageCountChars()
|
||||||
if (charCount <= 0) return@use
|
if (charCount <= 0) return@use
|
||||||
|
|
@ -511,4 +510,21 @@ object PdfToHtmlGenerator {
|
||||||
.replace(">", ">")
|
.replace(">", ">")
|
||||||
.replace("\"", """)
|
.replace("\"", """)
|
||||||
.replace("'", "'")
|
.replace("'", "'")
|
||||||
|
|
||||||
|
private fun getNativePointer(obj: Any): Long {
|
||||||
|
val priorityFields = listOf("pagePtr", "mNativePage", "page")
|
||||||
|
for (name in priorityFields) {
|
||||||
|
try {
|
||||||
|
val field = obj.javaClass.getDeclaredField(name)
|
||||||
|
field.isAccessible = true
|
||||||
|
val value = field.get(obj)
|
||||||
|
if (value is Long && value != 0L) return value
|
||||||
|
if (value != null && value !is Long) {
|
||||||
|
val nestedPtr = getNativePointer(value)
|
||||||
|
if (nestedPtr != 0L) return nestedPtr
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
}
|
||||||
|
return 0L
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -358,8 +358,12 @@ internal fun PdfVerticalReader(
|
||||||
val constrainedX = targetPanX.coerceIn(minPanX, maxPanX)
|
val constrainedX = targetPanX.coerceIn(minPanX, maxPanX)
|
||||||
val constrainedY = targetPanY.coerceIn(minPanY, headerHeightPx)
|
val constrainedY = targetPanY.coerceIn(minPanY, headerHeightPx)
|
||||||
|
|
||||||
// DEDICATED LOG
|
if (constrainedZoom > 1.01f) {
|
||||||
Timber.tag("PdfZoomDebug").v("Clamp Internal: Zoom=$constrainedZoom, PanBoundsX=[$minPanX, $maxPanX], PanBoundsY=[$minPanY, $headerHeightPx]")
|
Timber.tag("PdfZoomIssue").v(
|
||||||
|
"Clamp: Zoom=$constrainedZoom, targetY=$targetPanY, finalY=$constrainedY, " +
|
||||||
|
"boundsY=[$minPanY, $headerHeightPx], zoomedHeight=$zoomedDocHeight"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return Triple(constrainedZoom, constrainedX, constrainedY)
|
return Triple(constrainedZoom, constrainedX, constrainedY)
|
||||||
}
|
}
|
||||||
|
|
@ -370,10 +374,14 @@ internal fun PdfVerticalReader(
|
||||||
return clampValues(targetZoom, targetPanX, targetPanY)
|
return clampValues(targetZoom, targetPanX, targetPanY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isFlinging by remember { mutableStateOf(false) }
|
||||||
|
var isFastFlinging by remember { mutableStateOf(false) }
|
||||||
|
var isInteracting by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(
|
LaunchedEffect(
|
||||||
totalDocHeight, screenHeight, headerHeightPx, footerHeightPx, zoomAnimatable.value
|
totalDocHeight, screenHeight, headerHeightPx, footerHeightPx, zoomAnimatable.value, isInteracting, isFlinging
|
||||||
) {
|
) {
|
||||||
if (zoomAnimatable.isRunning || panXAnimatable.isRunning || panYAnimatable.isRunning) {
|
if (zoomAnimatable.isRunning || panXAnimatable.isRunning || panYAnimatable.isRunning || isInteracting || isFlinging) {
|
||||||
return@LaunchedEffect
|
return@LaunchedEffect
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,10 +454,6 @@ internal fun PdfVerticalReader(
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectionClearTrigger by remember { mutableLongStateOf(0L) }
|
var selectionClearTrigger by remember { mutableLongStateOf(0L) }
|
||||||
|
|
||||||
var isFlinging by remember { mutableStateOf(false) }
|
|
||||||
var isFastFlinging by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
var draggingBoxId by remember { mutableStateOf<String?>(null) }
|
var draggingBoxId by remember { mutableStateOf<String?>(null) }
|
||||||
var draggingBoxOffset by remember { mutableStateOf(Offset.Zero) }
|
var draggingBoxOffset by remember { mutableStateOf(Offset.Zero) }
|
||||||
var draggingBoxSize by remember { mutableStateOf(Size.Zero) }
|
var draggingBoxSize by remember { mutableStateOf(Size.Zero) }
|
||||||
|
|
@ -549,7 +553,6 @@ internal fun PdfVerticalReader(
|
||||||
}
|
}
|
||||||
|
|
||||||
var highResScale by remember { mutableFloatStateOf(1f) }
|
var highResScale by remember { mutableFloatStateOf(1f) }
|
||||||
var isInteracting by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(isInteracting) {
|
LaunchedEffect(isInteracting) {
|
||||||
if (isInteracting && isAutoScrollPlaying) {
|
if (isInteracting && isAutoScrollPlaying) {
|
||||||
|
|
@ -617,8 +620,12 @@ internal fun PdfVerticalReader(
|
||||||
imeBottom,
|
imeBottom,
|
||||||
isEditMode,
|
isEditMode,
|
||||||
selectedTool,
|
selectedTool,
|
||||||
zoomAnimatable.value
|
zoomAnimatable.value,
|
||||||
|
isInteracting,
|
||||||
|
isFlinging
|
||||||
) {
|
) {
|
||||||
|
if (isInteracting || isFlinging) return@LaunchedEffect
|
||||||
|
|
||||||
val currentZoom = zoomAnimatable.value
|
val currentZoom = zoomAnimatable.value
|
||||||
val zoomedDocHeight = totalDocHeight * currentZoom
|
val zoomedDocHeight = totalDocHeight * currentZoom
|
||||||
|
|
||||||
|
|
@ -897,6 +904,7 @@ internal fun PdfVerticalReader(
|
||||||
)
|
)
|
||||||
|
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
|
isInteracting = true
|
||||||
|
|
||||||
Timber.tag("PointerTypeDebug").d("VerticalReader: Input Type detected: ${down.type}")
|
Timber.tag("PointerTypeDebug").d("VerticalReader: Input Type detected: ${down.type}")
|
||||||
|
|
||||||
|
|
@ -1015,31 +1023,35 @@ internal fun PdfVerticalReader(
|
||||||
(!isEditMode || selectedTool == InkType.TEXT || isMultiTouch || (isStylusOnlyMode && isTouchInput))
|
(!isEditMode || selectedTool == InkType.TEXT || isMultiTouch || (isStylusOnlyMode && isTouchInput))
|
||||||
|
|
||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
if (!isInteracting) {
|
|
||||||
Timber.tag("PdfTouchDebug").i(
|
|
||||||
"VerticalReader: Taking control."
|
|
||||||
)
|
|
||||||
isInteracting = true
|
|
||||||
}
|
|
||||||
|
|
||||||
panLocked = true
|
panLocked = true
|
||||||
if (zoomChange != 1f || panChange != Offset.Zero) {
|
if (zoomChange != 1f || panChange != Offset.Zero) {
|
||||||
|
|
||||||
var effectiveZoomChange = zoomChange
|
var effectiveZoomChange = zoomChange
|
||||||
|
if (gestureDisambiguationMode == 1) effectiveZoomChange = 1f
|
||||||
if (gestureDisambiguationMode == 1) {
|
|
||||||
effectiveZoomChange = 1f
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldZoom = accumulatedZoom
|
val oldZoom = accumulatedZoom
|
||||||
val rawTargetZoom = oldZoom * effectiveZoomChange
|
val rawTargetZoom = oldZoom * effectiveZoomChange
|
||||||
val constrainedZoom = rawTargetZoom.coerceIn(1f, 5f)
|
val constrainedZoom = rawTargetZoom.coerceIn(1f, 5f)
|
||||||
val actualZoomFactor = if (oldZoom == 0f) 1f
|
|
||||||
else constrainedZoom / oldZoom
|
val prevCentroid = centroid - panChange
|
||||||
val rawNewPanX =
|
val contentPivotX = (prevCentroid.x - accumulatedPanX) / oldZoom
|
||||||
(accumulatedPanX + panChange.x) - (centroid.x - accumulatedPanX) * (actualZoomFactor - 1)
|
val contentPivotY = (prevCentroid.y - accumulatedPanY) / oldZoom
|
||||||
val rawNewPanY =
|
|
||||||
(accumulatedPanY + panChange.y) - (centroid.y - accumulatedPanY) * (actualZoomFactor - 1)
|
Timber.tag("PdfZoomIssue").v(
|
||||||
|
"PivotCalc: ScreenCentroidY=${centroid.y}, DocumentPanY=$accumulatedPanY, " +
|
||||||
|
"CalculatedContentPivotY=$contentPivotY"
|
||||||
|
)
|
||||||
|
|
||||||
|
val rawNewPanX = centroid.x - (contentPivotX * constrainedZoom)
|
||||||
|
val rawNewPanY = centroid.y - (contentPivotY * constrainedZoom)
|
||||||
|
|
||||||
|
if (effectiveZoomChange > 1.0f) {
|
||||||
|
Timber.tag("PdfZoomIssue").d(
|
||||||
|
"PinchIn FIXED: ZoomFactor=$effectiveZoomChange, CentroidY=${centroid.y}, " +
|
||||||
|
"OldPanY=$accumulatedPanY, ResultRawPanY=$rawNewPanY"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
val (finalZoom, finalX, finalY) = clampCamera(
|
val (finalZoom, finalX, finalY) = clampCamera(
|
||||||
constrainedZoom, rawNewPanX, rawNewPanY
|
constrainedZoom, rawNewPanX, rawNewPanY
|
||||||
)
|
)
|
||||||
|
|
@ -1064,11 +1076,8 @@ internal fun PdfVerticalReader(
|
||||||
|
|
||||||
if (event.changes.isNotEmpty()) {
|
if (event.changes.isNotEmpty()) {
|
||||||
velocityTrackerAccumulator += panChange
|
velocityTrackerAccumulator += panChange
|
||||||
|
|
||||||
val time = event.changes[0].uptimeMillis
|
val time = event.changes[0].uptimeMillis
|
||||||
tracker.addPosition(
|
tracker.addPosition(time, velocityTrackerAccumulator)
|
||||||
time, velocityTrackerAccumulator
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,14 @@ import android.graphics.Bitmap
|
||||||
import android.graphics.RectF
|
import android.graphics.RectF
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.CancellationSignal
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
|
import android.print.PageRange
|
||||||
|
import android.print.PrintAttributes
|
||||||
|
import android.print.PrintDocumentAdapter
|
||||||
|
import android.print.PrintDocumentInfo
|
||||||
|
import android.print.PrintManager
|
||||||
import android.provider.OpenableColumns
|
import android.provider.OpenableColumns
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
|
|
@ -43,6 +50,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.Animatable
|
import androidx.compose.animation.core.Animatable
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
|
@ -57,12 +65,16 @@ import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
import androidx.compose.foundation.gestures.awaitEachGesture
|
import androidx.compose.foundation.gestures.awaitEachGesture
|
||||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||||
import androidx.compose.foundation.gestures.detectDragGestures
|
import androidx.compose.foundation.gestures.detectDragGestures
|
||||||
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
|
@ -74,6 +86,7 @@ import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.ime
|
import androidx.compose.foundation.layout.ime
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
|
@ -83,7 +96,10 @@ import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
|
@ -94,6 +110,7 @@ import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
|
||||||
import androidx.compose.material.icons.filled.ArrowDownward
|
import androidx.compose.material.icons.filled.ArrowDownward
|
||||||
import androidx.compose.material.icons.filled.ArrowUpward
|
import androidx.compose.material.icons.filled.ArrowUpward
|
||||||
import androidx.compose.material.icons.filled.Brush
|
import androidx.compose.material.icons.filled.Brush
|
||||||
|
|
@ -129,7 +146,6 @@ import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.ListItem
|
import androidx.compose.material3.ListItem
|
||||||
import androidx.compose.material3.ListItemDefaults
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.MenuDefaults
|
import androidx.compose.material3.MenuDefaults
|
||||||
import androidx.compose.material3.ModalDrawerSheet
|
import androidx.compose.material3.ModalDrawerSheet
|
||||||
|
|
@ -245,8 +261,8 @@ import com.aryan.reader.R
|
||||||
import com.aryan.reader.SearchResult
|
import com.aryan.reader.SearchResult
|
||||||
import com.aryan.reader.SearchTopBar
|
import com.aryan.reader.SearchTopBar
|
||||||
import com.aryan.reader.SummarizationPopup
|
import com.aryan.reader.SummarizationPopup
|
||||||
import com.aryan.reader.TooltipIconButton
|
|
||||||
import com.aryan.reader.SummarizationResult
|
import com.aryan.reader.SummarizationResult
|
||||||
|
import com.aryan.reader.TooltipIconButton
|
||||||
import com.aryan.reader.TtsSettingsSheet
|
import com.aryan.reader.TtsSettingsSheet
|
||||||
import com.aryan.reader.countWords
|
import com.aryan.reader.countWords
|
||||||
import com.aryan.reader.epubreader.AutoScrollControls
|
import com.aryan.reader.epubreader.AutoScrollControls
|
||||||
|
|
@ -270,8 +286,7 @@ import com.aryan.reader.tts.TtsPlaybackManager
|
||||||
import com.aryan.reader.tts.loadTtsMode
|
import com.aryan.reader.tts.loadTtsMode
|
||||||
import com.aryan.reader.tts.rememberTtsController
|
import com.aryan.reader.tts.rememberTtsController
|
||||||
import com.aryan.reader.tts.splitTextIntoChunks
|
import com.aryan.reader.tts.splitTextIntoChunks
|
||||||
import io.legere.pdfiumandroid.PdfDocument
|
import io.legere.pdfiumandroid.api.Bookmark
|
||||||
import io.legere.pdfiumandroid.PdfPasswordException
|
|
||||||
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
|
||||||
import io.legere.pdfiumandroid.suspend.PdfPageKt
|
import io.legere.pdfiumandroid.suspend.PdfPageKt
|
||||||
import io.legere.pdfiumandroid.suspend.PdfTextPageKt
|
import io.legere.pdfiumandroid.suspend.PdfTextPageKt
|
||||||
|
|
@ -292,6 +307,8 @@ import org.json.JSONObject
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
import java.net.URL
|
import java.net.URL
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
@ -574,8 +591,153 @@ private fun loadDisplayMode(context: Context): DisplayMode {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val MAX_FIXED_RECURSION = 128
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patches the library bug where siblings are truncated due to depth-state leakage.
|
||||||
|
*/
|
||||||
|
suspend fun PdfDocumentKt.getFixedTableOfContents(): List<Bookmark> {
|
||||||
|
val tag = "PdfTocFix"
|
||||||
|
Timber.tag(tag).i("Starting Pure Reflection Traversal...")
|
||||||
|
|
||||||
|
return try {
|
||||||
|
// 1. Get the 'document' field (PdfDocumentU) from PdfDocumentKt
|
||||||
|
val documentField = PdfDocumentKt::class.java.getDeclaredField("document").apply { isAccessible = true }
|
||||||
|
val docUInstance = documentField.get(this) ?: return getTableOfContents()
|
||||||
|
|
||||||
|
// 2. Get the 'nativeDocument' field from PdfDocumentU
|
||||||
|
val nativeDocField = docUInstance.javaClass.getDeclaredField("nativeDocument").apply { isAccessible = true }
|
||||||
|
val nativeDocInstance = nativeDocField.get(docUInstance) ?: return getTableOfContents()
|
||||||
|
|
||||||
|
// 3. Get the native pointer (long) from PdfDocumentU
|
||||||
|
val ptrField = docUInstance.javaClass.getDeclaredField("mNativeDocPtr").apply { isAccessible = true }
|
||||||
|
val mNativeDocPtr = ptrField.get(docUInstance) as Long
|
||||||
|
|
||||||
|
// 4. Look up native methods using primitive 'long' types (mandatory for JNI)
|
||||||
|
val nClass = nativeDocInstance.javaClass
|
||||||
|
val lp = Long::class.javaPrimitiveType!! // Shorthand for 'long'
|
||||||
|
|
||||||
|
val getTitleM = nClass.getMethod("getBookmarkTitle", lp)
|
||||||
|
val getDestIdxM = nClass.getMethod("getBookmarkDestIndex", lp, lp)
|
||||||
|
val getFirstChildM = nClass.getMethod("getFirstChildBookmark", lp, lp)
|
||||||
|
val getSiblingM = nClass.getMethod("getSiblingBookmark", lp, lp)
|
||||||
|
|
||||||
|
val topLevel = mutableListOf<Bookmark>()
|
||||||
|
val visited = mutableSetOf<Long>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Corrected traversal: Iterative for siblings, recursive for children.
|
||||||
|
*/
|
||||||
|
fun walk(parentList: MutableList<Bookmark>, startPtr: Long, level: Int) {
|
||||||
|
var currentPtr = startPtr
|
||||||
|
var itemIndex = 0
|
||||||
|
|
||||||
|
while (currentPtr != 0L) {
|
||||||
|
if (visited.contains(currentPtr)) break
|
||||||
|
visited.add(currentPtr)
|
||||||
|
|
||||||
|
val title = getTitleM.invoke(nativeDocInstance, currentPtr) as? String ?: "Untitled"
|
||||||
|
val pageIdx = getDestIdxM.invoke(nativeDocInstance, mNativeDocPtr, currentPtr) as Long
|
||||||
|
|
||||||
|
Timber.tag(tag).v("Lvl $level | Item $itemIndex | Ptr: 0x${java.lang.Long.toHexString(currentPtr)} | $title")
|
||||||
|
|
||||||
|
val bookmark = Bookmark().apply {
|
||||||
|
this.mNativePtr = currentPtr
|
||||||
|
this.title = title
|
||||||
|
this.pageIdx = pageIdx
|
||||||
|
}
|
||||||
|
parentList.add(bookmark)
|
||||||
|
|
||||||
|
// Recursive dive into children
|
||||||
|
val firstChild = getFirstChildM.invoke(nativeDocInstance, mNativeDocPtr, currentPtr) as Long
|
||||||
|
if (firstChild != 0L && level < MAX_FIXED_RECURSION) {
|
||||||
|
walk(bookmark.children, firstChild, level + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterative move to next sibling
|
||||||
|
currentPtr = getSiblingM.invoke(nativeDocInstance, mNativeDocPtr, currentPtr) as Long
|
||||||
|
itemIndex++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Start from the root (Pass 0L as primitive long)
|
||||||
|
val firstRoot = getFirstChildM.invoke(nativeDocInstance, mNativeDocPtr, 0L) as Long
|
||||||
|
if (firstRoot != 0L) {
|
||||||
|
walk(topLevel, firstRoot, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (topLevel.isEmpty()) {
|
||||||
|
Timber.tag(tag).w("No items found, falling back to library.")
|
||||||
|
getTableOfContents()
|
||||||
|
} else {
|
||||||
|
Timber.tag(tag).i("TOC Successfully Patched! Nodes: ${visited.size}")
|
||||||
|
topLevel
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag(tag).e(e, "Reflection traversal critical error.")
|
||||||
|
this.getTableOfContents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal data class PdfBookmark(val pageIndex: Int, val title: String, val totalPages: Int)
|
internal data class PdfBookmark(val pageIndex: Int, val title: String, val totalPages: Int)
|
||||||
|
|
||||||
|
class PdfPrintDocumentAdapter(
|
||||||
|
private val context: Context,
|
||||||
|
private val pdfUri: Uri,
|
||||||
|
private val fileName: String
|
||||||
|
) : PrintDocumentAdapter() {
|
||||||
|
|
||||||
|
override fun onLayout(
|
||||||
|
oldAttributes: PrintAttributes?,
|
||||||
|
newAttributes: PrintAttributes?,
|
||||||
|
cancellationSignal: CancellationSignal?,
|
||||||
|
callback: LayoutResultCallback?,
|
||||||
|
extras: Bundle?
|
||||||
|
) {
|
||||||
|
if (cancellationSignal?.isCanceled == true) {
|
||||||
|
callback?.onLayoutCancelled()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val info = PrintDocumentInfo.Builder(fileName)
|
||||||
|
.setContentType(PrintDocumentInfo.CONTENT_TYPE_DOCUMENT)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
callback?.onLayoutFinished(info, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onWrite(
|
||||||
|
pages: Array<out PageRange>?,
|
||||||
|
destination: ParcelFileDescriptor?,
|
||||||
|
cancellationSignal: CancellationSignal?,
|
||||||
|
callback: WriteResultCallback?
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
context.contentResolver.openFileDescriptor(pdfUri, "r")?.use { pfd ->
|
||||||
|
FileInputStream(pfd.fileDescriptor).use { input ->
|
||||||
|
FileOutputStream(destination?.fileDescriptor).use { output ->
|
||||||
|
val buf = ByteArray(8192)
|
||||||
|
var bytesRead: Int
|
||||||
|
while (input.read(buf).also { bytesRead = it } > 0) {
|
||||||
|
if (cancellationSignal?.isCanceled == true) {
|
||||||
|
Timber.tag("PdfPrint").d("Print job cancelled during write")
|
||||||
|
callback?.onWriteCancelled()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
output.write(buf, 0, bytesRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Timber.tag("PdfPrint").i("PDF successfully streamed to print spooler")
|
||||||
|
callback?.onWriteFinished(arrayOf(PageRange.ALL_PAGES))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("PdfPrint").e(e, "Error writing PDF to print spooler")
|
||||||
|
callback?.onWriteFailed(e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun loadPdfBookmarksFromJson(bookmarksJson: String?): Set<PdfBookmark> {
|
private fun loadPdfBookmarksFromJson(bookmarksJson: String?): Set<PdfBookmark> {
|
||||||
if (bookmarksJson.isNullOrBlank()) return emptySet()
|
if (bookmarksJson.isNullOrBlank()) return emptySet()
|
||||||
return try {
|
return try {
|
||||||
|
|
@ -615,23 +777,188 @@ private data class TtsPageData(
|
||||||
|
|
||||||
private data class TocEntry(val title: String, val pageIndex: Int, val nestLevel: Int)
|
private data class TocEntry(val title: String, val pageIndex: Int, val nestLevel: Int)
|
||||||
|
|
||||||
private fun flattenToc(bookmarks: List<PdfDocument.Bookmark>, level: Int = 0): List<TocEntry> {
|
private fun flattenToc(bookmarks: List<Bookmark>, level: Int = 0): List<TocEntry> {
|
||||||
|
Timber.tag("PdfTocDebug").d("Processing level $level with ${bookmarks.size} items")
|
||||||
val entries = mutableListOf<TocEntry>()
|
val entries = mutableListOf<TocEntry>()
|
||||||
for (bookmark in bookmarks) {
|
for ((index, bookmark) in bookmarks.withIndex()) {
|
||||||
|
val title = bookmark.title ?: "Untitled Chapter"
|
||||||
|
val childCount = bookmark.children.size
|
||||||
|
|
||||||
|
Timber.tag("PdfTocDebug").d(
|
||||||
|
"Lvl $level | Item $index: \"$title\" (Page: ${bookmark.pageIdx}) | Children: $childCount"
|
||||||
|
)
|
||||||
|
|
||||||
entries.add(
|
entries.add(
|
||||||
TocEntry(
|
TocEntry(
|
||||||
title = bookmark.title ?: "Untitled Chapter",
|
title = title,
|
||||||
pageIndex = bookmark.pageIdx.toInt(),
|
pageIndex = bookmark.pageIdx.toInt(),
|
||||||
nestLevel = level
|
nestLevel = level
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (bookmark.children.isNotEmpty()) {
|
|
||||||
|
if (childCount > 0) {
|
||||||
|
Timber.tag("PdfTocDebug").v("Entering children of \"$title\"")
|
||||||
entries.addAll(flattenToc(bookmark.children, level + 1))
|
entries.addAll(flattenToc(bookmark.children, level + 1))
|
||||||
|
Timber.tag("PdfTocDebug").v("Returned to Lvl $level from \"$title\"")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class ScrollbarCalculations(
|
||||||
|
val thumbHeight: Float,
|
||||||
|
val thumbOffset: Float,
|
||||||
|
val contentHeight: Float,
|
||||||
|
val viewportHeight: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun VerticalScrollbar(
|
||||||
|
listState: LazyListState,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isDragged by interactionSource.collectIsDraggedAsState()
|
||||||
|
|
||||||
|
val scrollbarState by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val totalItems = layoutInfo.totalItemsCount
|
||||||
|
val visibleItemsInfo = layoutInfo.visibleItemsInfo
|
||||||
|
val viewportHeight = layoutInfo.viewportSize.height.toFloat()
|
||||||
|
|
||||||
|
if (totalItems == 0 || visibleItemsInfo.isEmpty() || viewportHeight <= 0f) {
|
||||||
|
return@derivedStateOf null
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate total height
|
||||||
|
val averageItemHeight = visibleItemsInfo.sumOf { it.size } / visibleItemsInfo.size.toFloat()
|
||||||
|
val estimatedContentHeight = (averageItemHeight * totalItems).coerceAtLeast(viewportHeight)
|
||||||
|
val viewportRatio = viewportHeight / estimatedContentHeight
|
||||||
|
|
||||||
|
if (viewportRatio >= 1f) return@derivedStateOf null
|
||||||
|
|
||||||
|
val thumbHeight = (viewportHeight * viewportRatio).coerceIn(80f, viewportHeight / 2)
|
||||||
|
|
||||||
|
val firstItemIndex = listState.firstVisibleItemIndex
|
||||||
|
val firstItemOffset = listState.firstVisibleItemScrollOffset
|
||||||
|
val currentScrollPixels = (firstItemIndex * averageItemHeight) + firstItemOffset
|
||||||
|
val maxScrollPixels = estimatedContentHeight - viewportHeight
|
||||||
|
val scrollProgress = (currentScrollPixels / maxScrollPixels).coerceIn(0f, 1f)
|
||||||
|
val trackHeight = viewportHeight - thumbHeight
|
||||||
|
val thumbOffset = trackHeight * scrollProgress
|
||||||
|
|
||||||
|
ScrollbarCalculations(
|
||||||
|
thumbHeight = thumbHeight,
|
||||||
|
thumbOffset = thumbOffset,
|
||||||
|
contentHeight = estimatedContentHeight,
|
||||||
|
viewportHeight = viewportHeight
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetAlpha = if (listState.isScrollInProgress || isDragged) 1f else 0f
|
||||||
|
val alpha by animateFloatAsState(
|
||||||
|
targetValue = targetAlpha,
|
||||||
|
animationSpec = tween(durationMillis = 200),
|
||||||
|
label = "ScrollbarAlpha"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (scrollbarState != null) {
|
||||||
|
val state = scrollbarState!!
|
||||||
|
val draggableState = rememberDraggableState { delta ->
|
||||||
|
val trackHeight = state.viewportHeight - state.thumbHeight
|
||||||
|
if (trackHeight > 0) {
|
||||||
|
val scrollRatio = delta / trackHeight
|
||||||
|
val totalScrollableDistance = state.contentHeight - state.viewportHeight
|
||||||
|
val scrollDelta = scrollRatio * totalScrollableDistance
|
||||||
|
listState.dispatchRawDelta(scrollDelta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.width(30.dp)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.draggable(
|
||||||
|
state = draggableState,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.graphicsLayer { translationY = state.thumbOffset }
|
||||||
|
.padding(end = 4.dp)
|
||||||
|
.width(6.dp)
|
||||||
|
.height(with(LocalDensity.current) { state.thumbHeight.toDp() })
|
||||||
|
.alpha(alpha)
|
||||||
|
.background(
|
||||||
|
color = if (isDragged) MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
|
||||||
|
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
|
||||||
|
shape = RoundedCornerShape(100)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PdfTocTreeItem(
|
||||||
|
label: String,
|
||||||
|
nestLevel: Int,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
hasChildren: Boolean,
|
||||||
|
isCurrent: Boolean,
|
||||||
|
onToggleExpand: () -> Unit,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val backgroundColor by animateColorAsState(
|
||||||
|
targetValue = if (isCurrent) MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.2f) else Color.Transparent,
|
||||||
|
label = "TocItemBackground"
|
||||||
|
)
|
||||||
|
|
||||||
|
val contentColor = if (isCurrent) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = 48.dp)
|
||||||
|
.background(backgroundColor)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Spacer(modifier = Modifier.width((16 * nestLevel).dp))
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clickable(enabled = hasChildren, onClick = onToggleExpand),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (hasChildren) {
|
||||||
|
Icon(
|
||||||
|
imageVector = if (isExpanded) Icons.Default.KeyboardArrowDown else Icons.AutoMirrored.Filled.KeyboardArrowRight,
|
||||||
|
contentDescription = if (isExpanded) "Collapse" else "Expand",
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = if (nestLevel == 0) MaterialTheme.typography.bodyLarge else MaterialTheme.typography.bodyMedium,
|
||||||
|
fontWeight = if (isCurrent) FontWeight.Bold else if (nestLevel == 0) FontWeight.SemiBold else FontWeight.Normal,
|
||||||
|
color = contentColor,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f).padding(end = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
private fun saveTtsMode(context: Context, mode: TtsPlaybackManager.TtsMode) {
|
private fun saveTtsMode(context: Context, mode: TtsPlaybackManager.TtsMode) {
|
||||||
|
|
@ -644,6 +971,7 @@ private suspend fun renderPageToBitmap(doc: PdfDocumentKt, pageIndex: Int): Bitm
|
||||||
var page: PdfPageKt? = null
|
var page: PdfPageKt? = null
|
||||||
try {
|
try {
|
||||||
page = doc.openPage(pageIndex)
|
page = doc.openPage(pageIndex)
|
||||||
|
if (page == null) return@withContext null
|
||||||
|
|
||||||
val bitmapWidth = 1080
|
val bitmapWidth = 1080
|
||||||
val aspectRatio =
|
val aspectRatio =
|
||||||
|
|
@ -853,6 +1181,25 @@ fun PdfViewerScreen(
|
||||||
isAutoScrollLocal = loadPdfAutoScrollLocalMode(context, bookId)
|
isAutoScrollLocal = loadPdfAutoScrollLocalMode(context, bookId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val onPrintDocument = {
|
||||||
|
val printManager = context.getSystemService(Context.PRINT_SERVICE) as PrintManager
|
||||||
|
val jobName = "${context.getString(R.string.app_name)} - $originalFileName"
|
||||||
|
|
||||||
|
try {
|
||||||
|
Timber.tag("PdfPrint").d("Starting print job: $jobName")
|
||||||
|
printManager.print(
|
||||||
|
jobName,
|
||||||
|
PdfPrintDocumentAdapter(context, pdfUri, originalFileName),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.tag("PdfPrint").e(e, "Failed to initialize print job")
|
||||||
|
coroutineScope.launch {
|
||||||
|
snackbarHostState.showSnackbar("Could not open print settings")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val initialSettings = remember(isAutoScrollLocal, bookId) {
|
val initialSettings = remember(isAutoScrollLocal, bookId) {
|
||||||
if (isAutoScrollLocal) {
|
if (isAutoScrollLocal) {
|
||||||
loadPdfAutoScrollLocalSettings(context, bookId) ?: Triple(
|
loadPdfAutoScrollLocalSettings(context, bookId) ?: Triple(
|
||||||
|
|
@ -1357,7 +1704,7 @@ fun PdfViewerScreen(
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
doc.openPage(pageIndex).use { page ->
|
doc.openPage(pageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val fullText = textPage.textPageGetText(newStart, newEnd - newStart) ?: text
|
val fullText = textPage.textPageGetText(newStart, newEnd - newStart) ?: text
|
||||||
val rects = textPage.textPageGetRectsForRanges(intArrayOf(newStart, newEnd - newStart))
|
val rects = textPage.textPageGetRectsForRanges(intArrayOf(newStart, newEnd - newStart))
|
||||||
|
|
@ -1895,7 +2242,7 @@ fun PdfViewerScreen(
|
||||||
if (pdfDocument != null) {
|
if (pdfDocument != null) {
|
||||||
try {
|
try {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
pdfDocument!!.openPage(pageIndex).use { page ->
|
pdfDocument!!.openPage(pageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val count = textPage.textPageCountChars()
|
val count = textPage.textPageCountChars()
|
||||||
|
|
||||||
|
|
@ -2414,10 +2761,10 @@ fun PdfViewerScreen(
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Timber.d("TTS: Opening page $pageToRead for Pdfium text extraction.")
|
Timber.d("TTS: Opening page $pageToRead for Pdfium text extraction.")
|
||||||
tempPage = pdfDocument!!.openPage(pageToRead)
|
tempPage = pdfDocument!!.openPage(pageToRead)
|
||||||
tempTextPage = tempPage.openTextPage()
|
tempTextPage = tempPage?.openTextPage()
|
||||||
val charCount = tempTextPage.textPageCountChars()
|
val charCount = tempTextPage?.textPageCountChars() ?: 0
|
||||||
if (charCount > 0) {
|
if (charCount > 0) {
|
||||||
rawPageText = tempTextPage.textPageGetText(0, charCount)?.trim()
|
rawPageText = tempTextPage?.textPageGetText(0, charCount)?.trim()
|
||||||
if (rawPageText.isNullOrBlank()) {
|
if (rawPageText.isNullOrBlank()) {
|
||||||
Timber.d(
|
Timber.d(
|
||||||
"TTS: Pdfium extracted text but it's blank (charCount: $charCount)."
|
"TTS: Pdfium extracted text but it's blank (charCount: $charCount)."
|
||||||
|
|
@ -2772,6 +3119,17 @@ fun PdfViewerScreen(
|
||||||
pdfDocument = doc
|
pdfDocument = doc
|
||||||
pfdState = currentPfdOpened
|
pfdState = currentPfdOpened
|
||||||
val pagesCount = doc.getPageCount()
|
val pagesCount = doc.getPageCount()
|
||||||
|
|
||||||
|
if (pagesCount > 0) {
|
||||||
|
try {
|
||||||
|
val tableOfContents = doc.getFixedTableOfContents()
|
||||||
|
val flattened = flattenToc(tableOfContents)
|
||||||
|
withContext(Dispatchers.Main) { flatTableOfContents = flattened }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e, "Failed to load TOC")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
totalPages = pagesCount
|
totalPages = pagesCount
|
||||||
|
|
||||||
if (pagesCount > 0) {
|
if (pagesCount > 0) {
|
||||||
|
|
@ -2782,7 +3140,7 @@ fun PdfViewerScreen(
|
||||||
cachedRatios
|
cachedRatios
|
||||||
} else {
|
} else {
|
||||||
val computedRatios = ArrayList<Float>(pagesCount)
|
val computedRatios = ArrayList<Float>(pagesCount)
|
||||||
doc.openPage(0).use { page ->
|
doc.openPage(0)?.use { page ->
|
||||||
val width = page.getPageWidthPoint()
|
val width = page.getPageWidthPoint()
|
||||||
val height = page.getPageHeightPoint()
|
val height = page.getPageHeightPoint()
|
||||||
val ratio = if (height > 0) width.toFloat() / height.toFloat()
|
val ratio = if (height > 0) width.toFloat() / height.toFloat()
|
||||||
|
|
@ -2797,7 +3155,7 @@ fun PdfViewerScreen(
|
||||||
for (i in 0 until pagesCount) {
|
for (i in 0 until pagesCount) {
|
||||||
if (!isActive) break
|
if (!isActive) break
|
||||||
try {
|
try {
|
||||||
doc.openPage(i).use { page ->
|
doc.openPage(i)?.use { page ->
|
||||||
val width = page.getPageWidthPoint()
|
val width = page.getPageWidthPoint()
|
||||||
val height = page.getPageHeightPoint()
|
val height = page.getPageHeightPoint()
|
||||||
val ratio =
|
val ratio =
|
||||||
|
|
@ -2843,7 +3201,7 @@ fun PdfViewerScreen(
|
||||||
for (i in 1 until pagesCount) {
|
for (i in 1 until pagesCount) {
|
||||||
if (!isActive) break
|
if (!isActive) break
|
||||||
try {
|
try {
|
||||||
doc.openPage(i).use { page ->
|
doc.openPage(i)?.use { page ->
|
||||||
val width = page.getPageWidthPoint()
|
val width = page.getPageWidthPoint()
|
||||||
val height = page.getPageHeightPoint()
|
val height = page.getPageHeightPoint()
|
||||||
val ratio = if (height > 0) width.toFloat() / height.toFloat()
|
val ratio = if (height > 0) width.toFloat() / height.toFloat()
|
||||||
|
|
@ -2866,16 +3224,6 @@ fun PdfViewerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
val tableOfContents = doc.getTableOfContents()
|
|
||||||
val flattened = flattenToc(tableOfContents)
|
|
||||||
withContext(Dispatchers.Main) { flatTableOfContents = flattened }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w(e, "Failed to load TOC")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
isDocumentReady = true
|
isDocumentReady = true
|
||||||
isLoadingDocument = false
|
isLoadingDocument = false
|
||||||
|
|
@ -2884,7 +3232,7 @@ fun PdfViewerScreen(
|
||||||
Timber.i("PDF document loaded optimistically. Total Pages: $totalPages.")
|
Timber.i("PDF document loaded optimistically. Total Pages: $totalPages.")
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is PdfPasswordException || e.cause is PdfPasswordException) {
|
if (e.javaClass.name.contains("PasswordException") || e.cause?.javaClass?.name?.contains("PasswordException") == true) {
|
||||||
Timber.w("PDF is password protected or password incorrect.")
|
Timber.w("PDF is password protected or password incorrect.")
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
if (documentPassword != null) {
|
if (documentPassword != null) {
|
||||||
|
|
@ -3358,9 +3706,7 @@ fun PdfViewerScreen(
|
||||||
0 -> { // Chapters Page
|
0 -> { // Chapters Page
|
||||||
if (flatTableOfContents.isEmpty()) {
|
if (flatTableOfContents.isEmpty()) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(16.dp),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -3370,54 +3716,99 @@ fun PdfViewerScreen(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val currentTocEntry by remember(
|
val listState = rememberLazyListState()
|
||||||
pagerState.currentPage, flatTableOfContents
|
|
||||||
) {
|
val allParentIndices = remember(flatTableOfContents) {
|
||||||
derivedStateOf {
|
flatTableOfContents.indices.filter { i ->
|
||||||
flatTableOfContents.lastOrNull {
|
val next = flatTableOfContents.getOrNull(i + 1)
|
||||||
it.pageIndex <= pagerState.currentPage
|
next != null && next.nestLevel > flatTableOfContents[i].nestLevel
|
||||||
|
}.toSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var expandedEntryIndices by rememberSaveable(flatTableOfContents) {
|
||||||
|
mutableStateOf(allParentIndices)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val visibleItemInfo = remember(flatTableOfContents, expandedEntryIndices) {
|
||||||
|
val result = mutableListOf<Pair<Int, TocEntry>>()
|
||||||
|
val visibilityStack = BooleanArray(20) { false }
|
||||||
|
visibilityStack[0] = true
|
||||||
|
|
||||||
|
for (i in flatTableOfContents.indices) {
|
||||||
|
val entry = flatTableOfContents[i]
|
||||||
|
val level = entry.nestLevel.coerceIn(0, 19)
|
||||||
|
|
||||||
|
if (visibilityStack[level]) {
|
||||||
|
result.add(i to entry)
|
||||||
|
val isExpanded = expandedEntryIndices.contains(i)
|
||||||
|
if (level + 1 < visibilityStack.size) {
|
||||||
|
visibilityStack[level + 1] = isExpanded
|
||||||
}
|
}
|
||||||
LazyColumn(modifier = Modifier.fillMaxHeight()) {
|
|
||||||
itemsIndexed(
|
|
||||||
items = flatTableOfContents, key = { index, entry ->
|
|
||||||
"toc_${index}_${entry.pageIndex}_${entry.title.hashCode()}"
|
|
||||||
}) { _, entry ->
|
|
||||||
val isCurrentChapter = entry == currentTocEntry
|
|
||||||
ListItem(
|
|
||||||
headlineContent = {
|
|
||||||
Text(
|
|
||||||
entry.title,
|
|
||||||
fontWeight = if (isCurrentChapter) FontWeight.Bold
|
|
||||||
else FontWeight.Normal,
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
start = (16 * entry.nestLevel).dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, colors = if (isCurrentChapter) {
|
|
||||||
ListItemDefaults.colors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.primaryContainer,
|
|
||||||
headlineColor = MaterialTheme.colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
ListItemDefaults.colors()
|
if (level + 1 < visibilityStack.size) {
|
||||||
}, modifier = Modifier.clickable {
|
visibilityStack[level + 1] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentTocEntry by remember(pagerState.currentPage, verticalReaderState.currentPage, displayMode, flatTableOfContents) {
|
||||||
|
derivedStateOf {
|
||||||
|
val activePage = if (displayMode == DisplayMode.PAGINATION) pagerState.currentPage else verticalReaderState.currentPage
|
||||||
|
flatTableOfContents.lastOrNull { it.pageIndex <= activePage }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
LazyColumn(
|
||||||
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = visibleItemInfo,
|
||||||
|
key = { it.second.title + it.first }
|
||||||
|
) { item ->
|
||||||
|
val (originalIndex, entry) = item
|
||||||
|
|
||||||
|
val nextItem = flatTableOfContents.getOrNull(originalIndex + 1)
|
||||||
|
val hasChildren = nextItem != null && nextItem.nestLevel > entry.nestLevel
|
||||||
|
val isExpanded = expandedEntryIndices.contains(originalIndex)
|
||||||
|
val isCurrentChapter = entry == currentTocEntry
|
||||||
|
|
||||||
|
PdfTocTreeItem(
|
||||||
|
label = entry.title,
|
||||||
|
nestLevel = entry.nestLevel,
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
hasChildren = hasChildren,
|
||||||
|
isCurrent = isCurrentChapter,
|
||||||
|
onToggleExpand = {
|
||||||
|
expandedEntryIndices = if (isExpanded) {
|
||||||
|
expandedEntryIndices - originalIndex
|
||||||
|
} else {
|
||||||
|
expandedEntryIndices + originalIndex
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
drawerState.close()
|
drawerState.close()
|
||||||
if (displayMode == DisplayMode.PAGINATION) {
|
if (displayMode == DisplayMode.PAGINATION) {
|
||||||
pagerState.scrollToPage(
|
pagerState.scrollToPage(entry.pageIndex)
|
||||||
entry.pageIndex
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
verticalReaderState.scrollToPage(
|
verticalReaderState.scrollToPage(entry.pageIndex)
|
||||||
entry.pageIndex
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
HorizontalDivider()
|
VerticalScrollbar(
|
||||||
}
|
listState = listState,
|
||||||
|
modifier = Modifier.align(Alignment.CenterEnd)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3767,7 +4158,13 @@ fun PdfViewerScreen(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
key = { it },
|
key = { it },
|
||||||
beyondViewportPageCount = dynamicBeyondViewportPageCount,
|
beyondViewportPageCount = dynamicBeyondViewportPageCount,
|
||||||
userScrollEnabled= currentPageScale == 1f && !(ttsState.isPlaying || ttsState.isLoading || searchState.isSearchActive) && !isPageSliderVisible && paginationDraggingBoxId == null
|
userScrollEnabled = run {
|
||||||
|
val enabled = currentPageScale == 1f && !(ttsState.isPlaying || ttsState.isLoading || searchState.isSearchActive) && !isPageSliderVisible && paginationDraggingBoxId == null
|
||||||
|
SideEffect {
|
||||||
|
Timber.tag("PdfZoomDebug").v("Pager Scroll Enabled: $enabled (Scale: $currentPageScale, Playing: ${ttsState.isPlaying}, Slider: $isPageSliderVisible, DraggingBox: $paginationDraggingBoxId)")
|
||||||
|
}
|
||||||
|
enabled
|
||||||
|
}
|
||||||
) { pageIndex ->
|
) { pageIndex ->
|
||||||
val isVisiblePage = remember(pagerState.currentPage, pageIndex) {
|
val isVisiblePage = remember(pagerState.currentPage, pageIndex) {
|
||||||
kotlin.math.abs(pagerState.currentPage - pageIndex) <= 1
|
kotlin.math.abs(pagerState.currentPage - pageIndex) <= 1
|
||||||
|
|
@ -5221,6 +5618,20 @@ fun PdfViewerScreen(
|
||||||
Icons.Default.Save, contentDescription = null
|
Icons.Default.Save, contentDescription = null
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Print") },
|
||||||
|
onClick = {
|
||||||
|
showMoreMenu = false
|
||||||
|
onPrintDocument()
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.print),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -6929,6 +7340,7 @@ private fun debugPdfLinks(
|
||||||
if (pageCount > 0) {
|
if (pageCount > 0) {
|
||||||
val pageIndex = 0 // Testing the first page
|
val pageIndex = 0 // Testing the first page
|
||||||
page = doc.openPage(pageIndex)
|
page = doc.openPage(pageIndex)
|
||||||
|
if (page == null) return@launch
|
||||||
Timber.d("Opened page $pageIndex")
|
Timber.d("Opened page $pageIndex")
|
||||||
|
|
||||||
Timber.d(
|
Timber.d(
|
||||||
|
|
@ -6957,7 +7369,7 @@ private fun debugPdfLinks(
|
||||||
|
|
||||||
// Method 2: The one that is working
|
// Method 2: The one that is working
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
textPage.loadWebLink().use { webLinks ->
|
textPage.loadWebLink()?.use { webLinks ->
|
||||||
val webLinkCount = webLinks.countWebLinks()
|
val webLinkCount = webLinks.countWebLinks()
|
||||||
Timber.d("[METHOD 2] loadWebLink() found $webLinkCount links.")
|
Timber.d("[METHOD 2] loadWebLink() found $webLinkCount links.")
|
||||||
if (webLinkCount > 0) {
|
if (webLinkCount > 0) {
|
||||||
|
|
|
||||||
|
|
@ -171,7 +171,7 @@ class PdfTextRepository(context: Context) {
|
||||||
var ocrUsed = false
|
var ocrUsed = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
document.openPage(pageIndex).use { page ->
|
document.openPage(pageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
val count = textPage.textPageCountChars()
|
val count = textPage.textPageCountChars()
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
|
|
@ -188,7 +188,7 @@ class PdfTextRepository(context: Context) {
|
||||||
|
|
||||||
if (text.isBlank()) {
|
if (text.isBlank()) {
|
||||||
try {
|
try {
|
||||||
document.openPage(pageIndex).use { page ->
|
document.openPage(pageIndex)?.use { page ->
|
||||||
val targetWidth = 1080
|
val targetWidth = 1080
|
||||||
val ptrWidth = page.getPageWidthPoint()
|
val ptrWidth = page.getPageWidthPoint()
|
||||||
val ptrHeight = page.getPageHeightPoint()
|
val ptrHeight = page.getPageHeightPoint()
|
||||||
|
|
@ -270,11 +270,11 @@ class PdfTextRepository(context: Context) {
|
||||||
suspend fun hasNativeText(document: PdfDocumentKt, pageIndex: Int): Boolean {
|
suspend fun hasNativeText(document: PdfDocumentKt, pageIndex: Int): Boolean {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
document.openPage(pageIndex).use { page ->
|
document.openPage(pageIndex)?.use { page ->
|
||||||
page.openTextPage().use { textPage ->
|
page.openTextPage().use { textPage ->
|
||||||
textPage.textPageCountChars() > 0
|
textPage.textPageCountChars() > 0
|
||||||
}
|
}
|
||||||
}
|
} ?: false
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
@ -290,7 +290,7 @@ class PdfTextRepository(context: Context) {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
val rects = mutableListOf<RectF>()
|
val rects = mutableListOf<RectF>()
|
||||||
try {
|
try {
|
||||||
document.openPage(pageIndex).use { page ->
|
document.openPage(pageIndex)?.use { page ->
|
||||||
val targetWidth = 1080
|
val targetWidth = 1080
|
||||||
val ptrWidth = page.getPageWidthPoint()
|
val ptrWidth = page.getPageWidthPoint()
|
||||||
val ptrHeight = page.getPageHeightPoint()
|
val ptrHeight = page.getPageHeightPoint()
|
||||||
|
|
|
||||||
10
app/src/main/res/drawable-nodpi/print.xml
Normal file
10
app/src/main/res/drawable-nodpi/print.xml
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M640,320L640,200L320,200L320,320L240,320L240,120L720,120L720,320L640,320ZM160,400L160,400Q160,400 171.5,400Q183,400 200,400L760,400Q777,400 788.5,400Q800,400 800,400L800,400L720,400L720,400L240,400L240,400L160,400ZM720,500Q737,500 748.5,488.5Q760,477 760,460Q760,443 748.5,431.5Q737,420 720,420Q703,420 691.5,431.5Q680,443 680,460Q680,477 691.5,488.5Q703,500 720,500ZM640,760L640,600L320,600L320,760L640,760ZM720,840L240,840L240,680L80,680L80,440Q80,389 115,354.5Q150,320 200,320L760,320Q811,320 845.5,354.5Q880,389 880,440L880,680L720,680L720,840ZM800,600L800,440Q800,423 788.5,411.5Q777,400 760,400L200,400Q183,400 171.5,411.5Q160,423 160,440L160,600L240,600L240,520L720,520L720,600L800,600Z"/>
|
||||||
|
</vector>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue