Implement ink annotations on iOS 11 PDFKit document

前端 未结 3 671
广开言路
广开言路 2021-01-31 12:47

I want to allow the user to draw on an iOS 11 PDFKit document viewed in a PDFView. The drawing should ultimately be embedded inside the PDF.

The latter I have solved by

相关标签:
3条回答
  • 2021-01-31 13:16

    EDIT:

    jksoegaard's answer, while being the inspiration to all of my work on this matter, has a flaw: during touchedMoved, multiple PDF annotations are created, and consequently the PDF page becomes bogged down with annotations, which affects its loading time severely. I wrote code that draws on a CAShapeLayer during the touchedMoved phase, and creates the completed PDF annotation only in the touchesEnded phase.

    My implementation is a subclass of UIGestureRecognizer, and it allows you to choose between pen, highlighter and eraser, and choose color and width. It also includes an undo manager. The example project is here.

    Original Answer

    Adding to jksoegaard's excellent answer, a few clarifications for newbies like myself:

    1. You need to include UIBezierPath+.swift in your project for .moveCenter and rect.center to be recognized. Download from https://github.com/xhamr/paintcode-path-scale.

    2. These lines can be excluded:

       UIGraphicsBeginImageContext(CGSize(width: 800, height: 600))
      

    and

        let page = pdfView.page(for: position, nearest: true)!
    
    1. You need to declare a few global vars outside the functions:

       var signingPath = UIBezierPath()
       var annotationAdded : Bool?
       var lastPoint : CGPoint?
       var currentAnnotation : PDFAnnotation?
      
    2. Finally, if you want the ink to be wider and nicely colored, you need to do two things:

    a. Every time before you see annotation.add or currentAnnotation.add, you need (use annotation or currentAnnotation as needed by that function):

        let b = PDFBorder()
        b.lineWidth = { choose a pixel number here }
        currentAnnotation?.border = b
        currentAnnotation?.color=UIColor.{ your color of choosing }
    

    I recommend specifying a low alpha for the color. The result is beautiful, and affected by the speed of your stroke. For example, red would be:

        UIColor(red: 255/255.0, green: 0/255.0, blue: 0/255.0, alpha: 0.1)
    

    b. The rect in which every touch is recorded needs to accommodate for the thicker lines. Instead of

        let rect = signingPath.bounds
    

    Try, for an example of 10px of thickness:

        let rect = CGRect(x:signingPath.bounds.minX-5, 
        y:signingPath.bounds.minY-5, width:signingPath.bounds.maxX- 
        signingPath.bounds.minX+10, height:signingPath.bounds.maxY- 
        signingPath.bounds.minY+10)
    

    IMPORTANT: The touchesEnded function also makes use of the currentAnnotation variable. You must repeat the definition of rect within that function as well (either the short one or my suggested one above), and repeat the definition of currentAnnotation there as well:

        currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
    

    If you don't, a single tap that did not move will make your app crash.

    I can verify that once the file is saved, the annotations are retained. Sample code for saving:

        let pdfData = pdfDocument?.dataRepresentation()
        let annotatedPdfUrl = URL(fileURLWithPath: "\ 
        (NSSearchPathForDirectoriesInDomains(.documentsDirectory, .userDomainMask, 
            true)[0])/AnnotatedPDF.pdf")
        try! pdfData!.write(to: annotatedPdfUrl)
    
    0 讨论(0)
  • 2021-01-31 13:18

    In the end I solved the problem by creating a PDFViewController class extending UIViewController and UIGestureRecognizerDelegate. I added a PDFView as a subview, and a UIBarButtonItem to the navigationItem, that serves to toggle annotation mode.

    I record the touches in a UIBezierPath called signingPath, and have the current annotation in currentAnnotation of type PDFAnnotation using the following code:

     override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let position = touch.location(in: pdfView)
            signingPath = UIBezierPath()
            signingPath.move(to: pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!))
            annotationAdded = false
            UIGraphicsBeginImageContext(CGSize(width: 800, height: 600))
            lastPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!)
        }
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let position = touch.location(in: pdfView)
            let convertedPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!)
            let page = pdfView.page(for: position, nearest: true)!
            signingPath.addLine(to: convertedPoint)
            let rect = signingPath.bounds
    
            if( annotationAdded ) {
                pdfView.document?.page(at: 0)?.removeAnnotation(currentAnnotation)
                currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
    
                var signingPathCentered = UIBezierPath()
                signingPathCentered.cgPath = signingPath.cgPath
                signingPathCentered.moveCenter(to: rect.center)
                currentAnnotation.add(signingPathCentered)
                pdfView.document?.page(at: 0)?.addAnnotation(currentAnnotation)
    
            } else {
                lastPoint = pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!)
                annotationAdded = true
                currentAnnotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
                currentAnnotation.add(signingPath)
                pdfView.document?.page(at: 0)?.addAnnotation(currentAnnotation)
            }
        }
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        if let touch = touches.first {
            let position = touch.location(in: pdfView)
            signingPath.addLine(to: pdfView.convert(position, to: pdfView.page(for: position, nearest: true)!))
    
            pdfView.document?.page(at: 0)?.removeAnnotation(currentAnnotation)
    
            let rect = signingPath.bounds
            let annotation = PDFAnnotation(bounds: rect, forType: .ink, withProperties: nil)
            annotation.color = UIColor(hex: 0x284283)
            signingPath.moveCenter(to: rect.center)
            annotation.add(signingPath)
            pdfView.document?.page(at: 0)?.addAnnotation(annotation)
        }
    }
    

    The annotation toggle button just runs:

    pdfView.isUserInteractionEnabled = !pdfView.isUserInteractionEnabled
    

    This was really the key to it, as this disables scrolling on the PDF and enables me to receive the touch events.

    The way the touch events are recorded and converted into PDFAnnotation immediately means that the annotation is visible while writing on the PDF, and that it is finally recorded into the correct position in the PDF - no matter the scroll position.

    Making sure it ends up on the right page is just a matter of similarly changing the hardcoded 0 for page number to the pdfView.page(for: position, nearest:true) value.

    0 讨论(0)
  • 2021-01-31 13:22

    I've done this by creating a new view class (eg Annotate View) and putting on top of the PDFView when the user is annotating.

    This view uses it's default touchesBegan/touchesMoved/touchesEnded methods to create a bezier path following the gesture. Once the touch has ended, my view then saves it as an annotation on the pdf.

    Note: you would need a way for the user to decide if they were in an annotating state.

    For my main class

    class MyViewController : UIViewController, PDFViewDelegate, VCDelegate {
    
    var pdfView: PDFView?
    var touchView: AnnotateView?
    
    override func loadView() {
       touchView = AnnotateView(frame: CGRect(x: 0, y: 0, width: 375, height: 600))
       touchView?.backgroundColor = .clear
       touchView?.delegate = self
       view.addSubview(touchView!)
    }
    
     func addAnnotation(_ annotation: PDFAnnotation) {
        print("Anotation added")
        pdfView?.document?.page(at: 0)?.addAnnotation(annotation)
    }
    }
    

    My annotation view

    class AnnotateView: UIView {
    var path: UIBezierPath?
    var delegate: VCDelegate?
    
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        // Initialize a new path for the user gesture
        path = UIBezierPath()
        path?.lineWidth = 4.0
    
        var touch: UITouch = touches.first!
        path?.move(to: touch.location(in: self))
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    
        // Add new points to the path
        let touch: UITouch = touches.first! 
        path?.addLine(to: touch.location(in: self))
        self.setNeedsDisplay()
    }
    
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
    
        let touch = touches.first 
        path?.addLine(to: touch!.location(in: self))
        self.setNeedsDisplay()
        let annotation = PDFAnnotation(bounds: self.bounds, forType: .ink, withProperties: nil)
        annotation.add(self.path!)
        delegate?.addAnnotation(annotation)
    }
    
    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        self.touchesEnded(touches, with: event)
    }
    
    override func draw(_ rect: CGRect) {
        // Draw the path
        path?.stroke()
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        self.isMultipleTouchEnabled = false
    }
    }
    
    0 讨论(0)
提交回复
热议问题