SwiftyDraw icon indicating copy to clipboard operation
SwiftyDraw copied to clipboard

How can I save and restore drawings from the canvas?

Open gitton opened this issue 5 years ago • 31 comments

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?

gitton avatar Feb 22 '19 08:02 gitton

see #18

LinusGeffarth avatar Feb 22 '19 11:02 LinusGeffarth

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.

gitton avatar Feb 22 '19 11:02 gitton

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 avatar Feb 22 '19 12:02 LinusGeffarth

Thank you for sharing.

gitton avatar Feb 22 '19 12:02 gitton

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

LinusGeffarth avatar Feb 22 '19 12:02 LinusGeffarth

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

EvanCooper9 avatar Feb 22 '19 15:02 EvanCooper9

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

LinusGeffarth avatar Feb 22 '19 16:02 LinusGeffarth

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!

lucashoeft avatar Apr 09 '19 13:04 lucashoeft

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

LinusGeffarth avatar Apr 09 '19 13:04 LinusGeffarth

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()
    }

lucashoeft avatar Apr 09 '19 13:04 lucashoeft

@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 :)

LinusGeffarth avatar Apr 09 '19 14:04 LinusGeffarth

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

EvanCooper9 avatar Apr 10 '19 01:04 EvanCooper9

@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 avatar Apr 10 '19 07:04 LinusGeffarth

@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

LinusGeffarth avatar Apr 17 '19 16:04 LinusGeffarth

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

lucashoeft avatar Apr 19 '19 11:04 lucashoeft

@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

LinusGeffarth avatar Apr 19 '19 13:04 LinusGeffarth

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
    }
}

bhaveshbc avatar Sep 02 '19 11:09 bhaveshbc

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

LinusGeffarth avatar Sep 02 '19 12:09 LinusGeffarth

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

bhaveshbc avatar Sep 02 '19 12:09 bhaveshbc

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

LinusGeffarth avatar Sep 02 '19 12:09 LinusGeffarth

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 avatar Sep 03 '19 23:09 LinusGeffarth

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

bhaveshbc avatar Sep 25 '19 08:09 bhaveshbc

@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 avatar Nov 04 '19 14:11 LinusGeffarth

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

LinusGeffarth avatar Nov 11 '19 06:11 LinusGeffarth

swiping between PDF images

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

LinusGeffarth avatar Nov 12 '19 11:11 LinusGeffarth

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.

blu3mo avatar Aug 15 '20 02:08 blu3mo

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

LinusGeffarth avatar Aug 15 '20 10:08 LinusGeffarth

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

blu3mo avatar Aug 15 '20 10:08 blu3mo

Cool. Can you open a PR?

LinusGeffarth avatar Aug 15 '20 10:08 LinusGeffarth

Could you check my PR?

blu3mo avatar Aug 20 '20 15:08 blu3mo