Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I save and restore drawings from the canvas? #21

Open
gitton opened this issue Feb 22, 2019 · 31 comments
Open

How can I save and restore drawings from the canvas? #21

gitton opened this issue Feb 22, 2019 · 31 comments

Comments

@gitton
Copy link

gitton commented Feb 22, 2019

How can I save the drawing to a database and later on give the possibility to edit or add new content to the same drawing?

@LinusGeffarth
Copy link
Collaborator

see #18

@gitton
Copy link
Author

gitton commented Feb 22, 2019

issue #18 help in save as image. But I want to restore the same drawing and user can select the drawing and remove it like Google keep.

@LinusGeffarth
Copy link
Collaborator

Ohh I get it. Sorry, I misread your question.

This is an issue I've been working on for a while. Doing it the image way is simple, but is not really a great solution because you can't edit it anymore.
I've tried Pathology, and it works well for small drawings. However, once you have more lines, it takes forever to read & transform them from a json string.

@LinusGeffarth LinusGeffarth reopened this Feb 22, 2019
@gitton
Copy link
Author

gitton commented Feb 22, 2019

Thank you for sharing.

@LinusGeffarth
Copy link
Collaborator

Let me know if you find a solution that we could use for the library. Would be a great addition :)

@EvanCooper9
Copy link

I've been able to accomplish this by storing the lines property, and then later passing them to display(lines: [Line])

@LinusGeffarth
Copy link
Collaborator

LinusGeffarth commented Feb 22, 2019

What exactly did you manage to store? Using Pathology?
How does it perform with a lot of lines?

@lucashoeft
Copy link

lucashoeft commented Apr 9, 2019

I build my own simple drawing app which temporary stores the lines as structs (similar to your approach). I made those line struct codable therefore I can export the array of line structs as JSON. I retrieve the drawing by decoding the JSON to the array of line structs and updating the view. I tested a drawing with roughly 2000 CGPoints and it took less than a second to display the drawing in the UIView. Let me know if you have any questions!

@LinusGeffarth
Copy link
Collaborator

@lucashoeft sounds great! Would you like to share your implementation? Did you use SwiftyDraw for your app or some other library/custom solution?

@lucashoeft
Copy link

lucashoeft commented Apr 9, 2019

Yeah, sure. I followed the tutorial on YT: https://www.youtube.com/watch?v=E2NTCmEsdSE (it's a three part series) so I did not use SwiftyDraw, but complexity should be similar

The structs (simplified):

struct Canvas: Codable {
    var lines: [Line]
}

struct Line: Codable {
    var points: [CGPoint]
    var color: String
    var strokeWidth: Float
    var strokeOpacity: Float
    var lineStyle: String
}

The User Input in the UIView

var canvas = Canvas()

override func draw(_ rect: CGRect) {
        super.draw(rect)

        guard let context = UIGraphicsGetCurrentContext() else { return }
        
        canvas.lines?.forEach { (line) in
            context.setStrokeColor(UIColor.init(hex: line.color)?.cgColor ?? UIColor.black.cgColor)
            context.setLineWidth(line.strokeWidth)
            context.setLineCap(.round)
            context.setLineJoin(.round)
            
            for (i, p) in line.points.enumerated() {
                if i == 0 {
                    context.move(to: p)
                } else {
                    context.addLine(to: p)
                }
            }
            context.strokePath()
        }
    }

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        canvas.lines?.append(Line(points: [], color: strokeColor, strokeWidth: strokeWidth, strokeOpacity: strokeOpacity, lineStyle: "Line"))
    }
    
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let point = touches.first?.location(in: self) else { return }
        guard var lastLine = canvas.lines?.popLast() else { return }
        lastLine.points.append(point)
        canvas.lines?.append(lastLine)
        
        setNeedsDisplay()
    }

The export functions (the conversion to string is optional as I wanted to use strings for testing saving/retrieving locally):

func getDrawing() -> String {
        let jsonEncoder = JSONEncoder()
        
        let jsonData = try! jsonEncoder.encode(canvas)
        
        if let jsonString = String(data: jsonData, encoding: String.Encoding.utf8) {
            return jsonString
        } else {
            return ""
        }
    }
    
   func setDrawing(jsonString: String) {
        let jsonDecoder = JSONDecoder()
        
        let retrievedCanvas = try! jsonDecoder.decode(Canvas.self, from: jsonString.data(using: .utf8)!)
        canvas = retrievedCanvas
        setNeedsDisplay()
    }

@LinusGeffarth
Copy link
Collaborator

LinusGeffarth commented Apr 9, 2019

@lucashoeft thanks for sharing! the implementation of the drawing logic looks pretty similar as far as I can tell. I will try to integrate this into SwiftyDraw and see if I can get it to work. Will keep you posted :)

