Rasterx is an SVG 2.0 path compliant rasterizer that can use either the golang vector or a derivative of the freetype anti-aliaser.

rasterx

Rasterx is a golang rasterizer that implements path stroking functions capable of SVG 2.0 compliant 'arc' joins and explicit loop closing.

  • Paths can be explicity closed or left open, resulting in a line join or end caps.
  • Arc joins are supported, which causes the extending edge from a Bezier curve to follow the radius of curvature at the end point rather than a straight line miter, resulting in a more fliud looking join.
  • Not specified in the SVG2.0 spec., but supported in rasterx is the arc-clip join, which is the arc join analog of a miter-clip join, both of which end the miter at a specified distance, rather than all or nothing.
  • Several cap and gap functions in addition to those specified by SVG2.0 are implemented, specifically quad and cubic caps and gaps.
  • Line start and end capping functions can be different.

rasterx example

The above image shows the effect of using different join modes for a stroked curving path. The top stroked path uses miter (green) or arc (red, yellow, orange) join functions with high miter limit. The middle and lower path shows the effect of using the miter-clip and arc-clip joins, repectively, with different miter-limit values. The black chevrons at the top show different cap and gap functions.

Scanner interface

Rasterx takes the path description of lines, bezier curves, and drawing parameters, and converts them into a set of straight line segments before rasterizing the lines to an image using some method of antialiasing. Rasterx abstracts this last step through the Scanner interface. There are two different structs that satisfy the Scanner interface; ScannerGV and ScannerFT. ScannerGV wraps the rasterizer found in the golang.org/x/image/vector package. ScannerFT contains a modified version of the antialiaser found in the golang freetype translation. These use different functions to connect an image to the antialiaser. ScannerFT uses a Painter to translate the raster onto the image, and ScannerGV uses the vector's Draw method with a source image and uses the path as an alpha mask. Please see the test files for examples. At this time, the ScannerFT is a bit faster as compared to ScannerGV for larger and less complicated images, while ScannerGV can be faster for smaller and more complex images. Also ScannerGV does not allow for using the even-odd winding rule, which is something the SVG specification uses. Since ScannerFT is subject to freetype style licensing rules, it lives here in a separate repository and must be imported into your project seperately. ScannerGV is included in the rasterx package, and has more go-friendly licensing.

Below are the results of some benchmarks performed on a sample shape (the letter Q ). The first test is the time it takes to scan the image after all the curves have been flattened. The second test is the time it takes to flatten, and scan a simple filled image. The last test is the time it takes to flatten a stroked and dashed outline of the shape and scan it. Results for three different image sizes are shown.

128x128 Image
Test                        Rep       Time
BenchmarkScanGV-16          5000      287180 ns/op
BenchmarkFillGV-16          5000      339831 ns/op
BenchmarkDashGV-16          2000      968265 ns/op

BenchmarkScanFT-16    	   20000       88118 ns/op
BenchmarkFillFT-16    	    5000      214370 ns/op
BenchmarkDashFT-16    	    1000     2063797 ns/op

256x256 Image
Test                        Rep       Time
BenchmarkScanGV-16          2000     1188452 ns/op
BenchmarkFillGV-16          1000     1277268 ns/op
BenchmarkDashGV-16          500      2238169 ns/op

BenchmarkScanFT-16    	    5000      290685 ns/op
BenchmarkFillFT-16    	    3000      446329 ns/op
BenchmarkDashFT-16    	     500     2923512 ns/op

512x512 Image
Test                        Rep       Time
BenchmarkScanGV-16           500     3341038 ns/op
BenchmarkFillGV-16           500     4032213 ns/op
BenchmarkDashGV-16           200     6003355 ns/op

BenchmarkScanFT-16    	    5000      292884 ns/op
BenchmarkFillFT-16    	    3000      449582 ns/op
BenchmarkDashFT-16    	     500     2800493 ns/op

The package uses an interface called Rasterx, which is satisfied by three structs, Filler, Stroker and Dasher. The Filler flattens Bezier curves into lines and uses an anonymously composed Scanner for the antialiasing step. The Stroker embeds a Filler and adds path stroking, and the Dasher embedds a Stroker and adds the ability to create dashed stroked curves.

rasterx Scheme

