feat: bookmark image file for secure access

This commit is contained in:
dscyrescotti
2024-06-16 11:50:21 +07:00
parent 333a57da2f
commit ec486bf412
13 changed files with 85 additions and 37 deletions

View File

@@ -86,6 +86,7 @@
ECBE52962C1D5900006BDB3D /* PhotoPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */; };
ECBE529C2C1D94A4006BDB3D /* CameraView.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529A2C1D94A4006BDB3D /* CameraView.swift */; };
ECBE529E2C1DAB21006BDB3D /* UIImage++.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */; };
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECC995A22C1E8F2800B2699A /* PhotoItem.swift */; };
ECD12A862C19EE3900B96E12 /* ElementObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A852C19EE3900B96E12 /* ElementObject.swift */; };
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A892C19EFB000B96E12 /* Element.swift */; };
ECD12A8C2C1AEAA900B96E12 /* PhotoObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */; };
@@ -187,6 +188,7 @@
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoPreview.swift; sourceTree = "<group>"; };
ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = "<group>"; };
ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = "<group>"; };
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = "<group>"; };
ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = "<group>"; };
ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = "<group>"; };
ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = "<group>"; };
@@ -678,6 +680,7 @@
isa = PBXGroup;
children = (
ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */,
ECC995A22C1E8F2800B2699A /* PhotoItem.swift */,
);
path = PhotoPreview;
sourceTree = "<group>";
@@ -926,6 +929,7 @@
ECD12A8A2C19EFB000B96E12 /* Element.swift in Sources */,
EC0D14282BF7BF20009BFE5F /* ContextMenuViewModifier.swift in Sources */,
EC2106AD2C10C2A700FBE27C /* AnyStroke.swift in Sources */,
ECC995A32C1E8F2800B2699A /* PhotoItem.swift in Sources */,
ECA738BC2BE60E0300A4542E /* Tool.swift in Sources */,
ECA738972BE6014200A4542E /* Graphic.metal in Sources */,
ECA7388A2BE6006A00A4542E /* PipelineStates.swift in Sources */,

View File

