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  }