codeberg.org/go-pdf/fpdf@v0.11.1/svgbasic.go (about) 1 // Copyright ©2023 The go-pdf Authors. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE file. 4 5 /* 6 * Copyright (c) 2014 Kurt Jung (Gmail: kurt.w.jung) 7 * 8 * Permission to use, copy, modify, and distribute this software for any 9 * purpose with or without fee is hereby granted, provided that the above 10 * copyright notice and this permission notice appear in all copies. 11 * 12 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 13 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 14 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 15 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 16 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 17 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 18 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 19 */ 20 21 package fpdf 22 23 import ( 24 "encoding/xml" 25 "fmt" 26 "os" 27 "strconv" 28 "strings" 29 ) 30 31 var pathCmdSub *strings.Replacer 32 33 func init() { 34 // Handle permitted constructions like "100L200,230" 35 pathCmdSub = strings.NewReplacer(",", " ", 36 "L", " L ", "l", " l ", 37 "C", " C ", "c", " c ", 38 "M", " M ", "m", " m ", 39 "H", " H ", "h", " h ", 40 "V", " V ", "v", " v ", 41 "Q", " Q ", "q", " q ", 42 "Z", " Z ", "z", " z ") 43 } 44 45 // SVGBasicSegmentType describes a single curve or position segment 46 type SVGBasicSegmentType struct { 47 Cmd byte // See http://www.w3.org/TR/SVG/paths.html for path command structure 48 Arg [6]float64 49 } 50 51 func absolutizePath(segs []SVGBasicSegmentType) { 52 var x, y float64 53 var segPtr *SVGBasicSegmentType 54 adjust := func(pos int, adjX, adjY float64) { 55 segPtr.Arg[pos] += adjX 56 segPtr.Arg[pos+1] += adjY 57 } 58 for j, seg := range segs { 59 segPtr = &segs[j] 60 if j == 0 && seg.Cmd == 'm' { 61 segPtr.Cmd = 'M' 62 } 63 switch segPtr.Cmd { 64 case 'M': 65 x = seg.Arg[0] 66 y = seg.Arg[1] 67 case 'm': 68 adjust(0, x, y) 69 segPtr.Cmd = 'M' 70 x = segPtr.Arg[0] 71 y = segPtr.Arg[1] 72 case 'L': 73 x = seg.Arg[0] 74 y = seg.Arg[1] 75 case 'l': 76 adjust(0, x, y) 77 segPtr.Cmd = 'L' 78 x = segPtr.Arg[0] 79 y = segPtr.Arg[1] 80 case 'C': 81 x = seg.Arg[4] 82 y = seg.Arg[5] 83 case 'c': 84 adjust(0, x, y) 85 adjust(2, x, y) 86 adjust(4, x, y) 87 segPtr.Cmd = 'C' 88 x = segPtr.Arg[4] 89 y = segPtr.Arg[5] 90 case 'Q': 91 x = seg.Arg[2] 92 y = seg.Arg[3] 93 case 'q': 94 adjust(0, x, y) 95 adjust(2, x, y) 96 segPtr.Cmd = 'Q' 97 x = segPtr.Arg[2] 98 y = segPtr.Arg[3] 99 case 'H': 100 x = seg.Arg[0] 101 case 'h': 102 segPtr.Arg[0] += x 103 segPtr.Cmd = 'H' 104 x += seg.Arg[0] 105 case 'V': 106 y = seg.Arg[0] 107 case 'v': 108 segPtr.Arg[0] += y 109 segPtr.Cmd = 'V' 110 y += seg.Arg[0] 111 case 'z': 112 segPtr.Cmd = 'Z' 113 } 114 } 115 } 116 117 func pathParse(pathStr string, adjustToPt float64) (segs []SVGBasicSegmentType, err error) { 118 var seg SVGBasicSegmentType 119 var j, argJ, argCount, prevArgCount int 120 setup := func(n int) { 121 // It is not strictly necessary to clear arguments, but result may be clearer 122 // to caller 123 for j := 0; j < len(seg.Arg); j++ { 124 seg.Arg[j] = 0.0 125 } 126 argJ = 0 127 argCount = n 128 prevArgCount = n 129 } 130 var str string 131 var c byte 132 pathStr = pathCmdSub.Replace(pathStr) 133 strList := strings.Fields(pathStr) 134 count := len(strList) 135 for j = 0; j < count && err == nil; j++ { 136 str = strList[j] 137 if argCount == 0 { // Look for path command or argument continuation 138 c = str[0] 139 if c == '-' || (c >= '0' && c <= '9') { // More arguments 140 if j > 0 { 141 setup(prevArgCount) 142 // Repeat previous action 143 if seg.Cmd == 'M' { 144 seg.Cmd = 'L' 145 } else if seg.Cmd == 'm' { 146 seg.Cmd = 'l' 147 } 148 } else { 149 err = fmt.Errorf("expecting SVG path command at first position, got %s", str) 150 } 151 } 152 } 153 if err == nil { 154 if argCount == 0 { 155 seg.Cmd = str[0] 156 switch seg.Cmd { 157 case 'M', 'm': // Absolute/relative moveto: x, y 158 setup(2) 159 case 'C', 'c': // Absolute/relative Bézier curve: cx0, cy0, cx1, cy1, x1, y1 160 setup(6) 161 case 'H', 'h': // Absolute/relative horizontal line to: x 162 setup(1) 163 case 'L', 'l': // Absolute/relative lineto: x, y 164 setup(2) 165 case 'Q', 'q': // Absolute/relative quadratic curve: x0, y0, x1, y1 166 setup(4) 167 case 'V', 'v': // Absolute/relative vertical line to: y 168 setup(1) 169 case 'Z', 'z': // closepath instruction (takes no arguments) 170 segs = append(segs, seg) 171 default: 172 err = fmt.Errorf("expecting SVG path command at position %d, got %s", j, str) 173 } 174 } else { 175 seg.Arg[argJ], err = strconv.ParseFloat(str, 64) 176 if err == nil { 177 seg.Arg[argJ] *= adjustToPt 178 argJ++ 179 argCount-- 180 if argCount == 0 { 181 segs = append(segs, seg) 182 } 183 } 184 } 185 } 186 } 187 if err == nil { 188 if argCount == 0 { 189 absolutizePath(segs) 190 } else { 191 err = fmt.Errorf("expecting additional (%d) numeric arguments", argCount) 192 } 193 } 194 return 195 } 196 197 // SVGBasicType aggregates the information needed to describe a multi-segment 198 // basic vector image 199 type SVGBasicType struct { 200 Wd, Ht float64 201 Segments [][]SVGBasicSegmentType 202 } 203 204 // parseFloatWithUnit parses a float and its unit, e.g. "42pt". 205 // 206 // The result is converted into pt values wich is the default document unit. 207 // parseFloatWithUnit returns the factor to apply to positions or distances to 208 // convert their values in point units. 209 func parseFloatWithUnit(val string) (float64, float64, error) { 210 var adjustToPt float64 211 var removeUnitChar int 212 var floatValue float64 213 var err error 214 215 switch { 216 case strings.HasSuffix(val, "pt"): 217 removeUnitChar = 2 218 adjustToPt = 1.0 219 case strings.HasSuffix(val, "in"): 220 removeUnitChar = 2 221 adjustToPt = 72.0 222 case strings.HasSuffix(val, "mm"): 223 removeUnitChar = 2 224 adjustToPt = 72.0 / 25.4 225 case strings.HasSuffix(val, "cm"): 226 removeUnitChar = 2 227 adjustToPt = 72.0 / 2.54 228 case strings.HasSuffix(val, "pc"): 229 removeUnitChar = 2 230 adjustToPt = 12.0 231 default: // default is pixel 232 removeUnitChar = 0 233 adjustToPt = 1.0 / 96.0 234 } 235 236 floatValue, err = strconv.ParseFloat(val[:len(val)-removeUnitChar], 64) 237 if err != nil { 238 return 0.0, 0.0, err 239 } 240 return floatValue * adjustToPt, adjustToPt, nil 241 } 242 243 // SVGBasicParse parses a simple scalable vector graphics (SVG) buffer into a 244 // descriptor. Only a small subset of the SVG standard, in particular the path 245 // information generated by jSignature, is supported. The returned path data 246 // includes only the commands 'M' (absolute moveto: x, y), 'L' (absolute 247 // lineto: x, y), 'C' (absolute cubic Bézier curve: cx0, cy0, cx1, cy1, 248 // x1,y1), 'Q' (absolute quadratic Bézier curve: x0, y0, x1, y1) and 'Z' 249 // (closepath). The document is returned with "pt" unit. 250 func SVGBasicParse(buf []byte) (sig SVGBasicType, err error) { 251 type pathType struct { 252 D string `xml:"d,attr"` 253 } 254 type rectType struct { 255 Width float64 `xml:"width,attr"` 256 Height float64 `xml:"height,attr"` 257 X float64 `xml:"x,attr"` 258 Y float64 `xml:"y,attr"` 259 } 260 type srcType struct { 261 Wd string `xml:"width,attr"` 262 Ht string `xml:"height,attr"` 263 Paths []pathType `xml:"path"` 264 Rects []rectType `xml:"rect"` 265 } 266 var src srcType 267 var wd float64 268 var ht float64 269 var adjustToPt float64 270 err = xml.Unmarshal(buf, &src) 271 if err == nil { 272 wd, adjustToPt, err = parseFloatWithUnit(src.Wd) 273 if err != nil { 274 return sig, err 275 } 276 ht, _, err = parseFloatWithUnit(src.Ht) 277 if err != nil { 278 return sig, err 279 } 280 if wd > 0 && ht > 0 { 281 sig.Wd, sig.Ht = wd, ht 282 var segs []SVGBasicSegmentType 283 for _, path := range src.Paths { 284 if err == nil { 285 segs, err = pathParse(path.D, adjustToPt) 286 if err == nil { 287 sig.Segments = append(sig.Segments, segs) 288 } 289 } 290 } 291 for _, rect := range src.Rects { 292 segs = nil 293 segs = append(segs, SVGBasicSegmentType{ 294 Cmd: 'M', 295 Arg: [6]float64{rect.X * adjustToPt, rect.Y * adjustToPt}, 296 }) 297 segs = append(segs, SVGBasicSegmentType{ 298 Cmd: 'L', 299 Arg: [6]float64{(rect.X + rect.Width) * adjustToPt, rect.Y * adjustToPt}, 300 }) 301 segs = append(segs, SVGBasicSegmentType{ 302 Cmd: 'L', 303 Arg: [6]float64{(rect.X + rect.Width) * adjustToPt, (rect.Y + rect.Height) * adjustToPt}, 304 }) 305 segs = append(segs, SVGBasicSegmentType{ 306 Cmd: 'L', 307 Arg: [6]float64{rect.X * adjustToPt, (rect.Y + rect.Height) * adjustToPt}, 308 }) 309 segs = append(segs, SVGBasicSegmentType{ 310 Cmd: 'Z', 311 }) 312 sig.Segments = append(sig.Segments, segs) 313 } 314 } else { 315 err = fmt.Errorf("unacceptable values for basic SVG extent: %.2f x %.2f", 316 sig.Wd, sig.Ht) 317 } 318 } 319 return 320 } 321 322 // SVGBasicFileParse parses a simple scalable vector graphics (SVG) file into a 323 // basic descriptor. The SVGBasicWrite() example demonstrates this method. 324 func SVGBasicFileParse(svgFileStr string) (sig SVGBasicType, err error) { 325 var buf []byte 326 buf, err = os.ReadFile(svgFileStr) 327 if err == nil { 328 sig, err = SVGBasicParse(buf) 329 } 330 return 331 }