@EvanCooper9
Copy link

Yikes. Forgot to follow up on this. Sorry guys. Looks like @lucashoeft has the right idea.

@LinusGeffarth
Copy link
Collaborator

LinusGeffarth commented Apr 10, 2019

@EvanCooper9 you can still share what you've done if you'd like. We can compare the approaches and see which one works best for SwiftyDraw :)

@LinusGeffarth
Copy link
Collaborator

@lucashoeft I've checked out your code above. Regarding the drawing itself: when drawing really fast, the lines get very choppy. Any idea how to deal with that?

IMG_DF25C7739B63-1

@lucashoeft
Copy link

It's because of the "simple" drawing function (it only connects the detected touches/points with direct lines..

for (i, p) in line.points.enumerated() {
  if i == 0 {
    context.move(to: p)
  } else {
    context.addLine(to: p)
  }
}
context.strokePath()

In SwiftyDraw, MidPoints get calculated and added to the array of points to have more entries to display a smoother line. I'm not sure if it is Hermite Interpolation but the approach looks quite similar

@LinusGeffarth
Copy link
Collaborator

LinusGeffarth commented Apr 19, 2019

@lucashoeft I figured that, but how do we combine the two approaches? The thing is that CGMutablePath (SwiftyDraw uses) isn't mutable - with Pathology it is, but then we're back at the major performance issues with decoding the CGMutablePaths

@bhaveshbc
Copy link

bhaveshbc commented Sep 2, 2019

I have same issue. I solve it by converting CGmutablePath to UIBezierPath to store in Coredata.then convert again UIbaizerPath to CGmutablePath to display on SwiftyDrawView.

Following is Code to Store CGmutablePath in Coredata :

for drawpath in drawbleView.drawingHistory {
                let baizerpath = UIBezierPath(cgPath: drawpath.path)
                
                
                let drawableBrush = drawpath.brush
                let brushobj = Brushh(context: context)
                brushobj.color = drawableBrush.color.toHexString()
                brushobj.width = Float(drawableBrush.width)
                if drawableBrush.blendMode == .clear {
                    brushobj.mode = 0
                }
                else {
                    brushobj.mode = 1
                }
                
                let linesobj = Lines(context: context)
                linesobj.path = baizerpath  as NSObject
                linesobj.brush = brushobj
                arrayofLines.append(linesobj)
            }

Following is code to Create CGmutablePath From UIbaizerPath

let element = baizaar.cgPath.pathElements()
                        let  mutablePath = CGMutablePath()
                        for pathElement in element {
                            switch pathElement {
                            case .moveToPoint(let pt): mutablePath.move(to: pt)
                            case .addLineToPoint(let pt): mutablePath.addLine(to: pt)
                            case .addQuadCurveToPoint(let pt1, let pt2): mutablePath.addQuadCurve(to: pt2, control: pt1)
                            case .addCurveToPoint(let pt1, let pt2, let pt3): print("do nothing \(pt1) \(pt2) \(pt3)")
                            case .closeSubpath: mutablePath.closeSubpath()
                            }
                        }
                        let drawableBrush = SwiftyDrawView.Line(path: mutablePath, brush: brush)

following is extension to get Pathelement From UIbaizerPath


public enum PathElement {
    case moveToPoint(CGPoint)
    case addLineToPoint(CGPoint)
    case addQuadCurveToPoint(CGPoint, CGPoint)
    case addCurveToPoint(CGPoint, CGPoint, CGPoint)
    case closeSubpath
}

internal class Info {
    var pathElements = [PathElement]()
}


//
//    CGPathRef
//
public extension CGPath {
    
    func pathElements() -> [PathElement] {
        var info = Info()
        
        
        self.apply(info: &info) { (info, element) -> Void in
            
            if let infoPointer = UnsafeMutablePointer<Info>(OpaquePointer(info)) {
                switch element.pointee.type {
                case .moveToPoint:
                    let pt = element.pointee.points[0]
                    infoPointer.pointee.pathElements.append(PathElement.moveToPoint(pt))
                //print("MoveToPoint \(pt)")
                case .addLineToPoint:
                    let pt = element.pointee.points[0]
                    infoPointer.pointee.pathElements.append(PathElement.addLineToPoint(pt))
                //print("AddLineToPoint \(pt)")
                case .addQuadCurveToPoint:
                    let pt1 = element.pointee.points[0]
                    let pt2 = element.pointee.points[1]
                    infoPointer.pointee.pathElements.append(PathElement.addQuadCurveToPoint(pt1, pt2))
                //print("AddQuadCurveToPoint \(pt1) \(pt2)")
                case .addCurveToPoint:
                    let pt1 = element.pointee.points[0]
                    let pt2 = element.pointee.points[1]
                    let pt3 = element.pointee.points[2]
                    infoPointer.pointee.pathElements.append(PathElement.addCurveToPoint(pt1, pt2, pt3))
                //print("AddCurveToPoint \(pt1) \(pt2) \(pt3)")
                case .closeSubpath:
                    infoPointer.pointee.pathElements.append(PathElement.closeSubpath)
                    //print("CloseSubpath")
                @unknown default:
                    print("do nothinh")
                }
            }
        }
        
        return info.pathElements
    }
    
}

