1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 15:58:11 +00:00
serenity/Meta/Lagom/Contrib/MacPDF/MacPDFView.mm

268 lines
7 KiB
Text

/*
* Copyright (c) 2023, Nico Weber <thakis@chromium.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#import "MacPDFView.h"
#include <LibGfx/Bitmap.h>
#include <LibPDF/Document.h>
#include <LibPDF/Renderer.h>
@interface MacPDFView ()
{
WeakPtr<PDF::Document> _doc;
NSBitmapImageRep* _cachedBitmap;
int _page_index;
__weak id<MacPDFViewDelegate> _delegate;
PDF::RenderingPreferences _preferences;
}
@end
static PDF::PDFErrorOr<NonnullRefPtr<Gfx::Bitmap>> render(PDF::Document& document, int page_index, NSSize size, PDF::RenderingPreferences const& preferences)
{
auto page = TRY(document.get_page(page_index));
Gfx::IntSize page_size;
page_size.set_width(size.width);
page_size.set_height(size.height);
auto bitmap = TRY(Gfx::Bitmap::create(Gfx::BitmapFormat::BGRx8888, page_size));
auto errors = PDF::Renderer::render(document, page, bitmap, Color::White, preferences);
if (errors.is_error()) {
for (auto const& error : errors.error().errors())
NSLog(@"warning: %@", @(error.message().characters()));
}
return bitmap;
}
static NSBitmapImageRep* ns_from_gfx(NonnullRefPtr<Gfx::Bitmap> bitmap_p)
{
auto& bitmap = bitmap_p.leak_ref();
CGBitmapInfo info = kCGBitmapByteOrder32Little | (CGBitmapInfo)kCGImageAlphaFirst;
auto data = CGDataProviderCreateWithData(
&bitmap, bitmap.begin(), bitmap.size_in_bytes(),
[](void* p, void const*, size_t) {
(void)adopt_ref(*reinterpret_cast<Gfx::Bitmap*>(p));
});
auto space = CGColorSpaceCreateDeviceRGB();
auto cgbmp = CGImageCreate(bitmap.width(), bitmap.height(), 8,
32, bitmap.pitch(), space,
info, data, nullptr, false, kCGRenderingIntentDefault);
CGColorSpaceRelease(space);
CGDataProviderRelease(data);
auto* bmp = [[NSBitmapImageRep alloc] initWithCGImage:cgbmp];
CGImageRelease(cgbmp);
return bmp;
}
@implementation MacPDFView
// Called from MacPDFDocument.
- (void)setDocument:(WeakPtr<PDF::Document>)doc
{
_doc = move(doc);
_page_index = 0;
[self addObserver:self
forKeyPath:@"safeAreaRect"
options:NSKeyValueObservingOptionNew
context:nil];
[self invalidateCachedBitmap];
}
- (void)observeValueForKeyPath:(NSString*)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey, id>*)change
context:(void*)context
{
// AppKit by default doesn't invalidate a view if safeAreaRect changes but the view's bounds don't change.
// This happens for example when toggling the visibility of the toolbar with a full-size content view.
// We do want a repaint in this case.
VERIFY([keyPath isEqualToString:@"safeAreaRect"]);
VERIFY(object == self);
[self setNeedsDisplay:YES];
}
- (void)goToPage:(int)page
{
if (!_doc)
return;
int new_index = max(0, min(page - 1, _doc->get_page_count() - 1));
if (new_index == _page_index)
return;
_page_index = new_index;
[self invalidateRestorableState];
[self invalidateCachedBitmap];
[_delegate pageChanged];
}
- (int)page
{
return _page_index + 1;
}
- (void)setDelegate:(id<MacPDFViewDelegate>)delegate
{
_delegate = delegate;
}
#pragma mark - Drawing
- (void)invalidateCachedBitmap
{
_cachedBitmap = nil;
[self setNeedsDisplay:YES];
}
- (void)ensureCachedBitmapIsUpToDate
{
if (!_doc || _doc->get_page_count() == 0)
return;
NSSize pixel_size = [self convertSizeToBacking:self.safeAreaRect.size];
if (NSEqualSizes([_cachedBitmap size], pixel_size))
return;
if (auto bitmap_or = render(*_doc, _page_index, pixel_size, _preferences); !bitmap_or.is_error())
_cachedBitmap = ns_from_gfx(bitmap_or.value());
}
- (void)drawRect:(NSRect)rect
{
[self ensureCachedBitmapIsUpToDate];
[_cachedBitmap drawInRect:self.safeAreaRect];
}
#pragma mark - Keyboard handling
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (IBAction)goToNextPage:(id)sender
{
int current_page = _page_index + 1;
[self goToPage:current_page + 1];
}
- (IBAction)goToPreviousPage:(id)sender
{
int current_page = _page_index + 1;
[self goToPage:current_page - 1];
}
- (BOOL)validateMenuItem:(NSMenuItem*)item
{
if ([item action] == @selector(goToNextPage:))
return _doc ? (_page_index < (int)_doc->get_page_count() - 1) : NO;
if ([item action] == @selector(goToPreviousPage:))
return _doc ? (_page_index > 0) : NO;
if ([item action] == @selector(toggleShowClippingPaths:)) {
[item setState:_preferences.show_clipping_paths ? NSControlStateValueOn : NSControlStateValueOff];
return _doc ? YES : NO;
}
if ([item action] == @selector(toggleClipImages:)) {
[item setState:_preferences.clip_images ? NSControlStateValueOn : NSControlStateValueOff];
return _doc ? YES : NO;
}
if ([item action] == @selector(toggleClipPaths:)) {
[item setState:_preferences.clip_paths ? NSControlStateValueOn : NSControlStateValueOff];
return _doc ? YES : NO;
}
if ([item action] == @selector(toggleClipText:)) {
[item setState:_preferences.clip_text ? NSControlStateValueOn : NSControlStateValueOff];
return _doc ? YES : NO;
}
return NO;
}
- (IBAction)toggleShowClippingPaths:(id)sender
{
if (_doc) {
_preferences.show_clipping_paths = !_preferences.show_clipping_paths;
[self invalidateCachedBitmap];
}
}
- (IBAction)toggleClipImages:(id)sender
{
if (_doc) {
_preferences.clip_images = !_preferences.clip_images;
[self invalidateCachedBitmap];
}
}
- (IBAction)toggleClipPaths:(id)sender
{
if (_doc) {
_preferences.clip_paths = !_preferences.clip_paths;
[self invalidateCachedBitmap];
}
}
- (IBAction)toggleClipText:(id)sender
{
if (_doc) {
_preferences.clip_text = !_preferences.clip_text;
[self invalidateCachedBitmap];
}
}
- (void)keyDown:(NSEvent*)event
{
// Calls moveLeft: or moveRight: below.
[self interpretKeyEvents:@[ event ]];
}
// Called on down arrow.
- (IBAction)moveDown:(id)sender
{
[self goToNextPage:self];
}
// Called on left arrow.
- (IBAction)moveLeft:(id)sender
{
[self goToPreviousPage:self];
}
// Called on right arrow.
- (IBAction)moveRight:(id)sender
{
[self goToNextPage:self];
}
// Called on up arrow.
- (IBAction)moveUp:(id)sender
{
[self goToPreviousPage:self];
}
#pragma mark - State restoration
- (void)encodeRestorableStateWithCoder:(NSCoder*)coder
{
[coder encodeInt:_page_index forKey:@"PageIndex"];
NSLog(@"encodeRestorableStateWithCoder encoded %d", _page_index);
}
- (void)restoreStateWithCoder:(NSCoder*)coder
{
if ([coder containsValueForKey:@"PageIndex"]) {
int page_index = [coder decodeIntForKey:@"PageIndex"];
_page_index = min(max(0, page_index), _doc->get_page_count() - 1);
NSLog(@"encodeRestorableStateWithCoder restored %d", _page_index);
[self invalidateCachedBitmap];
[_delegate pageChanged];
}
}
@end