Serenity Operating System
1/*
2 * Copyright (c) 2021-2022, Matthew Olsson <mattco@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 *
5 * SPDX-License-Identifier: BSD-2-Clause
6 */
7
8#include "PDFViewer.h"
9#include <AK/Array.h>
10#include <AK/BinarySearch.h>
11#include <AK/HashFunctions.h>
12#include <LibConfig/Client.h>
13#include <LibGUI/Action.h>
14#include <LibGUI/MessageBox.h>
15#include <LibGUI/Painter.h>
16#include <LibPDF/Renderer.h>
17
18static constexpr int PAGE_PADDING = 10;
19
20static constexpr Array zoom_levels = {
21 17,
22 21,
23 26,
24 33,
25 41,
26 51,
27 64,
28 80,
29 100,
30 120,
31 144,
32 173,
33 207,
34 249,
35 299,
36 358,
37 430
38};
39
40PDFViewer::PDFViewer()
41{
42 set_should_hide_unnecessary_scrollbars(true);
43 set_focus_policy(GUI::FocusPolicy::StrongFocus);
44 set_scrollbars_enabled(true);
45
46 start_timer(30'000);
47
48 m_page_view_mode = static_cast<PageViewMode>(Config::read_i32("PDFViewer"sv, "Display"sv, "PageMode"sv, 0));
49 m_rendering_preferences.show_clipping_paths = Config::read_bool("PDFViewer"sv, "Rendering"sv, "ShowClippingPaths"sv, false);
50 m_rendering_preferences.show_images = Config::read_bool("PDFViewer"sv, "Rendering"sv, "ShowImages"sv, true);
51}
52
53PDF::PDFErrorOr<void> PDFViewer::set_document(RefPtr<PDF::Document> document)
54{
55 m_document = document;
56 m_current_page_index = document->get_first_page_index();
57 m_zoom_level = initial_zoom_level;
58 m_rendered_page_list.clear();
59
60 m_rendered_page_list.ensure_capacity(document->get_page_count());
61 for (u32 i = 0; i < document->get_page_count(); i++)
62 m_rendered_page_list.unchecked_append(HashMap<u32, RenderedPage>());
63
64 TRY(cache_page_dimensions(true));
65 update();
66
67 return {};
68}
69
70PDF::PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> PDFViewer::get_rendered_page(u32 index)
71{
72 auto key = pair_int_hash(m_rendering_preferences.hash(), m_zoom_level);
73 auto& rendered_page_map = m_rendered_page_list[index];
74 auto existing_rendered_page = rendered_page_map.get(key);
75 if (existing_rendered_page.has_value() && existing_rendered_page.value().rotation == m_rotations)
76 return existing_rendered_page.value().bitmap;
77
78 auto rendered_page = TRY(render_page(index));
79 rendered_page_map.set(key, { rendered_page, m_rotations });
80 return rendered_page;
81}
82
83void PDFViewer::paint_event(GUI::PaintEvent& event)
84{
85 GUI::Frame::paint_event(event);
86
87 GUI::Painter painter(*this);
88 painter.add_clip_rect(widget_inner_rect());
89 painter.add_clip_rect(event.rect());
90 painter.fill_rect(event.rect(), Color(0x80, 0x80, 0x80));
91
92 if (!m_document)
93 return;
94
95 auto handle_error = [&](PDF::Error& error) {
96 warnln("{}", error.message());
97 GUI::MessageBox::show_error(nullptr, "Failed to render the page."sv);
98 m_document.clear();
99 };
100
101 if (m_page_view_mode == PageViewMode::Single) {
102 auto maybe_page = get_rendered_page(m_current_page_index);
103 if (maybe_page.is_error()) {
104 handle_error(maybe_page.error());
105 return;
106 }
107
108 auto page = maybe_page.release_value();
109 set_content_size(page->size());
110
111 painter.translate(frame_thickness(), frame_thickness());
112 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
113
114 int x = max(0, (width() - page->width()) / 2);
115 int y = max(0, (height() - page->height()) / 2);
116
117 painter.blit({ x, y }, *page, page->rect());
118 return;
119 }
120
121 set_content_size({ m_page_dimension_cache.max_width, m_page_dimension_cache.total_height });
122
123 size_t first_page_index = 0;
124 size_t last_page_index = 0;
125
126 binary_search(m_page_dimension_cache.render_info, vertical_scrollbar().value(), &first_page_index, [](int height, PageDimensionCache::RenderInfo const& render_info) {
127 return height - render_info.total_height_before_this_page;
128 });
129
130 binary_search(m_page_dimension_cache.render_info, vertical_scrollbar().value() + height(), &last_page_index, [](int height, PageDimensionCache::RenderInfo const& render_info) {
131 return height - render_info.total_height_before_this_page;
132 });
133
134 auto initial_offset = m_page_dimension_cache.render_info[first_page_index].total_height_before_this_page - vertical_scrollbar().value();
135
136 painter.translate(frame_thickness(), frame_thickness());
137 painter.translate(-horizontal_scrollbar().value(), initial_offset);
138 auto middle = height() / 2;
139 auto y_offset = initial_offset;
140
141 for (size_t page_index = first_page_index; page_index <= last_page_index; page_index++) {
142 auto maybe_page = get_rendered_page(page_index);
143 if (maybe_page.is_error()) {
144 handle_error(maybe_page.error());
145 return;
146 }
147
148 auto page = maybe_page.release_value();
149
150 auto x = max(0, (width() - page->width()) / 2);
151
152 painter.blit({ x, PAGE_PADDING }, *page, page->rect());
153 auto diff_y = page->height() + PAGE_PADDING * 2;
154 painter.translate(0, diff_y);
155
156 if (y_offset < middle && y_offset + diff_y >= middle)
157 change_page(page_index);
158
159 y_offset += diff_y;
160 }
161}
162
163void PDFViewer::set_current_page(u32 current_page)
164{
165 m_current_page_index = current_page;
166 vertical_scrollbar().set_value(m_page_dimension_cache.render_info[current_page].total_height_before_this_page);
167 update();
168}
169
170void PDFViewer::set_show_clipping_paths(bool show_clipping_paths)
171{
172 m_rendering_preferences.show_clipping_paths = show_clipping_paths;
173 Config::write_bool("PDFViewer"sv, "Rendering"sv, "ShowClippingPaths"sv, show_clipping_paths);
174 update();
175}
176
177void PDFViewer::set_show_images(bool show_images)
178{
179 m_rendering_preferences.show_images = show_images;
180 Config::write_bool("PDFViewer"sv, "Rendering"sv, "ShowImages"sv, show_images);
181 update();
182}
183
184void PDFViewer::resize_event(GUI::ResizeEvent&)
185{
186 for (auto& map : m_rendered_page_list)
187 map.clear();
188 if (m_document)
189 MUST(cache_page_dimensions());
190 update();
191}
192
193void PDFViewer::mousewheel_event(GUI::MouseEvent& event)
194{
195 if (!m_document)
196 return;
197
198 bool scrolled_down = event.wheel_delta_y() > 0;
199
200 if (event.ctrl()) {
201 if (scrolled_down) {
202 zoom_out();
203 } else {
204 zoom_in();
205 }
206 return;
207 }
208
209 auto& scrollbar = event.shift() ? horizontal_scrollbar() : vertical_scrollbar();
210 auto delta = abs(event.wheel_delta_y() * 20);
211
212 if (m_page_view_mode == PageViewMode::Multiple) {
213 if (scrolled_down) {
214 if (scrollbar.value() != scrollbar.max())
215 scrollbar.increase_slider_by(delta);
216 } else {
217 if (scrollbar.value() > 0)
218 scrollbar.decrease_slider_by(delta);
219 }
220 } else {
221 if (scrolled_down) {
222 if (scrollbar.value() == scrollbar.max()) {
223 if (m_current_page_index < m_document->get_page_count() - 1) {
224 change_page(m_current_page_index + 1);
225 scrollbar.set_value(0);
226 }
227 } else {
228 scrollbar.increase_slider_by(delta);
229 }
230 } else {
231 if (scrollbar.value() == 0) {
232 if (m_current_page_index > 0) {
233 change_page(m_current_page_index - 1);
234 scrollbar.set_value(scrollbar.max());
235 }
236 } else {
237 scrollbar.decrease_slider_by(delta);
238 }
239 }
240 }
241
242 update();
243}
244
245void PDFViewer::mousedown_event(GUI::MouseEvent& event)
246{
247 if (event.button() == GUI::MouseButton::Middle) {
248 m_pan_starting_position = to_content_position(event.position());
249 set_override_cursor(Gfx::StandardCursor::Drag);
250 }
251}
252
253void PDFViewer::mouseup_event(GUI::MouseEvent&)
254{
255 set_override_cursor(Gfx::StandardCursor::None);
256}
257
258void PDFViewer::mousemove_event(GUI::MouseEvent& event)
259{
260 if (event.buttons() & GUI::MouseButton::Middle) {
261 auto delta = to_content_position(event.position()) - m_pan_starting_position;
262 horizontal_scrollbar().decrease_slider_by(delta.x());
263 vertical_scrollbar().decrease_slider_by(delta.y());
264 update();
265 }
266}
267
268void PDFViewer::timer_event(Core::TimerEvent&)
269{
270 // Clear the bitmap vector of all pages except the current page
271 for (size_t i = 0; i < m_rendered_page_list.size(); i++) {
272 if (i != m_current_page_index)
273 m_rendered_page_list[i].clear();
274 }
275}
276
277void PDFViewer::zoom_in()
278{
279 if (m_zoom_level < zoom_levels.size() - 1) {
280 m_zoom_level++;
281 MUST(cache_page_dimensions());
282 update();
283 }
284}
285
286void PDFViewer::zoom_out()
287{
288 if (m_zoom_level > 0) {
289 m_zoom_level--;
290 MUST(cache_page_dimensions());
291 update();
292 }
293}
294
295void PDFViewer::reset_zoom()
296{
297 m_zoom_level = initial_zoom_level;
298 MUST(cache_page_dimensions());
299 update();
300}
301
302void PDFViewer::rotate(int degrees)
303{
304 m_rotations = (m_rotations + degrees + 360) % 360;
305 MUST(cache_page_dimensions());
306 update();
307}
308
309void PDFViewer::set_page_view_mode(PageViewMode mode)
310{
311 m_page_view_mode = mode;
312 Config::write_i32("PDFViewer"sv, "Display"sv, "PageMode"sv, static_cast<i32>(mode));
313 update();
314}
315
316PDF::PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> PDFViewer::render_page(u32 page_index)
317{
318 auto page = TRY(m_document->get_page(page_index));
319 auto& page_size = m_page_dimension_cache.render_info[page_index].size;
320 auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, page_size.to_type<int>()));
321
322 auto maybe_errors = PDF::Renderer::render(*m_document, page, bitmap, m_rendering_preferences);
323 if (maybe_errors.is_error()) {
324 auto errors = maybe_errors.release_error();
325 on_render_errors(page_index, errors);
326 return bitmap;
327 }
328
329 if (page.rotate + m_rotations != 0) {
330 int rotation_count = ((page.rotate + m_rotations) / 90) % 4;
331 if (rotation_count == 3) {
332 bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::CounterClockwise));
333 } else {
334 for (int i = 0; i < rotation_count; i++)
335 bitmap = TRY(bitmap->rotated(Gfx::RotationDirection::Clockwise));
336 }
337 }
338
339 return bitmap;
340}
341
342PDF::PDFErrorOr<void> PDFViewer::cache_page_dimensions(bool recalculate_fixed_info)
343{
344 if (recalculate_fixed_info)
345 m_page_dimension_cache.page_info.clear_with_capacity();
346
347 if (m_page_dimension_cache.page_info.is_empty()) {
348 m_page_dimension_cache.page_info.ensure_capacity(m_document->get_page_count());
349 for (size_t i = 0; i < m_document->get_page_count(); i++) {
350 auto page = TRY(m_document->get_page(i));
351 auto box = page.media_box;
352 m_page_dimension_cache.page_info.unchecked_append(PageDimensionCache::PageInfo {
353 { box.width(), box.height() },
354 page.rotate,
355 });
356 }
357 }
358
359 auto zoom_scale_factor = static_cast<float>(zoom_levels[m_zoom_level]) / 100.0f;
360
361 m_page_dimension_cache.render_info.clear_with_capacity();
362 m_page_dimension_cache.render_info.ensure_capacity(m_page_dimension_cache.page_info.size());
363
364 float max_width = 0;
365 float total_height = 0;
366
367 for (size_t i = 0; i < m_page_dimension_cache.page_info.size(); i++) {
368 auto& [size, rotation] = m_page_dimension_cache.page_info[i];
369 rotation += m_rotations;
370 auto page_scale_factor = size.height() / size.width();
371
372 auto height = static_cast<float>(this->height() - 2 * frame_thickness()) * zoom_scale_factor - PAGE_PADDING * 2;
373 auto width = height / page_scale_factor;
374 if (rotation % 2)
375 swap(width, height);
376
377 max_width = max(max_width, width);
378
379 m_page_dimension_cache.render_info.append({
380 { width, height },
381 total_height,
382 });
383
384 total_height += height;
385 }
386
387 m_page_dimension_cache.max_width = max_width;
388 m_page_dimension_cache.total_height = total_height;
389
390 return {};
391}
392
393void PDFViewer::change_page(u32 new_page)
394{
395 m_current_page_index = new_page;
396 if (on_page_change)
397 on_page_change(m_current_page_index);
398}