//
//    operator ==
//
public func == (lhs: PathElement, rhs: PathElement) -> Bool {
    switch (lhs, rhs) {
    case (.moveToPoint(let a), .moveToPoint(let b)):
        return a == b
    case (.addLineToPoint(let a), .addLineToPoint(let b)):
        return a == b
    case (.addQuadCurveToPoint(let a1, let a2), .addQuadCurveToPoint(let b1, let b2)):
        return a1.equalTo(b1) && a2.equalTo(b2)
    case (.addCurveToPoint(let a1, let a2, let a3), .addCurveToPoint(let b1, let b2, let b3)):
        return a1 == b1 && a2 == b2 && a3 == b3
    case (.closeSubpath, .closeSubpath):
        return true
    default:
        return false
    }
}

@LinusGeffarth
Copy link
Collaborator

Cool, thanks @bhaveshbc for sharing. Does your code:

  1. load & save data quickly?
  2. allow for smooth drawing? – as opposed to the previous approach with the choppy lines

@bhaveshbc
Copy link

  1. Yes it just take couple of second.
  2. Yes it preserve the quality.

@LinusGeffarth
Copy link
Collaborator

LinusGeffarth commented Sep 2, 2019

Cool, then I'll try it out and merge into the repo if suitable.

@LinusGeffarth
Copy link
Collaborator

So I just tried to implement your code and I have a couple of questions:

  1. in the first snippet, where do you get context from?
  2. did you create a custom init(context:) method for Brush and Line?
  3. in the end, which data do you save to core data? arrayoflines?
  4. in the second snippet, where do you get baizaar from?

Would appreciate your help on this!
@bhaveshbc

@LinusGeffarth LinusGeffarth changed the title How can i save the drawing and restore the drawing. How can I save and restore drawings from the canvas? Sep 15, 2019
@bhaveshbc
Copy link

bhaveshbc commented Sep 25, 2019

HI @LinusGeffarth sorry for late response.

  1. context is coredata ViewContext.ex: let context = appdel.persistentContainer.viewContext
  2. no those method created automatically by coredata according to model
  3. yes
  4. baizaar is baizaarpath

Screenshot 2019-09-25 at 2 06 49 PM

https://www.dropbox.com/s/adswe9z4cl6cbq3/DemoMLView%2019.zip?dl=0

@LinusGeffarth
Copy link
Collaborator

@bhaveshbc so far I've not been able to get your code running. Would you mind forking the project and creating a PR? That'd be really helpful!

@LinusGeffarth
Copy link
Collaborator

@kwccheng hey, see my comment from above about why that does not work as one would expect...

@LinusGeffarth
Copy link
Collaborator

swiping between PDF images

What do you mean by that?
I haven't seen any performance issues saving images...

@blu3mo
Copy link
Contributor

blu3mo commented Aug 15, 2020

I've managed to convert CGMutablePath to Data (and base64).

// encoding
let cgMutablePath = line.path
let uiBazierPath = UIBezierPath(cgPath: cgMutablePath)
let data = try? NSKeyedArchiver.archivedData(withRootObject: uiBaizerPath, requiringSecureCoding: false)
let base64String = data?.base64EncodedString()
// decoding
let data = Data(base64Encoded: base64String)
let uiBaizerPath = try! NSKeyedUnarchiver.unarchivedObject(ofClass: UIBezierPath.self, from: data)
let cgMutablePath = uiBaizerPath.cgPath as! CGMutablePath

I used this code to send/receive SwiftyDraw Lines using Firebase Realtime Database.
It might be useful for this issue.

@LinusGeffarth
Copy link
Collaborator

@blu3mo thanks for you contribution. How fast is the de-/encoding though? Can you load very many lines at a high speed?

@blu3mo
Copy link
Contributor

blu3mo commented Aug 15, 2020

It took about 0.01s for encoding, 0.005s for decoding + presenting the drawing below. (156 strokes)
https://user-images.githubusercontent.com/31824270/90310914-efa1bd00-df30-11ea-9d46-4acd290204e7.jpeg

@LinusGeffarth
Copy link
Collaborator

Cool. Can you open a PR?

@blu3mo
Copy link
Contributor

blu3mo commented Aug 20, 2020

Could you check my PR?

@blu3mo
Copy link
Contributor

blu3mo commented Mar 24, 2021

I think this issue can be closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants