WKDrawingView.mm   [plain text]


/*
 * Copyright (C) 2018 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#import "config.h"
#import "WKDrawingView.h"

#if HAVE(PENCILKIT)

#import "EditableImageController.h"
#import "WKInkPickerView.h"
#import <wtf/OSObjectPtr.h>
#import <wtf/RetainPtr.h>

#import "PencilKitSoftLink.h"

@interface WKDrawingView () <PKCanvasViewDelegate>
@end

@implementation WKDrawingView {
    RetainPtr<PKCanvasView> _pencilView;

#if !PLATFORM(IOS_FAMILY_SIMULATOR)
    OSObjectPtr<dispatch_queue_t> _renderQueue;
    RetainPtr<PKImageRenderer> _renderer;
#endif

    __weak WKContentView *_contentView;
}

- (instancetype)initWithEmbeddedViewID:(WebCore::GraphicsLayer::EmbeddedViewID)embeddedViewID contentView:(WKContentView *)contentView
{
    self = [super initWithEmbeddedViewID:embeddedViewID];
    if (!self)
        return nil;

    _contentView = contentView;

    _pencilView = adoptNS([WebKit::allocPKCanvasViewInstance() initWithFrame:CGRectZero]);

    [_pencilView setFingerDrawingEnabled:NO];
    [_pencilView setUserInteractionEnabled:YES];
    [_pencilView setOpaque:NO];
    [_pencilView setDelegate:self];
    [_pencilView setRulerHostingDelegate:_contentView._drawingCoordinator];

    [self addSubview:_pencilView.get()];

    return self;
}

- (void)layoutSubviews
{
    if (!CGRectEqualToRect([_pencilView frame], self.bounds)) {
        [_pencilView setFrame:self.bounds];

#if !PLATFORM(IOS_FAMILY_SIMULATOR)
        // The renderer is instantiated for a particular size output; if
        // the size changes, we need to re-create the renderer.
        _renderer = nil;
#endif

        [self invalidateAttachment];
    }
}

static UIImage *emptyImage()
{
    UIGraphicsBeginImageContext(CGSizeMake(1, 1));
    CGContextClearRect(UIGraphicsGetCurrentContext(), CGRectMake(0, 0, 1, 1));
    UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    return resultImage;
}

- (UIImage *)renderedDrawing
{
#if PLATFORM(IOS_FAMILY_SIMULATOR)
    // PKImageRenderer currently doesn't work in the simulator. In order to
    // allow strokes to persist regardless (mostly for testing), we'll
    // synthesize an empty 1x1 image.
    return emptyImage();
#else
    if (!self.bounds.size.width || !self.bounds.size.height || !self.window.screen.scale)
        return emptyImage();

    if (!_renderQueue)
        _renderQueue = adoptOSObject(dispatch_queue_create("com.apple.WebKit.WKDrawingView.Rendering", DISPATCH_QUEUE_SERIAL));

    if (!_renderer)
        _renderer = adoptNS([WebKit::allocPKImageRendererInstance() initWithSize:self.bounds.size scale:self.window.screen.scale renderQueue:_renderQueue.get()]);

    __block RetainPtr<UIImage> resultImage;

    [_renderer renderDrawing:[_pencilView drawing] completion:^(UIImage *image) {
        resultImage = image;
    }];

    // FIXME: Ideally we would not synchronously wait for this rendering,
    // but NSFileWrapper requires data synchronously, and our clients expect
    // an NSFileWrapper to be available synchronously.
    dispatch_sync(_renderQueue.get(), ^{ });

    return resultImage.autorelease();
#endif
}

- (NSData *)PNGRepresentation
{
    RetainPtr<UIImage> image = [self renderedDrawing];
    RetainPtr<NSMutableData> PNGData = adoptNS([[NSMutableData alloc] init]);
    RetainPtr<CGImageDestinationRef> imageDestination = adoptCF(CGImageDestinationCreateWithData((__bridge CFMutableDataRef)PNGData.get(), kUTTypePNG, 1, nil));
    NSString *base64Drawing = [[[_pencilView drawing] serialize] base64EncodedStringWithOptions:0];
    NSDictionary *properties = nil;
    if (base64Drawing) {
        // FIXME: We should put this somewhere less user-facing than the EXIF User Comment field.
        properties = @{
            (__bridge NSString *)kCGImagePropertyExifDictionary : @{
                (__bridge NSString *)kCGImagePropertyExifUserComment : base64Drawing
            }
        };
    }
    CGImageDestinationSetProperties(imageDestination.get(), (__bridge CFDictionaryRef)properties);
    CGImageDestinationAddImage(imageDestination.get(), [image CGImage], (__bridge CFDictionaryRef)properties);
    CGImageDestinationFinalize(imageDestination.get());

    return PNGData.autorelease();
}

- (void)loadDrawingFromPNGRepresentation:(NSData *)PNGData
{
    RetainPtr<CGImageSourceRef> imageSource = adoptCF(CGImageSourceCreateWithData((__bridge CFDataRef)PNGData, nullptr));
    if (!imageSource)
        return;
    RetainPtr<NSDictionary> properties = adoptNS((__bridge NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource.get(), 0, nil));
    NSString *base64Drawing = [[properties objectForKey:(NSString *)kCGImagePropertyExifDictionary] objectForKey:(NSString *)kCGImagePropertyExifUserComment];
    if (!base64Drawing)
        return;
    RetainPtr<NSData> drawingData = adoptNS([[NSData alloc] initWithBase64EncodedString:base64Drawing options:0]);
    RetainPtr<PKDrawing> drawing = adoptNS([WebKit::allocPKDrawingInstance() initWithData:drawingData.get() error:nil]);
    [_pencilView setDrawing:drawing.get()];
}

- (void)drawingDidChange:(PKCanvasView *)canvasView
{
    [self invalidateAttachment];
}

- (void)_canvasViewWillBeginDrawing:(PKCanvasView *)canvasView
{
    [_pencilView setInk:_contentView._drawingCoordinator.inkPicker.ink];
}

- (void)invalidateAttachment
{
    if (!_contentView.page)
        return;
    auto& page = *_contentView.page;

    page.editableImageController().invalidateAttachmentForEditableImage(self.embeddedViewID);
}

- (void)didChangeRulerState:(BOOL)rulerEnabled
{
    [_pencilView setRulerEnabled:rulerEnabled];
}

- (void)didChangeInk:(PKInk *)ink
{
    [_pencilView setInk:ink];
}

@end

#endif // HAVE(PENCILKIT)