ギーク
[iOS/Swift] 手書きした線画をSVGで保存する
Seiya Kobayashi
Apple Pencilなどを使った手書きアプリが増えていますが、描いた線画をSVGとして保存したいことがあったのでその方法を紹介します。
実装
手書きアプリの基本的なところは、下記のサイトを基として進めます(https://www.raywenderlich.com/1407-apple-pencil-tutorial-getting-started)
重要となるのはCanvasView.swift
のtouchesMoved
とdrawStroke
関数になります。
touchesMoved
ではPencil(または指)のなぞる動作を追うことができます。
また、全体のサイズ(width, height)を算出します。
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
// Get minimum and maximum point.
minimumX = location.x < minimumX ? location.x : minimumX
maximumX = location.x > maximumX ? location.x : maximumX
minimumY = location.y < minimumY ? location.y : minimumY
maximumY = location.y > maximumY ? location.y : maximumY
UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
guard let context = UIGraphicsGetCurrentContext() else { return }
drawStroke(context: context, touch: touch)
// Update image
image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
}
drawStroke
ではなぞった位置(previousLocationとlocation)を利用して線を描画します。
ここで、描画した位置(Point)を保存しておきそれらを使って線をSVGに変換します。
private func drawStroke(context: CGContext, touch: UITouch) {
let previousLocation = touch.previousLocation(in: self)
let location = touch.location(in: self)
// Calculate line width for drawing stroke
let lineWidth = lineWidthForDrawing(context: context, touch: touch)
// Set color
drawColor.setStroke()
// Configure line
context.setLineWidth(lineWidth)
context.setLineCap(.round)
// Set up the points
context.move(to: CGPoint(x: previousLocation.x, y: previousLocation.y))
moveToPoints.append(CGPoint(x: previousLocation.x, y: previousLocation.y))
context.addLine(to: CGPoint(x: location.x, y: location.y))
lineToPoints.append(CGPoint(x: location.x - previousLocation.x, y: location.y - previousLocation.y))
// Draw the stroke
context.strokePath()
}
moveToPoints
, lineToPoints
に位置を保存しています。
描画が終了したら、保存されている値をSVGのフォーマットに当てはめます。
SVGを生成
まず、SVGの例を示します。
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="267.5" height="219.0" version="1.1">
<path stroke="yellow" stroke-width="6.0" d="M9.0,-22.5 l-9.0,22.5 Z"></path>
</svg>
設定する値はwidth, height, stroke, stroke-width, dの値になります。
- width
- height
- stroke – 色
- stroke-width – 線の太さ
- d – コマンド
width, heightはtouchesMoved
で取得した値を使います。
stroke, stroke-widthには任意の値を入れてください。
dには、一連のコマンドを入れていきます。
今回のやり方だと、2点ずつしか位置を取得できないので、2点の直線を繋げていき一つの線画としていきます。
直線を引くには M x y
(Move to)コマンドとL x y
(Line to)コマンドを使用します。
M0.0 5.0 L0.0 10.0
とすると、(0.0, 5.0)に移動してそこから(0.0, 10.0)に向かって線を引きます。
SVGの詳細は以下を参考にしてください
https://developer.mozilla.org/ja/docs/Web/SVG/Tutorial/Paths
これらの値の設定するSVG.swift
を作りました。
SVG.swift
import AVFoundation
/// The Struct for SVG.
struct SVG {
/// The header for xml.
let header = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"
/// The footer for svg.
let footer = "</svg>"
/// The data string for all data.
var fileData: String = ""
/// The color string for stroke.
var stroke: String = "yellow"
/// The width for stroke.
var strokeWidth: CGFloat = 6
/// The string for command.
var command: String = ""
/// The whole width.
var width: CGFloat = 0
/// The whole height.
var height: CGFloat = 0
/**
Initialize and create data string.
- parameter width: width of image.
- parameter height: height of image.
- parameter moveToPoint: The points of moved.
- parameter lineToPoint: The points of lined.
*/
init(width: CGFloat, height: CGFloat, moveToPoint: [CGPoint], lineToPoint: [CGPoint]) {
self.width = width
self.height = height
self.command = createCommand(moveToPoints: moveToPoint, lineToPoints: lineToPoint)
let svgTag = "<svg xmlns=\"http://www.w3.org/2000/svg\" "
+ "width=\"" + String(describing: width) + "\" height=\"" + String(describing: height)
+ "\" version=\"1.1\">\n"
let path = "<path stroke=\"" + stroke + "\" stroke-width=\"" + String(describing: strokeWidth) + "\" d=\"" + command + "\"></path>\n"
self.fileData = header + svgTag + path + footer
print(fileData)
}
/**
Initialize with string of data. Parse command.
- parameter dataString: string of path.
*/
init(dataString: String) {
guard let cmd = dataString.components(separatedBy: "d=\"").last?.components(separatedBy: "\"").first else {
print("Faild to parse command")
return
}
command = cmd
}
/**
Create command string with points.
- parameter moveToPoint: The points of moved.
- parameter lineToPoint: The points of lined.
*/
func createCommand(moveToPoints: [CGPoint], lineToPoints: [CGPoint]) -> String {
var command = ""
for (index, point) in moveToPoints.enumerated() {
command += "M" + String(describing: point.x) + "," + String(describing: point.y) + " "
command += "l" + String(describing: lineToPoints[index].x) + "," + String(describing: lineToPoints[index].y) + " "
}
command += "Z"
return command
}
}
使用方法
SVG(width: width, height: height, moveToPoint: moveToPoints, lineToPoint: lineToPoints)
で作れます。
fileData
を端末内に保存していただければ、SwiftSVG(https://github.com/mchoe/SwiftSVG)などで読み込むことも可能です。
まとめ
今回は、画面をなぞった動作をSVGのコマンドに変換することで手書きの線画をSVGを生成してみました。
線を引くのに直線コマンドしか使用していないので曲線部は少々粗くなってしまいます。
参考にしてみてください。