From ec486bf412ce11049db3c9147d41c8fcfd44ddc8 Mon Sep 17 00:00:00 2001 From: dscyrescotti Date: Sun, 16 Jun 2024 11:50:21 +0700 Subject: [PATCH] feat: bookmark image file for secure access --- Memola.xcodeproj/project.pbxproj | 4 +++ Memola/Canvas/Contexts/GraphicContext.swift | 5 ++-- Memola/Canvas/Core/Canvas.swift | 25 ++++++++++++++----- Memola/Canvas/Elements/Photo/Photo.swift | 20 ++++++++++++--- Memola/Canvas/Tool/Core/Tool.swift | 2 +- .../ViewController/CanvasViewController.swift | 6 ++--- .../Views/CameraView/CameraView.swift | 7 ++---- Memola/Features/Memo/Memo/MemoView.swift | 4 +-- .../Memo/PhotoPreview/PhotoItem.swift | 15 +++++++++++ .../Memo/PhotoPreview/PhotoPreview.swift | 10 +++----- Memola/Features/Memo/Toolbar/Toolbar.swift | 22 ++++++++++------ Memola/Persistence/Objects/PhotoObject.swift | 1 + .../MemolaModel.xcdatamodel/contents | 1 + 13 files changed, 85 insertions(+), 37 deletions(-) create mode 100644 Memola/Features/Memo/PhotoPreview/PhotoItem.swift diff --git a/Memola.xcodeproj/project.pbxproj b/Memola.xcodeproj/project.pbxproj index c07e954..797e920 100644 --- a/Memola.xcodeproj/project.pbxproj +++ b/Memola.xcodeproj/project.pbxproj @@ -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 = ""; }; ECBE529A2C1D94A4006BDB3D /* CameraView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CameraView.swift; sourceTree = ""; }; ECBE529D2C1DAB21006BDB3D /* UIImage++.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage++.swift"; sourceTree = ""; }; + ECC995A22C1E8F2800B2699A /* PhotoItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoItem.swift; sourceTree = ""; }; ECD12A852C19EE3900B96E12 /* ElementObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ElementObject.swift; sourceTree = ""; }; ECD12A892C19EFB000B96E12 /* Element.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Element.swift; sourceTree = ""; }; ECD12A8B2C1AEAA900B96E12 /* PhotoObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PhotoObject.swift; sourceTree = ""; }; @@ -678,6 +680,7 @@ isa = PBXGroup; children = ( ECBE52952C1D5900006BDB3D /* PhotoPreview.swift */, + ECC995A22C1E8F2800B2699A /* PhotoItem.swift */, ); path = PhotoPreview; sourceTree = ""; @@ -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 */, diff --git a/Memola/Canvas/Contexts/GraphicContext.swift b/Memola/Canvas/Contexts/GraphicContext.swift index 9862b5c..38db14e 100644 --- a/Memola/Canvas/Contexts/GraphicContext.swift +++ b/Memola/Canvas/Contexts/GraphicContext.swift @@ -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 diff --git a/Memola/Canvas/Core/Canvas.swift b/Memola/Canvas/Core/Canvas.swift index f6c373e..774de81 100644 --- a/Memola/Canvas/Core/Canvas.swift +++ b/Memola/Canvas/Core/Canvas.swift @@ -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 } } diff --git a/Memola/Canvas/Elements/Photo/Photo.swift b/Memola/Canvas/Elements/Photo/Photo.swift index b20055f..15df4e3 100644 --- a/Memola/Canvas/Elements/Photo/Photo.swift +++ b/Memola/Canvas/Elements/Photo/Photo.swift @@ -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.stride, options: []) } - if texture == nil, let url { + if texture == nil, let url = getBookmarkURL() { texture = Textures.createPhotoTexture(for: url, on: device) } } diff --git a/Memola/Canvas/Tool/Core/Tool.swift b/Memola/Canvas/Tool/Core/Tool.swift index 547d059..92ffda8 100644 --- a/Memola/Canvas/Tool/Core/Tool.swift +++ b/Memola/Canvas/Tool/Core/Tool.swift @@ -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 diff --git a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift index 96dfebb..c995c08 100644 --- a/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift +++ b/Memola/Canvas/View/Bridge/ViewController/CanvasViewController.swift @@ -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() } } diff --git a/Memola/Components/Views/CameraView/CameraView.swift b/Memola/Components/Views/CameraView/CameraView.swift index 8cafeef..4be3e8b 100644 --- a/Memola/Components/Views/CameraView/CameraView.swift +++ b/Memola/Components/Views/CameraView/CameraView.swift @@ -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() } diff --git a/Memola/Features/Memo/Memo/MemoView.swift b/Memola/Features/Memo/Memo/MemoView.swift index f2547e1..362bffb 100644 --- a/Memola/Features/Memo/Memo/MemoView.swift +++ b/Memola/Features/Memo/Memo/MemoView.swift @@ -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: diff --git a/Memola/Features/Memo/PhotoPreview/PhotoItem.swift b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift new file mode 100644 index 0000000..a857da5 --- /dev/null +++ b/Memola/Features/Memo/PhotoPreview/PhotoItem.swift @@ -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 +} diff --git a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift index 7696b8b..ccdbc57 100644 --- a/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift +++ b/Memola/Features/Memo/PhotoPreview/PhotoPreview.swift @@ -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") diff --git a/Memola/Features/Memo/Toolbar/Toolbar.swift b/Memola/Features/Memo/Toolbar/Toolbar.swift index 2a40b36..427d9aa 100644 --- a/Memola/Features/Memo/Toolbar/Toolbar.swift +++ b/Memola/Features/Memo/Toolbar/Toolbar.swift @@ -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 = 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) diff --git a/Memola/Persistence/Objects/PhotoObject.swift b/Memola/Persistence/Objects/PhotoObject.swift index e43de2c..10283e8 100644 --- a/Memola/Persistence/Objects/PhotoObject.swift +++ b/Memola/Persistence/Objects/PhotoObject.swift @@ -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? } diff --git a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents index f12e8a8..0388678 100644 --- a/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents +++ b/Memola/Resources/Models/MemolaModel.xcdatamodeld/MemolaModel.xcdatamodel/contents @@ -42,6 +42,7 @@ +