Each of the Filler, Dasher, and Stroker can function on their own and each implement the Rasterx interface, so if you need just the curve filling but no stroking capability, you only need a Filler. On the other hand if you have created a Dasher and want to use it to Fill, you can just do this:

filler := &dasher.Filler

Now filler is a filling rasterizer. Please see rasterx_test.go for examples.

Non-standard library dependencies

rasterx requires the following imports which are not included in the go standard library:

  • golang.org/x/image/math/fixed
  • golang.org/x/image/vector

These can be included in your gopath by the following 'get' commands:

  • "go get golang.org/x/image/vector"
  • "go get golang.org/x/image/math/fixed"

If you want to use the freetype style antialiaser, 'go get' or clone into your workspace the scanFT package:

  • github.com/srwiley/scanFT
Owner
Comments
  • When the rasterizer is a different size to the source, scale

    When the rasterizer is a different size to the source, scale

    If our output is a different size to the icon that is loaded let's scale it up or down. This gives a really crisp rendering at the requested resolution.

    icon, _ := oksvg.ReadIcon("file.svg")
    iconViewWidth, iconViewHeight := int(icon.ViewBox.W), int(icon.ViewBox.H)
    
    raw := image.NewRGBA(image.Rect(0, 0, outWidth, outHeight))
    scanner := rasterx.NewScannerGV(iconViewWidth, iconViewHeight, raw, raw.Bounds())
    raster := rasterx.NewDasher(outWidth, outHeight, scanner)
    
    icon.Draw(raster, img.Alpha())
    
  • support UserSpaceOnUse and objMatrix transform of its points according to object's current transform

    support UserSpaceOnUse and objMatrix transform of its points according to object's current transform

    tested that that this still reproduces same results on the test data and it also works for my test cases of the UserSpaceOnUse version. For oksvg to support this, it would need to be able to read in points as either %'s or raw numbers, and not force them to max out at 1. It also needs to pass in the object transform.

  • fix for radial gradients with UserSpaceOnUse, c == f

    fix for radial gradients with UserSpaceOnUse, c == f

    I finally found the source of a radial gradient issue I'd noticed for a while: the fx, fy coordinates were not being transformed, and so the simple distance-based case was not triggering. Diff to fix is attached -- too lazy to do a PR but let me know if you prefer that.

    Parens around the d's probably doesn't matter but anyway it might be slightly more precision-preserving?

    diff --git a/gradient.go b/gradient.go
    index 6be8fe7..8f00985 100644
    --- a/gradient.go
    +++ b/gradient.go
    @@ -189,8 +189,10 @@ func (g *Gradient) GetColorFunctionUS(opacity float64, objMatrix Matrix2D) inter
     			ry *= g.Bounds.H
     		} else {
     			cx, cy = g.Matrix.Transform(cx, cy)
    +			fx, fy = g.Matrix.Transform(fx, fy)
     			rx, ry = g.Matrix.TransformVector(rx, ry)
     			cx, cy = objMatrix.Transform(cx, cy)
    +			fx, fy = objMatrix.Transform(fx, fy)
     			rx, ry = objMatrix.TransformVector(rx, ry)
     		}
     
    @@ -203,7 +205,7 @@ func (g *Gradient) GetColorFunctionUS(opacity float64, objMatrix Matrix2D) inter
     					x, y := gradT.Transform(float64(xi)+0.5, float64(yi)+0.5)
     					dx := float64(x) - cx
     					dy := float64(y) - cy
    -					return g.tColor(math.Sqrt(dx*dx/(rx*rx)+dy*dy/(ry*ry)), opacity)
    +					return g.tColor(math.Sqrt((dx*dx)/(rx*rx)+(dy*dy)/(ry*ry)), opacity)
     				})
     			}
     			return ColorFunc(func(xi, yi int) color.Color {
    @@ -211,7 +213,7 @@ func (g *Gradient) GetColorFunctionUS(opacity float64, objMatrix Matrix2D) inter
     				y := float64(yi) + 0.5
     				dx := x - cx
     				dy := y - cy
    -				return g.tColor(math.Sqrt(dx*dx/(rx*rx)+dy*dy/(ry*ry)), opacity)
    +				return g.tColor(math.Sqrt((dx*dx)/(rx*rx)+(dy*dy)/(ry*ry)), opacity)
     			})
     		}
     		fx /= rx
    
  • Dashes = [0,0] ends up in infinite loop

    Dashes = [0,0] ends up in infinite loop

    if you scale down lines with dashes to a sufficiently small size, the fixed.Int26_6 version can end up being 0, at which point the Dasher rendering code in dash.go ends up stuck in an infinite loop!

    Best soln would be to fix SetStroke to detect the problem and reset Dashes if all are 0.

    I'm now catching this upstream, but would be good to fix here too.

  • incorrect rasterization of open paths

    incorrect rasterization of open paths

    Hi, while testing rasterx, i write this example:

    package main
    
    import (
    	"image"
    	"image/color"
    	"image/draw"
    	"log"
    
    	"golang.org/x/exp/shiny/driver"
    	"golang.org/x/exp/shiny/screen"
    	"golang.org/x/exp/shiny/widget"
    
    	"golang.org/x/image/math/fixed"
    
    	"github.com/srwiley/rasterx"
    )
    
    const (
    	S = 512
    )
    
    func main() {
    	log.SetFlags(0)
    	driver.Main(func(s screen.Screen) {
    		src := TestMultiFunctionGV() // image.NewRGBA(image.Rect(0, 0, sw, sh))
    
    		w := widget.NewSheet(widget.NewImage(src, src.Bounds()))
    		if err := widget.RunWindow(s, w, &widget.RunWindowOptions{
    			NewWindowOptions: screen.NewWindowOptions{
    				Title:  "Rasterx Example",
    				Width:  S,
    				Height: S,
    			},
    		}); err != nil {
    			log.Fatal(err)
    		}
    	})
    }
    
    //////////////////////////////////////////////////////
    
    func toFixedP(x, y float64) (p fixed.Point26_6) {
    	p.X = fixed.Int26_6(x * 64)
    	p.Y = fixed.Int26_6(y * 64)
    	return
    }
    
    func GetTestPath() (testPath rasterx.Path) {
    	//Path for Q
    	//M210.08,222.97
    	testPath.Start(toFixedP(210.08, 222.97))
    	//L192.55,244.95
    	testPath.Line(toFixedP(192.55, 244.95))
    	//Q146.53,229.95,115.55,209.55
    	testPath.QuadBezier(toFixedP(146.53, 229.95), toFixedP(115.55, 209.55))
    	//Q102.50,211.00,95.38,211.00
    	testPath.QuadBezier(toFixedP(102.50, 211.00), toFixedP(95.38, 211.00))
    	//Q56.09,211.00,31.17,182.33
    	testPath.QuadBezier(toFixedP(56.09, 211.00), toFixedP(31.17, 182.33))
    	//Q6.27,153.66,6.27,108.44
    	testPath.QuadBezier(toFixedP(6.27, 153.66), toFixedP(6.27, 108.44))
    	//Q6.27,61.89,31.44,33.94
    	testPath.QuadBezier(toFixedP(6.27, 61.89), toFixedP(31.44, 33.94))
    	//Q56.62,6.00,98.55,6.00
    	testPath.QuadBezier(toFixedP(56.62, 6.00), toFixedP(98.55, 6.00))
    	//Q141.27,6.00,166.64,33.88
    	testPath.QuadBezier(toFixedP(141.27, 6.00), toFixedP(166.64, 33.88))
    	//Q192.02,61.77,192.02,108.70
    	testPath.QuadBezier(toFixedP(192.02, 61.77), toFixedP(192.02, 108.70))
    	//Q192.02,175.67,140.86,202.05
    	testPath.QuadBezier(toFixedP(192.02, 175.67), toFixedP(140.86, 202.05))
    	//Q173.42,216.66,210.08,222.97
    	testPath.QuadBezier(toFixedP(173.42, 216.66), toFixedP(210.08, 222.97))
    	//z
    	testPath.Stop(false)
    	//M162.22,109.69 M162.22,109.69
    	testPath.Start(toFixedP(162.22, 109.69))
    	//Q162.22,70.11,145.61,48.55
    	testPath.QuadBezier(toFixedP(162.22, 70.11), toFixedP(145.61, 48.55))
    	//Q129.00,27.00,98.42,27.00
    	testPath.QuadBezier(toFixedP(129.00, 27.00), toFixedP(98.42, 27.00))
    	//Q69.14,27.00,52.53,48.62
    	testPath.QuadBezier(toFixedP(69.14, 27.00), toFixedP(52.53, 48.62))
    	//Q35.92,70.25,35.92,108.50
    	testPath.QuadBezier(toFixedP(35.92, 70.25), toFixedP(35.92, 108.50))
    	//Q35.92,146.75,52.53,168.38
    	testPath.QuadBezier(toFixedP(35.92, 146.75), toFixedP(52.53, 168.38))
    	//Q69.14,190.00,98.42,190.00
    	testPath.QuadBezier(toFixedP(69.14, 190.00), toFixedP(98.42, 190.00))
    	//Q128.34,190.00,145.28,168.70
    	testPath.QuadBezier(toFixedP(128.34, 190.00), toFixedP(145.28, 168.70))
    	//Q162.22,147.41,162.22,109.69
    	testPath.QuadBezier(toFixedP(162.22, 147.41), toFixedP(162.22, 109.69))
    	//z
    	testPath.Stop(false)
    
    	return testPath
    }
    
    // TestMultiFunction tests a Dasher's ability to function
    // as a filler, stroker, and dasher by invoking the corresponding anonymous structs
    func TestMultiFunctionGV() image.Image {
    	img := image.NewRGBA(image.Rect(0, 0, S, S))
    	src := image.NewUniform(color.NRGBA{255, 0, 0, 255})
    	scannerGV := rasterx.NewScannerGV(S, S, img, img.Bounds(), src, image.ZP)
    
    	draw.Draw(img, img.Bounds(), image.White, image.ZP, draw.Src)
    
    	d := rasterx.NewDasher(S, S, scannerGV)
    	d.SetStroke(10*64, 4*64, rasterx.RoundCap, nil, rasterx.RoundGap, rasterx.ArcClip, []float64{33, 12}, 0)
    	p := GetTestPath()
    
    	p.AddTo(d)
    	d.Draw()
    	d.Clear()
    	return img
    }
    

    Note testPath.Stop(false) in GetTestPath. Result is in attached file: screenshot

    i able to resolve issue by adding new PathCommand:

    // Human readable path constants
    const (
    	PathMoveTo PathCommand = iota
    	PathLineTo
    	PathQuadTo
    	PathCubicTo
    	PathClose
    	PathCloseF
    )
    

    ...

    // Close joins the ends of the path
    func (p *Path) Stop(closeLoop bool) {
    	if closeLoop {
    		*p = append(*p, fixed.Int26_6(PathClose))
    	} else {
    		*p = append(*p, fixed.Int26_6(PathCloseF))
    	}
    }
    
    // AddPath adds the Path p to q. This bridges the path and adder interface.
    func (p Path) AddTo(q Adder) {
    	for i := 0; i < len(p); {
    		switch PathCommand(p[i]) {
    		case PathMoveTo:
    			q.Start(fixed.Point26_6{p[i+1], p[i+2]})
    			i += 3
    		case PathLineTo:
    			q.Line(fixed.Point26_6{p[i+1], p[i+2]})
    			i += 3
    		case PathQuadTo:
    			q.QuadBezier(fixed.Point26_6{p[i+1], p[i+2]}, fixed.Point26_6{p[i+3], p[i+4]})
    			i += 5
    		case PathCubicTo:
    			q.CubeBezier(fixed.Point26_6{p[i+1], p[i+2]},
    				fixed.Point26_6{p[i+3], p[i+4]}, fixed.Point26_6{p[i+5], p[i+6]})
    			i += 7
    		case PathClose:
    			q.Stop(true)
    			i += 1
    		case PathCloseF:
    		 	q.Stop(false)
    		  	i += 1
    		default:
    			panic("adder geom: bad path")
    		}
    	}
    	q.Stop(false)
    }
    

    but i am not sure if this is correct fix.

    Regards.

  • How do you use AddArc given the context from SVG2.0 data?

    How do you use AddArc given the context from SVG2.0 data?

    First, thanks for the library. This might be considered related to #13 Given documentation or a test as an example for AddArc may clarify both issues.

    I am trying to figure out how to use AddArc given an SVG 2.0 instruction. https://github.com/srwiley/rasterx/blob/85cb7272f5e99816a35e2c3fff0c4ab1b16e867c/shapes.go#L99 Let's assume I have M 0 0 a4.9916 4.9916 0 0 1 -0.59375 0.1875 notice this is the relative version which I think simply means that the last two values must be adjusted by the current point and in this case it is 0, 0.

    I have summarized so far that the Points is basically all the parameters of a, no issue there. What I can't figure out is what is cx, cy, px, py parameters?

    I can't seem to get the correct arc. Is cx,cy current point that would make since, is that correct?

    It is the px, py I don't really have any idea at all what it could be or how to derive it from the information I have.

    Given this is a relative instruction would I need to update points based on the current location or is that somehow related to the c or p parameters?

    Thanks for your time!

  • Please explain the AddArc parameters, e.g. to make a

    Please explain the AddArc parameters, e.g. to make a "pie part"

    Hi, I spent hours making tests and trying to understand the parameters, and the "points" content and I didn't find my way to make a "pie part". Whatever I do, the path becomes a circle.

    Using this

    	cx, cy := float64(w/2.0), float64(h/2.0)
    	r := float64(w / 3.0)
    	angle := 45.0
    	rot := angle * math.Pi / 180.0
    
    	// find the point on circle of radius r at angle rot
    	px := cx + r*math.Cos(rot)
    	py := cy + r*math.Sin(rot)
    
    	points := []float64{r, r, angle, 1, 0, px, py}
    
    	stroker.Start(rasterx.ToFixedP(cx, cy))
    	//stroker.Line(rasterx.ToFixedP(cx, cy+r))
    	stroker.Start(rasterx.ToFixedP(px, py))
    	rasterx.AddArc(points, cx, cy, px, py, stroker)
    	//stroker.Stop(false)
    	//stroker.Line(rasterx.ToFixedP(cx, cy))
    	stroker.Stop(false)
    	stroker.Draw()
    
    

    out

    With this:

    	stroker.Start(rasterx.ToFixedP(cx, cy))
    	stroker.Line(rasterx.ToFixedP(cx, cy+r))
    	stroker.Start(rasterx.ToFixedP(px, py))
    	rasterx.AddArc(points, cx, cy, px, py, stroker)
    	stroker.Line(rasterx.ToFixedP(cx, cy))
    	stroker.Stop(false)
    	stroker.Draw()
    

    out

    I made several other tests to start the path somewhere else, with no idea of how to make a "pie chart element".

    This is the wanted shape: image

    The goal is to make a pie chart (of course :) ) but (sorry to say this) the code is not well documented... Can you please give a simple example of a pie chart element?

  • zooming in on a shape with dashes on vertical axis of a rectangle creates weirdness

    zooming in on a shape with dashes on vertical axis of a rectangle creates weirdness

    I'll try to track this down better, and at least have a good test case, but just before I forget: I'm scaling dashes on stroke in proportion to the scaling in SVG, and as you zoom in on a rectangle with a pretty basic dash pattern, once the scaling gets above some level, the vertical lines go all wonky.

  • Implementing the rasterx interface

    Implementing the rasterx interface

    Hello! I want to be able to render svgs directly to, say, an opengl framebuffer rather than an image.Image. I've actually already got the engine and framework and shaders needed, and It seems to me that a lot of the types in this package could be interfaces, which could then allow me to implement them to allow me to do just that. Is that a direction you'd be willing to take? I'll put together the PR and write it myself, I'm just wondering if you had any tips or ideas on going about it. Thanks!

  • I tried adding rasterx to awesome-go page... needs some work before they will accept it

    I tried adding rasterx to awesome-go page... needs some work before they will accept it

    I was adding GoGi etc to it and figured I'd add rasterx while I was at it.. Anyway, looks like you'll have to do some things and submit it separately if you want to:

    https://github.com/avelino/awesome-go/pull/2222

    These should be improved, they have a lot of issues, especially with golint: https://goreportcard.com/report/github.com/srwiley/rasterx

    These miss a lot of documentation, which should also be improved: https://godoc.org/github.com/srwiley/rasterx

    these don't have enough coverage: https://github.com/srwiley/rasterx