@@ -304,11 +304,11 @@ extension GraphicContext {
// MARK: - Photo
extension GraphicContext {
func insertPhoto(at point: CGPoint, url: URL) {
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
let size = CGSize(width: 100, height: 100)
let origin = point
let bounds = [origin.x - size.width / 2, origin.y - size.height / 2, origin.x + size.width / 2, origin.y + size.height / 2]
let photo = Photo(url: url, size: size, origin: origin, bounds: bounds, createdAt: .now)
let photo = Photo(url: photoItem.id, size: size, origin: origin, bounds: bounds, createdAt: .now, bookmark: photoItem.bookmark)
tree.insert(photo.element, in: photo.photoBox)
withPersistence(\.backgroundContext) { [_photo = photo, graphicContext = object] context in
let photo = PhotoObject(\.backgroundContext)
@@ -319,6 +319,7 @@ extension GraphicContext {
photo.originX = _photo.origin.x
photo.height = _photo.size.height
photo.createdAt = _photo.createdAt
photo.bookmark = _photo.bookmark
let element = ElementObject(\.backgroundContext)
element.createdAt = _photo.createdAt
element.type = 1

View File

@@ -128,20 +128,26 @@ extension Canvas {
// MARK: - Photo
extension Canvas {
func insertPhoto(at point: CGPoint, url: URL) {
graphicContext.insertPhoto(at: point, url: url)
func insertPhoto(at point: CGPoint, photoItem: PhotoItem) {
graphicContext.insertPhoto(at: point, photoItem: photoItem)
}
func savePhoto(_ data: Data) -> URL? {
func bookmarkPhoto(of image: UIImage) -> PhotoItem? {
guard let data = image.jpegData(compressionQuality: 1) else { return nil }
let fileManager = FileManager.default
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else { return nil }
guard let directory = fileManager.urls(for: .documentDirectory, in: .userDomainMask).first else {
return nil
}
let fileName = "\(UUID().uuidString)-\(Date.now.timeIntervalSince1970)"
let folder = directory.appendingPathComponent(canvasID.uriRepresentation().lastPathComponent, conformingTo: .folder)
if !fileManager.fileExists(atPath: folder.path()) {
if folder.startAccessingSecurityScopedResource(), !fileManager.fileExists(atPath: folder.path()) {
do {
try fileManager.createDirectory(at: folder, withIntermediateDirectories: true)
folder.stopAccessingSecurityScopedResource()
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
folder.stopAccessingSecurityScopedResource()
return nil
}
}
@@ -152,7 +158,14 @@ extension Canvas {
NSLog("[Memola] - \(error.localizedDescription)")
return nil
}
return file
var photoBookmark: PhotoItem?
do {
let bookmark = try file.bookmarkData(options: .minimalBookmark)
photoBookmark = PhotoItem(id: file, image: image, bookmark: bookmark)
} catch {
NSLog("[Memola] - \(error.localizedDescription)")
}
return photoBookmark
}
}

View File

@@ -16,6 +16,7 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
var url: URL?
var bounds: [CGFloat]
var createdAt: Date
var bookmark: Data?
var object: PhotoObject?
@@ -24,12 +25,13 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
var vertexCount: Int = 0
var vertexBuffer: MTLBuffer?
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date) {
init(url: URL?, size: CGSize, origin: CGPoint, bounds: [CGFloat], createdAt: Date, bookmark: Data?) {
self.size = size
self.origin = origin
self.url = url
self.bounds = bounds
self.createdAt = createdAt
self.bookmark = bookmark
generateVertices()
}
@@ -39,7 +41,8 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
size: .init(width: object.width, height: object.height),
origin: .init(x: object.originX, y: object.originY),
bounds: object.bounds,
createdAt: object.createdAt ?? .now
createdAt: object.createdAt ?? .now,
bookmark: object.bookmark
)
self.object = object
}
@@ -56,6 +59,17 @@ final class Photo: @unchecked Sendable, Equatable, Comparable {
PhotoVertex(x: maxX, y: maxY, textCoord: CGPoint(x: 1, y: 1)),
]
}
func getBookmarkURL() -> URL? {
var isStale = false
guard let bookmark else {
return nil
}
guard let bookmarkURL = try? URL(resolvingBookmarkData: bookmark, options: .withoutUI, relativeTo: nil, bookmarkDataIsStale: &isStale) else {
return nil
}
return bookmarkURL
}
}
extension Photo: Drawable {
@@ -64,7 +78,7 @@ extension Photo: Drawable {
vertexCount = vertices.endIndex
vertexBuffer = device.makeBuffer(bytes: vertices, length: vertexCount * MemoryLayout<PhotoVertex>.stride, options: [])
}
if texture == nil, let url {
if texture == nil, let url = getBookmarkURL() {
texture = Textures.createPhotoTexture(for: url, on: device)
}
}

View File

@@ -19,7 +19,7 @@ public class Tool: NSObject, ObservableObject {
@Published var selectedPen: Pen?
@Published var draggedPen: Pen?
// MARK: - Photo
@Published var selectedImageURL: URL?
@Published var selectedPhotoItem: PhotoItem?
@Published var selection: ToolSelection = .none

View File

@@ -233,12 +233,12 @@ extension CanvasViewController {
}
@objc func recognizeTapGesture(_ gesture: UITapGestureRecognizer) {
guard let url = tool.selectedImageURL else { return }
guard let photoItem = tool.selectedPhotoItem else { return }
withAnimation {
tool.selectedImageURL = nil
tool.selectedPhotoItem = nil
}
let point = gesture.location(in: drawingView)
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), url: url)
canvas.insertPhoto(at: point.muliply(by: drawingView.ratio), photoItem: photoItem)
drawingView.draw()
}
}

View File

@@ -8,7 +8,7 @@
import SwiftUI
struct CameraView: UIViewControllerRepresentable {
@Binding var url: URL?
@Binding var image: UIImage?
@ObservedObject var canvas: Canvas
@@ -35,10 +35,7 @@ struct CameraView: UIViewControllerRepresentable {
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
let image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
if let image, let data = image.jpegData(compressionQuality: 1) {
parent.url = parent.canvas.savePhoto(data)
}
parent.image = (info[.originalImage] as? UIImage)?.imageWithUpOrientation()
parent.dismiss()
}

View File

@@ -35,8 +35,8 @@ struct MemoView: View {
PenDock(tool: tool, canvas: canvas)
.transition(.move(edge: .trailing))
case .photo:
if let url = tool.selectedImageURL {
PhotoPreview(url: url, tool: tool)
if let photoItem = tool.selectedPhotoItem {
PhotoPreview(photoItem: photoItem, tool: tool)
.transition(.move(edge: .trailing))
}
default:

View File

@@ -0,0 +1,15 @@
//
// PhotoItem.swift
// Memola
//
// Created by Dscyre Scotti on 6/16/24.
//
import UIKit
import Foundation
struct PhotoItem: Identifiable, Equatable {
var id: URL
let image: UIImage
let bookmark: Data
}

View File

@@ -8,15 +8,11 @@
import SwiftUI
struct PhotoPreview: View {
let url: URL
let photoItem: PhotoItem
@ObservedObject var tool: Tool
var data: Data {
(try? Data(contentsOf: url)) ?? Data()
}
var body: some View {
Image(uiImage: UIImage(data: data) ?? UIImage())
Image(uiImage: photoItem.image)
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
@@ -31,7 +27,7 @@ struct PhotoPreview: View {
.overlay(alignment: .topLeading) {
Button {
withAnimation {
tool.selectedImageURL = nil
tool.selectedPhotoItem = nil
}
} label: {
Image(systemName: "xmark.circle.fill")

View File

@@ -19,8 +19,8 @@ struct Toolbar: View {
@State var title: String
@State var memo: MemoObject
@State var photoItem: PhotosPickerItem?
@State var opensCamera: Bool = false
@State var photosPickerItem: PhotosPickerItem?
@State var isCameraAccessDenied: Bool = false
@FocusState var textFieldState: Bool
@@ -56,22 +56,28 @@ struct Toolbar: View {
}
.font(.subheadline)
.padding(10)
.onChange(of: photoItem) { oldValue, newValue in
.onChange(of: photosPickerItem) { oldValue, newValue in
if newValue != nil {
Task {
let data = try? await newValue?.loadTransferable(type: Data.self)
if let data {
let url = canvas.savePhoto(data)
if let data, let image = UIImage(data: data) {
let photoItem = canvas.bookmarkPhoto(of: image)
withAnimation {
tool.selectedImageURL = url
tool.selectedPhotoItem = photoItem
}
}
photoItem = nil
photosPickerItem = nil
}
}
}
.fullScreenCover(isPresented: $opensCamera) {
CameraView(url: $tool.selectedImageURL, canvas: canvas)
let image: Binding<UIImage?> = Binding {
tool.selectedPhotoItem?.image
} set: { image in
guard let image else { return }
tool.selectedPhotoItem = canvas.bookmarkPhoto(of: image)
}
CameraView(image: image, canvas: canvas)
.ignoresSafeArea()
}
.alert("Camera Access Denied", isPresented: $isCameraAccessDenied) {
@@ -167,7 +173,7 @@ struct Toolbar: View {
.clipShape(.rect(cornerRadius: 8))
}
.hoverEffect(.lift)
PhotosPicker(selection: $photoItem, matching: .images, preferredItemEncoding: .compatible) {
PhotosPicker(selection: $photosPickerItem, matching: .images, preferredItemEncoding: .compatible) {
Image(systemName: "photo.fill.on.rectangle.fill")
.contentShape(.circle)
.frame(width: size, height: size)

View File

@@ -17,5 +17,6 @@ class PhotoObject: NSManagedObject {
@NSManaged var bounds: [CGFloat]
@NSManaged var createdAt: Date?
@NSManaged var imageURL: URL?
@NSManaged var bookmark: Data?
@NSManaged var element: ElementObject?
}

View File

@@ -42,6 +42,7 @@
<relationship name="tool" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="ToolObject" inverseName="pens" inverseEntity="ToolObject"/>
</entity>
<entity name="PhotoObject" representedClassName="PhotoObject" syncable="YES">
<attribute name="bookmark" optional="YES" attributeType="Binary"/>
<attribute name="bounds" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromDataTransformer" customClassName="[CGFloat]"/>
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="height" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>