-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathoptimized_svg.go
200 lines (188 loc) · 7.1 KB
/
optimized_svg.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
package go_qr
import (
"fmt"
"strings"
)
func (q *QrCode) toSvgOptimizedString(config *QrCodeImgConfig, lightColor, darkColor string) string {
scale := config.scale
border := config.border
// Write the header of the svg.
sb := strings.Builder{}
sb.WriteString("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
sb.WriteString("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
// Determine the size of the svg.
n := q.GetSize()*scale + border*2
sb.WriteString(fmt.Sprintf("<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 %d %d\" stroke=\"none\" style=\"fill-rule:evenodd;clip-rule:evenodd\">\n",
n, n))
// If light color is set, the background layer is omitted to yield a
// transparent background.
if lightColor != "" {
sb.WriteString("\t<rect width=\"100%\" height=\"100%\" fill=\"" + lightColor + "\"/>\n")
}
// Create a graph representing the border of all areas with filled modules.
nodes := q.assembleBorderGraph()
// Create a path consisting of several closed loops, which connect all the
// just determined nodes along their edges.
sb.WriteString("\t<path d=\"")
connectedNodes := make(map[node]bool, len(nodes))
for startNode, edges := range nodes {
// Skip the node if it is already connected to a drawn path.
if connected, ok := connectedNodes[startNode]; ok && connected {
continue
}
// Skip the node if it is not in a corner. The starting nodes should
// always be in a corner, as nodes on straight lines do not have to be
// drawn anyway.
if !edges.formCorner() {
continue
}
// Move cursor to staring node.
startX, startY := startNode.imageXY(border, scale)
sb.WriteString(fmt.Sprintf("M%d,%d", startX, startY))
// Move along edges until the starting node is reached.
prevNode := startNode
curNode := startNode
nextNode := edges.first
for nextNode != startNode {
// The next node is set to be the current node.
prevNode = curNode
curNode = nextNode
// Get the edges of the current node.
curEdges := nodes[curNode]
// Select the edge of the current node, which does not connect back
// to the previous node.
if curEdges.first == prevNode {
nextNode = curEdges.second
} else {
nextNode = curEdges.first
}
// Only draw a line if the current node is at a corner,
// otherwise it is skipped.
if curEdges.formCorner() {
// Draw a line to the current node.
if prevNode.x == curNode.x {
_, y := curNode.imageXY(border, scale)
// Previous and current node are on a vertical line.
sb.WriteString(fmt.Sprintf("V%d", y))
} else {
// Previous and current node are on a horizontal line.
x, _ := curNode.imageXY(border, scale)
sb.WriteString(fmt.Sprintf("H%d", x))
}
}
// Mark the current node as being connected to the path.
connectedNodes[curNode] = true
}
// Draw the final line to the start.
sb.WriteString(fmt.Sprintf("L%d,%d", startX, startY))
// Mark the start node as being connected to the path.
connectedNodes[startNode] = true
}
sb.WriteString("\" style=\"fill:" + darkColor + "\"/>\n")
sb.WriteString("</svg>\n")
return sb.String()
}
// Node represents one node of the svg path creating the QR code.
// x,y are the pixel coordinates of the node in the path.
// top is set to true, if these coordinates can belong to two nodes and this
// node belongs to the path around the upper of these two nodes.
// This can happen if the top right corner of a module coincides with the bottom
// left corner of another module or the top left corner of a module coincides
// with the bottom right corner of another module, as shown below.
//
// _ _ _ _
// | | | |
// |_ _|_ _ _ _ |_ _|
// | | | |
// |_ _| |_ _|
//
// To distinguish both nodes in this case, the node belonging to the path around
// the upper module, will have top set to true.
type node struct {
x, y int
top bool
}
// imageXY converts the coordinates of a node in the QR code to the coordinates
// of a point in the svg image taking the border around the QR code and the
// scale applied to the code into account.
func (n node) imageXY(border int, scale int) (x int, y int) {
x = n.x*scale + border
y = n.y*scale + border
return
}
// edges are the two line segments originating from one node in the svg path.
// As the full path is a set of closed loops, each node will always have two
// edges attached to it. For the edges only the nodes to which they connect are
// stored.
type edges struct {
first, second node
}
// formCorner yields true if the edges of of a node form a corner and not a
// straight line. As the drawn svg path only consists of lines, which are
// parallel to the x or y axis, this is case if and only if the two nodes to
// to which a node is connected differ in both coordinates.
func (e edges) formCorner() bool {
return e.first.x != e.second.x && e.first.y != e.second.y
}
// addEdge adds an edge to toNode to the set of edges of fromNode.
func addEdge(nodes map[node]edges, fromNode node, toNode node) {
// Check if an edge for fromNode has already been added.
fromEdges, ok := nodes[fromNode]
if !ok {
// If not add an edge to toNode for it as its first edge.
nodes[fromNode] = edges{first: toNode}
} else {
// Otherwise add an edge to toNode for it as its second edge.
nodes[fromNode] = edges{first: fromEdges.first, second: toNode}
}
}
// assembleBorderGraph create a graph data structure representing the border of
// all connected areas in the QR code with filled modules. The borders between
// two filled and adjacent modules are all removed.
func (q *QrCode) assembleBorderGraph() map[node]edges {
nodes := make(map[node]edges)
n := q.GetSize()
for y := 0; y < n; y++ {
for x := 0; x < n; x++ {
if q.GetModule(x, y) {
// Select which edges of the module have to be in the svg path.
// These are all edges which are not adjacent to another filled
// module.
top := y == 0 || !q.GetModule(x, y-1)
right := x == n-1 || !q.GetModule(x+1, y)
bottom := y == n-1 || !q.GetModule(x, y+1)
left := x == 0 || !q.GetModule(x-1, y)
// Store edges in both directions.
if top {
leftNode := node{x: x, y: y}
rightNode := node{x: x + 1, y: y}
addEdge(nodes, leftNode, rightNode)
addEdge(nodes, rightNode, leftNode)
}
// If both edges of a bottom corner of the module have to be
// drawn, the coordinates could coincided with the a corner
// of another module. To distinguish the corners, this node
// is marked as belonging to the upper module.
if left {
topNode := node{x: x, y: y}
bottomNode := node{x: x, y: y + 1, top: bottom}
addEdge(nodes, topNode, bottomNode)
addEdge(nodes, bottomNode, topNode)
}
if bottom {
leftNode := node{x: x, y: y + 1, top: left}
rightNode := node{x: x + 1, y: y + 1, top: right}
addEdge(nodes, leftNode, rightNode)
addEdge(nodes, rightNode, leftNode)
}
if right {
topNode := node{x: x + 1, y: y}
bottomNode := node{x: x + 1, y: y + 1, top: bottom}
addEdge(nodes, topNode, bottomNode)
addEdge(nodes, bottomNode, topNode)
}
}
}
}
return nodes
}