From 3ecb41b7d9a2e5361ee5e6421d44dd1cbe944580 Mon Sep 17 00:00:00 2001 From: Matthew Olsson Date: Tue, 29 Mar 2022 21:27:17 -0700 Subject: [PATCH] PDFViewer: Support a continuous page view mode --- Userland/Applications/PDFViewer/PDFViewer.cpp | 187 ++++++++++++++---- Userland/Applications/PDFViewer/PDFViewer.h | 27 ++- .../PDFViewer/PDFViewerWidget.cpp | 24 ++- Userland/Libraries/LibPDF/Document.h | 3 + Userland/Libraries/LibPDF/Renderer.cpp | 4 +- 5 files changed, 196 insertions(+), 49 deletions(-) diff --git a/Userland/Applications/PDFViewer/PDFViewer.cpp b/Userland/Applications/PDFViewer/PDFViewer.cpp index 2437e1fec1e..fc7d8670c2b 100644 --- a/Userland/Applications/PDFViewer/PDFViewer.cpp +++ b/Userland/Applications/PDFViewer/PDFViewer.cpp @@ -7,13 +7,14 @@ #include "PDFViewer.h" #include +#include #include #include #include #include #include -static constexpr int PAGE_PADDING = 25; +static constexpr int PAGE_PADDING = 10; static constexpr Array zoom_levels = { 17, @@ -46,7 +47,7 @@ PDFViewer::PDFViewer() m_page_view_mode = static_cast(Config::read_i32("PDFViewer", "Display", "PageMode", 0)); } -void PDFViewer::set_document(RefPtr document) +PDF::PDFErrorOr PDFViewer::set_document(RefPtr document) { m_document = document; m_current_page_index = document->get_first_page_index(); @@ -57,7 +58,10 @@ void PDFViewer::set_document(RefPtr document) for (u32 i = 0; i < document->get_page_count(); i++) m_rendered_page_list.unchecked_append(HashMap()); + TRY(cache_page_dimensions(true)); update(); + + return {}; } PDF::PDFErrorOr> PDFViewer::get_rendered_page(u32 index) @@ -67,8 +71,7 @@ PDF::PDFErrorOr> PDFViewer::get_rendered_page(u32 ind if (existing_rendered_page.has_value() && existing_rendered_page.value().rotation == m_rotations) return existing_rendered_page.value().bitmap; - auto page = TRY(m_document->get_page(index)); - auto rendered_page = TRY(render_page(page)); + auto rendered_page = TRY(render_page(index)); rendered_page_map.set(m_zoom_level, { rendered_page, m_rotations }); return rendered_page; } @@ -85,29 +88,79 @@ void PDFViewer::paint_event(GUI::PaintEvent& event) if (!m_document) return; - auto maybe_page = get_rendered_page(m_current_page_index); - if (maybe_page.is_error()) { - auto error = maybe_page.release_error(); - GUI::MessageBox::show_error(nullptr, String::formatted("Error rendering page:\n{}", error.message())); + auto handle_error = [&](PDF::PDFErrorOr maybe_error) { + if (maybe_error.is_error()) { + auto error = maybe_error.release_error(); + GUI::MessageBox::show_error(nullptr, String::formatted("Error rendering page:\n{}", error.message())); + return true; + } + return false; + }; + + if (m_page_view_mode == PageViewMode::Single) { + auto maybe_page = get_rendered_page(m_current_page_index); + if (handle_error(maybe_page)) + return; + + auto page = maybe_page.release_value(); + set_content_size(page->size()); + + painter.translate(frame_thickness(), frame_thickness()); + painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); + + int x = max(0, (width() - page->width()) / 2); + int y = max(0, (height() - page->height()) / 2); + + painter.blit({ x, y }, *page, page->rect()); return; } - auto page = maybe_page.release_value(); - set_content_size(page->size()); + set_content_size({ m_page_dimension_cache.max_width, m_page_dimension_cache.total_height }); + + size_t first_page_index = 0; + size_t last_page_index = 0; + + binary_search(m_page_dimension_cache.render_info, vertical_scrollbar().value(), &first_page_index, [](int height, PageDimensionCache::RenderInfo const& render_info) { + return height - render_info.total_height_before_this_page; + }); + + binary_search(m_page_dimension_cache.render_info, vertical_scrollbar().value() + height(), &last_page_index, [](int height, PageDimensionCache::RenderInfo const& render_info) { + return height - render_info.total_height_before_this_page; + }); + + auto initial_offset = m_page_dimension_cache.render_info[first_page_index].total_height_before_this_page - vertical_scrollbar().value(); painter.translate(frame_thickness(), frame_thickness()); - painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value()); + painter.translate(-horizontal_scrollbar().value(), initial_offset); + auto middle = height() / 2; + auto y_offset = initial_offset; - int x = max(0, (width() - page->width()) / 2); - int y = max(0, (height() - page->height()) / 2); + for (size_t page_index = first_page_index; page_index <= last_page_index; page_index++) { + auto maybe_page = get_rendered_page(page_index); + if (handle_error(maybe_page)) + return; - painter.blit({ x, y }, *page, page->rect()); + auto page = maybe_page.release_value(); + + auto x = max(0, (width() - page->width()) / 2); + + painter.blit({ x, PAGE_PADDING }, *page, page->rect()); + auto diff_y = page->height() + PAGE_PADDING * 2; + painter.translate(0, diff_y); + + if (y_offset < middle && y_offset + diff_y >= middle) + change_page(page_index); + + y_offset += diff_y; + } } void PDFViewer::resize_event(GUI::ResizeEvent&) { for (auto& map : m_rendered_page_list) map.clear(); + if (m_document) + MUST(cache_page_dimensions()); update(); } @@ -124,15 +177,24 @@ void PDFViewer::mousewheel_event(GUI::MouseEvent& event) } else { zoom_in(); } - } else { - auto& scrollbar = event.shift() ? horizontal_scrollbar() : vertical_scrollbar(); + return; + } + auto& scrollbar = event.shift() ? horizontal_scrollbar() : vertical_scrollbar(); + + if (m_page_view_mode == PageViewMode::Multiple) { + if (scrolled_down) { + if (scrollbar.value() != scrollbar.max()) + scrollbar.increase_slider_by(20); + } else { + if (scrollbar.value() > 0) + scrollbar.decrease_slider_by(20); + } + } else { if (scrolled_down) { if (scrollbar.value() == scrollbar.max()) { - if (m_current_page_index < m_document->get_page_count() - 1 && !event.shift()) { - m_current_page_index++; - if (on_page_change) - on_page_change(m_current_page_index); + if (m_current_page_index < m_document->get_page_count() - 1) { + change_page(m_current_page_index + 1); scrollbar.set_value(0); } } else { @@ -140,18 +202,17 @@ void PDFViewer::mousewheel_event(GUI::MouseEvent& event) } } else { if (scrollbar.value() == 0) { - if (m_current_page_index > 0 && !event.shift()) { - m_current_page_index--; - if (on_page_change) - on_page_change(m_current_page_index); + if (m_current_page_index > 0) { + change_page(m_current_page_index - 1); scrollbar.set_value(scrollbar.max()); } } else { scrollbar.decrease_slider_by(20); } } - update(); } + + update(); } void PDFViewer::mousedown_event(GUI::MouseEvent& event) @@ -190,6 +251,7 @@ void PDFViewer::zoom_in() { if (m_zoom_level < zoom_levels.size() - 1) { m_zoom_level++; + MUST(cache_page_dimensions()); update(); } } @@ -198,6 +260,7 @@ void PDFViewer::zoom_out() { if (m_zoom_level > 0) { m_zoom_level--; + MUST(cache_page_dimensions()); update(); } } @@ -205,12 +268,14 @@ void PDFViewer::zoom_out() void PDFViewer::reset_zoom() { m_zoom_level = initial_zoom_level; + MUST(cache_page_dimensions()); update(); } void PDFViewer::rotate(int degrees) { m_rotations = (m_rotations + degrees + 360) % 360; + MUST(cache_page_dimensions()); update(); } @@ -221,17 +286,11 @@ void PDFViewer::set_page_view_mode(PageViewMode mode) update(); } -PDF::PDFErrorOr> PDFViewer::render_page(const PDF::Page& page) +PDF::PDFErrorOr> PDFViewer::render_page(u32 page_index) { - auto zoom_scale_factor = static_cast(zoom_levels[m_zoom_level]) / 100.0f; - - auto page_width = page.media_box.upper_right_x - page.media_box.lower_left_x; - auto page_height = page.media_box.upper_right_y - page.media_box.lower_left_y; - auto page_scale_factor = page_height / page_width; - - auto height = static_cast(this->height() - 2 * frame_thickness() - PAGE_PADDING * 2) * zoom_scale_factor; - auto width = height / page_scale_factor; - auto bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { width, height }).release_value_but_fixme_should_propagate_errors(); + auto page = TRY(m_document->get_page(page_index)); + auto& page_size = m_page_dimension_cache.render_info[page_index].size; + auto bitmap = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, page_size.to_type()).release_value_but_fixme_should_propagate_errors(); TRY(PDF::Renderer::render(*m_document, page, bitmap)); @@ -247,3 +306,61 @@ PDF::PDFErrorOr> PDFViewer::render_page(const PDF::Pa return bitmap; } + +PDF::PDFErrorOr PDFViewer::cache_page_dimensions(bool recalculate_fixed_info) +{ + if (recalculate_fixed_info) + m_page_dimension_cache.page_info.clear_with_capacity(); + + if (m_page_dimension_cache.page_info.is_empty()) { + m_page_dimension_cache.page_info.ensure_capacity(m_document->get_page_count()); + for (size_t i = 0; i < m_document->get_page_count(); i++) { + auto page = TRY(m_document->get_page(i)); + auto box = page.media_box; + m_page_dimension_cache.page_info.unchecked_append(PageDimensionCache::PageInfo { + { box.width(), box.height() }, + page.rotate, + }); + } + } + + auto zoom_scale_factor = static_cast(zoom_levels[m_zoom_level]) / 100.0f; + + m_page_dimension_cache.render_info.clear_with_capacity(); + m_page_dimension_cache.render_info.ensure_capacity(m_page_dimension_cache.page_info.size()); + + float max_width = 0; + float total_height = 0; + + for (size_t i = 0; i < m_page_dimension_cache.page_info.size(); i++) { + auto& [size, rotation] = m_page_dimension_cache.page_info[i]; + rotation += m_rotations; + auto page_scale_factor = size.height() / size.width(); + + auto height = static_cast(this->height() - 2 * frame_thickness()) * zoom_scale_factor - PAGE_PADDING * 2; + auto width = height / page_scale_factor; + if (rotation % 2) + swap(width, height); + + max_width = max(max_width, width); + + m_page_dimension_cache.render_info.append({ + { width, height }, + total_height, + }); + + total_height += height; + } + + m_page_dimension_cache.max_width = max_width; + m_page_dimension_cache.total_height = total_height; + + return {}; +} + +void PDFViewer::change_page(u32 new_page) +{ + m_current_page_index = new_page; + if (on_page_change) + on_page_change(m_current_page_index); +} diff --git a/Userland/Applications/PDFViewer/PDFViewer.h b/Userland/Applications/PDFViewer/PDFViewer.h index 7d18e041953..b93f753933b 100644 --- a/Userland/Applications/PDFViewer/PDFViewer.h +++ b/Userland/Applications/PDFViewer/PDFViewer.h @@ -14,6 +14,26 @@ static constexpr size_t initial_zoom_level = 8; +struct PageDimensionCache { + // Fixed for a given document + struct PageInfo { + Gfx::FloatSize size; + int rotation; + }; + + // Based on PageInfo, also depends on some dynamic factors like + // zoom level and app size + struct RenderInfo { + Gfx::FloatSize size; + float total_height_before_this_page; + }; + + Vector page_info; + Vector render_info; + float max_width; + float total_height; +}; + class PDFViewer : public GUI::AbstractScrollableWidget { C_OBJECT(PDFViewer) @@ -29,7 +49,7 @@ public: ALWAYS_INLINE void set_current_page(u32 current_page) { m_current_page_index = current_page; } ALWAYS_INLINE RefPtr const& document() const { return m_document; } - void set_document(RefPtr); + PDF::PDFErrorOr set_document(RefPtr); Function on_page_change; @@ -59,13 +79,16 @@ private: }; PDF::PDFErrorOr> get_rendered_page(u32 index); - PDF::PDFErrorOr> render_page(const PDF::Page&); + PDF::PDFErrorOr> render_page(u32 page_index); + PDF::PDFErrorOr cache_page_dimensions(bool recalculate_fixed_info = false); + void change_page(u32 new_page); RefPtr m_document; u32 m_current_page_index { 0 }; Vector> m_rendered_page_list; u8 m_zoom_level { initial_zoom_level }; + PageDimensionCache m_page_dimension_cache; PageViewMode m_page_view_mode; Gfx::IntPoint m_pan_starting_position; diff --git a/Userland/Applications/PDFViewer/PDFViewerWidget.cpp b/Userland/Applications/PDFViewer/PDFViewerWidget.cpp index b86e9a12ace..ef5baadb1fb 100644 --- a/Userland/Applications/PDFViewer/PDFViewerWidget.cpp +++ b/Userland/Applications/PDFViewer/PDFViewerWidget.cpp @@ -171,13 +171,19 @@ void PDFViewerWidget::open_file(Core::File& file) { window()->set_title(String::formatted("{} - PDF Viewer", file.filename())); + auto handle_error = [&](PDF::PDFErrorOr maybe_error) { + if (maybe_error.is_error()) { + auto error = maybe_error.release_error(); + GUI::MessageBox::show_error(nullptr, String::formatted("Couldn't load PDF {}:\n{}", file.filename(), error.message())); + return true; + } + return false; + }; + m_buffer = file.read_all(); auto maybe_document = PDF::Document::create(m_buffer); - if (maybe_document.is_error()) { - auto error = maybe_document.release_error(); - GUI::MessageBox::show_error(nullptr, String::formatted("Couldn't load PDF {}:\n{}", file.filename(), error.message())); + if (handle_error(maybe_document)) return; - } auto document = maybe_document.release_value(); @@ -186,14 +192,12 @@ void PDFViewerWidget::open_file(Core::File& file) VERIFY_NOT_REACHED(); } - auto result = document->initialize(); - if (result.is_error()) { - auto error = result.release_error(); - GUI::MessageBox::show_error(nullptr, String::formatted("Couldn't load PDF {}:\n{}", file.filename(), error.message())); + if (handle_error(document->initialize())) + return; + + if (handle_error(m_viewer->set_document(document))) return; - } - m_viewer->set_document(document); m_total_page_label->set_text(String::formatted("of {}", document->get_page_count())); m_page_text_box->set_enabled(true); diff --git a/Userland/Libraries/LibPDF/Document.h b/Userland/Libraries/LibPDF/Document.h index 127577de110..d7836bb2373 100644 --- a/Userland/Libraries/LibPDF/Document.h +++ b/Userland/Libraries/LibPDF/Document.h @@ -23,6 +23,9 @@ struct Rectangle { float lower_left_y; float upper_right_x; float upper_right_y; + + float width() const { return upper_right_x - lower_left_x; } + float height() const { return upper_right_y - lower_left_y; } }; struct Page { diff --git a/Userland/Libraries/LibPDF/Renderer.cpp b/Userland/Libraries/LibPDF/Renderer.cpp index 04f4fe5989c..dbeff272797 100644 --- a/Userland/Libraries/LibPDF/Renderer.cpp +++ b/Userland/Libraries/LibPDF/Renderer.cpp @@ -38,8 +38,8 @@ Renderer::Renderer(RefPtr document, Page const& page, RefPtr(bitmap->width()) / width; float scale_y = static_cast(bitmap->height()) / height; userspace_matrix.scale(scale_x, scale_y);