Radius parsing in golang using gopacket. You can parse from either live traffic or from pcap of your choice.

go-radius Radius parsing in golang using gopacket. You can parse from either live traffic or from pcap of your choice. RADIUS RADIUS is an AAA (authen

Dec 1, 2022
Publish Your GIS Data(Vector Data) to PostGIS and Geoserver
Publish Your GIS Data(Vector Data) to PostGIS and Geoserver

GISManager Publish Your GIS Data(Vector Data) to PostGIS and Geoserver How to install: go get -v github.com/hishamkaram/gismanager Usage: testdata fol

Sep 26, 2022
A Go package converting a monochrome 1-bit bitmap image into a set of vector paths.
A Go package converting a monochrome 1-bit bitmap image into a set of vector paths.

go-bmppath Overview Package bmppath converts a monochrome 1-bit bitmap image into a set of vector paths. Note that this package is by no means a sophi

Mar 22, 2022
2D rendering for different output (raster, pdf, svg)
2D rendering for different output (raster, pdf, svg)

draw2d Package draw2d is a go 2D vector graphics library with support for multiple outputs such as images (draw2d), pdf documents (draw2dpdf), opengl

Dec 25, 2022
Go Language Library for SVG generation
Go Language Library for SVG generation

SVGo: A Go library for SVG generation The library generates SVG as defined by the Scalable Vector Graphics 1.1 Specification (http://www.w3.org/TR/SVG

Jan 6, 2023
gensvg generates SVG to an io.Writer
gensvg generates SVG to an io.Writer

gensvg: A Go library for SVG generation The library generates SVG as defined by the Scalable Vector Graphics 1.1 Specification

Dec 28, 2022
A simple API written in Go that creates badges in SVG format, based on the requested route.

A simple API written in Go that creates badges in SVG format, based on the requested route. Those graphics can be used to style README.md files, or to add tags to webpages.

Jul 2, 2021
Very simple SVG to PNG converter library using the Inkscape.

svg2png Description Very simple SVG to PNG converter library using the Inkscape.

Jan 11, 2022
Snippit - Creates syntax-highlighted code snippets in png or svg format
Snippit - Creates syntax-highlighted code snippets in png or svg format

snippit creates syntax-highlighted code snippets in png or svg format. Installat

Oct 10, 2022
Go-binsize-treemap - Go binary size SVG treemap

?? Go binary size SVG treemap Make treemap breakdown of Go executable binary $ g

Dec 21, 2022
A path tracer written in Go.
A path tracer written in Go.

pt: a golang path tracer This is a CPU-only, unidirectional path tracing engine written in Go. It has lots of features and a simple API. Features Supp

Jan 5, 2023
code2img can generate image of source code
code2img can generate image of source code

code2img code2img can generate image of source code. This was inspired by carbon and silicon Features Doesn't need browser & Internet Copy image of so

Nov 15, 2022
A note taking app, that you can draw in, syncs to the cloud, and is on most platforms!

About NoteDraw About · How to contribute · How to run Structure Codebase Description SRC The sorce code for the client side (Go) Branches Only Ones th

Jul 11, 2022
A soothing face filter where you can appreciate the beauty but not fully identify the person.
A soothing face filter where you can appreciate the beauty but not fully identify the person.

⭐ the project to show your appreciation. ↗️ Showerglass A soothing face filter where you can appreciate the beauty but not fully identify the person.

Oct 26, 2022
Use Windows API to capture a image from a Webcam in GoLANG

Windows-API-Capture-Webcam Use Windows API to capture a image from a Webcam in GoLANG Other Go is a amazing and powerful programming language. If you

Aug 13, 2022
Go bindings for GStreamer (retired: currently I don't use/develop this package)

Retired. I don't use/develop this package anymore. Go bindings for GStreamer at a very early stage of maturity. This package is based on GLib bindings

Nov 10, 2022
A lightweight and easy to use tool for deflickering timelapse image sequences.
A lightweight and easy to use tool for deflickering timelapse image sequences.

Simple Deflicker A minimalist, lightning-fast and easy to use tool for deflickering image sequences such as timelapses. It's still in its early stages

Aug 12, 2022
An easy-to-use OCR and Japanese to English translation tool
An easy-to-use OCR and Japanese to English translation tool

Manga Translator An easy-to-use application for translating text in images from Japanese to English. The GUI was created using Gio. Gio supports a var

Dec 28, 2022
OpenStreetMap PBF golang parser

pbf OpenStreetMap PBF golang encoder/decoder A golang based OpenStreetMap PBF encoder/decoder with a handy command line utility, pbf. pbf Command Line

Oct 23, 2022