ladybird/Userland/Applications/PDFViewer/PDFViewer.cpp
Rodrigo Tobar e87fecf710 LibPDF: Switch to best-effort PDF rendering
The current rendering routine aborts as soon as an error is found during
rendering, which potentially severely limits the contents we show on
screen. Moreover, whenever an error happens the PDFViewer widget shows
an error dialog, and doesn't display the bitmap that has been painted so
far.

This commit improves the situation in both fronts, implementing
rendering now with a best-effort approach. Firstly, execution of
operations isn't halted after an operand results in an error, but
instead execution of all operations is always attempted, and all
collected errors are returned in bulk. Secondly, PDFViewer now always
displays the resulting bitmap, regardless of error being produced or
not. To communicate errors, an on_render_errors callback has been added
so clients can subscribe to these events and handle them as appropriate.
2022-12-16 10:04:23 +01:00

399 lines
12 KiB
C++

/*
* Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "PDFViewer.h"
#include <AK/Array.h>
#include <AK/BinarySearch.h>
#include <AK/HashFunctions.h>
#include <LibConfig/Client.h>
#include <LibGUI/Action.h>
#include <LibGUI/MessageBox.h>
#include <LibGUI/Painter.h>
#include <LibPDF/Renderer.h>
static constexpr int PAGE_PADDING = 10;
static constexpr Array zoom_levels = {
17,
21,
26,
33,
41,
51,
64,
80,
100,
120,
144,
173,
207,
249,
299,
358,
430
};
PDFViewer::PDFViewer()
{
set_should_hide_unnecessary_scrollbars(true);
set_focus_policy(GUI::FocusPolicy::StrongFocus);
set_scrollbars_enabled(true);
start_timer(30'000);
m_page_view_mode = static_cast<PageViewMode>(Config::read_i32("PDFViewer"sv, "Display"sv, "PageMode"sv, 0));
m_rendering_preferences.show_clipping_paths = Config::read_bool("PDFViewer"sv, "Rendering"sv, "ShowClippingPaths"sv, false);
m_rendering_preferences.show_images = Config::read_bool("PDFViewer"sv, "Rendering"sv, "ShowImages"sv, true);
}
PDF::PDFErrorOr<void> PDFViewer::set_document(RefPtr<PDF::Document> document)
{
m_document = document;
m_current_page_index = document->get_first_page_index();
m_zoom_level = initial_zoom_level;
m_rendered_page_list.clear();
m_rendered_page_list.ensure_capacity(document->get_page_count());
for (u32 i = 0; i < document->get_page_count(); i++)
m_rendered_page_list.unchecked_append(HashMap<u32, RenderedPage>());
TRY(cache_page_dimensions(true));
update();
return {};
}
PDF::PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> PDFViewer::get_rendered_page(u32 index)
{
auto key = pair_int_hash(m_rendering_preferences.hash(), m_zoom_level);
auto& rendered_page_map = m_rendered_page_list[index];
auto existing_rendered_page = rendered_page_map.get(key);
if (existing_rendered_page.has_value() && existing_rendered_page.value().rotation == m_rotations)
return existing_rendered_page.value().bitmap;
auto rendered_page = TRY(render_page(index));
rendered_page_map.set(key, { rendered_page, m_rotations });
return rendered_page;
}
void PDFViewer::paint_event(GUI::PaintEvent& event)
{
GUI::Frame::paint_event(event);
GUI::Painter painter(*this);
painter.add_clip_rect(widget_inner_rect());
painter.add_clip_rect(event.rect());
painter.fill_rect(event.rect(), Color(0x80, 0x80, 0x80));
if (!m_document)
return;
auto handle_error = [&](PDF::Error& error) {
warnln("{}", error.message());
GUI::MessageBox::show_error(nullptr, "Failed to render the page."sv);
m_document.clear();
};
if (m_page_view_mode == PageViewMode::Single) {
auto maybe_page = get_rendered_page(m_current_page_index);
if (maybe_page.is_error()) {
handle_error(maybe_page.error());
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;
}
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(), initial_offset);
auto middle = height() / 2;
auto y_offset = initial_offset;
for (size_t page_index = first_page_index; page_index <= last_page_index; page_index++) {
auto maybe_page = get_rendered_page(page_index);
if (maybe_page.is_error()) {
handle_error(maybe_page.error());
return;
}
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::set_current_page(u32 current_page)
{
m_current_page_index = current_page;
vertical_scrollbar().set_value(m_page_dimension_cache.render_info[current_page].total_height_before_this_page);
update();
}
void PDFViewer::set_show_clipping_paths(bool show_clipping_paths)
{
m_rendering_preferences.show_clipping_paths = show_clipping_paths;
Config::write_bool("PDFViewer"sv, "Rendering"sv, "ShowClippingPaths"sv, show_clipping_paths);
update();
}
void PDFViewer::set_show_images(bool show_images)
{
m_rendering_preferences.show_images = show_images;
Config::write_bool("PDFViewer"sv, "Rendering"sv, "ShowImages"sv, show_images);
update();
}
void PDFViewer::resize_event(GUI::ResizeEvent&)
{
for (auto& map : m_rendered_page_list)
map.clear();
if (m_document)
MUST(cache_page_dimensions());
update();
}
void PDFViewer::mousewheel_event(GUI::MouseEvent& event)
{
if (!m_document)
return;
bool scrolled_down = event.wheel_delta_y() > 0;
if (event.ctrl()) {
if (scrolled_down) {
zoom_out();
} else {
zoom_in();
}
return;
}
auto& scrollbar = event.shift() ? horizontal_scrollbar() : vertical_scrollbar();
auto delta = abs(event.wheel_delta_y() * 20);
if (m_page_view_mode == PageViewMode::Multiple) {
if (scrolled_down) {
if (scrollbar.value() != scrollbar.max())
scrollbar.increase_slider_by(delta);
} else {
if (scrollbar.value() > 0)
scrollbar.decrease_slider_by(delta);
}
} else {
if (scrolled_down) {
if (scrollbar.value() == scrollbar.max()) {
if (m_current_page_index < m_document->get_page_count() - 1) {
change_page(m_current_page_index + 1);
scrollbar.set_value(0);
}
} else {
scrollbar.increase_slider_by(delta);
}
} else {
if (scrollbar.value() == 0) {
if (m_current_page_index > 0) {
change_page(m_current_page_index - 1);
scrollbar.set_value(scrollbar.max());
}
} else {
scrollbar.decrease_slider_by(delta);
}
}
}
update();
}
void PDFViewer::mousedown_event(GUI::MouseEvent& event)
{
if (event.button() == GUI::MouseButton::Middle) {
m_pan_starting_position = to_content_position(event.position());
set_override_cursor(Gfx::StandardCursor::Drag);
}
}
void PDFViewer::mouseup_event(GUI::MouseEvent&)
{
set_override_cursor(Gfx::StandardCursor::None);
}
void PDFViewer::mousemove_event(GUI::MouseEvent& event)
{
if (event.buttons() & GUI::MouseButton::Middle) {
auto delta = to_content_position(event.position()) - m_pan_starting_position;
horizontal_scrollbar().decrease_slider_by(delta.x());
vertical_scrollbar().decrease_slider_by(delta.y());
update();
}
}
void PDFViewer::timer_event(Core::TimerEvent&)
{
// Clear the bitmap vector of all pages except the current page
for (size_t i = 0; i < m_rendered_page_list.size(); i++) {
if (i != m_current_page_index)
m_rendered_page_list[i].clear();
}
}
void PDFViewer::zoom_in()
{
if (m_zoom_level < zoom_levels.size() - 1) {
m_zoom_level++;
MUST(cache_page_dimensions());
update();
}
}
void PDFViewer::zoom_out()
{
if (m_zoom_level > 0) {
m_zoom_level--;
MUST(cache_page_dimensions());
update();
}
}
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();
}
void PDFViewer::set_page_view_mode(PageViewMode mode)
{
m_page_view_mode = mode;
Config::write_i32("PDFViewer"sv, "Display"sv, "PageMode"sv, static_cast<i32>(mode));
update();
}
PDF::PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> PDFViewer::render_page(u32 page_index)
{
auto page = TRY(m_document->get_page(page_index));
auto& page_size = m_page_dimension_cache.render_info[page_index].size;
auto bitmap = TRY(Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, page_size.to_type<int>()));
auto maybe_errors = PDF::Renderer::render(*m_document, page, bitmap, m_rendering_preferences);
if (maybe_errors.is_error()) {
auto errors = maybe_errors.release_error();
on_render_errors(page_index, errors);
return bitmap;
}
if (page.rotate + m_rotations != 0) {
int rotation_count = ((page.rotate + m_rotations) / 90) % 4;
if (rotation_count == 3) {
bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::CounterClockwise));
} else {
for (int i = 0; i < rotation_count; i++)
bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::Clockwise));
}
}
return bitmap;
}
PDF::PDFErrorOr<void> 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<float>(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<float>(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);
}