This commit is contained in:
nuknal
2024-10-29 16:30:02 +08:00
parent bd93e9f543
commit 9c2d93dff4
2013 changed files with 117 additions and 690917 deletions

View File

@@ -35,6 +35,7 @@ var procCmd = &cobra.Command{
config.GCONFIG.Log.LogLevel)
logrus.SetLevel(config.GCONFIG.Log.LogLevel)
logrus.Info("image-proc version:", Version)
godal.RegisterAll()
@@ -61,6 +62,12 @@ var procCmd = &cobra.Command{
logrus.Fatal(err)
}
// for i, img := range reg.MssImages {
// edge := producer.CV_Sobel(img)
// utils.SavePanToGDALGTiff(edge, 0, 0, "data/temp/out_"+strconv.Itoa(i)+".tif", 1.3)
// }
// return
os.MkdirAll(params.OutputDir, 0755)
if doLUTRRC {

View File

@@ -30,7 +30,7 @@ var testCmd = &cobra.Command{
mv := gocv.Split(*input0_mat)
for i := 0; i < len(mv); i++ {
out := producer.FindEdges(mv[i])
out := producer.CV_Sobel(mv[i])
utils.SavePanToGDALGTiff(out, 0, 0, "data/temp/out_"+strconv.Itoa(i)+".tif", 1.3)
}
},

View File

@@ -25,9 +25,13 @@ var (
ShowVersion bool
)
const (
Version = "v1.2.0-beta"
)
func version() string {
v := fmt.Sprintf(`
Main Version: v1.2.0-beta
Main Version: %s
---------------------------------------------------------
BUILT_ON_IP %s
BUILT_ON_OS %s
@@ -40,7 +44,7 @@ RUNTIME_VER %s
OpenCV Version 4.9.0
GDAL Version 3.8.4
---------------------------------------------------------
`, BuiltOnIP, BuiltOnOs, BuildDate, LatestCommit, Branch, CommitCnt, BuildNumber, RuntimeVer)
`, Version, BuiltOnIP, BuiltOnOs, BuildDate, LatestCommit, Branch, CommitCnt, BuildNumber, RuntimeVer)
return v
}

2
go.mod
View File

@@ -4,7 +4,7 @@ go 1.20
require (
github.com/airbusgeo/godal v0.0.11
github.com/duke-git/lancet/v2 v2.3.1
github.com/duke-git/lancet/v2 v2.3.3
github.com/dustin/go-humanize v1.0.1
github.com/fsnotify/fsnotify v1.7.0
github.com/hebl/gofa v1.19.1

2
go.sum
View File

@@ -853,6 +853,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/duke-git/lancet/v2 v2.3.1 h1:cYZHQp57CZKP41EFkV/7TGbUrmhjaPMI5vi3Q+9KJNo=
github.com/duke-git/lancet/v2 v2.3.1/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/duke-git/lancet/v2 v2.3.3 h1:OhqzNzkbJBS9ZlWLo/C7g+WSAOAAyNj7p9CAiEHurUc=
github.com/duke-git/lancet/v2 v2.3.3/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=

View File

@@ -12,7 +12,8 @@ import (
// by performing phase correlation on detected
// edges instead of the raw image
func FindEdges(img0 gocv.Mat) gocv.Mat {
// SKIP: 多光谱各个波段的边缘检测结果不佳
func CV_Canny(img0 gocv.Mat) gocv.Mat {
fmt.Println(img0.Cols(), img0.Rows(), img0.Type().String())
dst8 := gocv.NewMatWithSize(img0.Rows(), img0.Cols(), gocv.MatTypeCV8U)
defer dst8.Close()
@@ -23,3 +24,20 @@ func FindEdges(img0 gocv.Mat) gocv.Mat {
dstEdge.ConvertTo(&dstEdge, gocv.MatTypeCV16U)
return dstEdge
}
func CV_Sobel(img0 gocv.Mat) gocv.Mat {
// x 方向
sobelX := gocv.NewMat()
gocv.Sobel(img0, &sobelX, gocv.MatTypeCV32F, 1, 0, 5, 1.5, 0, gocv.BorderDefault)
sobelX.ConvertTo(&sobelX, gocv.MatTypeCV16U)
// y 方向
sobelY := gocv.NewMat()
gocv.Sobel(img0, &sobelY, gocv.MatTypeCV32F, 0, 1, 5, 1.5, 0, gocv.BorderIsolated)
sobelY.ConvertTo(&sobelY, gocv.MatTypeCV16U)
// 合并
sobelXY := gocv.NewMat()
gocv.Sobel(img0, &sobelXY, gocv.MatTypeCV32F, 1, 1, 5, 1.5, 0, gocv.BorderDefault)
sobelXY.ConvertTo(&sobelXY, gocv.MatTypeCV16U)
return sobelY
}

View File

@@ -171,25 +171,41 @@ func (r *Registrator) CalcDownPhaseCorrelation() error {
blockWidth := r.MssWidth / BlockNW
// 在 MSS 4 个波段上进行配准
err := r.doMSSPhaseCorrelation(r.MssImages[0],
err := r.doPhaseCorrelation(r.MssImages[0],
[]gocv.Mat{r.MssImages[0], r.MssImages[1], r.MssImages[2], r.MssImages[3]},
r.MssHeight, r.MssWidth, blockHeight, blockWidth)
if err != nil {
return err
}
r.DoMSSCoRegistration()
r.fileterPhaseShift([]float64{64, 64, 64, 64}, true)
r.calcMSSDeltaCoeffs(4)
r.DoMSSCoRegistration(false)
// 边缘检测后再做一次配准
// var mssEdges []gocv.Mat
// for band := 0; band < len(r.registeredMssImages); band++ {
// edge := CV_Sobel(r.registeredMssImages[band])
// mssEdges = append(mssEdges, edge)
// }
// r.doPhaseCorrelation(mssEdges[0], mssEdges,
// r.MssHeight, r.MssWidth, blockHeight, blockWidth)
// r.fileterPhaseShift([]float64{5, 5, 5, 5}, false)
// r.calcMSSDeltaCoeffs(4)
// r.DoMSSCoRegistration(true)
// 基于 PAN 图像进行配准
r.phaseShifts[0] = make([]PhaseShiftM, 0)
r.deltaXCoeffs[0] = make([]float64, 0)
r.deltaYCoeffs[0] = make([]float64, 0)
err = r.doMSSPhaseCorrelation(downsampledPanImage,
err = r.doPhaseCorrelation(downsampledPanImage,
[]gocv.Mat{r.registeredMssImages[0]},
r.MssHeight, r.MssWidth, blockHeight, blockWidth)
if err != nil {
return err
}
return r.DoPANCoRegistration()
r.fileterPhaseShift([]float64{30.0}, true)
r.calcMSSDeltaCoeffs(1)
r.DoPANCoRegistration()
return nil
}
// 将MSS升采样采样后计算相位相关的偏移量
@@ -203,13 +219,13 @@ func (r *Registrator) CalcUpPhaseCorrelation() error {
}
// 在 MSS 4 个波段上进行配准
err := r.doMSSPhaseCorrelation(r.MssImages[0],
err := r.doPhaseCorrelation(r.MssImages[0],
[]gocv.Mat{r.MssImages[0], r.MssImages[1], r.MssImages[2], r.MssImages[3]},
r.MssHeight, r.MssWidth, r.MssHeight/BlockNH, r.MssWidth/BlockNW)
if err != nil {
return err
}
r.DoMSSCoRegistration()
r.DoMSSCoRegistration(false)
upsampledMssImages := make([]gocv.Mat, MssBands)
for i := 0; i < MssBands; i++ {
@@ -227,13 +243,13 @@ func (r *Registrator) CalcUpPhaseCorrelation() error {
log.Infof("blockHeight=%d, blockWidth=%d", blockHeight, blockWidth)
// 基于 PAN 图像进行配准
err = r.doMSSPhaseCorrelation(r.PanImage,
r.doPhaseCorrelation(r.PanImage,
[]gocv.Mat{upsampledMssImages[0]},
r.MssHeight, r.MssWidth, blockHeight, blockWidth)
return err
return r.DoPANCoRegistration()
}
func (r *Registrator) doMSSPhaseCorrelation(base gocv.Mat,
func (r *Registrator) doPhaseCorrelation(base gocv.Mat,
mssImages []gocv.Mat,
height, width,
blockHeight, blockWidth int) error {
@@ -241,6 +257,8 @@ func (r *Registrator) doMSSPhaseCorrelation(base gocv.Mat,
for band := 0; band < len(mssImages); band++ {
r.phaseShifts[band] = make([]PhaseShiftM, 0)
r.deltaXCoeffs[band] = make([]float64, 0)
r.deltaYCoeffs[band] = make([]float64, 0)
}
for bh := 0; bh < BlockNH; bh++ {
@@ -306,7 +324,7 @@ func (r *Registrator) doMSSPhaseCorrelation(base gocv.Mat,
}
}
return r.calcMSSDeltaCoeffs(len(mssImages))
return nil
}
func (r *Registrator) Clean() {
@@ -335,9 +353,9 @@ func (r *Registrator) calcMSSDeltaCoeffs(bands int) error {
}
// 经验值过滤
if shift.dy < 64.0 {
continue
}
// if shift.dy < 64.0 {
// continue
// }
effectShift++
cx = append(cx, float64(shift.Block.coord.X+shift.Block.width/2)) // MSS 块在X方向没有分块
@@ -355,12 +373,12 @@ func (r *Registrator) calcMSSDeltaCoeffs(bands int) error {
var err error
if r.deltaXCoeffs[i], err = PolynomialFit(cx, dx, 1); err != nil {
log.Error("Error fitting deltaX coeffs: ", err)
return err
continue
}
if r.deltaYCoeffs[i], err = PolynomialFit(cx, dy, 2); err != nil {
log.Error("Error fitting deltaY coeffs: ", err)
return err
continue
}
}
}
@@ -379,11 +397,13 @@ func (r *Registrator) calcMSSDeltaCoeffs(bands int) error {
return nil
}
func (r *Registrator) DoMSSCoRegistration() error {
func (r *Registrator) DoMSSCoRegistration(byEdge bool) error {
for band := 0; band < MssBands; band++ {
if len(r.deltaXCoeffs[band]) < 2 || len(r.deltaYCoeffs[band]) < 3 {
log.Errorf("delta coefficients not calculated, skip co-registration %d", band)
r.registeredMssImages[band] = r.MssImages[band].Clone()
log.Errorf("delta coefficients not calculated, skip co-registration %d", band+1)
if !byEdge {
r.registeredMssImages[band] = r.MssImages[band].Clone()
}
continue
}
@@ -408,13 +428,22 @@ func (r *Registrator) DoMSSCoRegistration() error {
}
log.Println("co-registration for band", band+1)
r.registeredMssImages[band] = gocv.NewMatWithSize(r.MssHeight, r.MssWidth, gocv.MatTypeCV16UC1)
gocv.Remap(r.MssImages[band],
&r.registeredMssImages[band],
&mapX, &mapY,
gocv.InterpolationCubic,
gocv.BorderConstant,
color.RGBA{0, 0, 0, 0})
if !byEdge {
r.registeredMssImages[band] = gocv.NewMatWithSize(r.MssHeight, r.MssWidth, gocv.MatTypeCV16UC1)
gocv.Remap(r.MssImages[band],
&r.registeredMssImages[band],
&mapX, &mapY,
gocv.InterpolationCubic,
gocv.BorderConstant,
color.RGBA{0, 0, 0, 0})
} else {
gocv.Remap(r.registeredMssImages[band],
&r.registeredMssImages[band],
&mapX, &mapY,
gocv.InterpolationCubic,
gocv.BorderConstant,
color.RGBA{0, 0, 0, 0})
}
mapX.Close()
mapY.Close()
@@ -458,7 +487,6 @@ func (r *Registrator) DoPANCoRegistration() error {
log.Println("co-registration for MSS (Align with PAN)")
for i := 0; i < MssBands; i++ {
log.Infof("r.registeredMssImages[%d]: %v", i, r.registeredMssImages[i].Size())
registeredMSS := gocv.NewMatWithSize(r.MssHeight, r.MssWidth, gocv.MatTypeCV16UC1)
gocv.Remap(r.registeredMssImages[i],
&registeredMSS,

View File

@@ -1,8 +1,10 @@
package producer
import (
"errors"
"image"
"github.com/duke-git/lancet/v2/slice"
log "github.com/sirupsen/logrus"
"gocv.io/x/gocv"
)
@@ -40,3 +42,25 @@ func (r *Registrator) calculateBlockPhaseShift(panBlock, mssBlock gocv.Mat) (goc
return shift, response
}
func (r *Registrator) fileterPhaseShift(thredholds []float64, greaterThan bool) error {
if len(thredholds) > 4 {
return errors.New("thredholds length should be less than 4")
}
for i := 0; i < len(thredholds); i++ {
th := thredholds[i]
r.phaseShifts[i] = slice.Filter(r.phaseShifts[i], func(i int, value PhaseShiftM) bool {
if value.response > 0.999999 {
return false
}
if greaterThan {
return value.dy > float32(th)
}
return value.dy < float32(th)
})
}
return nil
}

View File

@@ -1,20 +0,0 @@
Copyright (C) 2022 The gg Authors
Copyright (C) 2016 Michael Fogleman
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,239 +0,0 @@
# Go Graphics
[![Build status](https://builds.sr.ht/~sbinet/gg.svg)](https://builds.sr.ht/~sbinet/gg?)
[![GoDoc](https://pkg.go.dev/badge/git.sr.ht/~sbinet/gg)](https://pkg.go.dev/git.sr.ht/~sbinet/gg)
[![godocs.io](https://godocs.io/git.sr.ht/~sbinet/gg?status.svg)](https://godocs.io/git.sr.ht/~sbinet/gg)
`gg` is a library for rendering 2D graphics in pure Go.
`git.sr.ht/~sbinet/gg` is a fork of [fogleman/gg](https://github.com/fogleman/gg) which doesn't seem to be maintained (as of January 2022).
![Stars](https://git.sr.ht/~sbinet/gg/blob/main/examples/testdata/stars_golden.png)
## Installation
go get -u git.sr.ht/~sbinet/gg
## Documentation
- godoc: https://godoc.org/git.sr.ht/~sbinet/gg
- pkg.go.dev: https://pkg.go.dev/git.sr.ht/~sbinet/gg?tab=doc
## Hello, Circle!
Look how easy!
[embedmd]:# (examples/circle_example_test.go go /func ExampleCircle/ /\n}/)
```go
func ExampleCircle() {
dc := gg.NewContext(1000, 1000)
dc.DrawCircle(500, 500, 400)
dc.SetRGB(0, 0, 0)
dc.Fill()
err := dc.SavePNG("testdata/circle.png")
if err != nil {
log.Fatalf("could not save to file: %+v", err)
}
// Output:
}
```
## Examples
There are [lots of examples](https://git.sr.ht/~sbinet/gg/tree/main/examples) included. They're mostly for testing the code, but they're good for learning, too.
![Examples](http://i.imgur.com/tMFoyzu.png)
## Creating Contexts
There are a few ways of creating a context.
```go
NewContext(width, height int) *Context
NewContextForImage(im image.Image) *Context
NewContextForRGBA(im *image.RGBA) *Context
```
## Drawing Functions
Ever used a graphics library that didn't have functions for drawing rectangles
or circles? What a pain!
```go
DrawPoint(x, y, r float64)
DrawLine(x1, y1, x2, y2 float64)
DrawRectangle(x, y, w, h float64)
DrawRoundedRectangle(x, y, w, h, r float64)
DrawCircle(x, y, r float64)
DrawArc(x, y, r, angle1, angle2 float64)
DrawEllipse(x, y, rx, ry float64)
DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64)
DrawRegularPolygon(n int, x, y, r, rotation float64)
DrawImage(im image.Image, x, y int)
DrawImageAnchored(im image.Image, x, y int, ax, ay float64)
SetPixel(x, y int)
MoveTo(x, y float64)
LineTo(x, y float64)
QuadraticTo(x1, y1, x2, y2 float64)
CubicTo(x1, y1, x2, y2, x3, y3 float64)
ClosePath()
ClearPath()
NewSubPath()
Clear()
Stroke()
Fill()
StrokePreserve()
FillPreserve()
```
It is often desired to center an image at a point. Use `DrawImageAnchored` with `ax` and `ay` set to 0.5 to do this. Use 0 to left or top align. Use 1 to right or bottom align. `DrawStringAnchored` does the same for text, so you don't need to call `MeasureString` yourself.
## Text Functions
It will even do word wrap for you!
```go
DrawString(s string, x, y float64)
DrawStringAnchored(s string, x, y, ax, ay float64)
DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align)
MeasureString(s string) (w, h float64)
MeasureMultilineString(s string, lineSpacing float64) (w, h float64)
WordWrap(s string, w float64) []string
SetFontFace(fontFace font.Face)
LoadFontFace(path string, points float64) error
LoadFontFaceFromBytes(raw []byte, points float64) error
LoadFontFaceFromFS(fsys fs.FS, path string, points float64) error
```
## Color Functions
Colors can be set in several different ways for your convenience.
```go
SetRGB(r, g, b float64)
SetRGBA(r, g, b, a float64)
SetRGB255(r, g, b int)
SetRGBA255(r, g, b, a int)
SetColor(c color.Color)
SetHexColor(x string)
```
## Stroke & Fill Options
```go
SetLineWidth(lineWidth float64)
SetLineCap(lineCap LineCap)
SetLineJoin(lineJoin LineJoin)
SetDash(dashes ...float64)
SetDashOffset(offset float64)
SetFillRule(fillRule FillRule)
```
## Gradients & Patterns
`gg` supports linear, radial and conic gradients and surface patterns. You can also implement your own patterns.
```go
SetFillStyle(pattern Pattern)
SetStrokeStyle(pattern Pattern)
NewSolidPattern(color color.Color)
NewLinearGradient(x0, y0, x1, y1 float64)
NewRadialGradient(x0, y0, r0, x1, y1, r1 float64)
NewConicGradient(cx, cy, deg float64)
NewSurfacePattern(im image.Image, op RepeatOp)
```
## Transformation Functions
```go
Identity()
Translate(x, y float64)
Scale(x, y float64)
Rotate(angle float64)
Shear(x, y float64)
ScaleAbout(sx, sy, x, y float64)
RotateAbout(angle, x, y float64)
ShearAbout(sx, sy, x, y float64)
TransformPoint(x, y float64) (tx, ty float64)
InvertY()
```
It is often desired to rotate or scale about a point that is not the origin. The functions `RotateAbout`, `ScaleAbout`, `ShearAbout` are provided as a convenience.
`InvertY` is provided in case Y should increase from bottom to top vs. the default top to bottom.
## Stack Functions
Save and restore the state of the context. These can be nested.
```go
Push()
Pop()
```
## Clipping Functions
Use clipping regions to restrict drawing operations to an area that you
defined using paths.
```go
Clip()
ClipPreserve()
ResetClip()
AsMask() *image.Alpha
SetMask(mask *image.Alpha)
InvertMask()
```
## Helper Functions
Sometimes you just don't want to write these yourself.
```go
Radians(degrees float64) float64
Degrees(radians float64) float64
LoadImage(path string) (image.Image, error)
LoadPNG(path string) (image.Image, error)
SavePNG(path string, im image.Image) error
```
![Separator](https://git.sr.ht/~sbinet/gg/blob/main/examples/testdata/sine_golden.png)
## Another Example
See the output of this example below.
[embedmd]:# (examples/ellipse_example_test.go go /func ExampleEllipse/ /\n}/)
```go
func ExampleEllipse() {
const S = 1024
dc := gg.NewContext(S, S)
dc.SetRGBA(0, 0, 0, 0.1)
for i := 0; i < 360; i += 15 {
dc.Push()
dc.RotateAbout(gg.Radians(float64(i)), S/2, S/2)
dc.DrawEllipse(S/2, S/2, S*7/16, S/8)
dc.Fill()
dc.Pop()
}
im, err := gg.LoadImage("testdata/gopher.png")
if err != nil {
panic(err)
}
dc.DrawImageAnchored(im, S/2, S/2, 0.5, 0.5)
err = dc.SavePNG("testdata/ellipse.png")
if err != nil {
log.Fatalf("could not save to file: %+v", err)
}
// Output:
}
```
![Ellipses](https://git.sr.ht/~sbinet/gg/blob/main/examples/testdata/ellipse_golden.png)

View File

@@ -1,63 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import "math"
func quadratic(x0, y0, x1, y1, x2, y2, t float64) (x, y float64) {
u := 1 - t
a := u * u
b := 2 * u * t
c := t * t
x = a*x0 + b*x1 + c*x2
y = a*y0 + b*y1 + c*y2
return
}
func QuadraticBezier(x0, y0, x1, y1, x2, y2 float64) []Point {
l := (math.Hypot(x1-x0, y1-y0) +
math.Hypot(x2-x1, y2-y1))
n := int(l + 0.5)
if n < 4 {
n = 4
}
d := float64(n) - 1
result := make([]Point, n)
for i := 0; i < n; i++ {
t := float64(i) / d
x, y := quadratic(x0, y0, x1, y1, x2, y2, t)
result[i] = Point{x, y}
}
return result
}
func cubic(x0, y0, x1, y1, x2, y2, x3, y3, t float64) (x, y float64) {
u := 1 - t
a := u * u * u
b := 3 * u * u * t
c := 3 * u * t * t
d := t * t * t
x = a*x0 + b*x1 + c*x2 + d*x3
y = a*y0 + b*y1 + c*y2 + d*y3
return
}
func CubicBezier(x0, y0, x1, y1, x2, y2, x3, y3 float64) []Point {
l := (math.Hypot(x1-x0, y1-y0) +
math.Hypot(x2-x1, y2-y1) +
math.Hypot(x3-x2, y3-y2))
n := int(l + 0.5)
if n < 4 {
n = 4
}
d := float64(n) - 1
result := make([]Point, n)
for i := 0; i < n; i++ {
t := float64(i) / d
x, y := cubic(x0, y0, x1, y1, x2, y2, x3, y3, t)
result[i] = Point{x, y}
}
return result
}

View File

@@ -1,964 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"errors"
"image"
"image/color"
"image/jpeg"
"image/png"
"io"
"io/fs"
"math"
"strings"
"github.com/golang/freetype/raster"
"golang.org/x/image/draw"
"golang.org/x/image/font"
"golang.org/x/image/font/basicfont"
"golang.org/x/image/math/f64"
)
type LineCap int
const (
LineCapRound LineCap = iota
LineCapButt
LineCapSquare
)
type LineJoin int
const (
LineJoinRound LineJoin = iota
LineJoinBevel
)
type FillRule int
const (
FillRuleWinding FillRule = iota
FillRuleEvenOdd
)
type Align int
const (
AlignLeft Align = iota
AlignCenter
AlignRight
)
var (
defaultFillStyle = NewSolidPattern(color.White)
defaultStrokeStyle = NewSolidPattern(color.Black)
)
type Context struct {
width int
height int
rasterizer *raster.Rasterizer
im *image.RGBA
mask *image.Alpha
color color.Color
fillPattern Pattern
strokePattern Pattern
strokePath raster.Path
fillPath raster.Path
start Point
current Point
hasCurrent bool
dashes []float64
dashOffset float64
lineWidth float64
lineCap LineCap
lineJoin LineJoin
fillRule FillRule
fontFace font.Face
fontHeight float64
matrix Matrix
stack []*Context
interp draw.Interpolator
}
// NewContext creates a new image.RGBA with the specified width and height
// and prepares a context for rendering onto that image.
func NewContext(width, height int) *Context {
return NewContextForRGBA(image.NewRGBA(image.Rect(0, 0, width, height)))
}
// NewContextForImage copies the specified image into a new image.RGBA
// and prepares a context for rendering onto that image.
func NewContextForImage(im image.Image) *Context {
return NewContextForRGBA(imageToRGBA(im))
}
// NewContextForRGBA prepares a context for rendering onto the specified image.
// No copy is made.
func NewContextForRGBA(im *image.RGBA) *Context {
w := im.Bounds().Size().X
h := im.Bounds().Size().Y
return &Context{
width: w,
height: h,
rasterizer: raster.NewRasterizer(w, h),
im: im,
color: color.Transparent,
fillPattern: defaultFillStyle,
strokePattern: defaultStrokeStyle,
lineWidth: 1,
fillRule: FillRuleWinding,
fontFace: basicfont.Face7x13,
fontHeight: 13,
matrix: Identity(),
interp: draw.BiLinear,
}
}
// GetCurrentPoint will return the current point and if there is a current point.
// The point will have been transformed by the context's transformation matrix.
func (dc *Context) GetCurrentPoint() (Point, bool) {
if dc.hasCurrent {
return dc.current, true
}
return Point{}, false
}
// Image returns the image that has been drawn by this context.
func (dc *Context) Image() image.Image {
return dc.im
}
// Width returns the width of the image in pixels.
func (dc *Context) Width() int {
return dc.width
}
// Height returns the height of the image in pixels.
func (dc *Context) Height() int {
return dc.height
}
// SavePNG encodes the image as a PNG and writes it to disk.
func (dc *Context) SavePNG(path string) error {
return SavePNG(path, dc.im)
}
// SaveJPG encodes the image as a JPG and writes it to disk.
func (dc *Context) SaveJPG(path string, quality int) error {
return SaveJPG(path, dc.im, quality)
}
// EncodePNG encodes the image as a PNG and writes it to the provided io.Writer.
func (dc *Context) EncodePNG(w io.Writer) error {
return png.Encode(w, dc.im)
}
// EncodeJPG encodes the image as a JPG and writes it to the provided io.Writer
// in JPEG 4:2:0 baseline format with the given options.
// Default parameters are used if a nil *jpeg.Options is passed.
func (dc *Context) EncodeJPG(w io.Writer, o *jpeg.Options) error {
return jpeg.Encode(w, dc.im, o)
}
// SetDash sets the current dash pattern to use. Call with zero arguments to
// disable dashes. The values specify the lengths of each dash, with
// alternating on and off lengths.
func (dc *Context) SetDash(dashes ...float64) {
dc.dashes = dashes
}
// SetDashOffset sets the initial offset into the dash pattern to use when
// stroking dashed paths.
func (dc *Context) SetDashOffset(offset float64) {
dc.dashOffset = offset
}
func (dc *Context) SetLineWidth(lineWidth float64) {
dc.lineWidth = lineWidth
}
func (dc *Context) SetLineCap(lineCap LineCap) {
dc.lineCap = lineCap
}
func (dc *Context) SetLineCapRound() {
dc.lineCap = LineCapRound
}
func (dc *Context) SetLineCapButt() {
dc.lineCap = LineCapButt
}
func (dc *Context) SetLineCapSquare() {
dc.lineCap = LineCapSquare
}
func (dc *Context) SetLineJoin(lineJoin LineJoin) {
dc.lineJoin = lineJoin
}
func (dc *Context) SetLineJoinRound() {
dc.lineJoin = LineJoinRound
}
func (dc *Context) SetLineJoinBevel() {
dc.lineJoin = LineJoinBevel
}
func (dc *Context) SetFillRule(fillRule FillRule) {
dc.fillRule = fillRule
}
func (dc *Context) SetFillRuleWinding() {
dc.fillRule = FillRuleWinding
}
func (dc *Context) SetFillRuleEvenOdd() {
dc.fillRule = FillRuleEvenOdd
}
// Color Setters
func (dc *Context) setFillAndStrokeColor(c color.Color) {
dc.color = c
dc.fillPattern = NewSolidPattern(c)
dc.strokePattern = NewSolidPattern(c)
}
// SetFillStyle sets current fill style
func (dc *Context) SetFillStyle(pattern Pattern) {
// if pattern is SolidPattern, also change dc.color(for dc.Clear, dc.drawString)
if fillStyle, ok := pattern.(*solidPattern); ok {
dc.color = fillStyle.color
}
dc.fillPattern = pattern
}
// SetStrokeStyle sets current stroke style
func (dc *Context) SetStrokeStyle(pattern Pattern) {
dc.strokePattern = pattern
}
// SetColor sets the current color(for both fill and stroke).
func (dc *Context) SetColor(c color.Color) {
dc.setFillAndStrokeColor(c)
}
// SetHexColor sets the current color using a hex string. The leading pound
// sign (#) is optional. Both 3- and 6-digit variations are supported. 8 digits
// may be provided to set the alpha value as well.
func (dc *Context) SetHexColor(x string) {
r, g, b, a := parseHexColor(x)
dc.SetRGBA255(r, g, b, a)
}
// SetRGBA255 sets the current color. r, g, b, a values should be between 0 and
// 255, inclusive.
func (dc *Context) SetRGBA255(r, g, b, a int) {
dc.color = color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)}
dc.setFillAndStrokeColor(dc.color)
}
// SetRGB255 sets the current color. r, g, b values should be between 0 and 255,
// inclusive. Alpha will be set to 255 (fully opaque).
func (dc *Context) SetRGB255(r, g, b int) {
dc.SetRGBA255(r, g, b, 255)
}
// SetRGBA sets the current color. r, g, b, a values should be between 0 and 1,
// inclusive.
func (dc *Context) SetRGBA(r, g, b, a float64) {
dc.color = color.NRGBA{
uint8(r * 255),
uint8(g * 255),
uint8(b * 255),
uint8(a * 255),
}
dc.setFillAndStrokeColor(dc.color)
}
// SetRGB sets the current color. r, g, b values should be between 0 and 1,
// inclusive. Alpha will be set to 1 (fully opaque).
func (dc *Context) SetRGB(r, g, b float64) {
dc.SetRGBA(r, g, b, 1)
}
// Path Manipulation
// MoveTo starts a new subpath within the current path starting at the
// specified point.
func (dc *Context) MoveTo(x, y float64) {
if dc.hasCurrent {
dc.fillPath.Add1(dc.start.Fixed())
}
x, y = dc.TransformPoint(x, y)
p := Point{x, y}
dc.strokePath.Start(p.Fixed())
dc.fillPath.Start(p.Fixed())
dc.start = p
dc.current = p
dc.hasCurrent = true
}
// LineTo adds a line segment to the current path starting at the current
// point. If there is no current point, it is equivalent to MoveTo(x, y)
func (dc *Context) LineTo(x, y float64) {
if !dc.hasCurrent {
dc.MoveTo(x, y)
} else {
x, y = dc.TransformPoint(x, y)
p := Point{x, y}
dc.strokePath.Add1(p.Fixed())
dc.fillPath.Add1(p.Fixed())
dc.current = p
}
}
// QuadraticTo adds a quadratic bezier curve to the current path starting at
// the current point. If there is no current point, it first performs
// MoveTo(x1, y1)
func (dc *Context) QuadraticTo(x1, y1, x2, y2 float64) {
if !dc.hasCurrent {
dc.MoveTo(x1, y1)
}
x1, y1 = dc.TransformPoint(x1, y1)
x2, y2 = dc.TransformPoint(x2, y2)
p1 := Point{x1, y1}
p2 := Point{x2, y2}
dc.strokePath.Add2(p1.Fixed(), p2.Fixed())
dc.fillPath.Add2(p1.Fixed(), p2.Fixed())
dc.current = p2
}
// CubicTo adds a cubic bezier curve to the current path starting at the
// current point. If there is no current point, it first performs
// MoveTo(x1, y1). Because freetype/raster does not support cubic beziers,
// this is emulated with many small line segments.
func (dc *Context) CubicTo(x1, y1, x2, y2, x3, y3 float64) {
if !dc.hasCurrent {
dc.MoveTo(x1, y1)
}
x0, y0 := dc.current.X, dc.current.Y
x1, y1 = dc.TransformPoint(x1, y1)
x2, y2 = dc.TransformPoint(x2, y2)
x3, y3 = dc.TransformPoint(x3, y3)
points := CubicBezier(x0, y0, x1, y1, x2, y2, x3, y3)
previous := dc.current.Fixed()
for _, p := range points[1:] {
f := p.Fixed()
if f == previous {
// TODO: this fixes some rendering issues but not all
continue
}
previous = f
dc.strokePath.Add1(f)
dc.fillPath.Add1(f)
dc.current = p
}
}
// ClosePath adds a line segment from the current point to the beginning
// of the current subpath. If there is no current point, this is a no-op.
func (dc *Context) ClosePath() {
if dc.hasCurrent {
dc.strokePath.Add1(dc.start.Fixed())
dc.fillPath.Add1(dc.start.Fixed())
dc.current = dc.start
}
}
// ClearPath clears the current path. There is no current point after this
// operation.
func (dc *Context) ClearPath() {
dc.strokePath.Clear()
dc.fillPath.Clear()
dc.hasCurrent = false
}
// NewSubPath starts a new subpath within the current path. There is no current
// point after this operation.
func (dc *Context) NewSubPath() {
if dc.hasCurrent {
dc.fillPath.Add1(dc.start.Fixed())
}
dc.hasCurrent = false
}
// Path Drawing
func (dc *Context) capper() raster.Capper {
switch dc.lineCap {
case LineCapButt:
return raster.ButtCapper
case LineCapRound:
return raster.RoundCapper
case LineCapSquare:
return raster.SquareCapper
}
return nil
}
func (dc *Context) joiner() raster.Joiner {
switch dc.lineJoin {
case LineJoinBevel:
return raster.BevelJoiner
case LineJoinRound:
return raster.RoundJoiner
}
return nil
}
func (dc *Context) stroke(painter raster.Painter) {
path := dc.strokePath
if len(dc.dashes) > 0 {
path = dashed(path, dc.dashes, dc.dashOffset)
} else {
// TODO: this is a temporary workaround to remove tiny segments
// that result in rendering issues
path = rasterPath(flattenPath(path))
}
r := dc.rasterizer
r.UseNonZeroWinding = true
r.Clear()
r.AddStroke(path, fix(dc.lineWidth), dc.capper(), dc.joiner())
r.Rasterize(painter)
}
func (dc *Context) fill(painter raster.Painter) {
path := dc.fillPath
if dc.hasCurrent {
path = make(raster.Path, len(dc.fillPath))
copy(path, dc.fillPath)
path.Add1(dc.start.Fixed())
}
r := dc.rasterizer
r.UseNonZeroWinding = dc.fillRule == FillRuleWinding
r.Clear()
r.AddPath(path)
r.Rasterize(painter)
}
// StrokePreserve strokes the current path with the current color, line width,
// line cap, line join and dash settings. The path is preserved after this
// operation.
func (dc *Context) StrokePreserve() {
var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.strokePattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.strokePattern)
}
dc.stroke(painter)
}
// Stroke strokes the current path with the current color, line width,
// line cap, line join and dash settings. The path is cleared after this
// operation.
func (dc *Context) Stroke() {
dc.StrokePreserve()
dc.ClearPath()
}
// FillPreserve fills the current path with the current color. Open subpaths
// are implicitly closed. The path is preserved after this operation.
func (dc *Context) FillPreserve() {
var painter raster.Painter
if dc.mask == nil {
if pattern, ok := dc.fillPattern.(*solidPattern); ok {
// with a nil mask and a solid color pattern, we can be more efficient
// TODO: refactor so we don't have to do this type assertion stuff?
p := raster.NewRGBAPainter(dc.im)
p.SetColor(pattern.color)
painter = p
}
}
if painter == nil {
painter = newPatternPainter(dc.im, dc.mask, dc.fillPattern)
}
dc.fill(painter)
}
// Fill fills the current path with the current color. Open subpaths
// are implicitly closed. The path is cleared after this operation.
func (dc *Context) Fill() {
dc.FillPreserve()
dc.ClearPath()
}
// ClipPreserve updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by dc.Fill().
// The path is preserved after this operation.
func (dc *Context) ClipPreserve() {
clip := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height))
painter := raster.NewAlphaOverPainter(clip)
dc.fill(painter)
if dc.mask == nil {
dc.mask = clip
} else {
mask := image.NewAlpha(image.Rect(0, 0, dc.width, dc.height))
draw.DrawMask(mask, mask.Bounds(), clip, image.Point{}, dc.mask, image.Point{}, draw.Over)
dc.mask = mask
}
}
// SetMask allows you to directly set the *image.Alpha to be used as a clipping
// mask. It must be the same size as the context, else an error is returned
// and the mask is unchanged.
func (dc *Context) SetMask(mask *image.Alpha) error {
if mask.Bounds().Size() != dc.im.Bounds().Size() {
return errors.New("mask size must match context size")
}
dc.mask = mask
return nil
}
// AsMask returns an *image.Alpha representing the alpha channel of this
// context. This can be useful for advanced clipping operations where you first
// render the mask geometry and then use it as a mask.
func (dc *Context) AsMask() *image.Alpha {
mask := image.NewAlpha(dc.im.Bounds())
draw.Draw(mask, dc.im.Bounds(), dc.im, image.Point{}, draw.Src)
return mask
}
// InvertMask inverts the alpha values in the current clipping mask such that
// a fully transparent region becomes fully opaque and vice versa.
func (dc *Context) InvertMask() {
if dc.mask == nil {
dc.mask = image.NewAlpha(dc.im.Bounds())
} else {
for i, a := range dc.mask.Pix {
dc.mask.Pix[i] = 255 - a
}
}
}
// Clip updates the clipping region by intersecting the current
// clipping region with the current path as it would be filled by dc.Fill().
// The path is cleared after this operation.
func (dc *Context) Clip() {
dc.ClipPreserve()
dc.ClearPath()
}
// ResetClip clears the clipping region.
func (dc *Context) ResetClip() {
dc.mask = nil
}
// Convenient Drawing Functions
// Clear fills the entire image with the current color.
func (dc *Context) Clear() {
src := image.NewUniform(dc.color)
draw.Draw(dc.im, dc.im.Bounds(), src, image.Point{}, draw.Src)
}
// SetPixel sets the color of the specified pixel using the current color.
func (dc *Context) SetPixel(x, y int) {
dc.im.Set(x, y, dc.color)
}
// DrawPoint is like DrawCircle but ensures that a circle of the specified
// size is drawn regardless of the current transformation matrix. The position
// is still transformed, but not the shape of the point.
func (dc *Context) DrawPoint(x, y, r float64) {
dc.Push()
tx, ty := dc.TransformPoint(x, y)
dc.Identity()
dc.DrawCircle(tx, ty, r)
dc.Pop()
}
func (dc *Context) DrawLine(x1, y1, x2, y2 float64) {
dc.MoveTo(x1, y1)
dc.LineTo(x2, y2)
}
func (dc *Context) DrawRectangle(x, y, w, h float64) {
dc.NewSubPath()
dc.MoveTo(x, y)
dc.LineTo(x+w, y)
dc.LineTo(x+w, y+h)
dc.LineTo(x, y+h)
dc.ClosePath()
}
func (dc *Context) DrawRoundedRectangle(x, y, w, h, r float64) {
x0, x1, x2, x3 := x, x+r, x+w-r, x+w
y0, y1, y2, y3 := y, y+r, y+h-r, y+h
dc.NewSubPath()
dc.MoveTo(x1, y0)
dc.LineTo(x2, y0)
dc.DrawArc(x2, y1, r, Radians(270), Radians(360))
dc.LineTo(x3, y2)
dc.DrawArc(x2, y2, r, Radians(0), Radians(90))
dc.LineTo(x1, y3)
dc.DrawArc(x1, y2, r, Radians(90), Radians(180))
dc.LineTo(x0, y1)
dc.DrawArc(x1, y1, r, Radians(180), Radians(270))
dc.ClosePath()
}
func (dc *Context) DrawEllipticalArc(x, y, rx, ry, angle1, angle2 float64) {
const n = 16
for i := 0; i < n; i++ {
p1 := float64(i+0) / n
p2 := float64(i+1) / n
a1 := angle1 + (angle2-angle1)*p1
a2 := angle1 + (angle2-angle1)*p2
x0 := x + rx*math.Cos(a1)
y0 := y + ry*math.Sin(a1)
x1 := x + rx*math.Cos((a1+a2)/2)
y1 := y + ry*math.Sin((a1+a2)/2)
x2 := x + rx*math.Cos(a2)
y2 := y + ry*math.Sin(a2)
cx := 2*x1 - x0/2 - x2/2
cy := 2*y1 - y0/2 - y2/2
if i == 0 {
if dc.hasCurrent {
dc.LineTo(x0, y0)
} else {
dc.MoveTo(x0, y0)
}
}
dc.QuadraticTo(cx, cy, x2, y2)
}
}
func (dc *Context) DrawEllipse(x, y, rx, ry float64) {
dc.NewSubPath()
dc.DrawEllipticalArc(x, y, rx, ry, 0, 2*math.Pi)
dc.ClosePath()
}
func (dc *Context) DrawArc(x, y, r, angle1, angle2 float64) {
dc.DrawEllipticalArc(x, y, r, r, angle1, angle2)
}
func (dc *Context) DrawCircle(x, y, r float64) {
dc.NewSubPath()
dc.DrawEllipticalArc(x, y, r, r, 0, 2*math.Pi)
dc.ClosePath()
}
func (dc *Context) DrawRegularPolygon(n int, x, y, r, rotation float64) {
angle := 2 * math.Pi / float64(n)
rotation -= math.Pi / 2
if n%2 == 0 {
rotation += angle / 2
}
dc.NewSubPath()
for i := 0; i < n; i++ {
a := rotation + angle*float64(i)
dc.LineTo(x+r*math.Cos(a), y+r*math.Sin(a))
}
dc.ClosePath()
}
// SetInterpolator sets the current context's drawing interpolator.
func (dc *Context) SetInterpolator(interp draw.Interpolator) {
if interp == nil {
panic(errors.New("gg: invalid interpolator"))
}
dc.interp = interp
}
// DrawImage draws the specified image at the specified point.
func (dc *Context) DrawImage(im image.Image, x, y int) {
dc.DrawImageAnchored(im, x, y, 0, 0)
}
// DrawImageAnchored draws the specified image at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// image. Use ax=0.5, ay=0.5 to center the image at the specified point.
func (dc *Context) DrawImageAnchored(im image.Image, x, y int, ax, ay float64) {
s := im.Bounds().Size()
x -= int(ax * float64(s.X))
y -= int(ay * float64(s.Y))
var (
fx = float64(x)
fy = float64(y)
m = dc.matrix.Translate(fx, fy)
s2d = f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
opt *draw.Options
)
if dc.mask != nil {
opt = &draw.Options{
DstMask: dc.mask,
DstMaskP: image.Point{},
}
}
dc.interp.Transform(dc.im, s2d, im, im.Bounds(), draw.Over, opt)
}
// Text Functions
func (dc *Context) SetFontFace(fontFace font.Face) {
dc.fontFace = fontFace
dc.fontHeight = float64(fontFace.Metrics().Height) / 64
}
func (dc *Context) LoadFontFace(path string, points float64) error {
face, err := LoadFontFace(path, points)
if err != nil {
return err
}
dc.fontFace = face
dc.fontHeight = points * 72 / 96
return nil
}
func (dc *Context) LoadFontFaceFromFS(fsys fs.FS, path string, points float64) error {
face, err := LoadFontFaceFromFS(fsys, path, points)
if err != nil {
return err
}
dc.fontFace = face
dc.fontHeight = points * 72 / 96
return nil
}
func (dc *Context) LoadFontFaceFromBytes(raw []byte, points float64) error {
face, err := LoadFontFaceFromBytes(raw, points)
if err != nil {
return err
}
dc.fontFace = face
dc.fontHeight = points * 72 / 96
return nil
}
func (dc *Context) FontHeight() float64 {
return dc.fontHeight
}
func (dc *Context) drawString(im *image.RGBA, s string, x, y float64) {
d := &font.Drawer{
Dst: im,
Src: image.NewUniform(dc.color),
Face: dc.fontFace,
Dot: fixp(x, y),
}
// based on Drawer.DrawString() in golang.org/x/image/font/font.go
prevC := rune(-1)
for _, c := range s {
if prevC >= 0 {
d.Dot.X += d.Face.Kern(prevC, c)
}
dr, mask, maskp, advance, ok := d.Face.Glyph(d.Dot, c)
if !ok {
// TODO: is falling back on the U+FFFD glyph the responsibility of
// the Drawer or the Face?
// TODO: set prevC = '\ufffd'?
continue
}
sr := dr.Sub(dr.Min)
fx, fy := float64(dr.Min.X), float64(dr.Min.Y)
m := dc.matrix.Translate(fx, fy)
s2d := f64.Aff3{m.XX, m.XY, m.X0, m.YX, m.YY, m.Y0}
dc.interp.Transform(d.Dst, s2d, d.Src, sr, draw.Over, &draw.Options{
SrcMask: mask,
SrcMaskP: maskp,
})
d.Dot.X += advance
prevC = c
}
}
// DrawString draws the specified text at the specified point.
func (dc *Context) DrawString(s string, x, y float64) {
dc.DrawStringAnchored(s, x, y, 0, 0)
}
// DrawStringAnchored draws the specified text at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// text. Use ax=0.5, ay=0.5 to center the text at the specified point.
func (dc *Context) DrawStringAnchored(s string, x, y, ax, ay float64) {
w, h := dc.MeasureString(s)
x -= ax * w
y += ay * h
if dc.mask == nil {
dc.drawString(dc.im, s, x, y)
} else {
im := image.NewRGBA(image.Rect(0, 0, dc.width, dc.height))
dc.drawString(im, s, x, y)
draw.DrawMask(dc.im, dc.im.Bounds(), im, image.Point{}, dc.mask, image.Point{}, draw.Over)
}
}
// DrawStringWrapped word-wraps the specified string to the given max width
// and then draws it at the specified anchor point using the given line
// spacing and text alignment.
func (dc *Context) DrawStringWrapped(s string, x, y, ax, ay, width, lineSpacing float64, align Align) {
lines := dc.WordWrap(s, width)
// sync h formula with MeasureMultilineString
h := float64(len(lines)) * dc.fontHeight * lineSpacing
h -= (lineSpacing - 1) * dc.fontHeight
x -= ax * width
y -= ay * h
switch align {
case AlignLeft:
ax = 0
case AlignCenter:
ax = 0.5
x += width / 2
case AlignRight:
ax = 1
x += width
}
ay = 1
for _, line := range lines {
dc.DrawStringAnchored(line, x, y, ax, ay)
y += dc.fontHeight * lineSpacing
}
}
func (dc *Context) MeasureMultilineString(s string, lineSpacing float64) (width, height float64) {
lines := strings.Split(s, "\n")
// sync h formula with DrawStringWrapped
height = float64(len(lines)) * dc.fontHeight * lineSpacing
height -= (lineSpacing - 1) * dc.fontHeight
d := &font.Drawer{
Face: dc.fontFace,
}
// max width from lines
for _, line := range lines {
adv := d.MeasureString(line)
currentWidth := float64(adv >> 6) // from gg.Context.MeasureString
if currentWidth > width {
width = currentWidth
}
}
return width, height
}
// MeasureString returns the rendered width and height of the specified text
// given the current font face.
func (dc *Context) MeasureString(s string) (w, h float64) {
d := &font.Drawer{
Face: dc.fontFace,
}
a := d.MeasureString(s)
return float64(a >> 6), dc.fontHeight
}
// WordWrap wraps the specified string to the given max width and current
// font face.
func (dc *Context) WordWrap(s string, w float64) []string {
return wordWrap(dc, s, w)
}
// Transformation Matrix Operations
// Identity resets the current transformation matrix to the identity matrix.
// This results in no translating, scaling, rotating, or shearing.
func (dc *Context) Identity() {
dc.matrix = Identity()
}
// Translate updates the current matrix with a translation.
func (dc *Context) Translate(x, y float64) {
dc.matrix = dc.matrix.Translate(x, y)
}
// Scale updates the current matrix with a scaling factor.
// Scaling occurs about the origin.
func (dc *Context) Scale(x, y float64) {
dc.matrix = dc.matrix.Scale(x, y)
}
// ScaleAbout updates the current matrix with a scaling factor.
// Scaling occurs about the specified point.
func (dc *Context) ScaleAbout(sx, sy, x, y float64) {
dc.Translate(x, y)
dc.Scale(sx, sy)
dc.Translate(-x, -y)
}
// Rotate updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the origin. Angle is specified in radians.
func (dc *Context) Rotate(angle float64) {
dc.matrix = dc.matrix.Rotate(angle)
}
// RotateAbout updates the current matrix with a anticlockwise rotation.
// Rotation occurs about the specified point. Angle is specified in radians.
func (dc *Context) RotateAbout(angle, x, y float64) {
dc.Translate(x, y)
dc.Rotate(angle)
dc.Translate(-x, -y)
}
// Shear updates the current matrix with a shearing angle.
// Shearing occurs about the origin.
func (dc *Context) Shear(x, y float64) {
dc.matrix = dc.matrix.Shear(x, y)
}
// ShearAbout updates the current matrix with a shearing angle.
// Shearing occurs about the specified point.
func (dc *Context) ShearAbout(sx, sy, x, y float64) {
dc.Translate(x, y)
dc.Shear(sx, sy)
dc.Translate(-x, -y)
}
// TransformPoint multiplies the specified point by the current matrix,
// returning a transformed position.
func (dc *Context) TransformPoint(x, y float64) (tx, ty float64) {
return dc.matrix.TransformPoint(x, y)
}
// InvertY flips the Y axis so that Y grows from bottom to top and Y=0 is at
// the bottom of the image.
func (dc *Context) InvertY() {
dc.Translate(0, float64(dc.height))
dc.Scale(1, -1)
}
// Stack
// Push saves the current state of the context for later retrieval. These
// can be nested.
func (dc *Context) Push() {
x := *dc
dc.stack = append(dc.stack, &x)
}
// Pop restores the last saved context state from the stack.
func (dc *Context) Pop() {
var (
before = *dc
s = dc.stack
ctx *Context
)
ctx, dc.stack = s[len(s)-1], s[:len(s)-1]
*dc = *ctx
dc.mask = before.mask
dc.strokePath = before.strokePath
dc.fillPath = before.fillPath
dc.start = before.start
dc.current = before.current
dc.hasCurrent = before.hasCurrent
}

View File

@@ -1,8 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
//go:generate go run github.com/campoy/embedmd -w README.md
// Package gg provides a simple API for rendering 2D graphics in pure Go.
package gg // import "git.sr.ht/~sbinet/gg"

View File

@@ -1,11 +0,0 @@
// Copyright ©2023 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
//go:build tools
package gg
import (
_ "github.com/campoy/embedmd" // needed for generating gallery
)

View File

@@ -1,252 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"image/color"
"math"
"sort"
)
type stop struct {
pos float64
color color.Color
}
type stops []stop
// Len satisfies the Sort interface.
func (s stops) Len() int {
return len(s)
}
// Less satisfies the Sort interface.
func (s stops) Less(i, j int) bool {
return s[i].pos < s[j].pos
}
// Swap satisfies the Sort interface.
func (s stops) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
type Gradient interface {
Pattern
AddColorStop(offset float64, color color.Color)
}
// Linear Gradient
type linearGradient struct {
x0, y0, x1, y1 float64
stops stops
}
func (g *linearGradient) ColorAt(x, y int) color.Color {
if len(g.stops) == 0 {
return color.Transparent
}
fx, fy := float64(x), float64(y)
x0, y0, x1, y1 := g.x0, g.y0, g.x1, g.y1
dx, dy := x1-x0, y1-y0
// Horizontal
if dy == 0 && dx != 0 {
return getColor((fx-x0)/dx, g.stops)
}
// Vertical
if dx == 0 && dy != 0 {
return getColor((fy-y0)/dy, g.stops)
}
// Dot product
s0 := dx*(fx-x0) + dy*(fy-y0)
if s0 < 0 {
return g.stops[0].color
}
// Calculate distance to (x0,y0) alone (x0,y0)->(x1,y1)
mag := math.Hypot(dx, dy)
u := ((fx-x0)*-dy + (fy-y0)*dx) / (mag * mag)
x2, y2 := x0+u*-dy, y0+u*dx
d := math.Hypot(fx-x2, fy-y2) / mag
return getColor(d, g.stops)
}
func (g *linearGradient) AddColorStop(offset float64, color color.Color) {
g.stops = append(g.stops, stop{pos: offset, color: color})
sort.Sort(g.stops)
}
func NewLinearGradient(x0, y0, x1, y1 float64) Gradient {
g := &linearGradient{
x0: x0, y0: y0,
x1: x1, y1: y1,
}
return g
}
// Radial Gradient
type circle struct {
x, y, r float64
}
type radialGradient struct {
c0, c1, cd circle
a, inva float64
mindr float64
stops stops
}
func dot3(x0, y0, z0, x1, y1, z1 float64) float64 {
return x0*x1 + y0*y1 + z0*z1
}
func (g *radialGradient) ColorAt(x, y int) color.Color {
if len(g.stops) == 0 {
return color.Transparent
}
// copy from pixman's pixman-radial-gradient.c
dx, dy := float64(x)+0.5-g.c0.x, float64(y)+0.5-g.c0.y
b := dot3(dx, dy, g.c0.r, g.cd.x, g.cd.y, g.cd.r)
c := dot3(dx, dy, -g.c0.r, dx, dy, g.c0.r)
if g.a == 0 {
if b == 0 {
return color.Transparent
}
t := 0.5 * c / b
if t*g.cd.r >= g.mindr {
return getColor(t, g.stops)
}
return color.Transparent
}
discr := dot3(b, g.a, 0, b, -c, 0)
if discr >= 0 {
sqrtdiscr := math.Sqrt(discr)
t0 := (b + sqrtdiscr) * g.inva
t1 := (b - sqrtdiscr) * g.inva
if t0*g.cd.r >= g.mindr {
return getColor(t0, g.stops)
} else if t1*g.cd.r >= g.mindr {
return getColor(t1, g.stops)
}
}
return color.Transparent
}
func (g *radialGradient) AddColorStop(offset float64, color color.Color) {
g.stops = append(g.stops, stop{pos: offset, color: color})
sort.Sort(g.stops)
}
func NewRadialGradient(x0, y0, r0, x1, y1, r1 float64) Gradient {
c0 := circle{x0, y0, r0}
c1 := circle{x1, y1, r1}
cd := circle{x1 - x0, y1 - y0, r1 - r0}
a := dot3(cd.x, cd.y, -cd.r, cd.x, cd.y, cd.r)
var inva float64
if a != 0 {
inva = 1.0 / a
}
mindr := -c0.r
g := &radialGradient{
c0: c0,
c1: c1,
cd: cd,
a: a,
inva: inva,
mindr: mindr,
}
return g
}
// Conic Gradient
type conicGradient struct {
cx, cy float64
rotation float64
stops stops
}
func (g *conicGradient) ColorAt(x, y int) color.Color {
if len(g.stops) == 0 {
return color.Transparent
}
a := math.Atan2(float64(y)-g.cy, float64(x)-g.cx)
t := norm(a, -math.Pi, math.Pi) - g.rotation
if t < 0 {
t += 1
}
return getColor(t, g.stops)
}
func (g *conicGradient) AddColorStop(offset float64, color color.Color) {
g.stops = append(g.stops, stop{pos: offset, color: color})
sort.Sort(g.stops)
}
func NewConicGradient(cx, cy, deg float64) Gradient {
g := &conicGradient{
cx: cx,
cy: cy,
rotation: normalizeAngle(deg) / 360,
}
return g
}
func normalizeAngle(t float64) float64 {
t = math.Mod(t, 360)
if t < 0 {
t += 360
}
return t
}
// Map value which is in range [a..b] to range [0..1]
func norm(value, a, b float64) float64 {
return (value - a) * (1.0 / (b - a))
}
func getColor(pos float64, stops stops) color.Color {
if pos <= 0.0 || len(stops) == 1 {
return stops[0].color
}
last := stops[len(stops)-1]
if pos >= last.pos {
return last.color
}
for i, stop := range stops[1:] {
if pos < stop.pos {
pos = (pos - stops[i].pos) / (stop.pos - stops[i].pos)
return colorLerp(stops[i].color, stop.color, pos)
}
}
return last.color
}
func colorLerp(c0, c1 color.Color, t float64) color.Color {
r0, g0, b0, a0 := c0.RGBA()
r1, g1, b1, a1 := c1.RGBA()
return color.RGBA{
lerp(r0, r1, t),
lerp(g0, g1, t),
lerp(b0, b1, t),
lerp(a0, a1, t),
}
}
func lerp(a, b uint32, t float64) uint8 {
return uint8(int32(float64(a)*(1.0-t)+float64(b)*t) >> 8)
}

View File

@@ -1,92 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import "math"
type Matrix struct {
XX, YX, XY, YY, X0, Y0 float64
}
func Identity() Matrix {
return Matrix{
1, 0,
0, 1,
0, 0,
}
}
func Translate(x, y float64) Matrix {
return Matrix{
1, 0,
0, 1,
x, y,
}
}
func Scale(x, y float64) Matrix {
return Matrix{
x, 0,
0, y,
0, 0,
}
}
func Rotate(angle float64) Matrix {
c := math.Cos(angle)
s := math.Sin(angle)
return Matrix{
c, s,
-s, c,
0, 0,
}
}
func Shear(x, y float64) Matrix {
return Matrix{
1, y,
x, 1,
0, 0,
}
}
func (a Matrix) Multiply(b Matrix) Matrix {
return Matrix{
a.XX*b.XX + a.YX*b.XY,
a.XX*b.YX + a.YX*b.YY,
a.XY*b.XX + a.YY*b.XY,
a.XY*b.YX + a.YY*b.YY,
a.X0*b.XX + a.Y0*b.XY + b.X0,
a.X0*b.YX + a.Y0*b.YY + b.Y0,
}
}
func (a Matrix) TransformVector(x, y float64) (tx, ty float64) {
tx = a.XX*x + a.XY*y
ty = a.YX*x + a.YY*y
return
}
func (a Matrix) TransformPoint(x, y float64) (tx, ty float64) {
tx = a.XX*x + a.XY*y + a.X0
ty = a.YX*x + a.YY*y + a.Y0
return
}
func (a Matrix) Translate(x, y float64) Matrix {
return Translate(x, y).Multiply(a)
}
func (a Matrix) Scale(x, y float64) Matrix {
return Scale(x, y).Multiply(a)
}
func (a Matrix) Rotate(angle float64) Matrix {
return Rotate(angle).Multiply(a)
}
func (a Matrix) Shear(x, y float64) Matrix {
return Shear(x, y).Multiply(a)
}

View File

@@ -1,167 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"math"
"github.com/golang/freetype/raster"
"golang.org/x/image/math/fixed"
)
func flattenPath(p raster.Path) [][]Point {
var result [][]Point
var path []Point
var cx, cy float64
for i := 0; i < len(p); {
switch p[i] {
case 0:
if len(path) > 0 {
result = append(result, path)
path = nil
}
x := unfix(p[i+1])
y := unfix(p[i+2])
path = append(path, Point{x, y})
cx, cy = x, y
i += 4
case 1:
x := unfix(p[i+1])
y := unfix(p[i+2])
path = append(path, Point{x, y})
cx, cy = x, y
i += 4
case 2:
x1 := unfix(p[i+1])
y1 := unfix(p[i+2])
x2 := unfix(p[i+3])
y2 := unfix(p[i+4])
points := QuadraticBezier(cx, cy, x1, y1, x2, y2)
path = append(path, points...)
cx, cy = x2, y2
i += 6
case 3:
x1 := unfix(p[i+1])
y1 := unfix(p[i+2])
x2 := unfix(p[i+3])
y2 := unfix(p[i+4])
x3 := unfix(p[i+5])
y3 := unfix(p[i+6])
points := CubicBezier(cx, cy, x1, y1, x2, y2, x3, y3)
path = append(path, points...)
cx, cy = x3, y3
i += 8
default:
panic("bad path")
}
}
if len(path) > 0 {
result = append(result, path)
}
return result
}
func dashPath(paths [][]Point, dashes []float64, offset float64) [][]Point {
var result [][]Point
if len(dashes) == 0 {
return paths
}
if len(dashes) == 1 {
dashes = append(dashes, dashes[0])
}
for _, path := range paths {
if len(path) < 2 {
continue
}
previous := path[0]
pathIndex := 1
dashIndex := 0
segmentLength := 0.0
// offset
if offset != 0 {
var totalLength float64
for _, dashLength := range dashes {
totalLength += dashLength
}
offset = math.Mod(offset, totalLength)
if offset < 0 {
offset += totalLength
}
for i, dashLength := range dashes {
offset -= dashLength
if offset < 0 {
dashIndex = i
segmentLength = dashLength + offset
break
}
}
}
var segment []Point
segment = append(segment, previous)
for pathIndex < len(path) {
dashLength := dashes[dashIndex]
point := path[pathIndex]
d := previous.Distance(point)
maxd := dashLength - segmentLength
if d > maxd {
t := maxd / d
p := previous.Interpolate(point, t)
segment = append(segment, p)
if dashIndex%2 == 0 && len(segment) > 1 {
result = append(result, segment)
}
segment = nil
segment = append(segment, p)
segmentLength = 0
previous = p
dashIndex = (dashIndex + 1) % len(dashes)
} else {
segment = append(segment, point)
previous = point
segmentLength += d
pathIndex++
}
}
if dashIndex%2 == 0 && len(segment) > 1 {
result = append(result, segment)
}
}
return result
}
func rasterPath(paths [][]Point) raster.Path {
var result raster.Path
for _, path := range paths {
var previous fixed.Point26_6
for i, point := range path {
f := point.Fixed()
if i == 0 {
result.Start(f)
} else {
dx := f.X - previous.X
dy := f.Y - previous.Y
if dx < 0 {
dx = -dx
}
if dy < 0 {
dy = -dy
}
if dx+dy > 4 {
// TODO: this is a hack for cases where two points are
// too close - causes rendering issues with joins / caps
result.Add1(f)
}
}
previous = f
}
}
return result
}
func dashed(path raster.Path, dashes []float64, offset float64) raster.Path {
return rasterPath(dashPath(flattenPath(path), dashes, offset))
}

View File

@@ -1,127 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"image"
"image/color"
"github.com/golang/freetype/raster"
)
type RepeatOp int
const (
RepeatBoth RepeatOp = iota
RepeatX
RepeatY
RepeatNone
)
type Pattern interface {
ColorAt(x, y int) color.Color
}
// Solid Pattern
type solidPattern struct {
color color.Color
}
func (p *solidPattern) ColorAt(x, y int) color.Color {
return p.color
}
func NewSolidPattern(color color.Color) Pattern {
return &solidPattern{color: color}
}
// Surface Pattern
type surfacePattern struct {
im image.Image
op RepeatOp
}
func (p *surfacePattern) ColorAt(x, y int) color.Color {
b := p.im.Bounds()
switch p.op {
case RepeatX:
if y >= b.Dy() {
return color.Transparent
}
case RepeatY:
if x >= b.Dx() {
return color.Transparent
}
case RepeatNone:
if x >= b.Dx() || y >= b.Dy() {
return color.Transparent
}
}
x = x%b.Dx() + b.Min.X
y = y%b.Dy() + b.Min.Y
return p.im.At(x, y)
}
func NewSurfacePattern(im image.Image, op RepeatOp) Pattern {
return &surfacePattern{im: im, op: op}
}
type patternPainter struct {
im *image.RGBA
mask *image.Alpha
p Pattern
}
// Paint satisfies the Painter interface.
func (r *patternPainter) Paint(ss []raster.Span, done bool) {
b := r.im.Bounds()
for _, s := range ss {
if s.Y < b.Min.Y {
continue
}
if s.Y >= b.Max.Y {
return
}
if s.X0 < b.Min.X {
s.X0 = b.Min.X
}
if s.X1 > b.Max.X {
s.X1 = b.Max.X
}
if s.X0 >= s.X1 {
continue
}
const m = 1<<16 - 1
y := s.Y - r.im.Rect.Min.Y
x0 := s.X0 - r.im.Rect.Min.X
// RGBAPainter.Paint() in $GOPATH/src/github.com/golang/freetype/raster/paint.go
i0 := (s.Y-r.im.Rect.Min.Y)*r.im.Stride + (s.X0-r.im.Rect.Min.X)*4
i1 := i0 + (s.X1-s.X0)*4
for i, x := i0, x0; i < i1; i, x = i+4, x+1 {
ma := s.Alpha
if r.mask != nil {
ma = ma * uint32(r.mask.AlphaAt(x, y).A) / 255
if ma == 0 {
continue
}
}
c := r.p.ColorAt(x, y)
cr, cg, cb, ca := c.RGBA()
dr := uint32(r.im.Pix[i+0])
dg := uint32(r.im.Pix[i+1])
db := uint32(r.im.Pix[i+2])
da := uint32(r.im.Pix[i+3])
a := (m - (ca * ma / m)) * 0x101
r.im.Pix[i+0] = uint8((dr*a + cr*ma) / m >> 8)
r.im.Pix[i+1] = uint8((dg*a + cg*ma) / m >> 8)
r.im.Pix[i+2] = uint8((db*a + cb*ma) / m >> 8)
r.im.Pix[i+3] = uint8((da*a + ca*ma) / m >> 8)
}
}
}
func newPatternPainter(im *image.RGBA, mask *image.Alpha, p Pattern) *patternPainter {
return &patternPainter{im, mask, p}
}

View File

@@ -1,29 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"math"
"golang.org/x/image/math/fixed"
)
type Point struct {
X, Y float64
}
func (a Point) Fixed() fixed.Point26_6 {
return fixp(a.X, a.Y)
}
func (a Point) Distance(b Point) float64 {
return math.Hypot(a.X-b.X, a.Y-b.Y)
}
func (a Point) Interpolate(b Point, t float64) Point {
x := a.X + (b.X-a.X)*t
y := a.Y + (b.Y-a.Y)*t
return Point{x, y}
}

View File

@@ -1,203 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"fmt"
"image"
"image/draw"
"image/jpeg"
"image/png"
"io/fs"
"math"
"os"
"path/filepath"
"strings"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
"golang.org/x/image/math/fixed"
)
func Radians(degrees float64) float64 {
return degrees * math.Pi / 180
}
func Degrees(radians float64) float64 {
return radians * 180 / math.Pi
}
func LoadImage(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
im, _, err := image.Decode(file)
return im, err
}
func LoadPNG(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return png.Decode(file)
}
func SavePNG(path string, im image.Image) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
err = png.Encode(file, im)
if err != nil {
return fmt.Errorf("could not encode PNG to %q: %w", path, err)
}
return file.Close()
}
func LoadJPG(path string) (image.Image, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
return jpeg.Decode(file)
}
func SaveJPG(path string, im image.Image, quality int) error {
file, err := os.Create(path)
if err != nil {
return err
}
defer file.Close()
var opt jpeg.Options
opt.Quality = quality
err = jpeg.Encode(file, im, &opt)
if err != nil {
return fmt.Errorf("could not encode JPG to %q: %w", path, err)
}
return file.Close()
}
func imageToRGBA(src image.Image) *image.RGBA {
bounds := src.Bounds()
dst := image.NewRGBA(bounds)
draw.Draw(dst, bounds, src, bounds.Min, draw.Src)
return dst
}
func parseHexColor(x string) (r, g, b, a int) {
x = strings.TrimPrefix(x, "#")
a = 255
if len(x) == 3 {
format := "%1x%1x%1x"
fmt.Sscanf(x, format, &r, &g, &b)
r |= r << 4
g |= g << 4
b |= b << 4
}
if len(x) == 6 {
format := "%02x%02x%02x"
fmt.Sscanf(x, format, &r, &g, &b)
}
if len(x) == 8 {
format := "%02x%02x%02x%02x"
fmt.Sscanf(x, format, &r, &g, &b, &a)
}
return
}
func fixp(x, y float64) fixed.Point26_6 {
return fixed.Point26_6{fix(x), fix(y)}
}
func fix(x float64) fixed.Int26_6 {
return fixed.Int26_6(math.Round(x * 64))
}
func unfix(x fixed.Int26_6) float64 {
const shift, mask = 6, 1<<6 - 1
if x >= 0 {
return float64(x>>shift) + float64(x&mask)/64
}
x = -x
if x >= 0 {
return -(float64(x>>shift) + float64(x&mask)/64)
}
return 0
}
// LoadFontFace is a helper function to load the specified font file with
// the specified point size. Note that the returned `font.Face` objects
// are not thread safe and cannot be used in parallel across goroutines.
// You can usually just use the Context.LoadFontFace function instead of
// this package-level function.
func LoadFontFace(path string, points float64) (font.Face, error) {
fontBytes, err := os.ReadFile(path)
if err != nil {
return nil, err
}
return LoadFontFaceFromBytes(fontBytes, points)
}
// LoadFontFaceFromFS is a helper function to load the specified font file from
// the provided filesystem and path, with the specified point size.
//
// Note that the returned `font.Face` objects are not thread safe and
// cannot be used in parallel across goroutines.
// You can usually just use the Context.LoadFontFace function instead of
// this package-level function.
func LoadFontFaceFromFS(fsys fs.FS, path string, points float64) (font.Face, error) {
if fsys == nil {
switch {
case filepath.IsAbs(path):
var (
err error
orig = path
root = filepath.FromSlash("/")
)
path, err = filepath.Rel(root, path)
if err != nil {
return nil, fmt.Errorf("could not find relative path for %q from %q: %w", orig, root, err)
}
fsys = os.DirFS(root)
default:
fsys = os.DirFS(".")
}
}
fontBytes, err := fs.ReadFile(fsys, path)
if err != nil {
return nil, err
}
return LoadFontFaceFromBytes(fontBytes, points)
}
// LoadFontFace is a helper function to load the specified font with
// the specified point size. Note that the returned `font.Face` objects
// are not thread safe and cannot be used in parallel across goroutines.
// You can usually just use the Context.LoadFontFace function instead of
// this package-level function.
func LoadFontFaceFromBytes(raw []byte, points float64) (font.Face, error) {
f, err := opentype.Parse(raw)
if err != nil {
return nil, err
}
face, err := opentype.NewFace(f, &opentype.FaceOptions{
Size: points,
DPI: 72,
// Hinting: font.HintingFull,
})
return face, err
}

View File

@@ -1,62 +0,0 @@
// Copyright ©2022 The gg Authors. All rights reserved.
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file.
package gg
import (
"strings"
"unicode"
)
type measureStringer interface {
MeasureString(s string) (w, h float64)
}
func splitOnSpace(x string) []string {
var result []string
pi := 0
ps := false
for i, c := range x {
s := unicode.IsSpace(c)
if s != ps && i > 0 {
result = append(result, x[pi:i])
pi = i
}
ps = s
}
result = append(result, x[pi:])
return result
}
func wordWrap(m measureStringer, s string, width float64) []string {
var result []string
for _, line := range strings.Split(s, "\n") {
fields := splitOnSpace(line)
if len(fields)%2 == 1 {
fields = append(fields, "")
}
x := ""
for i := 0; i < len(fields); i += 2 {
w, _ := m.MeasureString(x + fields[i])
if w > width {
if x == "" {
result = append(result, fields[i])
x = ""
continue
} else {
result = append(result, x)
x = ""
}
}
x += fields[i] + fields[i+1]
}
if x != "" {
result = append(result, x)
}
}
for i, line := range result {
result[i] = strings.TrimSpace(line)
}
return result
}

View File

@@ -1,47 +0,0 @@
# How to contribute #
Thank you for stopping by. Please read these few small guidelines before
creating an issue or a pull request on godal.
## Reporting issues ##
Bugs, feature requests, and development-related questions should be directed to
our [GitHub issue tracker](https://github.com/airbusgeo/godal/issues). If
reporting a bug, please try and provide as much context as possible such as
your Go version, GDAL version and anything else that might be relevant to
the bug. For feature requests, please explain what you're trying to do, and
how the requested feature would help you do that.
## Submitting a patch ##
1. Patches are to be submitted through pull-requests: https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
1. Make sure each group of changes be done in distinct branches in order to
ensure that a pull request only includes code related to that bug or feature.
1. Always run `go fmt` on your code before committing it.
1. Do not squash / force-push your commits inside the pull request branch as
these tend to mess up the review comments.
1. As far as possible, the public API exposed by godal should remain backwards
compatible, meaning that existing code using godal should compile correctly
when using a newer godal version, and that the resulting behavior of the
compiled program should be unchanged. godal makes heavy use of
[optional parameters](https://dave.cheney.net/2014/10/17/functional-options-for-friendly-apis)
in its exposed API in order to allow this.
1. Any changes should almost always be accompanied by tests. Look at some of
the existing tests if you're unsure how to go about it. Tests should ensure
that the godal wrapper itself is working correctly, not the underlying GDAL
library (e.g. the test could check that a particular option has been taken
into account by gdal, not that gdal has produced correct output for all
possible option values)
1. Pull requests will be automatically tested, vetted and checked for test
coverage. You can run the following tools locally before submitting your
changes to ensure the checks will pass:
* `go test ./... -cover`
* `golangci-lint run --skip-files doc_test.go`

View File

@@ -1,191 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

View File

@@ -1,95 +0,0 @@
# Golang bindings for GDAL
[![Go Reference](https://pkg.go.dev/badge/github.com/airbusgeo/godal.svg)](https://pkg.go.dev/github.com/airbusgeo/godal)
[![License](https://img.shields.io/github/license/airbusgeo/godal.svg)](https://github.com/airbusgeo/godal/blob/main/LICENSE)
[![Build Status](https://github.com/airbusgeo/godal/workflows/build/badge.svg?branch=main&event=push)](https://github.com/airbusgeo/godal/actions?query=workflow%3Agodal+event%3Apush+branch%3Amain)
[![Coverage Status](https://coveralls.io/repos/github/airbusgeo/godal/badge.svg?branch=main)](https://coveralls.io/github/airbusgeo/godal?branch=main)
[![Go Report Card](https://goreportcard.com/badge/github.com/airbusgeo/godal)](https://goreportcard.com/report/github.com/airbusgeo/godal)
### Goals
Godal aims at providing an idiomatic go wrapper around the <img src="https://gdal.org/_static/gdalicon.png" width="25" height="25">
[GDAL](https://gdal.org) library:
* Function calls return a result and an error. The result will be valid if
no error was returned. The error message will contain the root cause of why
the error happened.
* Calls between go and native libraries incur some overhead. As such godal does
not strictly expose GDAL's API, but groups often-used calls in a single cgo function
to reduce this overhead. For example, C code like
```c++
hDS = GDALOpen(filename, GA_Readonly)
if (hDS == NULL) exit(1);
int sx = GDALGetRasterXSize(hDS);
int sy = GDALGetRasterYSize(hDS);
int nBands = GDALGetRasterCount(hDS);
printf("dataset size: %dx%dx%d\n",sx,sy,nBands);
for (int i=1; i<=nBands; i++) {
hBand = GDALGetRasterBand(hDS,i);
int ovrCount = GDALGetOverviewCount(hBand)
for(int o=0; o<=ovrCount; o++) {
GDALRasterBandH oBand = GDALGetOverview(hBand,o);
int osx = GDALGetRasterBandXSize(oBand);
int osy = GDALGetRasterBandYSize(oBand);
printf("overview %d size: %dx%d\n",o,osx,osy);
}
}
```
will be written as
```go
hDS,err := godal.Open(filename)
if err!=nil {
panic(err)
}
structure := hDS.Structure()
fmt.Printf("dataset size: %dx%dx%d\n", structure.SizeX,structure.SizeY,structure.NBands)
for _,band := range hDS.Bands() {
for o,ovr := range band.Overviews() {
bstruct := ovr.Structure()
fmt.Printf("overview %d size: %dx%d\n",o,bstruct.SizeX,bstruct.SizeY)
}
}
```
* Unfrequently used or non-default parameters are passed as options:
```go
ds,err := godal.Open(filename) //read-only
ds,err := godal.Open(filename, Update()) //read-write
```
* Godal exposes a VSI handler that can easily allow you to expose an
[io.ReaderAt](https://golang.org/pkg/io/#ReaderAt) as a filename that can be
opened by GDAL. A handler for opening `gs://` google cloud storage URIs is
provided through https://github.com/airbusgeo/osio
### Documentation
[![GoReference](https://pkg.go.dev/badge/github.com/airbusgeo/godal.svg)](https://pkg.go.dev/github.com/airbusgeo/godal)
contains the API reference and example code to get you started. The
`*_test.go` files can also be used as reference.
### Status
Godal is not feature complete. The raster side is nearing completion and
should remain stable. The vector and spatial-referencing sides are far from
complete, meaning that the API might evolve in backwards incompatible ways
until essential functionality is covered.
### Contributing
Contributions are welcome. Please read the [contribution guidelines](CONTRIBUTING.md)
before submitting fixes or enhancements.
### Installation
Godal requires a GDAL version greater than 3.0. Make sure the GDAL headers
are installed on the system used for compiling go+godal code. If using a GDAL
installation in a non standard location, you can set your `PKG_CONFIG_PATH`
environment variable, e.g. `export PKG_CONFIG_PATH=/opt/include/pkgconfig`.
### Licensing
Godal is licensed under the Apache License, Version 2.0. See
[LICENSE](https://github.com/airbusgeo/godal/blob/main/LICENSE) for the full
license text.

View File

@@ -1,158 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
//DriverName is GDAL driver
type DriverName string
const (
//GTiff GeoTIFF
GTiff DriverName = "GTiff"
//GeoJSON RFCxxxx geojson
GeoJSON DriverName = "GeoJSON"
//Memory in memory driver
Memory DriverName = "Memory"
//VRT is a VRT
VRT DriverName = "VRT"
//Shapefile is an ESRI Shapefile
Shapefile DriverName = "ESRI Shapefile"
//GeoPackage is a geo-package
GeoPackage DriverName = "GPKG"
//JP2KAK is a Kakadu Jpeg2000
JP2KAK DriverName = "JP2KAK"
//OpenJPEG is an OpenJPEG JPEG2000
OpenJPEG DriverName = "OpenJPEG"
//DIMAP is a Dimap
DIMAP DriverName = "DIMAP"
//HFA is an erdas img
HFA DriverName = "HFA"
//Mitab is a mapinfo mif/tab file
Mitab DriverName = "Mitab"
//CSV comma-separated values driver
CSV DriverName = "CSV"
)
type driverMapping struct {
rasterName string
vectorName string
rasterRegister string
vectorRegister string
}
var driverMappings = map[DriverName]driverMapping{
GTiff: {
rasterName: "GTiff",
rasterRegister: "GDALRegister_GTiff",
},
Memory: {
rasterName: "MEM",
vectorName: "Memory",
rasterRegister: "GDALRegister_MEM",
vectorRegister: "RegisterOGRMEM",
},
GeoJSON: {
vectorName: "GeoJSON",
vectorRegister: "RegisterOGRGeoJSON",
},
VRT: {
rasterName: "VRT",
vectorName: "OGR_VRT",
rasterRegister: "GDALRegister_VRT",
vectorRegister: "RegisterOGRVRT",
},
Shapefile: {
vectorName: "ESRI Shapefile",
vectorRegister: "RegisterOGRShape",
},
GeoPackage: {
rasterName: "GPKG",
vectorName: "GPKG",
rasterRegister: "RegisterOGRGeoPackage",
vectorRegister: "RegisterOGRGeoPackage",
},
JP2KAK: {
rasterName: "JP2KAK",
rasterRegister: "GDALRegister_JP2KAK",
},
OpenJPEG: {
rasterName: "OpenJPEG",
rasterRegister: "GDALRegister_JP2OpenJPEG",
},
DIMAP: {
rasterName: "DIMAP",
rasterRegister: "GDALRegister_DIMAP",
},
HFA: {
rasterName: "HFA",
rasterRegister: "GDALRegister_HFA",
},
Mitab: {
vectorName: "Mapinfo File",
vectorRegister: "RegisterOGRTAB",
},
CSV: {
vectorName: "CSV",
vectorRegister: "RegisterOGRCSV",
},
}
func (dn DriverName) setDatasetVectorTranslateOpt(to *dsVectorTranslateOpts) {
to.driver = dn
}
func (dn DriverName) setDatasetTranslateOpt(to *dsTranslateOpts) {
to.driver = dn
}
func (dn DriverName) setDatasetWarpOpt(to *dsWarpOpts) {
to.driver = dn
}
func (dn DriverName) setRasterizeOpt(to *rasterizeOpts) {
to.driver = dn
}
type driversOpt struct {
drivers []string
}
//Drivers specifies the list of drivers that are allowed to try opening the dataset
func Drivers(drivers ...string) interface {
OpenOption
} {
return driversOpt{drivers}
}
func (do driversOpt) setOpenOpt(oo *openOpts) {
oo.drivers = append(oo.drivers, do.drivers...)
}
type driverOpenOption struct {
oo []string
}
//DriverOpenOption adds a list of Open Options (-oo switch) to the open command. Each keyval must
//be provided in a "KEY=value" format
func DriverOpenOption(keyval ...string) interface {
OpenOption
BuildVRTOption
} {
return driverOpenOption{keyval}
}
func (doo driverOpenOption) setOpenOpt(oo *openOpts) {
oo.options = append(oo.options, doo.oo...)
}
func (doo driverOpenOption) setBuildVRTOpt(bvo *buildVRTOpts) {
bvo.openOptions = append(bvo.openOptions, doo.oo...)
}

View File

@@ -1,461 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
import (
"bytes"
"errors"
"sync"
)
var errorHandlerMu sync.Mutex
var errorHandlerIndex int
// ErrorHandler is a function that can be used to override godal's default behavior
// of treating all messages with severity >= CE_Warning as errors. When an ErrorHandler
// is passed as an option to a godal function, all logs/errors emitted by gdal will be passed
// to this function, which can decide wether the parameters correspond to an actual error
// or not.
//
// If the ErrorHandler returns nil, the parent function will not return an error. It is up
// to the ErrorHandler to log the message if needed.
//
// If the ErrorHandler returns an error, that error will be returned as-is to the caller
// of the parent function
type ErrorHandler func(ec ErrorCategory, code int, msg string) error
type errorHandlerWrapper struct {
fn ErrorHandler
err error
}
var errorHandlers = make(map[int]*errorHandlerWrapper)
func registerErrorHandler(fn ErrorHandler) int {
errorHandlerMu.Lock()
defer errorHandlerMu.Unlock()
for errorHandlerIndex == 0 || errorHandlers[errorHandlerIndex] != nil {
errorHandlerIndex++
}
errorHandlers[errorHandlerIndex] = &errorHandlerWrapper{fn: fn}
return errorHandlerIndex
}
func getErrorHandler(i int) *errorHandlerWrapper {
errorHandlerMu.Lock()
defer errorHandlerMu.Unlock()
return errorHandlers[i]
}
func unregisterErrorHandler(i int) {
errorHandlerMu.Lock()
defer errorHandlerMu.Unlock()
delete(errorHandlers, i)
}
type errorAndLoggingOpts struct {
eh ErrorHandler
config []string
}
type errorCallback struct {
fn ErrorHandler
}
type errorAndLoggingOption interface {
setErrorAndLoggingOpt(elo *errorAndLoggingOpts)
}
// ErrLogger is an option to override default error handling.
//
// See ErrorHandler.
func ErrLogger(fn ErrorHandler) interface {
errorAndLoggingOption
AddGeometryOption
BandCreateMaskOption
BandIOOption
BoundsOption
BufferOption
BuildOverviewsOption
BuildVRTOption
ClearOverviewsOption
CloseOption
CopyLayerOption
CreateFeatureOption
CreateLayerOption
CreateSpatialRefOption
DatasetCreateMaskOption
DatasetCreateOption
DatasetIOOption
DatasetTranslateOption
DatasetVectorTranslateOption
DatasetWarpIntoOption
DatasetWarpOption
DeleteFeatureOption
DifferenceOption
FeatureCountOption
FillBandOption
FillNoDataOption
GeoJSONOption
GeometryTransformOption
GeometryReprojectOption
GeometryWKBOption
GeometryWKTOption
GetGeoTransformOption
GMLExportOption
HistogramOption
IntersectsOption
IntersectionOption
MetadataOption
NewFeatureOption
NewGeometryOption
OpenOption
PolygonizeOption
RasterizeGeometryOption
RasterizeOption
RasterizeIntoOption
SetColorInterpOption
SetColorTableOption
SetDescriptionOption
SetGeometryOption
SetFieldValueOption
SetNoDataOption
SetScaleOffsetOption
SetGeoTransformOption
SetGeometryColumnNameOption
SetProjectionOption
SetSpatialRefOption
SieveFilterOption
SimplifyOption
SpatialRefValidateOption
SubGeometryOption
TransformOption
UnionOption
UpdateFeatureOption
VSIHandlerOption
VSIOpenOption
VSIUnlinkOption
WKTExportOption
StatisticsOption
SetStatisticsOption
ClearStatisticsOption
GridOption
NearblackOption
DemOption
SetGCPsOption
GCPsToGeoTransformOption
RegisterPluginOption
} {
return errorCallback{fn}
}
func (ec errorCallback) setErrorAndLoggingOpt(elo *errorAndLoggingOpts) {
elo.eh = ec.fn
}
func (ec errorCallback) setAddGeometryOpt(ao *addGeometryOpts) {
ao.errorHandler = ec.fn
}
func (ec errorCallback) setBandCreateMaskOpt(o *bandCreateMaskOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setBandIOOpt(o *bandIOOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setBoundsOpt(o *boundsOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setBufferOpt(o *bufferOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setBuildOverviewsOpt(o *buildOvrOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setBuildVRTOpt(o *buildVRTOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setClearOverviewsOpt(o *clearOvrOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setCloseOpt(o *closeOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setCopyLayerOpt(o *copyLayerOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setCreateFeatureOpt(cfo *createFeatureOpts) {
cfo.errorHandler = ec.fn
}
func (ec errorCallback) setCreateLayerOpt(o *createLayerOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setCreateSpatialRefOpt(o *createSpatialRefOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetCreateMaskOpt(o *dsCreateMaskOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetCreateOpt(o *dsCreateOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetIOOpt(o *datasetIOOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetTranslateOpt(o *dsTranslateOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetVectorTranslateOpt(o *dsVectorTranslateOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetWarpIntoOpt(o *dsWarpIntoOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDatasetWarpOpt(o *dsWarpOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDeleteFeatureOpt(o *deleteFeatureOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDifferenceOpt(do *differenceOpts) {
do.errorHandler = ec.fn
}
func (ec errorCallback) setFeatureCountOpt(o *featureCountOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setFillBandOpt(o *fillBandOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setFillnodataOpt(o *fillnodataOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeojsonOpt(o *geojsonOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeometryColumnNameOpt(o *setGeometryColumnNameOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeometryTransformOpt(o *geometryTransformOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeometryReprojectOpt(o *geometryReprojectOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeometryWKBOpt(o *geometryWKBOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGeometryWKTOpt(o *geometryWKTOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGetGeoTransformOpt(o *getGeoTransformOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGMLExportOpt(o *gmlExportOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setHistogramOpt(o *histogramOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setIntersectsOpt(o *intersectsOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setIntersectionOpt(o *intersectionOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setMetadataOpt(o *metadataOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDescriptionOpt(o *setDescriptionOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setNewFeatureOpt(o *newFeatureOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setNewGeometryOpt(o *newGeometryOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setOpenOpt(oo *openOpts) {
oo.errorHandler = ec.fn
}
func (ec errorCallback) setPolygonizeOpt(o *polygonizeOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setRasterizeGeometryOpt(o *rasterizeGeometryOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setRasterizeOpt(o *rasterizeOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setRasterizeIntoOpt(o *rasterizeIntoOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetColorInterpOpt(ndo *setColorInterpOpts) {
ndo.errorHandler = ec.fn
}
func (ec errorCallback) setSetColorTableOpt(ndo *setColorTableOpts) {
ndo.errorHandler = ec.fn
}
func (ec errorCallback) setSetGeometryOpt(o *setGeometryOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetFieldValueOpt(o *setFieldValueOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetGeoTransformOpt(o *setGeoTransformOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetProjectionOpt(o *setProjectionOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetNoDataOpt(ndo *setNodataOpts) {
ndo.errorHandler = ec.fn
}
func (ec errorCallback) setSetScaleOffsetOpt(soo *setScaleOffsetOpts) {
soo.errorHandler = ec.fn
}
func (ec errorCallback) setSetSpatialRefOpt(ndo *setSpatialRefOpts) {
ndo.errorHandler = ec.fn
}
func (ec errorCallback) setSieveFilterOpt(sfo *sieveFilterOpts) {
sfo.errorHandler = ec.fn
}
func (ec errorCallback) setSimplifyOpt(o *simplifyOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSpatialRefValidateOpt(o *spatialRefValidateOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSubGeometryOpt(so *subGeometryOpts) {
so.errorHandler = ec.fn
}
func (ec errorCallback) setTransformOpt(o *trnOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setUnionOpt(uo *unionOpts) {
uo.errorHandler = ec.fn
}
func (ec errorCallback) setUpdateFeatureOpt(o *updateFeatureOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setVSIHandlerOpt(o *vsiHandlerOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setVSIOpenOpt(o *vsiOpenOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setVSIUnlinkOpt(o *vsiUnlinkOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setWKTExportOpt(o *srWKTOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setStatisticsOpt(o *statisticsOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetStatisticsOpt(o *setStatisticsOpt) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setClearStatisticsOpt(o *clearStatisticsOpt) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGridCreateOpt(o *gridCreateOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGridOpt(o *gridOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setNearblackOpt(o *nearBlackOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setDemOpt(o *demOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setSetGCPsOpt(o *setGCPsOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setGCPsToGeoTransformOpts(o *gcpsToGeoTransformOpts) {
o.errorHandler = ec.fn
}
func (ec errorCallback) setRegisterPluginOpt(o *registerPluginOpts) {
o.errorHandler = ec.fn
}
type multiError struct {
errs []error
}
// Error is the standard error interface
func (me *multiError) Error() string {
w := bytes.NewBufferString(me.errs[0].Error())
for i := 1; i < len(me.errs); i++ {
w.WriteByte('\n')
w.WriteString(me.errs[i].Error())
}
return w.String()
}
// As is the standard error wrapping interface
func (me *multiError) As(target interface{}) bool {
for _, err := range me.errs {
if errors.As(err, target) {
return true
}
}
return false
}
// Is is the standard error wrapping interface
func (me *multiError) Is(target error) bool {
for _, err := range me.errs {
if errors.Is(err, target) {
return true
}
}
return false
}
func combine(e1, e2 error) error {
switch {
case e1 == nil:
return e2
case e2 == nil:
return e1
}
if me1, ok := e1.(*multiError); ok {
if me2, ok := e2.(*multiError); ok {
me1.errs = append(me1.errs, me2.errs...)
} else {
me1.errs = append(me1.errs, e2)
}
return me1
} else if me2, ok := e2.(*multiError); ok {
me := &multiError{}
me.errs = make([]error, 1, len(me2.errs)+1)
me.errs[0] = e1
me.errs = append(me.errs, me2.errs...)
return me
} else {
return &multiError{errs: []error{e1, e2}}
}
}
var SkipWarnings = ErrLogger(
func(ec ErrorCategory, code int, message string) error {
if ec > CE_Warning {
return errors.New(message)
}
return nil
})

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,188 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#ifndef _GODAL_H_
#define _GODAL_H_
#define _GNU_SOURCE 1
#include <gdal.h>
#include <gdal_alg.h>
#include <ogr_srs_api.h>
#include <cpl_conv.h>
#include "cpl_port.h"
#include <gdal_frmts.h>
#if GDAL_VERSION_NUM < 3000000
#error "this code is only compatible with gdal version >= 3.0"
#endif
#ifdef __cplusplus
extern "C" {
#endif
typedef struct {
char *errMessage;
int handlerIdx;
int failed;
char **configOptions;
} cctx;
void godalSetMetadataItem(cctx *ctx, GDALMajorObjectH mo, char *ckey, char *cval, char *cdom);
void godalSetDescription(cctx *ctx, GDALMajorObjectH mo, char *desc);
void godalClearMetadata(cctx *ctx, GDALMajorObjectH mo, char *cdom);
GDALDatasetH godalOpen(cctx *ctx, const char *name, unsigned int nOpenFlags, const char *const *papszAllowedDrivers,
const char *const *papszOpenOptions, const char *const *papszSiblingFiles);
GDALDatasetH godalCreate(cctx *ctx, GDALDriverH drv, const char *name, int width, int height, int nbands,
GDALDataType dtype, char **creationOption);
void godalClose(cctx *ctx, GDALDatasetH ds);
int godalRegisterDriver(const char *funcname);
void godalRegisterPlugins();
void godalRegisterPlugin(cctx *ctx, const char *name);
void godalRasterSize(GDALDatasetH ds, int *xsize, int *ysize);
//returns a null terminated list of bands. the caller must free the returned list
GDALRasterBandH *godalRasterBands(GDALDatasetH ds);
OGRLayerH *godalVectorLayers(GDALDatasetH ds);
GDALRasterBandH* godalBandOverviews(GDALRasterBandH bnd);
void godalSetRasterNoDataValue(cctx *ctx, GDALRasterBandH bnd, double nd);
void godalSetDatasetNoDataValue(cctx *ctx, GDALDatasetH bnd, double nd);
void godalDeleteRasterNoDataValue(cctx *ctx, GDALRasterBandH bnd);
void godalSetRasterScaleOffset(cctx *ctx, GDALRasterBandH bnd, double scale, double offset);
void godalSetDatasetScaleOffset(cctx *ctx, GDALDatasetH bnd, double scale, double offset);
void godalSetRasterColorInterpretation(cctx *ctx, GDALRasterBandH bnd, GDALColorInterp ci);
GDALRasterBandH godalCreateMaskBand(cctx *ctx, GDALRasterBandH bnd, int flags);
GDALRasterBandH godalCreateDatasetMaskBand(cctx *ctx, GDALDatasetH ds, int flags);
OGRSpatialReferenceH godalCreateUserSpatialRef(cctx *ctx, char *userInput);
OGRSpatialReferenceH godalCreateWKTSpatialRef(cctx *ctx, char *wkt);
OGRSpatialReferenceH godalCreateProj4SpatialRef(cctx *ctx, char *proj);
OGRSpatialReferenceH godalCreateEPSGSpatialRef(cctx *ctx, int epsgCode);
void godalValidateSpatialRef(cctx *ctx, OGRSpatialReferenceH sr);
char* godalExportToWKT(cctx *ctx, OGRSpatialReferenceH sr);
OGRCoordinateTransformationH godalNewCoordinateTransformation(cctx *ctx, OGRSpatialReferenceH src, OGRSpatialReferenceH dst);
void godalDatasetSetSpatialRef(cctx *ctx, GDALDatasetH ds, OGRSpatialReferenceH sr);
void godalSetGeoTransform(cctx *ctx, GDALDatasetH ds, double *gt);
void godalGetGeoTransform(cctx *ctx, GDALDatasetH ds, double *gt);
void godalSetProjection(cctx *ctx, GDALDatasetH ds, char *wkt);
GDALDatasetH godalTranslate(cctx *ctx, char *dstName, GDALDatasetH ds, char **switches);
GDALDatasetH godalDatasetWarp(cctx *ctx, char *dstName, int nSrcCount, GDALDatasetH *srcDS, char **switches);
void godalDatasetWarpInto(cctx *ctx, GDALDatasetH dstDs, int nSrcCount, GDALDatasetH *srcDS, char **switches);
GDALDatasetH godalDatasetVectorTranslate(cctx *ctx, char *dstName, GDALDatasetH ds, char **switches);
GDALDatasetH godalRasterize(cctx *ctx, char *dstName, GDALDatasetH dstDS, GDALDatasetH ds, char **switches);
void godalRasterizeGeometry(cctx *ctx, GDALDatasetH ds, OGRGeometryH geom, int *bands, int nBands, double *vals, int allTouched);
void godalBuildOverviews(cctx *ctx, GDALDatasetH ds, const char *resampling, int nLevels, int *levels, int nBands, int *bands);
void godalClearOverviews(cctx *ctx, GDALDatasetH ds);
void godalDatasetStructure(GDALDatasetH ds, int *sx, int *sy, int *bsx, int *bsy, double *scale, double *offset, int *bandCount, int *dtype);
void godalBandStructure(GDALRasterBandH bnd, int *sx, int *sy, int *bsx, int *bsy, double *scale, double *offset, int *dtype);
void godalDatasetRasterIO(cctx *ctx, GDALDatasetH ds, GDALRWFlag rw, int nDSXOff, int nDSYOff, int nDSXSize, int nDSYSize, void *pBuffer,
int nBXSize, int nBYSize, GDALDataType eBDataType, int nBandCount, int *panBandCount,
int nPixelSpace, int nLineSpace, int nBandSpace, GDALRIOResampleAlg alg);
void godalBandRasterIO(cctx *ctx, GDALRasterBandH bnd, GDALRWFlag rw, int nDSXOff, int nDSYOff, int nDSXSize, int nDSYSize, void *pBuffer,
int nBXSize, int nBYSize, GDALDataType eBDataType, int nPixelSpace, int nLineSpace, GDALRIOResampleAlg alg);
void godalFillRaster(cctx *ctx, GDALRasterBandH bnd, double real, double imag);
void godalPolygonize(cctx *ctx, GDALRasterBandH in, GDALRasterBandH mask, OGRLayerH layer, int fieldIndex, char **opts);
void godalFillNoData(cctx *ctx, GDALRasterBandH in, GDALRasterBandH mask, int maxDistance, int iterations, char **opts);
void godalSieveFilter(cctx *ctx, GDALRasterBandH bnd, GDALRasterBandH mask, GDALRasterBandH dst, int sizeThreshold, int connectedNess);
void godalLayerGetExtent(cctx *ctx, OGRLayerH layer, OGREnvelope *envelope);
void godalLayerFeatureCount(cctx *ctx, OGRLayerH layer, int *count);
void godalLayerSetFeature(cctx *ctx, OGRLayerH layer, OGRFeatureH feat);
void godalLayerCreateFeature(cctx *ctx, OGRLayerH layer, OGRFeatureH feat);
OGRFeatureH godalLayerNewFeature(cctx *ctx, OGRLayerH layer, OGRGeometryH geom);
void godalLayerDeleteFeature(cctx *ctx, OGRLayerH layer, OGRFeatureH feat);
void godalLayerSetGeometryColumnName(cctx *ctx, OGRLayerH layer, char *name);
void godalFeatureSetGeometryColumnName(cctx *ctx, OGRFeatureH feat, char *name);
void godalFeatureSetGeometry(cctx *ctx, OGRFeatureH feat, OGRGeometryH geom);
void godalFeatureSetFieldInteger(cctx *ctx, OGRFeatureH feat, int fieldIndex, int value);
void godalFeatureSetFieldInteger64(cctx *ctx, OGRFeatureH feat, int fieldIndex, long long value);
void godalFeatureSetFieldDouble(cctx *ctx, OGRFeatureH feat, int fieldIndex, double value);
void godalFeatureSetFieldString(cctx *ctx, OGRFeatureH feat, int fieldIndex, char *value);
void godalFeatureSetFieldDateTime(cctx *ctx, OGRFeatureH feat, int fieldIndex, int year, int month, int day, int hour, int minute, int second, int tzFlag);
void godalFeatureSetFieldIntegerList(cctx *ctx, OGRFeatureH feat, int fieldIndex, int nbValues, int *values);
void godalFeatureSetFieldInteger64List(cctx *ctx, OGRFeatureH feat, int fieldIndex, int nbValues, long long *values);
void godalFeatureSetFieldDoubleList(cctx *ctx, OGRFeatureH feat, int fieldIndex, int nbValues, double *values);
void godalFeatureSetFieldStringList(cctx *ctx, OGRFeatureH feat, int fieldIndex, char **values);
void godalFeatureSetFieldBinary(cctx *ctx, OGRFeatureH feat, int fieldIndex, int nbBytes, void *value);
OGRLayerH godalCreateLayer(cctx *ctx, GDALDatasetH ds, char *name, OGRSpatialReferenceH sr, OGRwkbGeometryType gtype);
OGRLayerH godalCopyLayer(cctx *ctx, GDALDatasetH ds, OGRLayerH layer, char *name);
void VSIInstallGoHandler(cctx *ctx, const char *pszPrefix, size_t bufferSize, size_t cacheSize);
void godalGetColorTable(GDALRasterBandH bnd, GDALPaletteInterp *interp, int *nEntries, short **entries);
void godalSetColorTable(cctx *ctx, GDALRasterBandH bnd, GDALPaletteInterp interp, int nEntries, short *entries);
void godalRasterHistogram(cctx *ctx, GDALRasterBandH bnd, double *min, double *max, int *buckets,
unsigned long long **values, int bIncludeOutOfRange, int bApproxOK);
VSILFILE *godalVSIOpen(cctx *ctx, const char *name);
void godalVSIUnlink(cctx *ctx, const char *name);
char* godalVSIClose(VSILFILE *f);
size_t godalVSIRead(VSILFILE *f, void *buf, int len, char **errmsg);
void godal_OGR_G_AddGeometry(cctx *ctx, OGRGeometryH geom, OGRGeometryH subGeom);
OGRGeometryH godal_OGR_G_Simplify(cctx *ctx, OGRGeometryH in, double tolerance);
OGRGeometryH godal_OGR_G_Buffer(cctx *ctx, OGRGeometryH in, double tolerance, int segments);
OGRGeometryH godal_OGR_G_Difference(cctx *ctx, OGRGeometryH geom1, OGRGeometryH geom2);
OGRGeometryH godal_OGR_G_GetGeometryRef(cctx *ctx, OGRGeometryH in, int subGeomIndex);
int godal_OGR_G_Intersects(cctx *ctx, OGRGeometryH geom1, OGRGeometryH geom2);
OGRGeometryH godal_OGR_G_Intersection(cctx *ctx, OGRGeometryH geom1, OGRGeometryH geom2);
OGRGeometryH godal_OGR_G_Union(cctx *ctx, OGRGeometryH geom1, OGRGeometryH geom2);
OGRGeometryH godalNewGeometryFromGeoJSON(cctx *ctx, char *geoJSON);
OGRGeometryH godalNewGeometryFromWKT(cctx *ctx, char *wkt, OGRSpatialReferenceH sr);
OGRGeometryH godalNewGeometryFromWKB(cctx *ctx, void *wkb, int wkbLen,OGRSpatialReferenceH sr);
char* godalExportGeometryWKT(cctx *ctx, OGRGeometryH in);
char* godalExportGeometryGeoJSON(cctx *ctx, OGRGeometryH in, int precision);
char* godalExportGeometryGML(cctx *ctx, OGRGeometryH in, char **switches);
void godalExportGeometryWKB(cctx *ctx, void **wkb, int *wkbLen, OGRGeometryH in);
void godalGeometryTransformTo(cctx *ctx, OGRGeometryH geom, OGRSpatialReferenceH sr);
void godalGeometryTransform(cctx *ctx, OGRGeometryH geom, OGRCoordinateTransformationH trn, OGRSpatialReferenceH dst);
GDALDatasetH godalBuildVRT(cctx *ctx, char *dstname, char **sources, char **switches);
void test_godal_error_handling(cctx *ctx);
void godalClearRasterStatistics(cctx *ctx, GDALDatasetH ds);
void godalComputeRasterStatistics(cctx *ctx, GDALRasterBandH bnd, int bApproxOK, double *pdfMin, double *pdfMax, double *pdfMean, double *pdfStdDev);
int godalGetRasterStatistics(cctx *ctx, GDALRasterBandH bnd, int bApproxOK, double *pdfMin, double *pdfMax, double *pdfMean, double *pdfStdDev);
void godalSetRasterStatistics(cctx *ctx, GDALRasterBandH bnd, double dfMin, double dfMax, double dfMean, double dfStdDev);
void godalGridCreate(cctx *ctx, char *pszAlgorithm, GDALGridAlgorithm eAlgorithm, GUInt32 nPoints, const double *padfX, const double *padfY, const double *padfZ, double dfXMin, double dfXMax, double dfYMin, double dfYMax, GUInt32 nXSize, GUInt32 nYSize, GDALDataType eType, void *pData);
GDALDatasetH godalGrid(cctx *ctx, const char *pszDest, GDALDatasetH hSrcDS, char **switches);
GDALDatasetH godalNearblack(cctx *ctx, const char *pszDest, GDALDatasetH hDstDS, GDALDatasetH hSrcDS, char **switches);
GDALDatasetH godalDem(cctx *ctx, const char *pszDest, const char *pszProcessing, const char *pszColorFilename, GDALDatasetH hSrcDS, char **switches);
typedef struct {
const GDAL_GCP *gcpList;
int numGCPs;
} GCPsAndCount;
typedef struct {
char **pszIds;
char **pszInfos;
double *dfGCPPixels;
double *dfGCPLines;
double *dfGCPXs;
double *dfGCPYs;
double *dfGCPZs;
} goGCPList;
OGRSpatialReferenceH godalGetGCPSpatialRef(GDALDatasetH hSrcDS);
const GCPsAndCount godalGetGCPs(GDALDatasetH hSrcDS);
const char *godalGetGCPProjection(GDALDatasetH hSrcDS);
void godalSetGCPs(cctx *ctx, GDALDatasetH hSrcDS, int numGCPs, goGCPList GCPList, const char *pszGCPProjection);
void godalSetGCPs2(cctx *ctx, GDALDatasetH hSrcDS, int numGCPs, goGCPList GCPList, OGRSpatialReferenceH hSRS);
GDAL_GCP *goGCPListToGDALGCP(goGCPList GCPList, int numGCPs);
void godalGCPListToGeoTransform(cctx *ctx, goGCPList GCPList, int numGCPs, double *gt);
#ifdef __cplusplus
}
#endif
#endif // GO_GDAL_H_

View File

@@ -1,110 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
// Histogram is a band's histogram.
type Histogram struct {
min, max float64
counts []uint64
}
// Bucket is a histogram entry. It spans [Min,Max] and contains Count entries.
type Bucket struct {
Min, Max float64
Count uint64
}
//Len returns the number of buckets contained in the histogram
func (h Histogram) Len() int {
return len(h.counts)
}
//Bucket returns the i'th bucket in the histogram. i must be between 0 and Len()-1.
func (h Histogram) Bucket(i int) Bucket {
width := (h.max - h.min) / float64(len(h.counts))
return Bucket{
Min: h.min + width*float64(i),
Max: h.min + width*float64(i+1),
Count: h.counts[i],
}
}
type histogramOpts struct {
approx int
includeOutside int
min, max float64
buckets int32
errorHandler ErrorHandler
}
// HistogramOption is an option that can be passed to Band.Histogram()
//
// Available HistogramOptions are:
// - Approximate() to allow the algorithm to operate on a subset of the full resolution data
// - Intervals(count int, min,max float64) to compute a histogram with count buckets, spanning [min,max].
// Each bucket will be (max-min)/count wide. If not provided, the default histogram will be returned.
// - IncludeOutOfRange() to populate the first and last bucket with values under/over the specified min/max
// when used in conjuntion with Intervals()
// - ErrLogger
type HistogramOption interface {
setHistogramOpt(ho *histogramOpts)
}
type includeOutsideOpt struct{}
func (ioo includeOutsideOpt) setHistogramOpt(ho *histogramOpts) {
ho.includeOutside = 1
}
// IncludeOutOfRange populates the first and last bucket with values under/over the specified min/max
// when used in conjuntion with Intervals()
func IncludeOutOfRange() interface {
HistogramOption
} {
return includeOutsideOpt{}
}
type approximateOkOption struct{}
func (aoo approximateOkOption) setHistogramOpt(ho *histogramOpts) {
ho.approx = 1
}
// Approximate allows the histogram algorithm to operate on a subset of the full resolution data
func Approximate() interface {
HistogramOption
StatisticsOption
} {
return approximateOkOption{}
}
type intervalsOption struct {
min, max float64
buckets int32
}
func (io intervalsOption) setHistogramOpt(ho *histogramOpts) {
ho.min = io.min
ho.max = io.max
ho.buckets = io.buckets
}
// Intervals computes a histogram with count buckets, spanning [min,max].
// Each bucket will be (max-min)/count wide. If not provided, the default histogram will be returned.
func Intervals(count int, min, max float64) interface {
HistogramOption
} {
return intervalsOption{min: min, max: max, buckets: int32(count)}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,106 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
import "fmt"
type srWKTOpts struct {
errorHandler ErrorHandler
}
//WKTExportOption is an option that can be passed to SpatialRef.WKT()
//
// Available WKTExportOptions are:
// - ErrLogger
type WKTExportOption interface {
setWKTExportOpt(sro *srWKTOpts)
}
type trnOpts struct {
errorHandler ErrorHandler
}
// TransformOption is an option that can be passed to NewTransform
//
// Available TransformOptions are:
// - ErrLogger
type TransformOption interface {
setTransformOpt(o *trnOpts)
}
func (sr *SpatialRef) setBoundsOpt(o *boundsOpts) {
o.sr = sr
}
type boundsOpts struct {
sr *SpatialRef
errorHandler ErrorHandler
}
// BoundsOption is an option that can be passed to Dataset.Bounds or Geometry.Bounds
//
// Available options are:
// - *SpatialRef
// - ErrLogger
type BoundsOption interface {
setBoundsOpt(o *boundsOpts)
}
type createSpatialRefOpts struct {
errorHandler ErrorHandler
}
// CreateSpatialRefOption is an option that can be passed when creating a new spatial
// reference object
//
// Available options are:
// - ErrLogger
type CreateSpatialRefOption interface {
setCreateSpatialRefOpt(so *createSpatialRefOpts)
}
func reprojectBounds(bnds [4]float64, src, dst *SpatialRef) ([4]float64, error) {
var ret [4]float64
trn, err := NewTransform(src, dst)
if err != nil {
return ret, fmt.Errorf("create coordinate transform: %w", err)
}
defer trn.Close()
x := []float64{bnds[0], bnds[0], bnds[2], bnds[2]}
y := []float64{bnds[1], bnds[3], bnds[3], bnds[1]}
err = trn.TransformEx(x, y, nil, nil)
if err != nil {
return ret, fmt.Errorf("reproject bounds: %w", err)
}
ret[0] = x[0]
ret[1] = y[0]
ret[2] = x[0]
ret[3] = y[0]
for i := 1; i < 4; i++ {
if x[i] < ret[0] {
ret[0] = x[i]
}
if x[i] > ret[2] {
ret[2] = x[i]
}
if y[i] < ret[1] {
ret[1] = y[i]
}
if y[i] > ret[3] {
ret[3] = y[i]
}
}
return ret, nil
}

View File

@@ -1,61 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
// Statisitics on a given band.
type Statistics struct {
Min, Max, Mean, Std float64
Approximate bool
}
type statisticsOpts struct {
approx int
errorHandler ErrorHandler
}
//StatisticsOption is an option that can be passed to Band.Statistics
//
//Available Statistics options are:
// - Aproximate() to allow the satistics to be computed on overviews or a subset of all tiles.
// - ErrLogger
type StatisticsOption interface {
setStatisticsOpt(so *statisticsOpts)
}
func (aoo approximateOkOption) setStatisticsOpt(so *statisticsOpts) {
so.approx = 1
}
//SetStatistics is an option that can passed to Band.SetStatistics()
//Available options are:
// -ErrLogger
type SetStatisticsOption interface {
setSetStatisticsOpt(sts *setStatisticsOpt)
}
type setStatisticsOpt struct {
errorHandler ErrorHandler
}
//ClearStatistics is an option passed to Dataset.ClearStatistics
//Available options are:
// -ErrLogger
type ClearStatisticsOption interface {
setClearStatisticsOpt(sts *clearStatisticsOpt)
}
type clearStatisticsOpt struct {
errorHandler ErrorHandler
}

View File

@@ -1,114 +0,0 @@
// Copyright 2021 Airbus Defence and Space
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package godal
// Block is a window inside a dataset, starting at pixel X0,Y0 and spanning
// W,H pixels.
type Block struct {
X0, Y0 int
W, H int
bw, bh int //block size
sx, sy int //img size
nx, ny int //num blocks
i, j int //cur
}
// Next returns the following block in scanline order. It returns Block{},false
// when there are no more blocks in the scanlines
func (b Block) Next() (Block, bool) {
nb := b
nb.i++
if nb.i >= nb.nx {
nb.i = 0
nb.j++
}
if nb.j >= nb.ny {
return Block{}, false
}
nb.X0 = nb.i * nb.bw
nb.Y0 = nb.j * nb.bh
nb.W, nb.H = actualBlockSize(nb.sx, nb.sy, nb.bw, nb.bh, nb.i, nb.j)
return nb, true
}
// BlockIterator returns the blocks covering a sizeX,sizeY dataset.
// All sizes must be strictly positive.
func BlockIterator(sizeX, sizeY int, blockSizeX, blockSizeY int) Block {
bl := Block{
X0: 0,
Y0: 0,
i: 0,
j: 0,
bw: blockSizeX,
bh: blockSizeY,
sx: sizeX,
sy: sizeY,
}
bl.nx, bl.ny = (sizeX+blockSizeX-1)/blockSizeX,
(sizeY+blockSizeY-1)/blockSizeY
bl.W, bl.H = actualBlockSize(sizeX, sizeY, blockSizeX, blockSizeY, 0, 0)
return bl
}
// BandStructure implements Structure for a Band
type BandStructure struct {
SizeX, SizeY int
BlockSizeX, BlockSizeY int
Scale, Offset float64
DataType DataType
}
// DatasetStructure implements Structure for a Dataset
type DatasetStructure struct {
BandStructure
NBands int
}
// FirstBlock returns the topleft block definition
func (is BandStructure) FirstBlock() Block {
return BlockIterator(is.SizeX, is.SizeY, is.BlockSizeX, is.BlockSizeY)
}
// BlockCount returns the number of blocks in the x and y dimensions
func (is BandStructure) BlockCount() (int, int) {
return (is.SizeX + is.BlockSizeX - 1) / is.BlockSizeX,
(is.SizeY + is.BlockSizeY - 1) / is.BlockSizeY
}
// ActualBlockSize returns the number of pixels in the x and y dimensions
// that actually contain data for the given x,y block
func (is BandStructure) ActualBlockSize(blockX, blockY int) (int, int) {
return actualBlockSize(is.SizeX, is.SizeY, is.BlockSizeX, is.BlockSizeY, blockX, blockY)
}
func actualBlockSize(sizeX, sizeY int, blockSizeX, blockSizeY int, blockX, blockY int) (int, int) {
cx, cy := (sizeX+blockSizeX-1)/blockSizeX,
(sizeY+blockSizeY-1)/blockSizeY
if blockX < 0 || blockY < 0 || blockX >= cx || blockY >= cy {
return 0, 0
}
retx := blockSizeX
rety := blockSizeY
if blockX == cx-1 {
nXPixelOff := blockX * blockSizeX
retx = sizeX - nXPixelOff
}
if blockY == cy-1 {
nYPixelOff := blockY * blockSizeY
rety = sizeY - nYPixelOff
}
return retx, rety
}

View File

@@ -1,220 +0,0 @@
Creative Commons Attribution 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to
be bound by the terms and conditions of this Creative Commons Attribution
4.0 International Public License ("Public License"). To the extent this
Public License may be interpreted as a contract, You are granted the
Licensed Rights in consideration of Your acceptance of these terms and
conditions, and the Licensor grants You such rights in consideration
of benefits the Licensor receives from making the Licensed Material
available under these terms and conditions.
Section 1 Definitions.
Adapted Material means material subject to Copyright and Similar Rights
that is derived from or based upon the Licensed Material and in which
the Licensed Material is translated, altered, arranged, transformed, or
otherwise modified in a manner requiring permission under the Copyright
and Similar Rights held by the Licensor. For purposes of this Public
License, where the Licensed Material is a musical work, performance,
or sound recording, Adapted Material is always produced where the
Licensed Material is synched in timed relation with a moving image.
Adapter's License means the license You apply to Your Copyright and
Similar Rights in Your contributions to Adapted Material in accordance
with the terms and conditions of this Public License. Copyright and
Similar Rights means copyright and/or similar rights closely related to
copyright including, without limitation, performance, broadcast, sound
recording, and Sui Generis Database Rights, without regard to how the
rights are labeled or categorized. For purposes of this Public License,
the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar
Rights. Effective Technological Measures means those measures that,
in the absence of proper authority, may not be circumvented under laws
fulfilling obligations under Article 11 of the WIPO Copyright Treaty
adopted on December 20, 1996, and/or similar international agreements.
Exceptions and Limitations means fair use, fair dealing, and/or any other
exception or limitation to Copyright and Similar Rights that applies to
Your use of the Licensed Material. Licensed Material means the artistic
or literary work, database, or other material to which the Licensor
applied this Public License. Licensed Rights means the rights granted
to You subject to the terms and conditions of this Public License, which
are limited to all Copyright and Similar Rights that apply to Your use
of the Licensed Material and that the Licensor has authority to license.
Licensor means the individual(s) or entity(ies) granting rights under
this Public License. Share means to provide material to the public by
any means or process that requires permission under the Licensed Rights,
such as reproduction, public display, public performance, distribution,
dissemination, communication, or importation, and to make material
available to the public including in ways that members of the public
may access the material from a place and at a time individually chosen
by them. Sui Generis Database Rights means rights other than copyright
resulting from Directive 96/9/EC of the European Parliament and of the
Council of 11 March 1996 on the legal protection of databases, as amended
and/or succeeded, as well as other essentially equivalent rights anywhere
in the world. You means the individual or entity exercising the Licensed
Rights under this Public License. Your has a corresponding meaning.
Section 2 Scope.
License grant. Subject to the terms and conditions of this Public
License, the Licensor hereby grants You a worldwide, royalty-free,
non-sublicensable, non-exclusive, irrevocable license to exercise the
Licensed Rights in the Licensed Material to: reproduce and Share the
Licensed Material, in whole or in part; and produce, reproduce, and
Share Adapted Material. Exceptions and Limitations. For the avoidance
of doubt, where Exceptions and Limitations apply to Your use, this
Public License does not apply, and You do not need to comply with
its terms and conditions. Term. The term of this Public License is
specified in Section 6(a). Media and formats; technical modifications
allowed. The Licensor authorizes You to exercise the Licensed Rights in
all media and formats whether now known or hereafter created, and to make
technical modifications necessary to do so. The Licensor waives and/or
agrees not to assert any right or authority to forbid You from making
technical modifications necessary to exercise the Licensed Rights,
including technical modifications necessary to circumvent Effective
Technological Measures. For purposes of this Public License, simply making
modifications authorized by this Section 2(a)(4) never produces Adapted
Material. Downstream recipients. Offer from the Licensor Licensed
Material. Every recipient of the Licensed Material automatically receives
an offer from the Licensor to exercise the Licensed Rights under the terms
and conditions of this Public License. No downstream restrictions. You
may not offer or impose any additional or different terms or conditions
on, or apply any Effective Technological Measures to, the Licensed
Material if doing so restricts exercise of the Licensed Rights by any
recipient of the Licensed Material. No endorsement. Nothing in this
Public License constitutes or may be construed as permission to assert
or imply that You are, or that Your use of the Licensed Material is,
connected with, or sponsored, endorsed, or granted official status by,
the Licensor or others designated to receive attribution as provided in
Section 3(a)(1)(A)(i). Other rights.
Moral rights, such as the right of integrity, are not licensed under
this Public License, nor are publicity, privacy, and/or other similar
personality rights; however, to the extent possible, the Licensor waives
and/or agrees not to assert any such rights held by the Licensor to the
limited extent necessary to allow You to exercise the Licensed Rights, but
not otherwise. Patent and trademark rights are not licensed under this
Public License. To the extent possible, the Licensor waives any right
to collect royalties from You for the exercise of the Licensed Rights,
whether directly or through a collecting society under any voluntary or
waivable statutory or compulsory licensing scheme. In all other cases
the Licensor expressly reserves any right to collect such royalties.
Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the
following conditions.
Attribution.
If You Share the Licensed Material (including in modified form), You must:
retain the following if it is supplied by the Licensor with the Licensed
Material: identification of the creator(s) of the Licensed Material and
any others designated to receive attribution, in any reasonable manner
requested by the Licensor (including by pseudonym if designated); a
copyright notice; a notice that refers to this Public License; a notice
that refers to the disclaimer of warranties; a URI or hyperlink to the
Licensed Material to the extent reasonably practicable; indicate if You
modified the Licensed Material and retain an indication of any previous
modifications; and indicate the Licensed Material is licensed under this
Public License, and include the text of, or the URI or hyperlink to,
this Public License. You may satisfy the conditions in Section 3(a)(1)
in any reasonable manner based on the medium, means, and context in which
You Share the Licensed Material. For example, it may be reasonable to
satisfy the conditions by providing a URI or hyperlink to a resource
that includes the required information. If requested by the Licensor,
You must remove any of the information required by Section 3(a)(1)(A)
to the extent reasonably practicable. If You Share Adapted Material You
produce, the Adapter's License You apply must not prevent recipients of
the Adapted Material from complying with this Public License. Section 4
Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply
to Your use of the Licensed Material:
for the avoidance of doubt, Section 2(a)(1) grants You the right to
extract, reuse, reproduce, and Share all or a substantial portion of the
contents of the database; if You include all or a substantial portion of
the database contents in a database in which You have Sui Generis Database
Rights, then the database in which You have Sui Generis Database Rights
(but not its individual contents) is Adapted Material; and You must comply
with the conditions in Section 3(a) if You Share all or a substantial
portion of the contents of the database. For the avoidance of doubt,
this Section 4 supplements and does not replace Your obligations under
this Public License where the Licensed Rights include other Copyright and
Similar Rights. Section 5 Disclaimer of Warranties and Limitation
of Liability.
Unless otherwise separately undertaken by the Licensor, to the
extent possible, the Licensor offers the Licensed Material as-is and
as-available, and makes no representations or warranties of any kind
concerning the Licensed Material, whether express, implied, statutory,
or other. This includes, without limitation, warranties of title,
merchantability, fitness for a particular purpose, non-infringement,
absence of latent or other defects, accuracy, or the presence or absence
of errors, whether or not known or discoverable. Where disclaimers of
warranties are not allowed in full or in part, this disclaimer may not
apply to You. To the extent possible, in no event will the Licensor
be liable to You on any legal theory (including, without limitation,
negligence) or otherwise for any direct, special, indirect, incidental,
consequential, punitive, exemplary, or other losses, costs, expenses,
or damages arising out of this Public License or use of the Licensed
Material, even if the Licensor has been advised of the possibility of
such losses, costs, expenses, or damages. Where a limitation of liability
is not allowed in full or in part, this limitation may not apply to You.
The disclaimer of warranties and limitation of liability provided above
shall be interpreted in a manner that, to the extent possible, most
closely approximates an absolute disclaimer and waiver of all liability.
Section 6 Term and Termination.
This Public License applies for the term of the Copyright and Similar
Rights licensed here. However, if You fail to comply with this
Public License, then Your rights under this Public License terminate
automatically. Where Your right to use the Licensed Material has
terminated under Section 6(a), it reinstates:
automatically as of the date the violation is cured, provided it is
cured within 30 days of Your discovery of the violation; or upon express
reinstatement by the Licensor. For the avoidance of doubt, this Section
6(b) does not affect any right the Licensor may have to seek remedies
for Your violations of this Public License. For the avoidance of doubt,
the Licensor may also offer the Licensed Material under separate terms
or conditions or stop distributing the Licensed Material at any time;
however, doing so will not terminate this Public License. Sections 1,
5, 6, 7, and 8 survive termination of this Public License. Section 7
Other Terms and Conditions.
The Licensor shall not be bound by any additional or different terms or
conditions communicated by You unless expressly agreed. Any arrangements,
understandings, or agreements regarding the Licensed Material not stated
herein are separate from and independent of the terms and conditions of
this Public License. Section 8 Interpretation.
For the avoidance of doubt, this Public License does not, and shall not be
interpreted to, reduce, limit, restrict, or impose conditions on any use
of the Licensed Material that could lawfully be made without permission
under this Public License. To the extent possible, if any provision of
this Public License is deemed unenforceable, it shall be automatically
reformed to the minimum extent necessary to make it enforceable. If
the provision cannot be reformed, it shall be severed from this Public
License without affecting the enforceability of the remaining terms
and conditions. No term or condition of this Public License will be
waived and no failure to comply consented to unless expressly agreed
to by the Licensor. Nothing in this Public License constitutes or may
be interpreted as a limitation upon, or waiver of, any privileges and
immunities that apply to the Licensor or You, including from the legal
processes of any jurisdiction or authority. Creative Commons is not
a party to its public licenses. Notwithstanding, Creative Commons may
elect to apply one of its public licenses to material it publishes and
in those instances will be considered the “Licensor.” The text of
the Creative Commons public licenses is dedicated to the public domain
under the CC0 Public Domain Dedication. Except for the limited purpose of
indicating that material is shared under a Creative Commons public license
or as otherwise permitted by the Creative Commons policies published at
creativecommons.org/policies, Creative Commons does not authorize the
use of the trademark “Creative Commons” or any other trademark or
logo of Creative Commons without its prior written consent including,
without limitation, in connection with any unauthorized modifications
to any of its public licenses or any other arrangements, understandings,
or agreements concerning use of licensed material. For the avoidance of
doubt, this paragraph does not form part of the public licenses.
Creative Commons may be contacted at creativecommons.org.

View File

@@ -1,3 +0,0 @@
The contents of this repository are Licensed under
the Creative Commons Attribution 4.0 International license as described in
https://creativecommons.org/licenses/by/4.0/legalcode

View File

@@ -1,739 +0,0 @@
# 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/SVG11/>).
Output goes to the specified io.Writer.
## Supported SVG elements and functions ##
### Shapes, lines, text
circle, ellipse, polygon, polyline, rect (including roundrects), line, text
### Paths
general, arc, cubic and quadratic bezier paths,
### Image and Gradients
image, linearGradient, radialGradient,
### Transforms ###
translate, rotate, scale, skewX, skewY
### Animation ###
animate, animateMotion, animateTranslate, animateRotate, animateScale, animateSkewX, animateSkewY
### Filter Effects
filter, feBlend, feColorMatrix, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting,
feDisplacementMap, feDistantLight, feFlood, feGaussianBlur, feImage, feMerge, feMorphology, feOffset, fePointLight,
feSpecularLighting, feSpotLight,feTile, feTurbulence
### Metadata elements ###
desc, defs, g (style, transform, id), marker, mask, pattern, title, (a)ddress, link, script, use
## Building and Usage ##
See svgdef.[svg|png|pdf] for a graphical view of the function calls
Usage: (assuming GOPATH is set)
go get github.com/ajstarks/svgo
go install github.com/ajstarks/svgo/...
You can use godoc to browse the documentation from the command line:
$ go doc github.com/ajstarks/svgo
a minimal program, to generate SVG to standard output.
package main
import (
"github.com/ajstarks/svgo"
"os"
)
func main() {
width := 500
height := 500
canvas := svg.New(os.Stdout)
canvas.Start(width, height)
canvas.Circle(width/2, height/2, 100)
canvas.Text(width/2, height/2, "Hello, SVG", "text-anchor:middle;font-size:30px;fill:white")
canvas.End()
}
Drawing in a web server: (http://localhost:2003/circle)
package main
import (
"log"
"github.com/ajstarks/svgo"
"net/http"
)
func main() {
http.Handle("/circle", http.HandlerFunc(circle))
err := http.ListenAndServe(":2003", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func circle(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
s := svg.New(w)
s.Start(500, 500)
s.Circle(250, 250, 125, "fill:none;stroke:black")
s.End()
}
You may view the SVG output with a browser that supports SVG (tested on Chrome, Opera, Firefox and Safari), or any other SVG user-agent such as Batik Squiggle.
### Graphics Sketching with SVGo and svgplay ###
Combined with the svgplay command, SVGo can be used to "sketch" with code in a browser.
To use svgplay and SVGo, first go to a directory with your code, and run:
$ svgplay
2014/06/25 22:05:28 ☠ ☠ ☠ Warning: this server allows a client connecting to 127.0.0.1:1999 to execute code on this computer ☠ ☠ ☠
Next open your browser to the svgplay server you just started.
svgplay only listens on localhost, and uses port 1999 (guess which year SVG was first introduced) by default
http://localhost:1999/
Enter your code in the textarea, and when you are ready to run press Shift--Enter. The code will be compiled, with the results
on the right. To update, change the code and repeat. Note that compilation errors are shown in red under the code. In order for svgplay/SVGo to work, make sure that the io.Writer specified with the New function is os.Stdout.
If you want to sketch with an existing file, enter its URL:
http://localhost:1999/foo.go
![SVGplay](https://farm4.staticflickr.com/3859/14322978157_31c0114850.jpg)
### SVGo Papers and presentations ###
* SVGo paper from SVGOpen 2011 <http://www.svgopen.org/2011/papers/34-SVGo_a_Go_Library_for_SVG_generation>
* Programming Pictures with SVGo <https://speakerdeck.com/u/ajstarks/p/programming-pictures-with-svgo>
* SVGo Workshop <https://speakerdeck.com/u/ajstarks/p/svgo-workshop>
### Tutorial Video ###
A video describing how to use the package can be seen on YouTube at <http://www.youtube.com/watch?v=ze6O2Dj5gQ4>
## Package contents ##
* svg.go: Library
* newsvg: Coding template command
* svgdef: Creates a SVG representation of the API
* animate: Animation demo
* am: Animate motion demo
* amt: Animate transformation demo
* android: The Android logo
* bubtrail: Bubble trails
* bulletgraph: Bullet Graphs (via Stephen Few)
* colortab: Display SVG named colors with RGB values
* compx: Component diagrams
* flower: Random "flowers"
* fontcompare: Compare two fonts
* f50: Get 50 photos from Flickr based on a query
* fe: Filter effects
* funnel: Funnel from transparent circles
* gradient: Linear and radial gradients
* html5logo: HTML5 logo with draggable elements
* imfade: Show image fading
* lewitt: Version of Sol Lewitt's Wall Drawing 91
* ltr: Layer Tennis Remixes
* marker: Test markers
* paths: Demonstrate SVG paths
* pattern: Test patterns
* planets: Show the scale of the Solar system
* pmap: Proportion maps
* randcomp: Compare random number generators
* richter: Gerhard Richter's 256 colors
* rl: Random lines (port of a Processing demo)
* skewabc: Skew ABC
* span: Text span
* stockproduct: Visualize product and stock prices
* svgopher: SVGo Mascot
* svgplay: SVGo sketching server
* svgplot: Plot data
* svgrid: Compose SVG files in a grid
* tsg: Twitter Search Grid
* tumblrgrid: Tumblr picture grid
* turbulence: Turbulence filter effect
* vismem: Visualize data from files
* webfonts: "Hello, World" with Google Web Fonts
* websvg: Generate SVG as a web server
## Functions and types ##
Many functions use x, y to specify an object's location, and w, h to specify the object's width and height.
Where applicable, a final optional argument specifies the style to be applied to the object.
The style strings follow the SVG standard; name:value pairs delimited by semicolons, or a
series of name="value" pairs. For example: `"fill:none; opacity:0.3"` or `fill="none" opacity="0.3"` (see: <http://www.w3.org/TR/SVG11/styling.html>)
The SVG type:
type SVG struct {
Writer io.Writer
}
Most operations are methods on this type, specifying the destination io.Writer.
The Offcolor type:
type Offcolor struct {
Offset uint8
Color string
Opacity float64
}
is used to specify the offset, color, and opacity of stop colors in linear and radial gradients
The Filterspec type:
type Filterspec struct {
In string
In2 string
Result string
}
is used to specify inputs and results for filter effects
### Structure, Scripting, Metadata, Transformation and Links ###
New(w io.Writer) *SVG
Constructor, Specify the output destination.
Start(w int, h int, attributes ...string)
begin the SVG document with the width w and height h. Optionally add additional elements
(such as additional namespaces or scripting events)
<http://www.w3.org/TR/SVG11/struct.html#SVGElement>
Startview(w, h, minx, miny, vw, vh int)
begin the SVG document with the width w, height h, with a viewBox at minx, miny, vw, vh.
<http://www.w3.org/TR/SVG11/struct.html#SVGElement>
Startunit(w int, h int, unit string, ns ...string)
begin the SVG document, with width and height in the specified units. Optionally add additional elements
(such as additional namespaces or scripting events)
<http://www.w3.org/TR/SVG11/struct.html#SVGElement>
Startpercent(w int, h int, ns ...string)
begin the SVG document, with width and height in percent. Optionally add additional elements
(such as additional namespaces or scripting events)
<http://www.w3.org/TR/SVG11/struct.html#SVGElement>
StartviewUnit(w, h int, unit string, minx, miny, vw, vh int)
begin the SVG document with the width w, height h, in the specified unit, with a viewBox at minx, miny, vw, vh.
<http://www.w3.org/TR/SVG11/struct.html#SVGElement>
End()
end the SVG document
Script(scriptype string, data ...string)
Script defines a script with a specified type, (for example "application/javascript").
if the first variadic argument is a link, use only the link reference.
Otherwise, treat variadic arguments as the text of the script (marked up as CDATA).
if no data is specified, simply close the script element.
<http://www.w3.org/TR/SVG/script.html>
Style(scriptype string, data ...string)
Style defines a script with a specified type, (for example "text/css").
if the first variadic argument is a link, use only the link reference.
Otherwise, treat variadic arguments as the text of the script (marked up as CDATA).
if no data is specified, simply close the style element.
<https://www.w3.org/TR/SVG/styling.html#StyleElement>
Group(s ...string)
begin a group, with arbitrary attributes
<http://www.w3.org/TR/SVG11/struct.html#GElement>
Gstyle(s string)
begin a group, with the specified style.
<http://www.w3.org/TR/SVG11/struct.html#GElement>
Gid(s string)
begin a group, with the specified id.
Gtransform(s string)
begin a group, with the specified transform, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
Translate(x, y int)
begins coordinate translation to (x,y), end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
Scale(n float64)
scales the coordinate system by n, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
ScaleXY(x, y float64)
scales the coordinate system by x, y. End with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
SkewX(a float64)
SkewX skews the x coordinate system by angle a, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
SkewY(a float64)
SkewY skews the y coordinate system by angle a, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
SkewXY(ax, ay float64)
SkewXY skews x and y coordinate systems by ax, ay respectively, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
Rotate(r float64)
rotates the coordinate system by r degrees, end with Gend().
<http://www.w3.org/TR/SVG11/coords.html#TransformAttribute>
TranslateRotate(x, y int, r float64)
translates the coordinate system to (x,y), then rotates to r degrees, end with Gend().
RotateTranslate(x, y int, r float64)
rotates the coordinate system r degrees, then translates to (x,y), end with Gend().
Gend()
end the group (must be paired with Gstyle, Gtransform, Gid).
ClipPath(s ...string)
Begin a ClipPath
<http://www.w3.org/TR/SVG/masking.html#ClippingPaths>
ClipEnd()
End a ClipPath
<http://www.w3.org/TR/SVG/masking.html#ClippingPaths>
Def()
begin a definition block.
<http://www.w3.org/TR/SVG11/struct.html#DefsElement>
DefEnd()
end a definition block.
Marker(id string, x, y, w, h int, s ...string)
define a marker
<http://www.w3.org/TR/SVG11/painting.html#MarkerElement>
MarkerEnd()
end a marker
Mask(id string, x int, y int, w int, h int, s ...string)
creates a mask with a specified id, dimension, and optional style.
<http://www.w3.org/TR/SVG/masking.html>
MaskEnd()
ends the Mask element.
Pattern(id string, x, y, width, height int, putype string, s ...string)
define a Pattern with the specified dimensions, the putype can be either "user" or "obj", which sets the patternUnits
attribute to be either userSpaceOnUse or objectBoundingBox.
<http://www.w3.org/TR/SVG11/pservers.html#Patterns>
Desc(s string)
specify the text of the description.
<http://www.w3.org/TR/SVG11/struct.html#DescElement>
Title(s string)
specify the text of the title.
<http://www.w3.org/TR/SVG11/struct.html#TitleElement>
Link(href string, title string)
begin a link named "href", with the specified title.
<http://www.w3.org/TR/SVG11/linking.html#Links>
LinkEnd()
end the link.
Use(x int, y int, link string, s ...string)
place the object referenced at link at the location x, y.
<http://www.w3.org/TR/SVG11/struct.html#UseElement>
### Shapes ###
Circle(x int, y int, r int, s ...string)
draw a circle, centered at x,y with radius r.
<http://www.w3.org/TR/SVG11/shapes.html#CircleElement>
![Circle](http://farm5.static.flickr.com/4144/5187953823_01a1741489_m.jpg)
Ellipse(x int, y int, w int, h int, s ...string)
draw an ellipse, centered at x,y with radii w, and h.
<http://www.w3.org/TR/SVG11/shapes.html#EllipseElement>
![Ellipse](http://farm2.static.flickr.com/1271/5187953773_a9d1fc406c_m.jpg)
Polygon(x []int, y []int, s ...string)
draw a series of line segments using an array of x, y coordinates.
<http://www.w3.org/TR/SVG11/shapes.html#PolygonElement>
![Polygon](http://farm2.static.flickr.com/1006/5187953873_337dc26597_m.jpg)
Rect(x int, y int, w int, h int, s ...string)
draw a rectangle with upper left-hand corner at x,y, with width w, and height h.
<http://www.w3.org/TR/SVG11/shapes.html#RectElement>
![Rect](http://farm2.static.flickr.com/1233/5188556032_86c90e354b_m.jpg)
CenterRect(x int, y int, w int, h int, s ...string)
draw a rectangle with its center at x,y, with width w, and height h.
Roundrect(x int, y int, w int, h int, rx int, ry int, s ...string)
draw a rounded rectangle with upper the left-hand corner at x,y,
with width w, and height h. The radii for the rounded portion
is specified by rx (width), and ry (height).
![Roundrect](http://farm2.static.flickr.com/1275/5188556120_e2a9998fee_m.jpg)
Square(x int, y int, s int, style ...string)
draw a square with upper left corner at x,y with sides of length s.
![Square](http://farm5.static.flickr.com/4110/5187953659_54dcce242e_m.jpg)
### Paths ###
Path(p string, s ...style)
draw the arbitrary path as specified in p, according to the style specified in s. <http://www.w3.org/TR/SVG11/paths.html>
Arc(sx int, sy int, ax int, ay int, r int, large bool, sweep bool, ex int, ey int, s ...string)
draw an elliptical arc beginning coordinate at sx,sy, ending coordinate at ex, ey
width and height of the arc are specified by ax, ay, the x axis rotation is r
if sweep is true, then the arc will be drawn in a "positive-angle" direction (clockwise),
if false, the arc is drawn counterclockwise.
if large is true, the arc sweep angle is greater than or equal to 180 degrees,
otherwise the arc sweep is less than 180 degrees.
<http://www.w3.org/TR/SVG11/paths.html#PathDataEllipticalArcCommands>
![Arc](http://farm2.static.flickr.com/1300/5188556148_df1a176074_m.jpg)
Bezier(sx int, sy int, cx int, cy int, px int, py int, ex int, ey int, s ...string)
draw a cubic bezier curve, beginning at sx,sy, ending at ex,ey
with control points at cx,cy and px,py.
<http://www.w3.org/TR/SVG11/paths.html#PathDataCubicBezierCommands>
![Bezier](http://farm2.static.flickr.com/1233/5188556246_a03e67d013.jpg)
Qbezier(sx int, sy int, cx int, cy int, ex int, ey int, tx int, ty int, s ...string)
draw a quadratic bezier curve, beginning at sx, sy, ending at tx,ty
with control points are at cx,cy, ex,ey.
<http://www.w3.org/TR/SVG11/paths.html#PathDataQuadraticBezierCommands>
![Qbezier](http://farm2.static.flickr.com/1018/5187953917_9a43cf64fb.jpg)
Qbez(sx int, sy int, cx int, cy int, ex int, ey int, s...string)
draws a quadratic bezier curver, with optional style beginning at sx,sy, ending at ex, sy
with the control point at cx, cy.
<http://www.w3.org/TR/SVG11/paths.html#PathDataQuadraticBezierCommands>
![Qbez](http://farm6.static.flickr.com/5176/5569879349_5f726aab5e.jpg)
### Lines ###
Line(x1 int, y1 int, x2 int, y2 int, s ...string)
draw a line segment between x1,y1 and x2,y2.
<http://www.w3.org/TR/SVG11/shapes.html#LineElement>
![Line](http://farm5.static.flickr.com/4154/5188556080_0be19da0bc.jpg)
Polyline(x []int, y []int, s ...string)
draw a polygon using coordinates specified in x,y arrays.
<http://www.w3.org/TR/SVG11/shapes.html#PolylineElement>
![Polyline](http://farm2.static.flickr.com/1266/5188556384_a863273a69.jpg)
### Image and Text ###
Image(x int, y int, w int, h int, link string, s ...string)
place at x,y (upper left hand corner), the image with width w, and height h, referenced at link.
<http://www.w3.org/TR/SVG11/struct.html#ImageElement>
![Image](http://farm5.static.flickr.com/4058/5188556346_e5ce3dcbc2_m.jpg)
Text(x int, y int, t string, s ...string)
Place the specified text, t at x,y according to the optional style specified in s.
<http://www.w3.org/TR/SVG11/text.html#TextElement>
Textspan(x int, y int, t string, s ...string)
Place specified text, t at x,y according to the optional style specified in s.
<https://www.w3.org/TR/SVG11/text.html#TSpanElement>
Use this method with Span(...). End with TextEnd()
Span(t string, s ...string)
Create a text span t, using optional style s
TextEnd()
End a text span
Textlines(x, y int, s []string, size, spacing int, fill, align string)
Places lines of text in s, starting at x,y, at the specified size, fill, and alignment, and spacing.
Textpath(t string, pathid string, s ...string)
places optionally styled text along a previously defined path.
<http://www.w3.org/TR/SVG11/text.html#TextPathElement>
![Image](http://farm4.static.flickr.com/3149/5694580737_4b291df768_m.jpg)
### Color ###
RGB(r int, g int, b int) string
creates a style string for the fill color designated
by the (r)ed, g(reen), (b)lue components.
<http://www.w3.org/TR/css3-color/>
RGBA(r int, g int, b int, a float64) string
as above, but includes the color's opacity as a value
between 0.0 (fully transparent) and 1.0 (opaque).
### Gradients ###
LinearGradient(id string, x1, y1, x2, y2 uint8, sc []Offcolor)
constructs a linear color gradient identified by id,
along the vector defined by (x1,y1), and (x2,y2).
The stop color sequence defined in sc. Coordinates are expressed as percentages.
<http://www.w3.org/TR/SVG11/pservers.html#LinearGradients>
![LinearGradient](http://farm5.static.flickr.com/4153/5187954033_3972f63fa9.jpg)
RadialGradient(id string, cx, cy, r, fx, fy uint8, sc []Offcolor)
constructs a radial color gradient identified by id,
centered at (cx,cy), with a radius of r.
(fx, fy) define the location of the focal point of the light source.
The stop color sequence defined in sc.
Coordinates are expressed as percentages.
<http://www.w3.org/TR/SVG11/pservers.html#RadialGradients>
![RadialGradient](http://farm2.static.flickr.com/1302/5187954065_7ddba7b819.jpg)
### Animation ###
Animate(link, attr string, from, to int, duration float64, repeat int, s ...string)
Animate animates the item referenced by the link, using the specified attribute
The animation starts at coordinate from, terminates at to, and repeats as specified.
Addtional attributes may be added as needed.
<https://www.w3.org/TR/SVG11/animate.html#AnimateElement>
AnimateMotion(link, path string, duration float64, repeat int, s ...string)
AnimateMotion animates the referenced object ```link``` along the specified ```path```
<https://www.w3.org/TR/SVG11/animate.html#AnimateMotionElement>
AnimateTranslate(link string, fx, fy, tx, ty int, duration float64, repeat int, s ...string)
AnimateTranslate animates the translation transformation (link refers to the object to animate, fx, fy are from coordinates, tx, ty are the to coordinates)
<https://www.w3.org/TR/SVG11/animate.html#AnimateTransformElement>
AnimateRotate(link string, fs, fc, fe, ts, tc, te int, duration float64, repeat int, s ...string)
AnimateRotate animates the rotation transformation (link refers to the object to animate, f[s,c,e] are the from start, center, and end angles, t[s,c,e] are the
start, center, and end angles)
<https://www.w3.org/TR/SVG11/animate.html#AnimateTransformElement>
AnimateScale(link string, from, to, duration float64, repeat int, s ...string)
AnimateScale animates the scale transformation (link refers to the object to animate, from and to specify the scaling factor)
<https://www.w3.org/TR/SVG11/animate.html#AnimateTransformElement>
AnimateSkewX(link string, from, to, duration float64, repeat int, s ...string)
AnimateSkewX animates the skewX transformation ((link refers to the object to animate, from and to specify the skew angle)
<https://www.w3.org/TR/SVG11/animate.html#AnimateTransformElement>
AnimateSkewY(link string, from, to, duration float64, repeat int, s ...string)
AnimateSkewY animates the skewY transformation (link refers to the object to animate, and from and to specify the skew angle)
<https://www.w3.org/TR/SVG11/animate.html#AnimateTransformElement>
### Filter Effects ###
Filter(id string, s ...string)
Filter begins a filter set
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#FilterElement>
Fend()
Fend ends a filter set
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#FilterElement>
FeBlend(fs Filterspec, mode string, s ...string)
FeBlend specifies a Blend filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feBlendElement>
FeColorMatrix(fs Filterspec, values [20]float64, s ...string)
FeColorMatrix specifies a color matrix filter primitive, with matrix values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement>
FeColorMatrixHue(fs Filterspec, value float64, s ...string)
FeColorMatrix specifies a color matrix filter primitive, with hue values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement>
FeColorMatrixSaturate(fs Filterspec, value float64, s ...string)
FeColorMatrix specifies a color matrix filter primitive, with saturation values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement>
FeColorMatrixLuminence(fs Filterspec, s ...string)
FeColorMatrix specifies a color matrix filter primitive, with luminence values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feColorMatrixElement>
FeComponentTransfer()
FeComponentTransfer begins a feComponent filter Element>
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement>
FeCompEnd()
FeCompEnd ends a feComponent filter Element>
FeComposite(fs Filterspec, operator string, k1, k2, k3, k4 int, s ...string)
FeComposite specifies a feComposite filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feCompositeElement>
FeConvolveMatrix(fs Filterspec, matrix [9]int, s ...string)
FeConvolveMatrix specifies a feConvolveMatrix filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feConvolveMatrixElement>
FeDiffuseLighting(fs Filterspec, scale, constant float64, s ...string)
FeDiffuseLighting specifies a diffuse lighting filter primitive,
a container for light source Element>s, end with DiffuseEnd()
FeDiffEnd()
FeDiffuseEnd ends a diffuse lighting filter primitive container
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feDiffuseLightingElement>
FeDisplacementMap(fs Filterspec, scale float64, xchannel, ychannel string, s ...string)
FeDisplacementMap specifies a feDisplacementMap filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feDisplacementMapElement>
FeDistantLight(fs Filterspec, azimuth, elevation float64, s ...string)
FeDistantLight specifies a feDistantLight filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feDistantLightElement>
FeFlood(fs Filterspec, color string, opacity float64, s ...string)
FeFlood specifies a flood filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feFloodElement>
FeFuncLinear(channel string, slope, intercept float64)
FeFuncLinear is the linear form of feFunc
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement>
FeFuncGamma(channel, amplitude, exponent, offset float64)
FeFuncGamma is the gamma curve form of feFunc
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement>
FeFuncTable(channel string, tv []float64)
FeFuncGamma is the form of feFunc using a table of values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement>
FeFuncDiscrete(channel string, tv []float64)
FeFuncGamma is the form of feFunc using discrete values
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feComponentTransferElement>
FeGaussianBlur(fs Filterspec, stdx, stdy float64, s ...string)
FeGaussianBlur specifies a Gaussian Blur filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feGaussianBlurElement>
FeImage(href string, result string, s ...string)
FeImage specifies a feImage filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feImageElement>
FeMerge(nodes []string, s ...string)
FeMerge specifies a feMerge filter primitive, containing feMerge Element>s
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feMergeElement>
FeMorphology(fs Filterspec, operator string, xradius, yradius float64, s ...string)
FeMorphologyLight specifies a feMorphologyLight filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feMorphologyElement>
FeOffset(fs Filterspec, dx, dy int, s ...string)
FeOffset specifies the feOffset filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feOffsetElement>
FePointLight(x, y, z float64, s ...string)
FePointLight specifies a fePpointLight filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#fePointLightElement>
FeSpecularLighting(fs Filterspec, scale, constant float64, exponent int, color string, s ...string)
FeSpecularLighting specifies a specular lighting filter primitive,
a container for light source elements, end with SpecularEnd()
FeSpecEnd()
FeSpecularEnd ends a specular lighting filter primitive container
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feSpecularLightingElement>
FeSpotLight(fs Filterspec, x, y, z, px, py, pz float64, s ...string)
FeSpotLight specifies a feSpotLight filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feSpotLightElement>
FeTile(fs Filterspec, in string, s ...string)
FeTile specifies the tile utility filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feTileElement>
FeTurbulence(fs Filterspec, ftype string, bfx, bfy float64, octaves int, seed int64, stitch bool, s ...string)
FeTurbulence specifies a turbulence filter primitive
Standard reference: <http://www.w3.org/TR/SVG11/filters.html#feTurbulenceElement>
### Filter convenience functions (modeled on CSS filter effects) ###
Blur(p float64)
Blur function by standard deviation
Brightness(p float64)
Brightness function (0-100)
Grayscale()
Apply a grayscale filter to the image
HueRotate(a float64)
Rotate Hues (0-360 degrees)
Invert()
Invert the image's colors
Saturate(p float64)
Percent saturation, 0 is grayscale
Sepia()
Apply sepia tone
### Utility ###
Grid(x int, y int, w int, h int, n int, s ...string)
draws a grid of straight lines starting at x,y, with a width w, and height h, and a size of n.
![Grid](http://farm5.static.flickr.com/4133/5190957924_7a31d0db34.jpg)
### Credits ###
Thanks to Jonathan Wright for the io.Writer update.

View File

@@ -1,126 +0,0 @@
/*
Package svg generates SVG as defined by the Scalable Vector Graphics 1.1 Specification (<http://www.w3.org/TR/SVG11/>).
Output goes to the specified io.Writer.
Supported SVG elements and functions
Shapes, lines, text
circle, ellipse, polygon, polyline, rect (including roundrects), line, text
Paths
general, arc, cubic and quadratic bezier paths,
Image and Gradients
image, linearGradient, radialGradient,
Transforms
translate, rotate, scale, skewX, skewY
Filter Effects
filter, feBlend, feColorMatrix, feColorMatrix, feComponentTransfer, feComposite, feConvolveMatrix, feDiffuseLighting,
feDisplacementMap, feDistantLight, feFlood, feGaussianBlur, feImage, feMerge, feMorphology, feOffset, fePointLight,
feSpecularLighting, feSpotLight,feTile, feTurbulence
Metadata elements
desc, defs, g (style, transform, id), mask, marker, pattern, title, (a)ddress, link, script, style, use
Usage: (assuming GOPATH is set)
go get github.com/ajstarks/svgo
go install github.com/ajstarks/svgo/...
You can use godoc to browse the documentation from the command line:
$ godoc github.com/ajstarks/svgo
a minimal program, to generate SVG to standard output.
package main
import (
"github.com/ajstarks/svgo"
"os"
)
func main() {
width := 500
height := 500
canvas := svg.New(os.Stdout)
canvas.Start(width, height)
canvas.Circle(width/2, height/2, 100)
canvas.Text(width/2, height/2, "Hello, SVG", "text-anchor:middle;font-size:30px;fill:white")
canvas.End()
}
Drawing in a web server: (http://localhost:2003/circle)
package main
import (
"log"
"github.com/ajstarks/svgo"
"net/http"
)
func main() {
http.Handle("/circle", http.HandlerFunc(circle))
err := http.ListenAndServe(":2003", nil)
if err != nil {
log.Fatal("ListenAndServe:", err)
}
}
func circle(w http.ResponseWriter, req *http.Request) {
w.Header().Set("Content-Type", "image/svg+xml")
s := svg.New(w)
s.Start(500, 500)
s.Circle(250, 250, 125, "fill:none;stroke:black")
s.End()
}
Functions and types
Many functions use x, y to specify an object's location, and w, h to specify the object's width and height.
Where applicable, a final optional argument specifies the style to be applied to the object.
The style strings follow the SVG standard; name:value pairs delimited by semicolons, or a
series of name="value" pairs. For example: `"fill:none; opacity:0.3"` or `fill="none" opacity="0.3"` (see: <http://www.w3.org/TR/SVG11/styling.html>)
The SVG type:
type SVG struct {
Writer io.Writer
}
Most operations are methods on this type, specifying the destination io.Writer.
The Offcolor type:
type Offcolor struct {
Offset uint8
Color string
Opacity float64
}
is used to specify the offset, color, and opacity of stop colors in linear and radial gradients
The Filterspec type:
type Filterspec struct {
In string
In2 string
Result string
}
is used to specify inputs and results for filter effects
*/
package svg

View File

@@ -1,39 +0,0 @@
#!/bin/sh
if test $# -lt 1
then
echo "specify a file"
exit 2
fi
if test ! -f $1
then
cat <<! > $1
package main
import (
"github.com/ajstarks/svgo"
"os"
)
var (
width = 500
height = 500
canvas = svg.New(os.Stdout)
)
func background(v int) { canvas.Rect(0, 0, width, height, canvas.RGB(v, v, v)) }
func main() {
canvas.Start(width, height)
background(255)
// your code here
canvas.Grid(0, 0, width, height, 10, "stroke:black;opacity:0.1")
canvas.End()
}
!
fi
$EDITOR $1

1080
vendor/github.com/ajstarks/svgo/svg.go generated vendored

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -1,395 +0,0 @@
<?xml version="1.0"?>
<!-- Generated by SVGo -->
<svg width="4500" height="3375"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink">
<desc>Object Definitions</desc>
<defs>
<linearGradient id="linear" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="white" stop-opacity="1.00"/>
<stop offset="25%" stop-color="lightblue" stop-opacity="1.00"/>
<stop offset="75%" stop-color="blue" stop-opacity="1.00"/>
<stop offset="100%" stop-color="rgb(0,0,127)" stop-opacity="1.00"/>
</linearGradient>
<radialGradient id="radial" cx="0%" cy="0%" r="100%" fx="50%" fy="50%">
<stop offset="0%" stop-color="white" stop-opacity="1.00"/>
<stop offset="25%" stop-color="lightblue" stop-opacity="1.00"/>
<stop offset="75%" stop-color="blue" stop-opacity="1.00"/>
<stop offset="100%" stop-color="rgb(0,0,127)" stop-opacity="1.00"/>
</radialGradient>
<path d="M 0,0 A62,62 0 0 1 250,0" id="tpath" />
<g id="square">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<rect x="0" y="0" width="125" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="62" y="-24" style="fill:gray; text-anchor:middle">w</text>
<text x="62" y="149" style="fill:black; text-anchor:middle;font-size:24px">Square(x, y, w int, style ...string)</text>
</g>
<g id="rect">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="-24" y="62" style="fill:gray; text-anchor:middle">h</text>
<text x="125" y="-24" style="fill:gray; text-anchor:middle">w</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Rect(x, y, w, h int, style ...string)</text>
</g>
<g id="crect">
<circle cx="125" cy="62" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="38" style="fill:gray; text-anchor:middle">x, y</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="-24" y="62" style="fill:gray; text-anchor:middle">h</text>
<text x="125" y="-24" style="fill:gray; text-anchor:middle">w</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">CenterRect(x, y, w, h int, style ...string)</text>
</g>
<g id="roundrect">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<rect x="0" y="0" width="250" height="125" rx="25" ry="25" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="-24" y="62" style="fill:gray; text-anchor:middle">h</text>
<text x="125" y="-24" style="fill:gray; text-anchor:middle">w</text>
<line x1="25" y1="0" x2="25" y2="25" style="stroke:black; stroke-width:1"/>
<line x1="0" y1="25" x2="25" y2="25" style="stroke:black; stroke-width:1"/>
<text x="49" y="13" style="fill:gray; text-anchor:middle">ry</text>
<text x="12" y="49" style="fill:gray; text-anchor:middle">rx</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Roundrect(x, y, w, h, rx, ry int, style ...string)</text>
</g>
<g id="polygon">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="125" cy="-31" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="-55" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="250" cy="93" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="69" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="125" cy="62" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="38" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="0" cy="93" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="69" style="fill:gray; text-anchor:middle">x, y</text>
<polygon points="0,0 125,-31 250,0 250,93 125,62 0,93" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Polygon(x, y []int, style ...string)</text>
</g>
<g id="circle">
<g transform="translate(125,62)">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="0" cy="0" r="62" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<line x1="0" y1="0" x2="62" y2="0" style="stroke:black; stroke-width:1"/>
<text x="31" y="24" style="fill:gray; text-anchor:middle">r</text>
<text x="0" y="86" style="fill:black; text-anchor:middle;font-size:24px">Circle(x, y, r int, style ...string)</text>
</g>
</g>
<g id="ellipse">
<g transform="translate(125,62)">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<ellipse cx="0" cy="0" rx="125" ry="62" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<line x1="0" y1="0" x2="125" y2="0" style="stroke:black; stroke-width:1"/>
<line x1="0" y1="0" x2="0" y2="62" style="stroke:black; stroke-width:1"/>
<text x="62" y="24" style="fill:gray; text-anchor:middle">rx</text>
<text x="-24" y="31" style="fill:gray; text-anchor:middle">ry</text>
<text x="0" y="86" style="fill:black; text-anchor:middle;font-size:24px">Ellipse(x, y, rx, ry int, style ...string)</text>
</g>
</g>
<g id="line">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x1, y1</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">x2, y2</text>
<line x1="0" y1="0" x2="250" y2="0" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Line(x1, y1, x2, y2 int, style ...string)</text>
</g>
<g id="polyline">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="83" cy="-62" r="4" style="fill:rgb(220,220,220)"/>
<text x="83" y="-86" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="187" cy="-41" r="4" style="fill:rgb(220,220,220)"/>
<text x="187" y="-65" style="fill:gray; text-anchor:middle">x, y</text>
<circle cx="250" cy="-125" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-149" style="fill:gray; text-anchor:middle">x, y</text>
<polyline points="0,0 83,-62 187,-41 250,-125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Polyline(x, y []int, style ...string)</text>
</g>
<g id="arc">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">sx, sy</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">ex, ey</text>
<path d="M0,0 A62,62 0 0 1 250,0" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="148" style="fill:black; text-anchor:middle;font-size:24px">Arc(sx, sy, ax, ay, r int, lflag, sflag bool, ex, ey int, style ...string)</text>
</g>
<g id="path">
<path d="M36,5l12,41l12-41h33v4l-13,21c30,10,2,69-21,28l7-2c15,27,33,-22,3,-19v-4l12-20h-15l-17,59h-1l-13-42l-12,42h-1l-20-67h9l12,41l8-28l-4-13h9" fill="rgb(0,0,127)" />
<path d="M94,53c15,32,30,14,35,7l-1-7c-16,26-32,3-34,0M122,16c-10-21-34,0-21,30c-5-30 16,-38 23,-21l5-10l-2-9" style="fill-opacity:0.50; fill:rgb(0,0,0)"/>
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<text x="62" y="160" style="fill:black; text-anchor:middle;font-size:24px">Path(s string, style ...string)</text>
</g>
<g id="qbez">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">sx, sy</text>
<circle cx="41" cy="-93" r="4" style="fill:rgb(220,220,220)"/>
<text x="41" y="-117" style="fill:gray; text-anchor:middle">cx, cy</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">ex, ey</text>
<path d="M0,0 Q41,-93 250,0" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Qbez(sx, sy, cx, cy, ex, ey int, style ...string)</text>
</g>
<g id="bezier">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">sx, sy</text>
<circle cx="125" cy="-62" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="-86" style="fill:gray; text-anchor:middle">cx, cy</text>
<circle cx="125" cy="62" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="38" style="fill:gray; text-anchor:middle">px, py</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">ex, ey</text>
<path d="M0,0 C125,-62 125,62 250,0" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Bezier(sx, sy, cx, cy, px, py, ex, ey int, style ...string)</text>
</g>
<g id="image">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<rect x="0" y="0" width="128" height="128" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<text x="-24" y="64" style="fill:gray; text-anchor:middle">h</text>
<text x="64" y="-24" style="fill:gray; text-anchor:middle">w</text>
<image x="0" y="0" width="128" height="128" xlink:href="gophercolor128x128.png" />
<text x="64" y="152" style="fill:black; text-anchor:middle;font-size:24px">Image(x, y, w, h, int path string, style ...string)</text>
</g>
<g id="lgrad">
<rect x="0" y="0" width="250" height="125" style="fill:url(#linear)"/>
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x1%, y1%</text>
<circle cx="250" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="250" y="-24" style="fill:gray; text-anchor:middle">x2%, y2%</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">LinearGradient(s string, x1, y1, x2, y2 uint8, oc []Offcolor)</text>
</g>
<g id="rgrad">
<rect x="0" y="0" width="250" height="125" style="fill:url(#radial)"/>
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">cx%, cy%</text>
<circle cx="125" cy="62" r="4" style="fill:rgb(220,220,220)"/>
<text x="125" y="38" style="fill:gray; text-anchor:middle">fx%, fy%</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">RadialGradient(s string, cx, cy, r, fx, fy uint8, oc []Offcolor)</text>
</g>
<g id="trans">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<circle cx="167" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="167" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Translate(x, y int)</text>
<rect x="0" y="0" width="83" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="translate(167,0)">
<rect x="0" y="0" width="83" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
</g>
<g id="grid">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">x, y</text>
<text x="-24" y="62" style="fill:gray; text-anchor:middle">h</text>
<text x="125" y="-24" style="fill:gray; text-anchor:middle">w</text>
<text x="55" y="15" style="fill:gray; text-anchor:middle">n</text>
<g style="stroke:rgb(0,0,127)">
<line x1="0" y1="0" x2="0" y2="125" />
<line x1="31" y1="0" x2="31" y2="125" />
<line x1="62" y1="0" x2="62" y2="125" />
<line x1="93" y1="0" x2="93" y2="125" />
<line x1="124" y1="0" x2="124" y2="125" />
<line x1="155" y1="0" x2="155" y2="125" />
<line x1="186" y1="0" x2="186" y2="125" />
<line x1="217" y1="0" x2="217" y2="125" />
<line x1="248" y1="0" x2="248" y2="125" />
<line x1="0" y1="0" x2="250" y2="0" />
<line x1="0" y1="31" x2="250" y2="31" />
<line x1="0" y1="62" x2="250" y2="62" />
<line x1="0" y1="93" x2="250" y2="93" />
<line x1="0" y1="124" x2="250" y2="124" />
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Grid(x, y, w, h, n int, style ...string)</text>
</g>
<g id="text">
<circle cx="0" cy="62" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="86" style="fill:gray; text-anchor:middle">x, y</text>
<text x="0" y="62" style="text-anchor:start;font-size:32pt">hello, this is SVG</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Text(x, y int, s string, style ...string)</text>
</g>
<g id="scale">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="scale(0.5)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Scale(n float64)</text>
</g>
<g id="scalexy">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="scale(0.5,0.75)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">ScaleXY(x, y float64)</text>
</g>
<g id="skewx">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="skewX(30)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">SkewX(a float64)</text>
</g>
<g id="skewy">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="skewY(10)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">SkewY(a float64)</text>
</g>
<g id="skewxy">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<g transform="skewX(10) skewY(10)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">SkewXY(x, y float64)</text>
</g>
<g id="rotate">
<circle cx="0" cy="0" r="4" style="fill:rgb(220,220,220)"/>
<text x="0" y="-24" style="fill:gray; text-anchor:middle">0, 0</text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Rotate(r float64)</text>
<rect x="0" y="0" width="250" height="125" style="fill:none; stroke-width:2;stroke:rgb(0,0,127)"/>
<path d="M125,0 Q135,31 108,62" style="fill:none;stroke:gray"/>
<text x="62" y="24" style="fill:gray; text-anchor:middle">r</text>
<g transform="rotate(30)">
<rect x="0" y="0" width="250" height="125" style="fill-opacity:0.25;fill:rgb(0,0,127)"/>
</g>
</g>
<g id="textpath">
<text fill="rgb(0,0,127)" text-anchor="start" font-size="16pt" ><textPath xlink:href="#tpath">It&#39;s &#34;fine&#34; &amp; &#34;dandy&#34; to draw text along a path</textPath></text>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">Textpath(s, pathid string, style ...string)</text>
</g>
<g id="meta">
<g style="font-size:24px;fill:black;text-anchor:start">
<text x="0" y="24" >New(w io Writer)</text>
<text x="0" y="52" >Start(w, h int, options ...string)/End()</text>
<text x="0" y="80" >Startview(w, h, minx, miny, vw, vh int)</text>
<text x="0" y="108" >Group(s ...string)/Gend()</text>
<text x="0" y="136" >Gstyle(s string)/Gend()</text>
<text x="0" y="164" >Gtransform(s string)/Gend()</text>
<text x="0" y="192" >Gid(id string)/Gend()</text>
<text x="0" y="220" >ClipPath(s ...string)/ClipEnd()</text>
<text x="0" y="248" >Def()/DefEnd()</text>
<text x="0" y="276" >Marker()/MarkerEnd()</text>
<text x="0" y="304" >Pattern()/PatternEnd()</text>
<text x="0" y="332" >Desc(s string)</text>
<text x="0" y="360" >Title(s string)</text>
<text x="0" y="388" >Script(type, data ...string)</text>
<text x="0" y="416" >Mask(id string, x,y,w,h int, style ...string)/MaskEnd()</text>
<text x="0" y="444" >Link(href string, title string)/LinkEnd()</text>
<text x="0" y="472" >Use(x int, y int, link string, style ...string)</text>
</g>
<g style="font-size:24px;fill:rgb(127,127,127);text-anchor:start">
<text x="650" y="24" >specify destination</text>
<text x="650" y="52" >begin/end the document</text>
<text x="650" y="80" >begin/end the document with viewport</text>
<text x="650" y="108" >begin/end group with attributes</text>
<text x="650" y="136" >begin/end group style</text>
<text x="650" y="164" >begin/end group transform</text>
<text x="650" y="192" >begin/end group id</text>
<text x="650" y="220" >begin/end clip path</text>
<text x="650" y="248" >begin/end a defintion block</text>
<text x="650" y="276" >begin/end markers</text>
<text x="650" y="304" >begin/end pattern</text>
<text x="650" y="332" >set the description element</text>
<text x="650" y="360" >set the title element</text>
<text x="650" y="388" >define a script</text>
<text x="650" y="416" >begin/end mask element</text>
<text x="650" y="444" >begin/end link to href, with a title</text>
<text x="650" y="472" >use defined objects</text>
</g>
<text x="500" y="534" style="fill:black; text-anchor:middle;font-size:24px">Textlines(x, y int, s []string, size, spacing int, fill, align string)</text>
</g>
<g id="rgb">
<g style="fill:gray; text-anchor:middle">
<circle cx="62" cy="0" r="15" style="fill:rgb(44,0,0)"/>
<circle cx="125" cy="0" r="15" style="fill:rgb(0,77,0)"/>
<circle cx="187" cy="0" r="15" style="fill:rgb(0,0,232)"/>
<circle cx="250" cy="0" r="15" style="fill:rgb(44,77,232)"/>
<text x="62" y="62" >r</text>
<text x="125" y="62" >g</text>
<text x="187" y="62" >b</text>
<text x="219" y="8" >-&gt;</text>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">RGB(r, g, b int)</text>
</g>
<g id="rgba">
<g style="fill:gray; text-anchor:middle">
<circle cx="62" cy="0" r="15" style="fill:rgb(44,0,0)"/>
<circle cx="125" cy="0" r="15" style="fill:rgb(0,77,0)"/>
<circle cx="187" cy="0" r="15" style="fill:rgb(0,0,232)"/>
<circle cx="250" cy="0" r="15" style="fill-opacity:0.33; fill:rgb(44,77,232)"/>
<circle cx="260" cy="0" r="15" style="fill-opacity:0.33; fill:rgb(44,77,232)"/>
<text x="250" y="62" >alpha</text>
<text x="62" y="62" >r</text>
<text x="125" y="62" >g</text>
<text x="187" y="62" >b</text>
<text x="219" y="8" >-&gt;</text>
</g>
<text x="125" y="149" style="fill:black; text-anchor:middle;font-size:24px">RGBA(r, g, b int, opacity float64)</text>
</g>
</defs>
<title>SVG Go Library Description</title>
<rect x="0" y="0" width="4500" height="3375" style="fill:white;stroke:black;stroke-width:2"/>
<g style="font-family:Calibri,sans; text-anchor:middle; font-size:24px">
<a xlink:href="http://github.com/ajstarks/svgo" xlink:title="SVGo Library">
<text x="2250" y="150" style="font-size:125px">SVG Go Library</text>
<text x="2250" y="200" style="font-size:50px;fill:gray">github.com/ajstarks/svgo</text>
</a>
<desc>Object Usage</desc>
<g transform="translate(400,400)">
<use x="0" y="0" xlink:href="#rect" />
<use x="700" y="0" xlink:href="#crect" />
<use x="1400" y="0" xlink:href="#roundrect" />
<use x="2100" y="0" xlink:href="#square" />
<use x="2800" y="0" xlink:href="#line" />
<use x="3500" y="0" xlink:href="#polyline" />
</g>
<g transform="translate(400,1000)">
<use x="0" y="0" xlink:href="#polygon" />
<use x="700" y="0" xlink:href="#circle" />
<use x="1400" y="0" xlink:href="#ellipse" />
<use x="2100" y="0" xlink:href="#arc" />
<use x="2800" y="0" xlink:href="#qbez" />
<use x="3500" y="0" xlink:href="#bezier" />
</g>
<g transform="translate(400,1600)">
<use x="0" y="0" xlink:href="#trans" />
<use x="700" y="0" xlink:href="#scale" />
<use x="1400" y="0" xlink:href="#scalexy" />
<use x="2100" y="0" xlink:href="#skewx" />
<use x="2800" y="0" xlink:href="#skewy" />
<use x="3500" y="0" xlink:href="#skewxy" />
</g>
<g transform="translate(400,2200)">
<use x="0" y="0" xlink:href="#rotate" />
<use x="700" y="0" xlink:href="#text" />
<use x="1400" y="0" xlink:href="#textpath" />
<use x="2100" y="0" xlink:href="#path" />
<use x="2800" y="0" xlink:href="#image" />
<use x="3500" y="0" xlink:href="#grid" />
</g>
<g transform="translate(400,2800)">
<use x="0" y="0" xlink:href="#lgrad" />
<use x="700" y="0" xlink:href="#rgrad" />
<use x="1400" y="0" xlink:href="#rgb" />
<use x="2100" y="0" xlink:href="#rgba" />
<use x="2800" y="0" xlink:href="#meta" />
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,4 +0,0 @@
.vscode/
*.test
embed
.DS_Store

View File

@@ -1,9 +0,0 @@
language: go
go:
- 1.7.x
- 1.8.x
- 1.x
- master
script:
- go test ./...

View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,152 +0,0 @@
[![Build Status](https://travis-ci.org/campoy/embedmd.svg)](https://travis-ci.org/campoy/embedmd) [![Go Report Card](https://goreportcard.com/badge/github.com/campoy/embedmd)](https://goreportcard.com/report/github.com/campoy/embedmd)
# embedmd
Are you tired of copy pasting your code into your `README.md` file, just to
forget about it later on and have unsynced copies? Or even worse, code
that does not even compile?
Then `embedmd` is for you!
`embedmd` embeds files or fractions of files into Markdown files. It does
so by searching `embedmd` commands, which are a subset of the Markdown
syntax for comments. This means they are invisible when Markdown is
rendered, so they can be kept in the file as pointers to the origin of
the embedded text.
The command receives a list of Markdown files. If no list is given, the command
reads from the standard input.
The format of an `embedmd` command is:
```Markdown
[embedmd]:# (pathOrURL language /start regexp/ /end regexp/)
```
The embedded code will be extracted from the file at `pathOrURL`,
which can either be a relative path to a file in the local file
system (using always forward slashes as directory separator) or
a URL starting with `http://` or `https://`.
If the `pathOrURL` is a URL the tool will fetch the content in that URL.
The embedded content starts at the first line that matches `/start regexp/`
and finishes at the first line matching `/end regexp/`.
Omitting the the second regular expression will embed only the piece of text
that matches `/regexp/`:
```Markdown
[embedmd]:# (pathOrURL language /regexp/)
```
To embed the whole line matching a regular expression you can use:
```Markdown
[embedmd]:# (pathOrURL language /.*regexp.*/)
```
To embed from a point to the end you should use:
```Markdown
[embedmd]:# (pathOrURL language /start regexp/ $)
```
To embed a whole file, omit both regular expressions:
```Markdown
[embedmd]:# (pathOrURL language)
```
You can omit the language in any of the previous commands, and the extension
of the file will be used for the snippet syntax highlighting.
This works when the file extensions matches the name of the language (like Go
files, since `.go` matches `go`). However, this will fail with other files like
`.md` whose language name is `markdown`.
```Markdown
[embedmd]:# (file.ext)
```
## Installation
> You can install Go by following [these instructions](https://golang.org/doc/install).
`embedmd` is written in Go, so if you have Go installed you can install it with
`go get`:
```
go get github.com/campoy/embedmd
```
This will download the code, compile it, and leave an `embedmd` binary
in `$GOPATH/bin`.
Eventually, and if there's enough interest, I will provide binaries for
every OS and architecture out there ... _eventually_.
## Usage:
Given the two files in [sample](sample):
*hello.go:*
[embedmd]:# (sample/hello.go)
```go
// Copyright 2016 Google Inc. All rights reserved.
// Use of this source code is governed by the Apache 2.0
// license that can be found in the LICENSE file.
package main
import (
"fmt"
"time"
)
func main() {
fmt.Println("Hello, there, it is", time.Now())
}
```
*docs.md:*
[embedmd]:# (sample/docs.md Markdown /./ /embedmd.*time.*/)
```Markdown
# A hello world in Go
Go is very simple, here you can see a whole "hello, world" program.
[embedmd]:# (hello.go)
We can try to embed a file from a directory.
[embedmd]:# (test/hello.go /func main/ $)
You always start with a `package` statement like:
[embedmd]:# (hello.go /package.*/)
Followed by an `import` statement:
[embedmd]:# (hello.go /import/ /\)/)
You can also see how to get the current time:
[embedmd]:# (hello.go /time\.[^)]*\)/)
```
# Flags
* `-w`: Executing `embedmd -w docs.md` will modify `docs.md`
and add the corresponding code snippets, as shown in
[sample/result.md](sample/result.md).
* `-d`: Executing `embedmd -d docs.md` will display the difference
between the contents of `docs.md` and the output of
`embedmd docs.md`.
### Disclaimer
This is not an official Google product (experimental or otherwise), it is just
code that happens to be owned by Google.

View File

@@ -1,101 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to writing, software distributed
// under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
package embedmd
import (
"errors"
"path/filepath"
"strings"
)
type command struct {
path, lang string
start, end *string
}
func parseCommand(s string) (*command, error) {
s = strings.TrimSpace(s)
if len(s) < 2 || s[0] != '(' || s[len(s)-1] != ')' {
return nil, errors.New("argument list should be in parenthesis")
}
args, err := fields(s[1 : len(s)-1])
if err != nil {
return nil, err
}
if len(args) == 0 {
return nil, errors.New("missing file name")
}
cmd := &command{path: args[0]}
args = args[1:]
if len(args) > 0 && args[0][0] != '/' {
cmd.lang, args = args[0], args[1:]
} else {
ext := filepath.Ext(cmd.path[1:])
if len(ext) == 0 {
return nil, errors.New("language is required when file has no extension")
}
cmd.lang = ext[1:]
}
switch {
case len(args) == 1:
cmd.start = &args[0]
case len(args) == 2:
cmd.start, cmd.end = &args[0], &args[1]
case len(args) > 2:
return nil, errors.New("too many arguments")
}
return cmd, nil
}
// fields returns a list of the groups of text separated by blanks,
// keeping all text surrounded by / as a group.
func fields(s string) ([]string, error) {
var args []string
for s = strings.TrimSpace(s); len(s) > 0; s = strings.TrimSpace(s) {
if s[0] == '/' {
sep := nextSlash(s[1:])
if sep < 0 {
return nil, errors.New("unbalanced /")
}
args, s = append(args, s[:sep+2]), s[sep+2:]
} else {
sep := strings.IndexByte(s[1:], ' ')
if sep < 0 {
return append(args, s), nil
}
args, s = append(args, s[:sep+1]), s[sep+1:]
}
}
return args, nil
}
// nextSlash will find the index of the next unescaped slash in a string.
func nextSlash(s string) int {
for sep := 0; ; sep++ {
i := strings.IndexByte(s[sep:], '/')
if i < 0 {
return -1
}
sep += i
if sep == 0 || s[sep-1] != '\\' {
return sep
}
}
}

View File

@@ -1,51 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to writing, software distributed
// under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
package embedmd
import (
"fmt"
"io/ioutil"
"net/http"
"path/filepath"
"strings"
)
// Fetcher provides an abstraction on a file system.
// The Fetch function is called anytime some content needs to be fetched.
// For now this includes files and URLs.
// The first parameter is the base directory that could be used to resolve
// relative paths. This base directory will be ignored for absolute paths,
// such as URLs.
type Fetcher interface {
Fetch(dir, path string) ([]byte, error)
}
type fetcher struct{}
func (fetcher) Fetch(dir, path string) ([]byte, error) {
if !strings.HasPrefix(path, "http://") && !strings.HasPrefix(path, "https://") {
path = filepath.Join(dir, filepath.FromSlash(path))
return ioutil.ReadFile(path)
}
res, err := http.Get(path)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status %s", res.Status)
}
return ioutil.ReadAll(res.Body)
}

View File

@@ -1,153 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to writing, software distributed
// under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
// Package embedmd provides a single function, Process, that parses markdown
// searching for markdown comments.
//
// The format of an embedmd command is:
//
// [embedmd]:# (pathOrURL language /start regexp/ /end regexp/)
//
// The embedded code will be extracted from the file at pathOrURL,
// which can either be a relative path to a file in the local file
// system (using always forward slashes as directory separator) or
// a url starting with http:// or https://.
// If the pathOrURL is a url the tool will fetch the content in that url.
// The embedded content starts at the first line that matches /start regexp/
// and finishes at the first line matching /end regexp/.
//
// Omitting the the second regular expression will embed only the piece of
// text that matches /regexp/:
//
// [embedmd]:# (pathOrURL language /regexp/)
//
// To embed the whole line matching a regular expression you can use:
//
// [embedmd]:# (pathOrURL language /.*regexp.*\n/)
//
// If you want to embed from a point to the end you should use:
//
// [embedmd]:# (pathOrURL language /start regexp/ $)
//
// Finally you can embed a whole file by omitting both regular expressions:
//
// [embedmd]:# (pathOrURL language)
//
// You can ommit the language in any of the previous commands, and the extension
// of the file will be used for the snippet syntax highlighting. Note that while
// this works Go files, since the file extension .go matches the name of the language
// go, this will fail with other files like .md whose language name is markdown.
//
// [embedmd]:# (file.ext)
//
package embedmd
import (
"fmt"
"io"
"regexp"
)
// Process reads markdown from the given io.Reader searching for an embedmd
// command. When a command is found, it is executed and the output is written
// into the given io.Writer with the rest of standard markdown.
func Process(out io.Writer, in io.Reader, opts ...Option) error {
e := embedder{Fetcher: fetcher{}}
for _, opt := range opts {
opt.f(&e)
}
return process(out, in, e.runCommand)
}
// An Option provides a way to adapt the Process function to your needs.
type Option struct{ f func(*embedder) }
// WithBaseDir indicates that the given path should be used to resolve relative
// paths.
func WithBaseDir(path string) Option {
return Option{func(e *embedder) { e.baseDir = path }}
}
// WithFetcher provides a custom Fetcher to be used whenever a path or url needs
// to be fetched.
func WithFetcher(c Fetcher) Option {
return Option{func(e *embedder) { e.Fetcher = c }}
}
type embedder struct {
Fetcher
baseDir string
}
func (e *embedder) runCommand(w io.Writer, cmd *command) error {
b, err := e.Fetch(e.baseDir, cmd.path)
if err != nil {
return fmt.Errorf("could not read %s: %v", cmd.path, err)
}
b, err = extract(b, cmd.start, cmd.end)
if err != nil {
return fmt.Errorf("could not extract content from %s: %v", cmd.path, err)
}
if len(b) > 0 && b[len(b)-1] != '\n' {
b = append(b, '\n')
}
fmt.Fprintln(w, "```"+cmd.lang)
w.Write(b)
fmt.Fprintln(w, "```")
return nil
}
func extract(b []byte, start, end *string) ([]byte, error) {
if start == nil && end == nil {
return b, nil
}
match := func(s string) ([]int, error) {
if len(s) <= 2 || s[0] != '/' || s[len(s)-1] != '/' {
return nil, fmt.Errorf("missing slashes (/) around %q", s)
}
re, err := regexp.CompilePOSIX(s[1 : len(s)-1])
if err != nil {
return nil, err
}
loc := re.FindIndex(b)
if loc == nil {
return nil, fmt.Errorf("could not match %q", s)
}
return loc, nil
}
if *start != "" {
loc, err := match(*start)
if err != nil {
return nil, err
}
if end == nil {
return b[loc[0]:loc[1]], nil
}
b = b[loc[0]:]
}
if *end != "$" {
loc, err := match(*end)
if err != nil {
return nil, err
}
b = b[:loc[1]]
}
return b, nil
}

View File

@@ -1,117 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to writing, software distributed
// under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
package embedmd
import (
"bufio"
"fmt"
"io"
"strings"
)
type commandRunner func(io.Writer, *command) error
func process(out io.Writer, in io.Reader, run commandRunner) error {
s := &countingScanner{bufio.NewScanner(in), 0}
state := parsingText
var err error
for state != nil {
state, err = state(out, s, run)
if err != nil {
return fmt.Errorf("%d: %v", s.line, err)
}
}
if err := s.Err(); err != nil {
return fmt.Errorf("%d: %v", s.line, err)
}
return nil
}
type countingScanner struct {
*bufio.Scanner
line int
}
func (c *countingScanner) Scan() bool {
b := c.Scanner.Scan()
if b {
c.line++
}
return b
}
type textScanner interface {
Text() string
Scan() bool
}
type state func(io.Writer, textScanner, commandRunner) (state, error)
func parsingText(out io.Writer, s textScanner, run commandRunner) (state, error) {
if !s.Scan() {
return nil, nil // end of file, which is fine.
}
switch line := s.Text(); {
case strings.HasPrefix(line, "[embedmd]:#"):
return parsingCmd, nil
case strings.HasPrefix(line, "```"):
return codeParser{print: true}.parse, nil
default:
fmt.Fprintln(out, s.Text())
return parsingText, nil
}
}
func parsingCmd(out io.Writer, s textScanner, run commandRunner) (state, error) {
line := s.Text()
fmt.Fprintln(out, line)
args := line[strings.Index(line, "#")+1:]
cmd, err := parseCommand(args)
if err != nil {
return nil, err
}
if err := run(out, cmd); err != nil {
return nil, err
}
if !s.Scan() {
return nil, nil // end of file, which is fine.
}
if strings.HasPrefix(s.Text(), "```") {
return codeParser{print: false}.parse, nil
}
fmt.Fprintln(out, s.Text())
return parsingText, nil
}
type codeParser struct{ print bool }
func (c codeParser) parse(out io.Writer, s textScanner, run commandRunner) (state, error) {
if c.print {
fmt.Fprintln(out, s.Text())
}
if !s.Scan() {
return nil, fmt.Errorf("unbalanced code section")
}
if !strings.HasPrefix(s.Text(), "```") {
return c.parse, nil
}
// print the end of the code section if needed and go back to parsing text.
if c.print {
fmt.Fprintln(out, s.Text())
}
return parsingText, nil
}

View File

@@ -1,185 +0,0 @@
// Copyright 2016 Google Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to writing, software distributed
// under the License is distributed on a "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
// embedmd
//
// embedmd embeds files or fractions of files into markdown files.
// It does so by searching embedmd commands, which are a subset of the
// markdown syntax for comments. This means they are invisible when
// markdown is rendered, so they can be kept in the file as pointers
// to the origin of the embedded text.
//
// The command receives a list of markdown files, if none is given it
// reads from the standard input.
//
// embedmd supports two flags:
// -d: will print the difference of the input file with what the output
// would have been if executed.
// -w: rewrites the given files rather than writing the output to the standard
// output.
//
// For more information on the format of the commands, read the documentation
// of the github.com/campoy/embedmd/embedmd package.
package main
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/campoy/embedmd/embedmd"
"github.com/pmezard/go-difflib/difflib"
)
// modified while building by -ldflags.
var version = "unkown"
func usage() {
fmt.Fprintf(os.Stderr, "usage: embedmd [flags] [path ...]\n")
flag.PrintDefaults()
}
func main() {
rewrite := flag.Bool("w", false, "write result to (markdown) file instead of stdout")
doDiff := flag.Bool("d", false, "display diffs instead of rewriting files")
printVersion := flag.Bool("v", false, "display embedmd version")
flag.Usage = usage
flag.Parse()
if *printVersion {
fmt.Println("embedmd version: " + version)
return
}
diff, err := embed(flag.Args(), *rewrite, *doDiff)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if diff && *doDiff {
os.Exit(2)
}
}
var (
stdout io.Writer = os.Stdout
stdin io.Reader = os.Stdin
)
func embed(paths []string, rewrite, doDiff bool) (foundDiff bool, err error) {
if rewrite && doDiff {
return false, fmt.Errorf("error: cannot use -w and -d simultaneously")
}
if len(paths) == 0 {
if rewrite {
return false, fmt.Errorf("error: cannot use -w with standard input")
}
if !doDiff {
return false, embedmd.Process(stdout, stdin)
}
var out, in bytes.Buffer
if err := embedmd.Process(&out, io.TeeReader(stdin, &in)); err != nil {
return false, err
}
d, err := diff(in.String(), out.String())
if err != nil || len(d) == 0 {
return false, err
}
fmt.Fprintf(stdout, "%s", d)
return true, nil
}
for _, path := range paths {
d, err := processFile(path, rewrite, doDiff)
if err != nil {
return false, fmt.Errorf("%s:%v", path, err)
}
foundDiff = foundDiff || d
}
return foundDiff, nil
}
type file interface {
io.ReadCloser
io.WriterAt
Truncate(int64) error
}
// replaced by testing functions.
var openFile = func(name string) (file, error) {
return os.OpenFile(name, os.O_RDWR, 0666)
}
func readFile(path string) ([]byte, error) {
f, err := openFile(path)
if err != nil {
return nil, err
}
defer f.Close()
return ioutil.ReadAll(f)
}
func processFile(path string, rewrite, doDiff bool) (foundDiff bool, err error) {
if filepath.Ext(path) != ".md" {
return false, fmt.Errorf("not a markdown file")
}
f, err := openFile(path)
if err != nil {
return false, err
}
defer f.Close()
buf := new(bytes.Buffer)
if err := embedmd.Process(buf, f, embedmd.WithBaseDir(filepath.Dir(path))); err != nil {
return false, err
}
if doDiff {
f, err := readFile(path)
if err != nil {
return false, fmt.Errorf("could not read %s for diff: %v", path, err)
}
data, err := diff(string(f), buf.String())
if err != nil || len(data) == 0 {
return false, err
}
fmt.Fprintf(stdout, "%s", data)
return true, nil
}
if rewrite {
n, err := f.WriteAt(buf.Bytes(), 0)
if err != nil {
return false, fmt.Errorf("could not write: %v", err)
}
return false, f.Truncate(int64(n))
}
io.Copy(stdout, buf)
return false, nil
}
func diff(a, b string) (string, error) {
return difflib.GetUnifiedDiffString(difflib.UnifiedDiff{
A: difflib.SplitLines(a),
B: difflib.SplitLines(b),
Context: 3,
})
}

View File

@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Beyond
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -1,397 +0,0 @@
// Copyright 2021 dudaodong@gmail.com. All rights reserved.
// Use of this source code is governed by MIT license
// Package mathutil implements some functions for math calculation.
package mathutil
import (
"fmt"
"math"
"strconv"
"strings"
"golang.org/x/exp/constraints"
)
// Exponent calculate x^n.
// Play: https://go.dev/play/p/uF3HGNPk8wr
func Exponent(x, n int64) int64 {
if n == 0 {
return 1
}
t := Exponent(x, n/2)
if n%2 == 1 {
return t * t * x
}
return t * t
}
// Fibonacci calculate fibonacci number before n.
// Play: https://go.dev/play/p/IscseUNMuUc
func Fibonacci(first, second, n int) int {
if n <= 0 {
return 0
}
if n < 3 {
return 1
} else if n == 3 {
return first + second
} else {
return Fibonacci(second, first+second, n-1)
}
}
// Factorial calculate x!.
// Play: https://go.dev/play/p/tt6LdOK67Nx
func Factorial(x uint) uint {
var f uint = 1
for ; x > 1; x-- {
f *= x
}
return f
}
// Percent calculate the percentage of value to total.
// Play: https://go.dev/play/p/s0NdFCtwuyd
func Percent(val, total float64, n int) float64 {
if total == 0 {
return float64(0)
}
tmp := val / total * 100
result := RoundToFloat(tmp, n)
return result
}
// RoundToString round off to n decimal places.
// Play: https://go.dev/play/p/kZwpBRAcllO
func RoundToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Round(float64(x))
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
// RoundToFloat round off to n decimal places.
// Play: https://go.dev/play/p/ghyb528JRJL
func RoundToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Round(float64(x))
return r / tmp
}
// TruncRound round off n decimal places.
// Play: https://go.dev/play/p/aumarSHIGzP
func TruncRound[T constraints.Float | constraints.Integer](x T, n int) T {
floatStr := fmt.Sprintf("%."+strconv.Itoa(n+1)+"f", x)
temp := strings.Split(floatStr, ".")
var newFloat string
if len(temp) < 2 || n >= len(temp[1]) {
newFloat = floatStr
} else {
newFloat = temp[0] + "." + temp[1][:n]
}
result, _ := strconv.ParseFloat(newFloat, 64)
return T(result)
}
// FloorToFloat round down to n decimal places.
// Play: https://go.dev/play/p/vbCBrQHZEED
func FloorToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Floor(float64(x))
return r / tmp
}
// FloorToString round down to n decimal places.
// Play: https://go.dev/play/p/Qk9KPd2IdDb
func FloorToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Floor(float64(x))
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
// CeilToFloat round up to n decimal places.
// Play: https://go.dev/play/p/8hOeSADZPCo
func CeilToFloat[T constraints.Float | constraints.Integer](x T, n int) float64 {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Ceil(float64(x))
return r / tmp
}
// CeilToString round up to n decimal places.
// Play: https://go.dev/play/p/wy5bYEyUKKG
func CeilToString[T constraints.Float | constraints.Integer](x T, n int) string {
tmp := math.Pow(10.0, float64(n))
x *= T(tmp)
r := math.Ceil(float64(x))
result := strconv.FormatFloat(r/tmp, 'f', n, 64)
return result
}
// Max return max value of numbers.
// Play: https://go.dev/play/p/cN8DHI0rTkH
func Max[T constraints.Integer | constraints.Float](numbers ...T) T {
max := numbers[0]
for _, v := range numbers {
if max < v {
max = v
}
}
return max
}
// MaxBy return the maximum value of a slice using the given comparator function.
// Play: https://go.dev/play/p/pbe2MT-7DV2
func MaxBy[T any](slice []T, comparator func(T, T) bool) T {
var max T
if len(slice) == 0 {
return max
}
max = slice[0]
for i := 1; i < len(slice); i++ {
val := slice[i]
if comparator(val, max) {
max = val
}
}
return max
}
// Min return min value of numbers.
// Play: https://go.dev/play/p/21BER_mlGUj
func Min[T constraints.Integer | constraints.Float](numbers ...T) T {
min := numbers[0]
for _, v := range numbers {
if min > v {
min = v
}
}
return min
}
// MinBy return the minimum value of a slice using the given comparator function.
// Play: https://go.dev/play/p/XuJDKrDdglW
func MinBy[T any](slice []T, comparator func(T, T) bool) T {
var min T
if len(slice) == 0 {
return min
}
min = slice[0]
for i := 1; i < len(slice); i++ {
val := slice[i]
if comparator(val, min) {
min = val
}
}
return min
}
// Sum return sum of passed numbers.
// Play: https://go.dev/play/p/1To2ImAMJA7
func Sum[T constraints.Integer | constraints.Float](numbers ...T) T {
var sum T
for _, v := range numbers {
sum += v
}
return sum
}
// Average return average value of numbers.
// Play: https://go.dev/play/p/Vv7LBwER-pz
func Average[T constraints.Integer | constraints.Float](numbers ...T) T {
var sum T
n := T(len(numbers))
for _, v := range numbers {
sum += v
}
return sum / n
}
// Range creates a slice of numbers from start with specified count, element step is 1.
// Play: https://go.dev/play/p/9ke2opxa8ZP
func Range[T constraints.Integer | constraints.Float](start T, count int) []T {
size := count
if count < 0 {
size = -count
}
result := make([]T, size)
for i, j := 0, start; i < size; i, j = i+1, j+1 {
result[i] = j
}
return result
}
// RangeWithStep creates a slice of numbers from start to end with specified step.
// Play: https://go.dev/play/p/akLWz0EqOSM
func RangeWithStep[T constraints.Integer | constraints.Float](start, end, step T) []T {
result := []T{}
if start >= end || step == 0 {
return result
}
for i := start; i < end; i += step {
result = append(result, i)
}
return result
}
// AngleToRadian converts angle value to radian value.
// Play: https://go.dev/play/p/CIvlICqrHql
func AngleToRadian(angle float64) float64 {
radian := angle * (math.Pi / 180)
return radian
}
// RadianToAngle converts radian value to angle value.
// Play: https://go.dev/play/p/dQtmOTUOMgi
func RadianToAngle(radian float64) float64 {
angle := radian * (180 / math.Pi)
return angle
}
// PointDistance get two points distance.
// Play: https://go.dev/play/p/RrG4JIaziM8
func PointDistance(x1, y1, x2, y2 float64) float64 {
a := x1 - x2
b := y1 - y2
c := math.Pow(a, 2) + math.Pow(b, 2)
return math.Sqrt(c)
}
// IsPrime checks if number is prime number.
// Play: https://go.dev/play/p/Rdd8UTHZJ7u
func IsPrime(n int) bool {
if n < 2 {
return false
}
for i := 2; i <= int(math.Sqrt(float64(n))); i++ {
if n%i == 0 {
return false
}
}
return true
}
// GCD return greatest common divisor (GCD) of integers.
// Play: https://go.dev/play/p/CiEceLSoAKB
func GCD[T constraints.Integer](integers ...T) T {
result := integers[0]
for k := range integers {
result = gcd(integers[k], result)
if result == 1 {
return 1
}
}
return result
}
// find greatest common divisor (GCD)
func gcd[T constraints.Integer](a, b T) T {
if b == 0 {
return a
}
return gcd(b, a%b)
}
// LCM return Least Common Multiple (LCM) of integers.
// Play: https://go.dev/play/p/EjcZxfY7G_g
func LCM[T constraints.Integer](integers ...T) T {
result := integers[0]
for k := range integers {
result = lcm(integers[k], result)
}
return result
}
// find Least Common Multiple (LCM) via GCD.
func lcm[T constraints.Integer](a, b T) T {
if a == 0 || b == 0 {
panic("lcm function: provide non zero integers only.")
}
return a * b / gcd(a, b)
}
// Cos returns the cosine of the radian argument.
// Play: https://go.dev/play/p/Sm89LoIfvFq
func Cos(radian float64, precision ...int) float64 {
t := 1.0 / (2.0 * math.Pi)
radian *= t
radian -= 0.25 + math.Floor(radian+0.25)
radian *= 16.0 * (math.Abs(radian) - 0.5)
radian += 0.225 * radian * (math.Abs(radian) - 1.0)
if len(precision) == 1 {
return TruncRound(radian, precision[0])
}
return TruncRound(radian, 3)
}
// Sin returns the sine of the radian argument.
// Play: https://go.dev/play/p/TWMQlMywDsP
func Sin(radian float64, precision ...int) float64 {
return Cos((math.Pi/2)-radian, precision...)
}
// Log returns the logarithm of base n.
// Play: https://go.dev/play/p/_d4bi8oyhat
func Log(n, base float64) float64 {
return math.Log(n) / math.Log(base)
}
// Abs returns the absolute value of x.
// Play: https://go.dev/play/p/fsyBh1Os-1d
func Abs[T constraints.Integer | constraints.Float](x T) T {
if x < 0 {
return (-x)
}
return x
}
// Div returns the result of x divided by y.
// Play: https://go.dev/play/p/WLxDdGXXYat
func Div[T constraints.Float | constraints.Integer](x T, y T) float64 {
return float64(x) / float64(y)
}

View File

@@ -1,21 +0,0 @@
sudo: false
language: go
go_import_path: github.com/dustin/go-humanize
go:
- 1.13.x
- 1.14.x
- 1.15.x
- 1.16.x
- stable
- master
matrix:
allow_failures:
- go: master
fast_finish: true
install:
- # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step).
script:
- diff -u <(echo -n) <(gofmt -d -s .)
- go vet .
- go install -v -race ./...
- go test -v -race ./...

View File

@@ -1,21 +0,0 @@
Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
<http://www.opensource.org/licenses/mit-license.php>

View File

@@ -1,124 +0,0 @@
# Humane Units [![Build Status](https://travis-ci.org/dustin/go-humanize.svg?branch=master)](https://travis-ci.org/dustin/go-humanize) [![GoDoc](https://godoc.org/github.com/dustin/go-humanize?status.svg)](https://godoc.org/github.com/dustin/go-humanize)
Just a few functions for helping humanize times and sizes.
`go get` it as `github.com/dustin/go-humanize`, import it as
`"github.com/dustin/go-humanize"`, use it as `humanize`.
See [godoc](https://pkg.go.dev/github.com/dustin/go-humanize) for
complete documentation.
## Sizes
This lets you take numbers like `82854982` and convert them to useful
strings like, `83 MB` or `79 MiB` (whichever you prefer).
Example:
```go
fmt.Printf("That file is %s.", humanize.Bytes(82854982)) // That file is 83 MB.
```
## Times
This lets you take a `time.Time` and spit it out in relative terms.
For example, `12 seconds ago` or `3 days from now`.
Example:
```go
fmt.Printf("This was touched %s.", humanize.Time(someTimeInstance)) // This was touched 7 hours ago.
```
Thanks to Kyle Lemons for the time implementation from an IRC
conversation one day. It's pretty neat.
## Ordinals
From a [mailing list discussion][odisc] where a user wanted to be able
to label ordinals.
0 -> 0th
1 -> 1st
2 -> 2nd
3 -> 3rd
4 -> 4th
[...]
Example:
```go
fmt.Printf("You're my %s best friend.", humanize.Ordinal(193)) // You are my 193rd best friend.
```
## Commas
Want to shove commas into numbers? Be my guest.
0 -> 0
100 -> 100
1000 -> 1,000
1000000000 -> 1,000,000,000
-100000 -> -100,000
Example:
```go
fmt.Printf("You owe $%s.\n", humanize.Comma(6582491)) // You owe $6,582,491.
```
## Ftoa
Nicer float64 formatter that removes trailing zeros.
```go
fmt.Printf("%f", 2.24) // 2.240000
fmt.Printf("%s", humanize.Ftoa(2.24)) // 2.24
fmt.Printf("%f", 2.0) // 2.000000
fmt.Printf("%s", humanize.Ftoa(2.0)) // 2
```
## SI notation
Format numbers with [SI notation][sinotation].
Example:
```go
humanize.SI(0.00000000223, "M") // 2.23 nM
```
## English-specific functions
The following functions are in the `humanize/english` subpackage.
### Plurals
Simple English pluralization
```go
english.PluralWord(1, "object", "") // object
english.PluralWord(42, "object", "") // objects
english.PluralWord(2, "bus", "") // buses
english.PluralWord(99, "locus", "loci") // loci
english.Plural(1, "object", "") // 1 object
english.Plural(42, "object", "") // 42 objects
english.Plural(2, "bus", "") // 2 buses
english.Plural(99, "locus", "loci") // 99 loci
```
### Word series
Format comma-separated words lists with conjuctions:
```go
english.WordSeries([]string{"foo"}, "and") // foo
english.WordSeries([]string{"foo", "bar"}, "and") // foo and bar
english.WordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar and baz
english.OxfordWordSeries([]string{"foo", "bar", "baz"}, "and") // foo, bar, and baz
```
[odisc]: https://groups.google.com/d/topic/golang-nuts/l8NhI74jl-4/discussion
[sinotation]: http://en.wikipedia.org/wiki/Metric_prefix

View File

@@ -1,31 +0,0 @@
package humanize
import (
"math/big"
)
// order of magnitude (to a max order)
func oomm(n, b *big.Int, maxmag int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
if mag == maxmag && maxmag >= 0 {
break
}
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}
// total order of magnitude
// (same as above, but with no upper limit)
func oom(n, b *big.Int) (float64, int) {
mag := 0
m := &big.Int{}
for n.Cmp(b) >= 0 {
n.DivMod(n, b, m)
mag++
}
return float64(n.Int64()) + (float64(m.Int64()) / float64(b.Int64())), mag
}

View File

@@ -1,189 +0,0 @@
package humanize
import (
"fmt"
"math/big"
"strings"
"unicode"
)
var (
bigIECExp = big.NewInt(1024)
// BigByte is one byte in bit.Ints
BigByte = big.NewInt(1)
// BigKiByte is 1,024 bytes in bit.Ints
BigKiByte = (&big.Int{}).Mul(BigByte, bigIECExp)
// BigMiByte is 1,024 k bytes in bit.Ints
BigMiByte = (&big.Int{}).Mul(BigKiByte, bigIECExp)
// BigGiByte is 1,024 m bytes in bit.Ints
BigGiByte = (&big.Int{}).Mul(BigMiByte, bigIECExp)
// BigTiByte is 1,024 g bytes in bit.Ints
BigTiByte = (&big.Int{}).Mul(BigGiByte, bigIECExp)
// BigPiByte is 1,024 t bytes in bit.Ints
BigPiByte = (&big.Int{}).Mul(BigTiByte, bigIECExp)
// BigEiByte is 1,024 p bytes in bit.Ints
BigEiByte = (&big.Int{}).Mul(BigPiByte, bigIECExp)
// BigZiByte is 1,024 e bytes in bit.Ints
BigZiByte = (&big.Int{}).Mul(BigEiByte, bigIECExp)
// BigYiByte is 1,024 z bytes in bit.Ints
BigYiByte = (&big.Int{}).Mul(BigZiByte, bigIECExp)
// BigRiByte is 1,024 y bytes in bit.Ints
BigRiByte = (&big.Int{}).Mul(BigYiByte, bigIECExp)
// BigQiByte is 1,024 r bytes in bit.Ints
BigQiByte = (&big.Int{}).Mul(BigRiByte, bigIECExp)
)
var (
bigSIExp = big.NewInt(1000)
// BigSIByte is one SI byte in big.Ints
BigSIByte = big.NewInt(1)
// BigKByte is 1,000 SI bytes in big.Ints
BigKByte = (&big.Int{}).Mul(BigSIByte, bigSIExp)
// BigMByte is 1,000 SI k bytes in big.Ints
BigMByte = (&big.Int{}).Mul(BigKByte, bigSIExp)
// BigGByte is 1,000 SI m bytes in big.Ints
BigGByte = (&big.Int{}).Mul(BigMByte, bigSIExp)
// BigTByte is 1,000 SI g bytes in big.Ints
BigTByte = (&big.Int{}).Mul(BigGByte, bigSIExp)
// BigPByte is 1,000 SI t bytes in big.Ints
BigPByte = (&big.Int{}).Mul(BigTByte, bigSIExp)
// BigEByte is 1,000 SI p bytes in big.Ints
BigEByte = (&big.Int{}).Mul(BigPByte, bigSIExp)
// BigZByte is 1,000 SI e bytes in big.Ints
BigZByte = (&big.Int{}).Mul(BigEByte, bigSIExp)
// BigYByte is 1,000 SI z bytes in big.Ints
BigYByte = (&big.Int{}).Mul(BigZByte, bigSIExp)
// BigRByte is 1,000 SI y bytes in big.Ints
BigRByte = (&big.Int{}).Mul(BigYByte, bigSIExp)
// BigQByte is 1,000 SI r bytes in big.Ints
BigQByte = (&big.Int{}).Mul(BigRByte, bigSIExp)
)
var bigBytesSizeTable = map[string]*big.Int{
"b": BigByte,
"kib": BigKiByte,
"kb": BigKByte,
"mib": BigMiByte,
"mb": BigMByte,
"gib": BigGiByte,
"gb": BigGByte,
"tib": BigTiByte,
"tb": BigTByte,
"pib": BigPiByte,
"pb": BigPByte,
"eib": BigEiByte,
"eb": BigEByte,
"zib": BigZiByte,
"zb": BigZByte,
"yib": BigYiByte,
"yb": BigYByte,
"rib": BigRiByte,
"rb": BigRByte,
"qib": BigQiByte,
"qb": BigQByte,
// Without suffix
"": BigByte,
"ki": BigKiByte,
"k": BigKByte,
"mi": BigMiByte,
"m": BigMByte,
"gi": BigGiByte,
"g": BigGByte,
"ti": BigTiByte,
"t": BigTByte,
"pi": BigPiByte,
"p": BigPByte,
"ei": BigEiByte,
"e": BigEByte,
"z": BigZByte,
"zi": BigZiByte,
"y": BigYByte,
"yi": BigYiByte,
"r": BigRByte,
"ri": BigRiByte,
"q": BigQByte,
"qi": BigQiByte,
}
var ten = big.NewInt(10)
func humanateBigBytes(s, base *big.Int, sizes []string) string {
if s.Cmp(ten) < 0 {
return fmt.Sprintf("%d B", s)
}
c := (&big.Int{}).Set(s)
val, mag := oomm(c, base, len(sizes)-1)
suffix := sizes[mag]
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// BigBytes produces a human readable representation of an SI size.
//
// See also: ParseBigBytes.
//
// BigBytes(82854982) -> 83 MB
func BigBytes(s *big.Int) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB", "RB", "QB"}
return humanateBigBytes(s, bigSIExp, sizes)
}
// BigIBytes produces a human readable representation of an IEC size.
//
// See also: ParseBigBytes.
//
// BigIBytes(82854982) -> 79 MiB
func BigIBytes(s *big.Int) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB", "YiB", "RiB", "QiB"}
return humanateBigBytes(s, bigIECExp, sizes)
}
// ParseBigBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See also: BigBytes, BigIBytes.
//
// ParseBigBytes("42 MB") -> 42000000, nil
// ParseBigBytes("42 mib") -> 44040192, nil
func ParseBigBytes(s string) (*big.Int, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
val := &big.Rat{}
_, err := fmt.Sscanf(num, "%f", val)
if err != nil {
return nil, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bigBytesSizeTable[extra]; ok {
mv := (&big.Rat{}).SetInt(m)
val.Mul(val, mv)
rv := &big.Int{}
rv.Div(val.Num(), val.Denom())
return rv, nil
}
return nil, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -1,143 +0,0 @@
package humanize
import (
"fmt"
"math"
"strconv"
"strings"
"unicode"
)
// IEC Sizes.
// kibis of bits
const (
Byte = 1 << (iota * 10)
KiByte
MiByte
GiByte
TiByte
PiByte
EiByte
)
// SI Sizes.
const (
IByte = 1
KByte = IByte * 1000
MByte = KByte * 1000
GByte = MByte * 1000
TByte = GByte * 1000
PByte = TByte * 1000
EByte = PByte * 1000
)
var bytesSizeTable = map[string]uint64{
"b": Byte,
"kib": KiByte,
"kb": KByte,
"mib": MiByte,
"mb": MByte,
"gib": GiByte,
"gb": GByte,
"tib": TiByte,
"tb": TByte,
"pib": PiByte,
"pb": PByte,
"eib": EiByte,
"eb": EByte,
// Without suffix
"": Byte,
"ki": KiByte,
"k": KByte,
"mi": MiByte,
"m": MByte,
"gi": GiByte,
"g": GByte,
"ti": TiByte,
"t": TByte,
"pi": PiByte,
"p": PByte,
"ei": EiByte,
"e": EByte,
}
func logn(n, b float64) float64 {
return math.Log(n) / math.Log(b)
}
func humanateBytes(s uint64, base float64, sizes []string) string {
if s < 10 {
return fmt.Sprintf("%d B", s)
}
e := math.Floor(logn(float64(s), base))
suffix := sizes[int(e)]
val := math.Floor(float64(s)/math.Pow(base, e)*10+0.5) / 10
f := "%.0f %s"
if val < 10 {
f = "%.1f %s"
}
return fmt.Sprintf(f, val, suffix)
}
// Bytes produces a human readable representation of an SI size.
//
// See also: ParseBytes.
//
// Bytes(82854982) -> 83 MB
func Bytes(s uint64) string {
sizes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
return humanateBytes(s, 1000, sizes)
}
// IBytes produces a human readable representation of an IEC size.
//
// See also: ParseBytes.
//
// IBytes(82854982) -> 79 MiB
func IBytes(s uint64) string {
sizes := []string{"B", "KiB", "MiB", "GiB", "TiB", "PiB", "EiB"}
return humanateBytes(s, 1024, sizes)
}
// ParseBytes parses a string representation of bytes into the number
// of bytes it represents.
//
// See Also: Bytes, IBytes.
//
// ParseBytes("42 MB") -> 42000000, nil
// ParseBytes("42 mib") -> 44040192, nil
func ParseBytes(s string) (uint64, error) {
lastDigit := 0
hasComma := false
for _, r := range s {
if !(unicode.IsDigit(r) || r == '.' || r == ',') {
break
}
if r == ',' {
hasComma = true
}
lastDigit++
}
num := s[:lastDigit]
if hasComma {
num = strings.Replace(num, ",", "", -1)
}
f, err := strconv.ParseFloat(num, 64)
if err != nil {
return 0, err
}
extra := strings.ToLower(strings.TrimSpace(s[lastDigit:]))
if m, ok := bytesSizeTable[extra]; ok {
f *= float64(m)
if f >= math.MaxUint64 {
return 0, fmt.Errorf("too large: %v", s)
}
return uint64(f), nil
}
return 0, fmt.Errorf("unhandled size name: %v", extra)
}

View File

@@ -1,116 +0,0 @@
package humanize
import (
"bytes"
"math"
"math/big"
"strconv"
"strings"
)
// Comma produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Comma(834142) -> 834,142
func Comma(v int64) string {
sign := ""
// Min int64 can't be negated to a usable value, so it has to be special cased.
if v == math.MinInt64 {
return "-9,223,372,036,854,775,808"
}
if v < 0 {
sign = "-"
v = 0 - v
}
parts := []string{"", "", "", "", "", "", ""}
j := len(parts) - 1
for v > 999 {
parts[j] = strconv.FormatInt(v%1000, 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
v = v / 1000
j--
}
parts[j] = strconv.Itoa(int(v))
return sign + strings.Join(parts[j:], ",")
}
// Commaf produces a string form of the given number in base 10 with
// commas after every three orders of magnitude.
//
// e.g. Commaf(834142.32) -> 834,142.32
func Commaf(v float64) string {
buf := &bytes.Buffer{}
if v < 0 {
buf.Write([]byte{'-'})
v = 0 - v
}
comma := []byte{','}
parts := strings.Split(strconv.FormatFloat(v, 'f', -1, 64), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}
// CommafWithDigits works like the Commaf but limits the resulting
// string to the given number of decimal places.
//
// e.g. CommafWithDigits(834142.32, 1) -> 834,142.3
func CommafWithDigits(f float64, decimals int) string {
return stripTrailingDigits(Commaf(f), decimals)
}
// BigComma produces a string form of the given big.Int in base 10
// with commas after every three orders of magnitude.
func BigComma(b *big.Int) string {
sign := ""
if b.Sign() < 0 {
sign = "-"
b.Abs(b)
}
athousand := big.NewInt(1000)
c := (&big.Int{}).Set(b)
_, m := oom(c, athousand)
parts := make([]string, m+1)
j := len(parts) - 1
mod := &big.Int{}
for b.Cmp(athousand) >= 0 {
b.DivMod(b, athousand, mod)
parts[j] = strconv.FormatInt(mod.Int64(), 10)
switch len(parts[j]) {
case 2:
parts[j] = "0" + parts[j]
case 1:
parts[j] = "00" + parts[j]
}
j--
}
parts[j] = strconv.Itoa(int(b.Int64()))
return sign + strings.Join(parts[j:], ",")
}

View File

@@ -1,41 +0,0 @@
//go:build go1.6
// +build go1.6
package humanize
import (
"bytes"
"math/big"
"strings"
)
// BigCommaf produces a string form of the given big.Float in base 10
// with commas after every three orders of magnitude.
func BigCommaf(v *big.Float) string {
buf := &bytes.Buffer{}
if v.Sign() < 0 {
buf.Write([]byte{'-'})
v.Abs(v)
}
comma := []byte{','}
parts := strings.Split(v.Text('f', -1), ".")
pos := 0
if len(parts[0])%3 != 0 {
pos += len(parts[0]) % 3
buf.WriteString(parts[0][:pos])
buf.Write(comma)
}
for ; pos < len(parts[0]); pos += 3 {
buf.WriteString(parts[0][pos : pos+3])
buf.Write(comma)
}
buf.Truncate(buf.Len() - 1)
if len(parts) > 1 {
buf.Write([]byte{'.'})
buf.WriteString(parts[1])
}
return buf.String()
}

View File

@@ -1,49 +0,0 @@
package humanize
import (
"strconv"
"strings"
)
func stripTrailingZeros(s string) string {
if !strings.ContainsRune(s, '.') {
return s
}
offset := len(s) - 1
for offset > 0 {
if s[offset] == '.' {
offset--
break
}
if s[offset] != '0' {
break
}
offset--
}
return s[:offset+1]
}
func stripTrailingDigits(s string, digits int) string {
if i := strings.Index(s, "."); i >= 0 {
if digits <= 0 {
return s[:i]
}
i++
if i+digits >= len(s) {
return s
}
return s[:i+digits]
}
return s
}
// Ftoa converts a float to a string with no trailing zeros.
func Ftoa(num float64) string {
return stripTrailingZeros(strconv.FormatFloat(num, 'f', 6, 64))
}
// FtoaWithDigits converts a float to a string but limits the resulting string
// to the given number of decimal places, and no trailing zeros.
func FtoaWithDigits(num float64, digits int) string {
return stripTrailingZeros(stripTrailingDigits(strconv.FormatFloat(num, 'f', 6, 64), digits))
}

View File

@@ -1,8 +0,0 @@
/*
Package humanize converts boring ugly numbers to human-friendly strings and back.
Durations can be turned into strings such as "3 days ago", numbers
representing sizes like 82854982 into useful strings like, "83 MB" or
"79 MiB" (whichever you prefer).
*/
package humanize

View File

@@ -1,192 +0,0 @@
package humanize
/*
Slightly adapted from the source to fit go-humanize.
Author: https://github.com/gorhill
Source: https://gist.github.com/gorhill/5285193
*/
import (
"math"
"strconv"
)
var (
renderFloatPrecisionMultipliers = [...]float64{
1,
10,
100,
1000,
10000,
100000,
1000000,
10000000,
100000000,
1000000000,
}
renderFloatPrecisionRounders = [...]float64{
0.5,
0.05,
0.005,
0.0005,
0.00005,
0.000005,
0.0000005,
0.00000005,
0.000000005,
0.0000000005,
}
)
// FormatFloat produces a formatted number as string based on the following user-specified criteria:
// * thousands separator
// * decimal separator
// * decimal precision
//
// Usage: s := RenderFloat(format, n)
// The format parameter tells how to render the number n.
//
// See examples: http://play.golang.org/p/LXc1Ddm1lJ
//
// Examples of format strings, given n = 12345.6789:
// "#,###.##" => "12,345.67"
// "#,###." => "12,345"
// "#,###" => "12345,678"
// "#\u202F###,##" => "12345,68"
// "#.###,###### => 12.345,678900
// "" (aka default format) => 12,345.67
//
// The highest precision allowed is 9 digits after the decimal symbol.
// There is also a version for integer number, FormatInteger(),
// which is convenient for calls within template.
func FormatFloat(format string, n float64) string {
// Special cases:
// NaN = "NaN"
// +Inf = "+Infinity"
// -Inf = "-Infinity"
if math.IsNaN(n) {
return "NaN"
}
if n > math.MaxFloat64 {
return "Infinity"
}
if n < (0.0 - math.MaxFloat64) {
return "-Infinity"
}
// default format
precision := 2
decimalStr := "."
thousandStr := ","
positiveStr := ""
negativeStr := "-"
if len(format) > 0 {
format := []rune(format)
// If there is an explicit format directive,
// then default values are these:
precision = 9
thousandStr = ""
// collect indices of meaningful formatting directives
formatIndx := []int{}
for i, char := range format {
if char != '#' && char != '0' {
formatIndx = append(formatIndx, i)
}
}
if len(formatIndx) > 0 {
// Directive at index 0:
// Must be a '+'
// Raise an error if not the case
// index: 0123456789
// +0.000,000
// +000,000.0
// +0000.00
// +0000
if formatIndx[0] == 0 {
if format[formatIndx[0]] != '+' {
panic("RenderFloat(): invalid positive sign directive")
}
positiveStr = "+"
formatIndx = formatIndx[1:]
}
// Two directives:
// First is thousands separator
// Raise an error if not followed by 3-digit
// 0123456789
// 0.000,000
// 000,000.00
if len(formatIndx) == 2 {
if (formatIndx[1] - formatIndx[0]) != 4 {
panic("RenderFloat(): thousands separator directive must be followed by 3 digit-specifiers")
}
thousandStr = string(format[formatIndx[0]])
formatIndx = formatIndx[1:]
}
// One directive:
// Directive is decimal separator
// The number of digit-specifier following the separator indicates wanted precision
// 0123456789
// 0.00
// 000,0000
if len(formatIndx) == 1 {
decimalStr = string(format[formatIndx[0]])
precision = len(format) - formatIndx[0] - 1
}
}
}
// generate sign part
var signStr string
if n >= 0.000000001 {
signStr = positiveStr
} else if n <= -0.000000001 {
signStr = negativeStr
n = -n
} else {
signStr = ""
n = 0.0
}
// split number into integer and fractional parts
intf, fracf := math.Modf(n + renderFloatPrecisionRounders[precision])
// generate integer part string
intStr := strconv.FormatInt(int64(intf), 10)
// add thousand separator if required
if len(thousandStr) > 0 {
for i := len(intStr); i > 3; {
i -= 3
intStr = intStr[:i] + thousandStr + intStr[i:]
}
}
// no fractional part, we can leave now
if precision == 0 {
return signStr + intStr
}
// generate fractional part
fracStr := strconv.Itoa(int(fracf * renderFloatPrecisionMultipliers[precision]))
// may need padding
if len(fracStr) < precision {
fracStr = "000000000000000"[:precision-len(fracStr)] + fracStr
}
return signStr + intStr + decimalStr + fracStr
}
// FormatInteger produces a formatted number as string.
// See FormatFloat.
func FormatInteger(format string, n int) string {
return FormatFloat(format, float64(n))
}

View File

@@ -1,25 +0,0 @@
package humanize
import "strconv"
// Ordinal gives you the input number in a rank/ordinal format.
//
// Ordinal(3) -> 3rd
func Ordinal(x int) string {
suffix := "th"
switch x % 10 {
case 1:
if x%100 != 11 {
suffix = "st"
}
case 2:
if x%100 != 12 {
suffix = "nd"
}
case 3:
if x%100 != 13 {
suffix = "rd"
}
}
return strconv.Itoa(x) + suffix
}

View File

@@ -1,127 +0,0 @@
package humanize
import (
"errors"
"math"
"regexp"
"strconv"
)
var siPrefixTable = map[float64]string{
-30: "q", // quecto
-27: "r", // ronto
-24: "y", // yocto
-21: "z", // zepto
-18: "a", // atto
-15: "f", // femto
-12: "p", // pico
-9: "n", // nano
-6: "µ", // micro
-3: "m", // milli
0: "",
3: "k", // kilo
6: "M", // mega
9: "G", // giga
12: "T", // tera
15: "P", // peta
18: "E", // exa
21: "Z", // zetta
24: "Y", // yotta
27: "R", // ronna
30: "Q", // quetta
}
var revSIPrefixTable = revfmap(siPrefixTable)
// revfmap reverses the map and precomputes the power multiplier
func revfmap(in map[float64]string) map[string]float64 {
rv := map[string]float64{}
for k, v := range in {
rv[v] = math.Pow(10, k)
}
return rv
}
var riParseRegex *regexp.Regexp
func init() {
ri := `^([\-0-9.]+)\s?([`
for _, v := range siPrefixTable {
ri += v
}
ri += `]?)(.*)`
riParseRegex = regexp.MustCompile(ri)
}
// ComputeSI finds the most appropriate SI prefix for the given number
// and returns the prefix along with the value adjusted to be within
// that prefix.
//
// See also: SI, ParseSI.
//
// e.g. ComputeSI(2.2345e-12) -> (2.2345, "p")
func ComputeSI(input float64) (float64, string) {
if input == 0 {
return 0, ""
}
mag := math.Abs(input)
exponent := math.Floor(logn(mag, 10))
exponent = math.Floor(exponent/3) * 3
value := mag / math.Pow(10, exponent)
// Handle special case where value is exactly 1000.0
// Should return 1 M instead of 1000 k
if value == 1000.0 {
exponent += 3
value = mag / math.Pow(10, exponent)
}
value = math.Copysign(value, input)
prefix := siPrefixTable[exponent]
return value, prefix
}
// SI returns a string with default formatting.
//
// SI uses Ftoa to format float value, removing trailing zeros.
//
// See also: ComputeSI, ParseSI.
//
// e.g. SI(1000000, "B") -> 1 MB
// e.g. SI(2.2345e-12, "F") -> 2.2345 pF
func SI(input float64, unit string) string {
value, prefix := ComputeSI(input)
return Ftoa(value) + " " + prefix + unit
}
// SIWithDigits works like SI but limits the resulting string to the
// given number of decimal places.
//
// e.g. SIWithDigits(1000000, 0, "B") -> 1 MB
// e.g. SIWithDigits(2.2345e-12, 2, "F") -> 2.23 pF
func SIWithDigits(input float64, decimals int, unit string) string {
value, prefix := ComputeSI(input)
return FtoaWithDigits(value, decimals) + " " + prefix + unit
}
var errInvalid = errors.New("invalid input")
// ParseSI parses an SI string back into the number and unit.
//
// See also: SI, ComputeSI.
//
// e.g. ParseSI("2.2345 pF") -> (2.2345e-12, "F", nil)
func ParseSI(input string) (float64, string, error) {
found := riParseRegex.FindStringSubmatch(input)
if len(found) != 4 {
return 0, "", errInvalid
}
mag := revSIPrefixTable[found[2]]
unit := found[3]
base, err := strconv.ParseFloat(found[1], 64)
return base * mag, unit, err
}

View File

@@ -1,117 +0,0 @@
package humanize
import (
"fmt"
"math"
"sort"
"time"
)
// Seconds-based time units
const (
Day = 24 * time.Hour
Week = 7 * Day
Month = 30 * Day
Year = 12 * Month
LongTime = 37 * Year
)
// Time formats a time into a relative string.
//
// Time(someT) -> "3 weeks ago"
func Time(then time.Time) string {
return RelTime(then, time.Now(), "ago", "from now")
}
// A RelTimeMagnitude struct contains a relative time point at which
// the relative format of time will switch to a new format string. A
// slice of these in ascending order by their "D" field is passed to
// CustomRelTime to format durations.
//
// The Format field is a string that may contain a "%s" which will be
// replaced with the appropriate signed label (e.g. "ago" or "from
// now") and a "%d" that will be replaced by the quantity.
//
// The DivBy field is the amount of time the time difference must be
// divided by in order to display correctly.
//
// e.g. if D is 2*time.Minute and you want to display "%d minutes %s"
// DivBy should be time.Minute so whatever the duration is will be
// expressed in minutes.
type RelTimeMagnitude struct {
D time.Duration
Format string
DivBy time.Duration
}
var defaultMagnitudes = []RelTimeMagnitude{
{time.Second, "now", time.Second},
{2 * time.Second, "1 second %s", 1},
{time.Minute, "%d seconds %s", time.Second},
{2 * time.Minute, "1 minute %s", 1},
{time.Hour, "%d minutes %s", time.Minute},
{2 * time.Hour, "1 hour %s", 1},
{Day, "%d hours %s", time.Hour},
{2 * Day, "1 day %s", 1},
{Week, "%d days %s", Day},
{2 * Week, "1 week %s", 1},
{Month, "%d weeks %s", Week},
{2 * Month, "1 month %s", 1},
{Year, "%d months %s", Month},
{18 * Month, "1 year %s", 1},
{2 * Year, "2 years %s", 1},
{LongTime, "%d years %s", Year},
{math.MaxInt64, "a long while %s", 1},
}
// RelTime formats a time into a relative string.
//
// It takes two times and two labels. In addition to the generic time
// delta string (e.g. 5 minutes), the labels are used applied so that
// the label corresponding to the smaller time is applied.
//
// RelTime(timeInPast, timeInFuture, "earlier", "later") -> "3 weeks earlier"
func RelTime(a, b time.Time, albl, blbl string) string {
return CustomRelTime(a, b, albl, blbl, defaultMagnitudes)
}
// CustomRelTime formats a time into a relative string.
//
// It takes two times two labels and a table of relative time formats.
// In addition to the generic time delta string (e.g. 5 minutes), the
// labels are used applied so that the label corresponding to the
// smaller time is applied.
func CustomRelTime(a, b time.Time, albl, blbl string, magnitudes []RelTimeMagnitude) string {
lbl := albl
diff := b.Sub(a)
if a.After(b) {
lbl = blbl
diff = a.Sub(b)
}
n := sort.Search(len(magnitudes), func(i int) bool {
return magnitudes[i].D > diff
})
if n >= len(magnitudes) {
n = len(magnitudes) - 1
}
mag := magnitudes[n]
args := []interface{}{}
escaped := false
for _, ch := range mag.Format {
if escaped {
switch ch {
case 's':
args = append(args, lbl)
case 'd':
args = append(args, diff/mag.DivBy)
}
escaped = false
} else {
escaped = ch == '%'
}
}
return fmt.Sprintf(mag.Format, args...)
}

View File

@@ -1,13 +0,0 @@
freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-13-2
install_script:
- pkg update -f
- pkg install -y go
test_script:
# run tests as user "cirrus" instead of root
- pw useradd cirrus -m
- chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...

View File

@@ -1,12 +0,0 @@
root = true
[*.go]
indent_style = tab
indent_size = 4
insert_final_newline = true
[*.{yml,yaml}]
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true

View File

@@ -1 +0,0 @@
go.sum linguist-generated

View File

@@ -1,7 +0,0 @@
# go test -c output
*.test
*.test.exe
# Output of go build ./cmd/fsnotify
/fsnotify
/fsnotify.exe

View File

@@ -1,2 +0,0 @@
Chris Howey <howeyc@gmail.com> <chris@howey.me>
Nathan Youngman <git@nathany.com> <4566+nathany@users.noreply.github.com>

View File

@@ -1,541 +0,0 @@
# Changelog
Unreleased
----------
Nothing yet.
1.7.0 - 2023-10-22
------------------
This version of fsnotify needs Go 1.17.
### Additions
- illumos: add FEN backend to support illumos and Solaris. ([#371])
- all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful
in cases where you can't control the kernel buffer and receive a large number
of events in bursts. ([#550], [#572])
- all: add `AddWith()`, which is identical to `Add()` but allows passing
options. ([#521])
- windows: allow setting the ReadDirectoryChangesW() buffer size with
`fsnotify.WithBufferSize()`; the default of 64K is the highest value that
works on all platforms and is enough for most purposes, but in some cases a
highest buffer is needed. ([#521])
### Changes and fixes
- inotify: remove watcher if a watched path is renamed ([#518])
After a rename the reported name wasn't updated, or even an empty string.
Inotify doesn't provide any good facilities to update it, so just remove the
watcher. This is already how it worked on kqueue and FEN.
On Windows this does work, and remains working.
- windows: don't listen for file attribute changes ([#520])
File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API,
with no way to see if they're a file write or attribute change, so would show
up as a fsnotify.Write event. This is never useful, and could result in many
spurious Write events.
- windows: return `ErrEventOverflow` if the buffer is full ([#525])
Before it would merely return "short read", making it hard to detect this
error.
- kqueue: make sure events for all files are delivered properly when removing a
watched directory ([#526])
Previously they would get sent with `""` (empty string) or `"."` as the path
name.
- kqueue: don't emit spurious Create events for symbolic links ([#524])
The link would get resolved but kqueue would "forget" it already saw the link
itself, resulting on a Create for every Write event for the directory.
- all: return `ErrClosed` on `Add()` when the watcher is closed ([#516])
- other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in
`backend_other.go`, making it easier to use on unsupported platforms such as
WASM, AIX, etc. ([#528])
- other: use the `backend_other.go` no-op if the `appengine` build tag is set;
Google AppEngine forbids usage of the unsafe package so the inotify backend
won't compile there.
[#371]: https://github.com/fsnotify/fsnotify/pull/371
[#516]: https://github.com/fsnotify/fsnotify/pull/516
[#518]: https://github.com/fsnotify/fsnotify/pull/518
[#520]: https://github.com/fsnotify/fsnotify/pull/520
[#521]: https://github.com/fsnotify/fsnotify/pull/521
[#524]: https://github.com/fsnotify/fsnotify/pull/524
[#525]: https://github.com/fsnotify/fsnotify/pull/525
[#526]: https://github.com/fsnotify/fsnotify/pull/526
[#528]: https://github.com/fsnotify/fsnotify/pull/528
[#537]: https://github.com/fsnotify/fsnotify/pull/537
[#550]: https://github.com/fsnotify/fsnotify/pull/550
[#572]: https://github.com/fsnotify/fsnotify/pull/572
1.6.0 - 2022-10-13
------------------
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
but not documented). It also increases the minimum Linux version to 2.6.32.
### Additions
- all: add `Event.Has()` and `Op.Has()` ([#477])
This makes checking events a lot easier; for example:
if event.Op&Write == Write && !(event.Op&Remove == Remove) {
}
Becomes:
if event.Has(Write) && !event.Has(Remove) {
}
- all: add cmd/fsnotify ([#463])
A command-line utility for testing and some examples.
### Changes and fixes
- inotify: don't ignore events for files that don't exist ([#260], [#470])
Previously the inotify watcher would call `os.Lstat()` to check if a file
still exists before emitting events.
This was inconsistent with other platforms and resulted in inconsistent event
reporting (e.g. when a file is quickly removed and re-created), and generally
a source of confusion. It was added in 2013 to fix a memory leak that no
longer exists.
- all: return `ErrNonExistentWatch` when `Remove()` is called on a path that's
not watched ([#460])
- inotify: replace epoll() with non-blocking inotify ([#434])
Non-blocking inotify was not generally available at the time this library was
written in 2014, but now it is. As a result, the minimum Linux version is
bumped from 2.6.27 to 2.6.32. This hugely simplifies the code and is faster.
- kqueue: don't check for events every 100ms ([#480])
The watcher would wake up every 100ms, even when there was nothing to do. Now
it waits until there is something to do.
- macos: retry opening files on EINTR ([#475])
- kqueue: skip unreadable files ([#479])
kqueue requires a file descriptor for every file in a directory; this would
fail if a file was unreadable by the current user. Now these files are simply
skipped.
- windows: fix renaming a watched directory if the parent is also watched ([#370])
- windows: increase buffer size from 4K to 64K ([#485])
- windows: close file handle on Remove() ([#288])
- kqueue: put pathname in the error if watching a file fails ([#471])
- inotify, windows: calling Close() more than once could race ([#465])
- kqueue: improve Close() performance ([#233])
- all: various documentation additions and clarifications.
[#233]: https://github.com/fsnotify/fsnotify/pull/233
[#260]: https://github.com/fsnotify/fsnotify/pull/260
[#288]: https://github.com/fsnotify/fsnotify/pull/288
[#370]: https://github.com/fsnotify/fsnotify/pull/370
[#434]: https://github.com/fsnotify/fsnotify/pull/434
[#460]: https://github.com/fsnotify/fsnotify/pull/460
[#463]: https://github.com/fsnotify/fsnotify/pull/463
[#465]: https://github.com/fsnotify/fsnotify/pull/465
[#470]: https://github.com/fsnotify/fsnotify/pull/470
[#471]: https://github.com/fsnotify/fsnotify/pull/471
[#475]: https://github.com/fsnotify/fsnotify/pull/475
[#477]: https://github.com/fsnotify/fsnotify/pull/477
[#479]: https://github.com/fsnotify/fsnotify/pull/479
[#480]: https://github.com/fsnotify/fsnotify/pull/480
[#485]: https://github.com/fsnotify/fsnotify/pull/485
## [1.5.4] - 2022-04-25
* Windows: add missing defer to `Watcher.WatchList` [#447](https://github.com/fsnotify/fsnotify/pull/447)
* go.mod: use latest x/sys [#444](https://github.com/fsnotify/fsnotify/pull/444)
* Fix compilation for OpenBSD [#443](https://github.com/fsnotify/fsnotify/pull/443)
## [1.5.3] - 2022-04-22
* This version is retracted. An incorrect branch is published accidentally [#445](https://github.com/fsnotify/fsnotify/issues/445)
## [1.5.2] - 2022-04-21
* Add a feature to return the directories and files that are being monitored [#374](https://github.com/fsnotify/fsnotify/pull/374)
* Fix potential crash on windows if `raw.FileNameLength` exceeds `syscall.MAX_PATH` [#361](https://github.com/fsnotify/fsnotify/pull/361)
* Allow build on unsupported GOOS [#424](https://github.com/fsnotify/fsnotify/pull/424)
* Don't set `poller.fd` twice in `newFdPoller` [#406](https://github.com/fsnotify/fsnotify/pull/406)
* fix go vet warnings: call to `(*T).Fatalf` from a non-test goroutine [#416](https://github.com/fsnotify/fsnotify/pull/416)
## [1.5.1] - 2021-08-24
* Revert Add AddRaw to not follow symlinks [#394](https://github.com/fsnotify/fsnotify/pull/394)
## [1.5.0] - 2021-08-20
* Go: Increase minimum required version to Go 1.12 [#381](https://github.com/fsnotify/fsnotify/pull/381)
* Feature: Add AddRaw method which does not follow symlinks when adding a watch [#289](https://github.com/fsnotify/fsnotify/pull/298)
* Windows: Follow symlinks by default like on all other systems [#289](https://github.com/fsnotify/fsnotify/pull/289)
* CI: Use GitHub Actions for CI and cover go 1.12-1.17
[#378](https://github.com/fsnotify/fsnotify/pull/378)
[#381](https://github.com/fsnotify/fsnotify/pull/381)
[#385](https://github.com/fsnotify/fsnotify/pull/385)
* Go 1.14+: Fix unsafe pointer conversion [#325](https://github.com/fsnotify/fsnotify/pull/325)
## [1.4.9] - 2020-03-11
* Move example usage to the readme #329. This may resolve #328.
## [1.4.8] - 2020-03-10
* CI: test more go versions (@nathany 1d13583d846ea9d66dcabbfefbfb9d8e6fb05216)
* Tests: Queued inotify events could have been read by the test before max_queued_events was hit (@matthias-stone #265)
* Tests: t.Fatalf -> t.Errorf in go routines (@gdey #266)
* CI: Less verbosity (@nathany #267)
* Tests: Darwin: Exchangedata is deprecated on 10.13 (@nathany #267)
* Tests: Check if channels are closed in the example (@alexeykazakov #244)
* CI: Only run golint on latest version of go and fix issues (@cpuguy83 #284)
* CI: Add windows to travis matrix (@cpuguy83 #284)
* Docs: Remover appveyor badge (@nathany 11844c0959f6fff69ba325d097fce35bd85a8e93)
* Linux: create epoll and pipe fds with close-on-exec (@JohannesEbke #219)
* Linux: open files with close-on-exec (@linxiulei #273)
* Docs: Plan to support fanotify (@nathany ab058b44498e8b7566a799372a39d150d9ea0119 )
* Project: Add go.mod (@nathany #309)
* Project: Revise editor config (@nathany #309)
* Project: Update copyright for 2019 (@nathany #309)
* CI: Drop go1.8 from CI matrix (@nathany #309)
* Docs: Updating the FAQ section for supportability with NFS & FUSE filesystems (@Pratik32 4bf2d1fec78374803a39307bfb8d340688f4f28e )
## [1.4.7] - 2018-01-09
* BSD/macOS: Fix possible deadlock on closing the watcher on kqueue (thanks @nhooyr and @glycerine)
* Tests: Fix missing verb on format string (thanks @rchiossi)
* Linux: Fix deadlock in Remove (thanks @aarondl)
* Linux: Watch.Add improvements (avoid race, fix consistency, reduce garbage) (thanks @twpayne)
* Docs: Moved FAQ into the README (thanks @vahe)
* Linux: Properly handle inotify's IN_Q_OVERFLOW event (thanks @zeldovich)
* Docs: replace references to OS X with macOS
## [1.4.2] - 2016-10-10
* Linux: use InotifyInit1 with IN_CLOEXEC to stop leaking a file descriptor to a child process when using fork/exec [#178](https://github.com/fsnotify/fsnotify/pull/178) (thanks @pattyshack)
## [1.4.1] - 2016-10-04
* Fix flaky inotify stress test on Linux [#177](https://github.com/fsnotify/fsnotify/pull/177) (thanks @pattyshack)
## [1.4.0] - 2016-10-01
* add a String() method to Event.Op [#165](https://github.com/fsnotify/fsnotify/pull/165) (thanks @oozie)
## [1.3.1] - 2016-06-28
* Windows: fix for double backslash when watching the root of a drive [#151](https://github.com/fsnotify/fsnotify/issues/151) (thanks @brunoqc)
## [1.3.0] - 2016-04-19
* Support linux/arm64 by [patching](https://go-review.googlesource.com/#/c/21971/) x/sys/unix and switching to to it from syscall (thanks @suihkulokki) [#135](https://github.com/fsnotify/fsnotify/pull/135)
## [1.2.10] - 2016-03-02
* Fix golint errors in windows.go [#121](https://github.com/fsnotify/fsnotify/pull/121) (thanks @tiffanyfj)
## [1.2.9] - 2016-01-13
kqueue: Fix logic for CREATE after REMOVE [#111](https://github.com/fsnotify/fsnotify/pull/111) (thanks @bep)
## [1.2.8] - 2015-12-17
* kqueue: fix race condition in Close [#105](https://github.com/fsnotify/fsnotify/pull/105) (thanks @djui for reporting the issue and @ppknap for writing a failing test)
* inotify: fix race in test
* enable race detection for continuous integration (Linux, Mac, Windows)
## [1.2.5] - 2015-10-17
* inotify: use epoll_create1 for arm64 support (requires Linux 2.6.27 or later) [#100](https://github.com/fsnotify/fsnotify/pull/100) (thanks @suihkulokki)
* inotify: fix path leaks [#73](https://github.com/fsnotify/fsnotify/pull/73) (thanks @chamaken)
* kqueue: watch for rename events on subdirectories [#83](https://github.com/fsnotify/fsnotify/pull/83) (thanks @guotie)
* kqueue: avoid infinite loops from symlinks cycles [#101](https://github.com/fsnotify/fsnotify/pull/101) (thanks @illicitonion)
## [1.2.1] - 2015-10-14
* kqueue: don't watch named pipes [#98](https://github.com/fsnotify/fsnotify/pull/98) (thanks @evanphx)
## [1.2.0] - 2015-02-08
* inotify: use epoll to wake up readEvents [#66](https://github.com/fsnotify/fsnotify/pull/66) (thanks @PieterD)
* inotify: closing watcher should now always shut down goroutine [#63](https://github.com/fsnotify/fsnotify/pull/63) (thanks @PieterD)
* kqueue: close kqueue after removing watches, fixes [#59](https://github.com/fsnotify/fsnotify/issues/59)
## [1.1.1] - 2015-02-05
* inotify: Retry read on EINTR [#61](https://github.com/fsnotify/fsnotify/issues/61) (thanks @PieterD)
## [1.1.0] - 2014-12-12
* kqueue: rework internals [#43](https://github.com/fsnotify/fsnotify/pull/43)
* add low-level functions
* only need to store flags on directories
* less mutexes [#13](https://github.com/fsnotify/fsnotify/issues/13)
* done can be an unbuffered channel
* remove calls to os.NewSyscallError
* More efficient string concatenation for Event.String() [#52](https://github.com/fsnotify/fsnotify/pull/52) (thanks @mdlayher)
* kqueue: fix regression in rework causing subdirectories to be watched [#48](https://github.com/fsnotify/fsnotify/issues/48)
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## [1.0.4] - 2014-09-07
* kqueue: add dragonfly to the build tags.
* Rename source code files, rearrange code so exported APIs are at the top.
* Add done channel to example code. [#37](https://github.com/fsnotify/fsnotify/pull/37) (thanks @chenyukang)
## [1.0.3] - 2014-08-19
* [Fix] Windows MOVED_TO now translates to Create like on BSD and Linux. [#36](https://github.com/fsnotify/fsnotify/issues/36)
## [1.0.2] - 2014-08-17
* [Fix] Missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
* [Fix] Make ./path and path equivalent. (thanks @zhsso)
## [1.0.0] - 2014-08-15
* [API] Remove AddWatch on Windows, use Add.
* Improve documentation for exported identifiers. [#30](https://github.com/fsnotify/fsnotify/issues/30)
* Minor updates based on feedback from golint.
## dev / 2014-07-09
* Moved to [github.com/fsnotify/fsnotify](https://github.com/fsnotify/fsnotify).
* Use os.NewSyscallError instead of returning errno (thanks @hariharan-uno)
## dev / 2014-07-04
* kqueue: fix incorrect mutex used in Close()
* Update example to demonstrate usage of Op.
## dev / 2014-06-28
* [API] Don't set the Write Op for attribute notifications [#4](https://github.com/fsnotify/fsnotify/issues/4)
* Fix for String() method on Event (thanks Alex Brainman)
* Don't build on Plan 9 or Solaris (thanks @4ad)
## dev / 2014-06-21
* Events channel of type Event rather than *Event.
* [internal] use syscall constants directly for inotify and kqueue.
* [internal] kqueue: rename events to kevents and fileEvent to event.
## dev / 2014-06-19
* Go 1.3+ required on Windows (uses syscall.ERROR_MORE_DATA internally).
* [internal] remove cookie from Event struct (unused).
* [internal] Event struct has the same definition across every OS.
* [internal] remove internal watch and removeWatch methods.
## dev / 2014-06-12
* [API] Renamed Watch() to Add() and RemoveWatch() to Remove().
* [API] Pluralized channel names: Events and Errors.
* [API] Renamed FileEvent struct to Event.
* [API] Op constants replace methods like IsCreate().
## dev / 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## dev / 2014-05-23
* [API] Remove current implementation of WatchFlags.
* current implementation doesn't take advantage of OS for efficiency
* provides little benefit over filtering events as they are received, but has extra bookkeeping and mutexes
* no tests for the current implementation
* not fully implemented on Windows [#93](https://github.com/howeyc/fsnotify/issues/93#issuecomment-39285195)
## [0.9.3] - 2014-12-31
* kqueue: cleanup internal watch before sending remove event [#51](https://github.com/fsnotify/fsnotify/issues/51)
## [0.9.2] - 2014-08-17
* [Backport] Fix missing create events on macOS. [#14](https://github.com/fsnotify/fsnotify/issues/14) (thanks @zhsso)
## [0.9.1] - 2014-06-12
* Fix data race on kevent buffer (thanks @tilaks) [#98](https://github.com/howeyc/fsnotify/pull/98)
## [0.9.0] - 2014-01-17
* IsAttrib() for events that only concern a file's metadata [#79][] (thanks @abustany)
* [Fix] kqueue: fix deadlock [#77][] (thanks @cespare)
* [NOTICE] Development has moved to `code.google.com/p/go.exp/fsnotify` in preparation for inclusion in the Go standard library.
## [0.8.12] - 2013-11-13
* [API] Remove FD_SET and friends from Linux adapter
## [0.8.11] - 2013-11-02
* [Doc] Add Changelog [#72][] (thanks @nathany)
* [Doc] Spotlight and double modify events on macOS [#62][] (reported by @paulhammond)
## [0.8.10] - 2013-10-19
* [Fix] kqueue: remove file watches when parent directory is removed [#71][] (reported by @mdwhatcott)
* [Fix] kqueue: race between Close and readEvents [#70][] (reported by @bernerdschaefer)
* [Doc] specify OS-specific limits in README (thanks @debrando)
## [0.8.9] - 2013-09-08
* [Doc] Contributing (thanks @nathany)
* [Doc] update package path in example code [#63][] (thanks @paulhammond)
* [Doc] GoCI badge in README (Linux only) [#60][]
* [Doc] Cross-platform testing with Vagrant [#59][] (thanks @nathany)
## [0.8.8] - 2013-06-17
* [Fix] Windows: handle `ERROR_MORE_DATA` on Windows [#49][] (thanks @jbowtie)
## [0.8.7] - 2013-06-03
* [API] Make syscall flags internal
* [Fix] inotify: ignore event changes
* [Fix] race in symlink test [#45][] (reported by @srid)
* [Fix] tests on Windows
* lower case error messages
## [0.8.6] - 2013-05-23
* kqueue: Use EVT_ONLY flag on Darwin
* [Doc] Update README with full example
## [0.8.5] - 2013-05-09
* [Fix] inotify: allow monitoring of "broken" symlinks (thanks @tsg)
## [0.8.4] - 2013-04-07
* [Fix] kqueue: watch all file events [#40][] (thanks @ChrisBuchholz)
## [0.8.3] - 2013-03-13
* [Fix] inoitfy/kqueue memory leak [#36][] (reported by @nbkolchin)
* [Fix] kqueue: use fsnFlags for watching a directory [#33][] (reported by @nbkolchin)
## [0.8.2] - 2013-02-07
* [Doc] add Authors
* [Fix] fix data races for map access [#29][] (thanks @fsouza)
## [0.8.1] - 2013-01-09
* [Fix] Windows path separators
* [Doc] BSD License
## [0.8.0] - 2012-11-09
* kqueue: directory watching improvements (thanks @vmirage)
* inotify: add `IN_MOVED_TO` [#25][] (requested by @cpisto)
* [Fix] kqueue: deleting watched directory [#24][] (reported by @jakerr)
## [0.7.4] - 2012-10-09
* [Fix] inotify: fixes from https://codereview.appspot.com/5418045/ (ugorji)
* [Fix] kqueue: preserve watch flags when watching for delete [#21][] (reported by @robfig)
* [Fix] kqueue: watch the directory even if it isn't a new watch (thanks @robfig)
* [Fix] kqueue: modify after recreation of file
## [0.7.3] - 2012-09-27
* [Fix] kqueue: watch with an existing folder inside the watched folder (thanks @vmirage)
* [Fix] kqueue: no longer get duplicate CREATE events
## [0.7.2] - 2012-09-01
* kqueue: events for created directories
## [0.7.1] - 2012-07-14
* [Fix] for renaming files
## [0.7.0] - 2012-07-02
* [Feature] FSNotify flags
* [Fix] inotify: Added file name back to event path
## [0.6.0] - 2012-06-06
* kqueue: watch files after directory created (thanks @tmc)
## [0.5.1] - 2012-05-22
* [Fix] inotify: remove all watches before Close()
## [0.5.0] - 2012-05-03
* [API] kqueue: return errors during watch instead of sending over channel
* kqueue: match symlink behavior on Linux
* inotify: add `DELETE_SELF` (requested by @taralx)
* [Fix] kqueue: handle EINTR (reported by @robfig)
* [Doc] Godoc example [#1][] (thanks @davecheney)
## [0.4.0] - 2012-03-30
* Go 1 released: build with go tool
* [Feature] Windows support using winfsnotify
* Windows does not have attribute change notifications
* Roll attribute notifications into IsModify
## [0.3.0] - 2012-02-19
* kqueue: add files when watch directory
## [0.2.0] - 2011-12-30
* update to latest Go weekly code
## [0.1.0] - 2011-10-19
* kqueue: add watch on file creation to match inotify
* kqueue: create file event
* inotify: ignore `IN_IGNORED` events
* event String()
* linux: common FileEvent functions
* initial commit
[#79]: https://github.com/howeyc/fsnotify/pull/79
[#77]: https://github.com/howeyc/fsnotify/pull/77
[#72]: https://github.com/howeyc/fsnotify/issues/72
[#71]: https://github.com/howeyc/fsnotify/issues/71
[#70]: https://github.com/howeyc/fsnotify/issues/70
[#63]: https://github.com/howeyc/fsnotify/issues/63
[#62]: https://github.com/howeyc/fsnotify/issues/62
[#60]: https://github.com/howeyc/fsnotify/issues/60
[#59]: https://github.com/howeyc/fsnotify/issues/59
[#49]: https://github.com/howeyc/fsnotify/issues/49
[#45]: https://github.com/howeyc/fsnotify/issues/45
[#40]: https://github.com/howeyc/fsnotify/issues/40
[#36]: https://github.com/howeyc/fsnotify/issues/36
[#33]: https://github.com/howeyc/fsnotify/issues/33
[#29]: https://github.com/howeyc/fsnotify/issues/29
[#25]: https://github.com/howeyc/fsnotify/issues/25
[#24]: https://github.com/howeyc/fsnotify/issues/24
[#21]: https://github.com/howeyc/fsnotify/issues/21

View File

@@ -1,26 +0,0 @@
Thank you for your interest in contributing to fsnotify! We try to review and
merge PRs in a reasonable timeframe, but please be aware that:
- To avoid "wasted" work, please discus changes on the issue tracker first. You
can just send PRs, but they may end up being rejected for one reason or the
other.
- fsnotify is a cross-platform library, and changes must work reasonably well on
all supported platforms.
- Changes will need to be compatible; old code should still compile, and the
runtime behaviour can't change in ways that are likely to lead to problems for
users.
Testing
-------
Just `go test ./...` runs all the tests; the CI runs this on all supported
platforms. Testing different platforms locally can be done with something like
[goon] or [Vagrant], but this isn't super-easy to set up at the moment.
Use the `-short` flag to make the "stress test" run faster.
[goon]: https://github.com/arp242/goon
[Vagrant]: https://www.vagrantup.com/
[integration_test.go]: /integration_test.go

View File

@@ -1,25 +0,0 @@
Copyright © 2012 The Go Authors. All rights reserved.
Copyright © fsnotify Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice, this
list of conditions and the following disclaimer in the documentation and/or
other materials provided with the distribution.
* Neither the name of Google Inc. nor the names of its contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,184 +0,0 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, BSD, and illumos.
Go 1.17 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify
---
Platform support:
| Backend | OS | Status |
| :-------------------- | :--------- | :------------------------------------------------------------------------ |
| inotify | Linux | Supported |
| kqueue | BSD, macOS | Supported |
| ReadDirectoryChangesW | Windows | Supported |
| FEN | illumos | Supported |
| fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
| FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
| USN Journals | Windows | [Needs support in x/sys/windows][usn] |
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
Linux and illumos should include Android and Solaris, but these are currently
untested.
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
Usage
-----
A basic example:
```go
package main
import (
"log"
"github.com/fsnotify/fsnotify"
)
func main() {
// Create new watcher.
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// Start listening for events.
go func() {
for {
select {
case event, ok := <-watcher.Events:
if !ok {
return
}
log.Println("event:", event)
if event.Has(fsnotify.Write) {
log.Println("modified file:", event.Name)
}
case err, ok := <-watcher.Errors:
if !ok {
return
}
log.Println("error:", err)
}
}
}()
// Add a path.
err = watcher.Add("/tmp")
if err != nil {
log.Fatal(err)
}
// Block main goroutine forever.
<-make(chan struct{})
}
```
Some more examples can be found in [cmd/fsnotify](cmd/fsnotify), which can be
run with:
% go run ./cmd/fsnotify
Further detailed documentation can be found in godoc:
https://pkg.go.dev/github.com/fsnotify/fsnotify
FAQ
---
### Will a file still be watched when it's moved to another directory?
No, not unless you are watching the location it was moved to.
### Are subdirectories watched?
No, you must add watches for any directory you want to watch (a recursive
watcher is on the roadmap: [#18]).
[#18]: https://github.com/fsnotify/fsnotify/issues/18
### Do I have to watch the Error and Event channels in a goroutine?
Yes. You can read both channels in the same goroutine using `select` (you don't
need a separate goroutine for both channels; see the example).
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
fsnotify requires support from underlying OS to work. The current NFS and SMB
protocols does not provide network level support for file notifications, and
neither do the /proc and /sys virtual filesystems.
This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
[#9]: https://github.com/fsnotify/fsnotify/issues/9
### Why do I get many Chmod events?
Some programs may generate a lot of attribute changes; for example Spotlight on
macOS, anti-virus programs, backup applications, and some others are known to do
this. As a rule, it's typically best to ignore Chmod events. They're often not
useful, and tend to cause problems.
Spotlight indexing on macOS can result in multiple events (see [#15]). A
temporary workaround is to add your folder(s) to the *Spotlight Privacy
settings* until we have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15
### Watching a file doesn't work well
Watching individual files (rather than directories) is generally not recommended
as many programs (especially editors) update files atomically: it will write to
a temporary file which is then moved to to destination, overwriting the original
(or some variant thereof). The watcher on the original file is now lost, as that
no longer exists.
The upshot of this is that a power failure or crash won't leave a half-written
file.
Watch the parent directory and use `Event.Name` to filter out files you're not
interested in. There is an example of this in `cmd/fsnotify/file.go`.
Platform-specific notes
-----------------------
### Linux
When a file is removed a REMOVE event won't be emitted until all file
descriptors are closed; it will emit a CHMOD instead:
fp := os.Open("file")
os.Remove("file") // CHMOD
fp.Close() // REMOVE
This is the event that inotify sends, so not much can be changed about this.
The `fs.inotify.max_user_watches` sysctl variable specifies the upper limit for
the number of watches per user, and `fs.inotify.max_user_instances` specifies
the maximum number of inotify instances per user. Every Watcher you create is an
"instance", and every path you add is a "watch".
These are also exposed in `/proc` as `/proc/sys/fs/inotify/max_user_watches` and
`/proc/sys/fs/inotify/max_user_instances`
To increase them you can use `sysctl` or write the value to proc file:
# The default values on Linux 5.18
sysctl fs.inotify.max_user_watches=124983
sysctl fs.inotify.max_user_instances=128
To make the changes persist on reboot edit `/etc/sysctl.conf` or
`/usr/lib/sysctl.d/50-default.conf` (details differ per Linux distro; check your
distro's documentation):
fs.inotify.max_user_watches=124983
fs.inotify.max_user_instances=128
Reaching the limit will result in a "no space left on device" or "too many open
files" error.
### kqueue (macOS, all BSD systems)
kqueue requires opening a file descriptor for every file that's being watched;
so if you're watching a directory with five files then that's six file
descriptors. You will run in to your system's "max open files" limit faster on
these platforms.
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
control the maximum number of open files.

View File

@@ -1,640 +0,0 @@
//go:build solaris
// +build solaris
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/unix"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
mu sync.Mutex
port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]struct{} // Explicitly watched directories
watches map[string]struct{} // Explicitly watched non-directories
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(0)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
w := &Watcher{
Events: make(chan Event, sz),
Errors: make(chan error),
dirs: make(map[string]struct{}),
watches: make(map[string]struct{}),
done: make(chan struct{}),
}
var err error
w.port, err = unix.NewEventPort()
if err != nil {
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
}
go w.readEvents()
return w, nil
}
// sendEvent attempts to send an event to the user, returning true if the event
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
select {
case w.Events <- Event{Name: name, Op: op}:
return true
case <-w.done:
return false
}
}
// sendError attempts to send an error to the user, returning true if the error
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendError(err error) (sent bool) {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
// Take the lock used by associateFile to prevent lingering events from
// being processed after the close
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed() {
return nil
}
close(w.done)
return w.port.Close()
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
if w.port.PathIsWatched(name) {
return nil
}
_ = getOptions(opts...)
// Currently we resolve symlinks that were explicitly requested to be
// watched. Otherwise we would use LStat here.
stat, err := os.Stat(name)
if err != nil {
return err
}
// Associate all files in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, true, w.associateFile)
if err != nil {
return err
}
w.mu.Lock()
w.dirs[name] = struct{}{}
w.mu.Unlock()
return nil
}
err = w.associateFile(name, stat, true)
if err != nil {
return err
}
w.mu.Lock()
w.watches[name] = struct{}{}
w.mu.Unlock()
return nil
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
if !w.port.PathIsWatched(name) {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
// The user has expressed an intent. Immediately remove this name from
// whichever watch list it might be in. If it's not in there the delete
// doesn't cause harm.
w.mu.Lock()
delete(w.watches, name)
delete(w.dirs, name)
w.mu.Unlock()
stat, err := os.Stat(name)
if err != nil {
return err
}
// Remove associations for every file in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, false, w.dissociateFile)
if err != nil {
return err
}
return nil
}
err = w.port.DissociatePath(name)
if err != nil {
return err
}
return nil
}
// readEvents contains the main loop that runs in a goroutine watching for events.
func (w *Watcher) readEvents() {
// If this function returns, the watcher has been closed and we can close
// these channels
defer func() {
close(w.Errors)
close(w.Events)
}()
pevents := make([]unix.PortEvent, 8)
for {
count, err := w.port.Get(pevents, 1, nil)
if err != nil && err != unix.ETIME {
// Interrupted system call (count should be 0) ignore and continue
if errors.Is(err, unix.EINTR) && count == 0 {
continue
}
// Get failed because we called w.Close()
if errors.Is(err, unix.EBADF) && w.isClosed() {
return
}
// There was an error not caused by calling w.Close()
if !w.sendError(err) {
return
}
}
p := pevents[:count]
for _, pevent := range p {
if pevent.Source != unix.PORT_SOURCE_FILE {
// Event from unexpected source received; should never happen.
if !w.sendError(errors.New("Event from unexpected source received")) {
return
}
continue
}
err = w.handleEvent(&pevent)
if err != nil {
if !w.sendError(err) {
return
}
}
}
}
}
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
files, err := os.ReadDir(path)
if err != nil {
return err
}
// Handle all children of the directory.
for _, entry := range files {
finfo, err := entry.Info()
if err != nil {
return err
}
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
if err != nil {
return err
}
}
// And finally handle the directory itself.
return handler(path, stat, follow)
}
// handleEvent might need to emit more than one fsnotify event if the events
// bitmap matches more than one event type (e.g. the file was both modified and
// had the attributes changed between when the association was created and the
// when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
var (
events = event.Events
path = event.Path
fmode = event.Cookie.(os.FileMode)
reRegister = true
)
w.mu.Lock()
_, watchedDir := w.dirs[path]
_, watchedPath := w.watches[path]
w.mu.Unlock()
isWatched := watchedDir || watchedPath
if events&unix.FILE_DELETE != 0 {
if !w.sendEvent(path, Remove) {
return nil
}
reRegister = false
}
if events&unix.FILE_RENAME_FROM != 0 {
if !w.sendEvent(path, Rename) {
return nil
}
// Don't keep watching the new file name
reRegister = false
}
if events&unix.FILE_RENAME_TO != 0 {
// We don't report a Rename event for this case, because Rename events
// are interpreted as referring to the _old_ name of the file, and in
// this case the event would refer to the new name of the file. This
// type of rename event is not supported by fsnotify.
// inotify reports a Remove event in this case, so we simulate this
// here.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't keep watching the file that was removed
reRegister = false
}
// The file is gone, nothing left to do.
if !reRegister {
if watchedDir {
w.mu.Lock()
delete(w.dirs, path)
w.mu.Unlock()
}
if watchedPath {
w.mu.Lock()
delete(w.watches, path)
w.mu.Unlock()
}
return nil
}
// If we didn't get a deletion the file still exists and we're going to have
// to watch it again. Let's Stat it now so that we can compare permissions
// and have what we need to continue watching the file
stat, err := os.Lstat(path)
if err != nil {
// This is unexpected, but we should still emit an event. This happens
// most often on "rm -r" of a subdirectory inside a watched directory We
// get a modify event of something happening inside, but by the time we
// get here, the sudirectory is already gone. Clearly we were watching
// this path but now it is gone. Let's tell the user that it was
// removed.
if !w.sendEvent(path, Remove) {
return nil
}
// Suppress extra write events on removed directories; they are not
// informative and can be confusing.
return nil
}
// resolve symlinks that were explicitly watched as we would have at Add()
// time. this helps suppress spurious Chmod events on watched symlinks
if isWatched {
stat, err = os.Stat(path)
if err != nil {
// The symlink still exists, but the target is gone. Report the
// Remove similar to above.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't return the error
}
}
if events&unix.FILE_MODIFIED != 0 {
if fmode.IsDir() {
if watchedDir {
if err := w.updateDirectory(path); err != nil {
return err
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
}
if events&unix.FILE_ATTRIB != 0 && stat != nil {
// Only send Chmod if perms changed
if stat.Mode().Perm() != fmode.Perm() {
if !w.sendEvent(path, Chmod) {
return nil
}
}
}
if stat != nil {
// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
return w.associateFile(path, stat, isWatched)
}
return nil
}
func (w *Watcher) updateDirectory(path string) error {
// The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen,
// as everything else should still be watched.
files, err := os.ReadDir(path)
if err != nil {
return err
}
for _, entry := range files {
path := filepath.Join(path, entry.Name())
if w.port.PathIsWatched(path) {
continue
}
finfo, err := entry.Info()
if err != nil {
return err
}
err = w.associateFile(path, finfo, false)
if err != nil {
if !w.sendError(err) {
return nil
}
}
if !w.sendEvent(path, Create) {
return nil
}
}
return nil
}
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
if w.isClosed() {
return ErrClosed
}
// This is primarily protecting the call to AssociatePath but it is
// important and intentional that the call to PathIsWatched is also
// protected by this mutex. Without this mutex, AssociatePath has been seen
// to error out that the path is already associated.
w.mu.Lock()
defer w.mu.Unlock()
if w.port.PathIsWatched(path) {
// Remove the old association in favor of this one If we get ENOENT,
// then while the x/sys/unix wrapper still thought that this path was
// associated, the underlying event port did not. This call will have
// cleared up that discrepancy. The most likely cause is that the event
// has fired but we haven't processed it yet.
err := w.port.DissociatePath(path)
if err != nil && err != unix.ENOENT {
return err
}
}
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
// targets.
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
if follow {
// We *DO* follow symlinks for explicitly watched entries.
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
}
return w.port.AssociatePath(path, stat,
events,
stat.Mode())
}
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
if !w.port.PathIsWatched(path) {
return nil
}
return w.port.DissociatePath(path)
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches)+len(w.dirs))
for pathname := range w.dirs {
entries = append(entries, pathname)
}
for pathname := range w.watches {
entries = append(entries, pathname)
}
return entries
}

View File

@@ -1,594 +0,0 @@
//go:build linux && !appengine
// +build linux,!appengine
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify
import (
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/unix"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
// Store fd here as os.File.Read() will no longer return on close after
// calling Fd(). See: https://github.com/golang/go/issues/26439
fd int
inotifyFile *os.File
watches *watches
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
closeMu sync.Mutex
doneResp chan struct{} // Channel to respond to Close
}
type (
watches struct {
mu sync.RWMutex
wd map[uint32]*watch // wd → watch
path map[string]uint32 // pathname → wd
}
watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path.
}
)
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
path: make(map[string]uint32),
}
}
func (w *watches) len() int {
w.mu.RLock()
defer w.mu.RUnlock()
return len(w.wd)
}
func (w *watches) add(ww *watch) {
w.mu.Lock()
defer w.mu.Unlock()
w.wd[ww.wd] = ww
w.path[ww.path] = ww.wd
}
func (w *watches) remove(wd uint32) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.path, w.wd[wd].path)
delete(w.wd, wd)
}
func (w *watches) removePath(path string) (uint32, bool) {
w.mu.Lock()
defer w.mu.Unlock()
wd, ok := w.path[path]
if !ok {
return 0, false
}
delete(w.path, path)
delete(w.wd, wd)
return wd, true
}
func (w *watches) byPath(path string) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[w.path[path]]
}
func (w *watches) byWd(wd uint32) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[wd]
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
w.mu.Lock()
defer w.mu.Unlock()
var existing *watch
wd, ok := w.path[path]
if ok {
existing = w.wd[wd]
}
upd, err := f(existing)
if err != nil {
return err
}
if upd != nil {
w.wd[upd.wd] = upd
w.path[upd.path] = upd.wd
if upd.wd != wd {
delete(w.wd, wd)
}
}
return nil
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(0)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 {
return nil, errno
}
w := &Watcher{
fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""),
watches: newWatches(),
Events: make(chan Event, sz),
Errors: make(chan error),
done: make(chan struct{}),
doneResp: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool {
select {
case w.Events <- e:
return true
case <-w.done:
return false
}
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
w.closeMu.Lock()
if w.isClosed() {
w.closeMu.Unlock()
return nil
}
close(w.done)
w.closeMu.Unlock()
// Causes any blocking reads to return with an error, provided the file
// still supports deadline operations.
err := w.inotifyFile.Close()
if err != nil {
return err
}
// Wait for goroutine to close
<-w.doneResp
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
name = filepath.Clean(name)
_ = getOptions(opts...)
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
if existing != nil {
flags |= existing.flags | unix.IN_MASK_ADD
}
wd, err := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return nil, err
}
if existing == nil {
return &watch{
wd: uint32(wd),
path: name,
flags: flags,
}, nil
}
existing.wd = uint32(wd)
existing.flags = flags
return existing, nil
})
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
return w.remove(filepath.Clean(name))
}
func (w *Watcher) remove(name string) error {
wd, ok := w.watches.removePath(name)
if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
success, errno := unix.InotifyRmWatch(w.fd, wd)
if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case;
// The only two possible errors are:
//
// - EBADF, which happens when w.fd is not a valid file descriptor
// of any kind.
// - EINVAL, which is when fd is not an inotify descriptor or wd
// is not a valid watch descriptor. Watch descriptors are
// invalidated when they are removed explicitly or implicitly;
// explicitly by inotify_rm_watch, implicitly when the file they
// are watching is deleted.
return errno
}
return nil
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
entries := make([]string, 0, w.watches.len())
w.watches.mu.RLock()
for pathname := range w.watches.path {
entries = append(entries, pathname)
}
w.watches.mu.RUnlock()
return entries
}
// readEvents reads from the inotify file descriptor, converts the
// received events into Event objects and sends them via the Events channel
func (w *Watcher) readEvents() {
defer func() {
close(w.doneResp)
close(w.Errors)
close(w.Events)
}()
var (
buf [unix.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events
errno error // Syscall errno
)
for {
// See if we have been closed.
if w.isClosed() {
return
}
n, err := w.inotifyFile.Read(buf[:])
switch {
case errors.Unwrap(err) == os.ErrClosed:
return
case err != nil:
if !w.sendError(err) {
return
}
continue
}
if n < unix.SizeofInotifyEvent {
var err error
if n == 0 {
err = io.EOF // If EOF is received. This should really never happen.
} else if n < 0 {
err = errno // If an error occurred while reading.
} else {
err = errors.New("notify: short read in readEvents()") // Read was too short.
}
if !w.sendError(err) {
return
}
continue
}
var offset uint32
// We don't know how many events we just read into the buffer
// While the offset points to at least one whole event...
for offset <= uint32(n-unix.SizeofInotifyEvent) {
var (
// Point "raw" to the event in the buffer
raw = (*unix.InotifyEvent)(unsafe.Pointer(&buf[offset]))
mask = uint32(raw.Mask)
nameLen = uint32(raw.Len)
)
if mask&unix.IN_Q_OVERFLOW != 0 {
if !w.sendError(ErrEventOverflow) {
return
}
}
// If the event happened to the watched directory or the watched file, the kernel
// doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map.
watch := w.watches.byWd(uint32(raw.Wd))
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd)
}
// We can't really update the state when a watched path is moved;
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
// the watch.
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return
}
}
}
var name string
if watch != nil {
name = watch.path
}
if nameLen > 0 {
// Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]
// The filename is padded with NULL bytes. TrimRight() gets rid of those.
name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000")
}
event := w.newEvent(name, mask)
// Send the events that are not ignored on the events channel
if mask&unix.IN_IGNORED == 0 {
if !w.sendEvent(event) {
return
}
}
// Move to the next event in the buffer
offset += unix.SizeofInotifyEvent + nameLen
}
}
}
// newEvent returns an platform-independent Event based on an inotify mask.
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.IN_CREATE == unix.IN_CREATE || mask&unix.IN_MOVED_TO == unix.IN_MOVED_TO {
e.Op |= Create
}
if mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF || mask&unix.IN_DELETE == unix.IN_DELETE {
e.Op |= Remove
}
if mask&unix.IN_MODIFY == unix.IN_MODIFY {
e.Op |= Write
}
if mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF || mask&unix.IN_MOVED_FROM == unix.IN_MOVED_FROM {
e.Op |= Rename
}
if mask&unix.IN_ATTRIB == unix.IN_ATTRIB {
e.Op |= Chmod
}
return e
}

View File

@@ -1,782 +0,0 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin
// +build freebsd openbsd netbsd dragonfly darwin
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/unix"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
done chan struct{}
kq int // File descriptor (as returned by the kqueue() syscall).
closepipe [2]int // Pipe used for closing.
mu sync.Mutex // Protects access to watcher data
watches map[string]int // Watched file descriptors (key: path).
watchesByDir map[string]map[int]struct{} // Watched file descriptors indexed by the parent directory (key: dirname(path)).
userWatches map[string]struct{} // Watches added with Watcher.Add()
dirFlags map[string]uint32 // Watched directories to fflags used in kqueue.
paths map[int]pathInfo // File descriptors to path names for processing kqueue events.
fileExists map[string]struct{} // Keep track of if we know this file exists (to stop duplicate create events).
isClosed bool // Set to true when Close() is first called
}
type pathInfo struct {
name string
isDir bool
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(0)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
kq, closepipe, err := newKqueue()
if err != nil {
return nil, err
}
w := &Watcher{
kq: kq,
closepipe: closepipe,
watches: make(map[string]int),
watchesByDir: make(map[string]map[int]struct{}),
dirFlags: make(map[string]uint32),
paths: make(map[int]pathInfo),
fileExists: make(map[string]struct{}),
userWatches: make(map[string]struct{}),
Events: make(chan Event, sz),
Errors: make(chan error),
done: make(chan struct{}),
}
go w.readEvents()
return w, nil
}
// newKqueue creates a new kernel event queue and returns a descriptor.
//
// This registers a new event on closepipe, which will trigger an event when
// it's closed. This way we can use kevent() without timeout/polling; without
// the closepipe, it would block forever and we wouldn't be able to stop it at
// all.
func newKqueue() (kq int, closepipe [2]int, err error) {
kq, err = unix.Kqueue()
if kq == -1 {
return kq, closepipe, err
}
// Register the close pipe.
err = unix.Pipe(closepipe[:])
if err != nil {
unix.Close(kq)
return kq, closepipe, err
}
// Register changes to listen on the closepipe.
changes := make([]unix.Kevent_t, 1)
// SetKevent converts int to the platform-specific types.
unix.SetKevent(&changes[0], closepipe[0], unix.EVFILT_READ,
unix.EV_ADD|unix.EV_ENABLE|unix.EV_ONESHOT)
ok, err := unix.Kevent(kq, changes, nil, nil)
if ok == -1 {
unix.Close(kq)
unix.Close(closepipe[0])
unix.Close(closepipe[1])
return kq, closepipe, err
}
return kq, closepipe, nil
}
// Returns true if the event was sent, or false if watcher is closed.
func (w *Watcher) sendEvent(e Event) bool {
select {
case w.Events <- e:
return true
case <-w.done:
return false
}
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
w.isClosed = true
// copy paths to remove while locked
pathsToRemove := make([]string, 0, len(w.watches))
for name := range w.watches {
pathsToRemove = append(pathsToRemove, name)
}
w.mu.Unlock() // Unlock before calling Remove, which also locks
for _, name := range pathsToRemove {
w.Remove(name)
}
// Send "quit" message to the reader goroutine.
unix.Close(w.closepipe[1])
close(w.done)
return nil
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
_ = getOptions(opts...)
w.mu.Lock()
w.userWatches[name] = struct{}{}
w.mu.Unlock()
_, err := w.addWatch(name, noteAllEvents)
return err
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
return w.remove(name, true)
}
func (w *Watcher) remove(name string, unwatchFiles bool) error {
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
watchfd, ok := w.watches[name]
w.mu.Unlock()
if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
err := w.register([]int{watchfd}, unix.EV_DELETE, 0)
if err != nil {
return err
}
unix.Close(watchfd)
w.mu.Lock()
isDir := w.paths[watchfd].isDir
delete(w.watches, name)
delete(w.userWatches, name)
parentName := filepath.Dir(name)
delete(w.watchesByDir[parentName], watchfd)
if len(w.watchesByDir[parentName]) == 0 {
delete(w.watchesByDir, parentName)
}
delete(w.paths, watchfd)
delete(w.dirFlags, name)
delete(w.fileExists, name)
w.mu.Unlock()
// Find all watched paths that are in this directory that are not external.
if unwatchFiles && isDir {
var pathsToRemove []string
w.mu.Lock()
for fd := range w.watchesByDir[name] {
path := w.paths[fd]
if _, ok := w.userWatches[path.name]; !ok {
pathsToRemove = append(pathsToRemove, path.name)
}
}
w.mu.Unlock()
for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error to
// the user, as that will just confuse them with an error about a
// path they did not explicitly watch themselves.
w.Remove(name)
}
}
return nil
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed {
return nil
}
entries := make([]string, 0, len(w.userWatches))
for pathname := range w.userWatches {
entries = append(entries, pathname)
}
return entries
}
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
// addWatch adds name to the watched file set; the flags are interpreted as
// described in kevent(2).
//
// Returns the real path to the file which was added, with symlinks resolved.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool
name = filepath.Clean(name)
w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return "", ErrClosed
}
watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags.
if alreadyWatching {
isDir = w.paths[watchfd].isDir
}
w.mu.Unlock()
if !alreadyWatching {
fi, err := os.Lstat(name)
if err != nil {
return "", err
}
// Don't watch sockets or named pipes
if (fi.Mode()&os.ModeSocket == os.ModeSocket) || (fi.Mode()&os.ModeNamedPipe == os.ModeNamedPipe) {
return "", nil
}
// Follow Symlinks.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := os.Readlink(name)
if err != nil {
// Return nil because Linux can add unresolvable symlinks to the
// watch list without problems, so maintain consistency with
// that. There will be no file events for broken symlinks.
// TODO: more specific check; returns os.PathError; ENOENT?
return "", nil
}
w.mu.Lock()
_, alreadyWatching = w.watches[link]
w.mu.Unlock()
if alreadyWatching {
// Add to watches so we don't get spurious Create events later
// on when we diff the directories.
w.watches[name] = 0
w.fileExists[name] = struct{}{}
return link, nil
}
name = link
fi, err = os.Lstat(name)
if err != nil {
return "", nil
}
}
// Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and Go issues 11180 and 39237.
for {
watchfd, err = unix.Open(name, openMode, 0)
if err == nil {
break
}
if errors.Is(err, unix.EINTR) {
continue
}
return "", err
}
isDir = fi.IsDir()
}
err := w.register([]int{watchfd}, unix.EV_ADD|unix.EV_CLEAR|unix.EV_ENABLE, flags)
if err != nil {
unix.Close(watchfd)
return "", err
}
if !alreadyWatching {
w.mu.Lock()
parentName := filepath.Dir(name)
w.watches[name] = watchfd
watchesByDir, ok := w.watchesByDir[parentName]
if !ok {
watchesByDir = make(map[int]struct{}, 1)
w.watchesByDir[parentName] = watchesByDir
}
watchesByDir[watchfd] = struct{}{}
w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock()
}
if isDir {
// Watch the directory if it has not been watched before, or if it was
// watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
(!alreadyWatching || (w.dirFlags[name]&unix.NOTE_WRITE) != unix.NOTE_WRITE)
// Store flags so this watch can be updated later
w.dirFlags[name] = flags
w.mu.Unlock()
if watchDir {
if err := w.watchDirectoryFiles(name); err != nil {
return "", err
}
}
}
return name, nil
}
// readEvents reads from kqueue and converts the received kevents into
// Event values that it sends down the Events channel.
func (w *Watcher) readEvents() {
defer func() {
close(w.Events)
close(w.Errors)
_ = unix.Close(w.kq)
unix.Close(w.closepipe[0])
}()
eventBuffer := make([]unix.Kevent_t, 10)
for closed := false; !closed; {
kevents, err := w.read(eventBuffer)
// EINTR is okay, the syscall was interrupted before timeout expired.
if err != nil && err != unix.EINTR {
if !w.sendError(fmt.Errorf("fsnotify.readEvents: %w", err)) {
closed = true
}
continue
}
// Flush the events we received to the Events channel
for _, kevent := range kevents {
var (
watchfd = int(kevent.Ident)
mask = uint32(kevent.Fflags)
)
// Shut down the loop when the pipe is closed, but only after all
// other events have been processed.
if watchfd == w.closepipe[0] {
closed = true
continue
}
w.mu.Lock()
path := w.paths[watchfd]
w.mu.Unlock()
event := w.newEvent(path.name, mask)
if event.Has(Rename) || event.Has(Remove) {
w.remove(event.Name, false)
w.mu.Lock()
delete(w.fileExists, event.Name)
w.mu.Unlock()
}
if path.isDir && event.Has(Write) && !event.Has(Remove) {
w.sendDirectoryChangeEvents(event.Name)
} else {
if !w.sendEvent(event) {
closed = true
continue
}
}
if event.Has(Remove) {
// Look for a file that may have overwritten this; for example,
// mv f1 f2 will delete f2, then create f2.
if path.isDir {
fileDir := filepath.Clean(event.Name)
w.mu.Lock()
_, found := w.watches[fileDir]
w.mu.Unlock()
if found {
err := w.sendDirectoryChangeEvents(fileDir)
if err != nil {
if !w.sendError(err) {
closed = true
}
}
}
} else {
filePath := filepath.Clean(event.Name)
if fi, err := os.Lstat(filePath); err == nil {
err := w.sendFileCreatedEventIfNew(filePath, fi)
if err != nil {
if !w.sendError(err) {
closed = true
}
}
}
}
}
}
}
}
// newEvent returns an platform-independent Event based on kqueue Fflags.
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&unix.NOTE_DELETE == unix.NOTE_DELETE {
e.Op |= Remove
}
if mask&unix.NOTE_WRITE == unix.NOTE_WRITE {
e.Op |= Write
}
if mask&unix.NOTE_RENAME == unix.NOTE_RENAME {
e.Op |= Rename
}
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
e.Op |= Chmod
}
// No point sending a write and delete event at the same time: if it's gone,
// then it's gone.
if e.Op.Has(Write) && e.Op.Has(Remove) {
e.Op &^= Write
}
return e
}
// watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files
files, err := os.ReadDir(dirPath)
if err != nil {
return err
}
for _, f := range files {
path := filepath.Join(dirPath, f.Name())
fi, err := f.Info()
if err != nil {
return fmt.Errorf("%q: %w", path, err)
}
cleanPath, err := w.internalWatch(path, fi)
if err != nil {
// No permission to read the file; that's not a problem: just skip.
// But do add it to w.fileExists to prevent it from being picked up
// as a "new" file later (it still shows up in the directory
// listing).
switch {
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
cleanPath = filepath.Clean(path)
default:
return fmt.Errorf("%q: %w", path, err)
}
}
w.mu.Lock()
w.fileExists[cleanPath] = struct{}{}
w.mu.Unlock()
}
return nil
}
// Search the directory for new files and send an event for them.
//
// This functionality is to have the BSD watcher match the inotify, which sends
// a create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
files, err := os.ReadDir(dir)
if err != nil {
// Directory no longer exists: we can ignore this safely. kqueue will
// still give us the correct events.
if errors.Is(err, os.ErrNotExist) {
return nil
}
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
}
for _, f := range files {
fi, err := f.Info()
if err != nil {
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
}
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil {
// Don't need to send an error if this file isn't readable.
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
return nil
}
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
}
}
return nil
}
// sendFileCreatedEvent sends a create event if the file isn't already being tracked.
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) {
w.mu.Lock()
_, doesExist := w.fileExists[filePath]
w.mu.Unlock()
if !doesExist {
if !w.sendEvent(Event{Name: filePath, Op: Create}) {
return
}
}
// like watchDirectoryFiles (but without doing another ReadDir)
filePath, err = w.internalWatch(filePath, fi)
if err != nil {
return err
}
w.mu.Lock()
w.fileExists[filePath] = struct{}{}
w.mu.Unlock()
return nil
}
func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
if fi.IsDir() {
// mimic Linux providing delete events for subdirectories, but preserve
// the flags used if currently watching subdirectory
w.mu.Lock()
flags := w.dirFlags[name]
w.mu.Unlock()
flags |= unix.NOTE_DELETE | unix.NOTE_RENAME
return w.addWatch(name, flags)
}
// watch file to mimic Linux inotify
return w.addWatch(name, noteAllEvents)
}
// Register events with the queue.
func (w *Watcher) register(fds []int, flags int, fflags uint32) error {
changes := make([]unix.Kevent_t, len(fds))
for i, fd := range fds {
// SetKevent converts int to the platform-specific types.
unix.SetKevent(&changes[i], fd, unix.EVFILT_VNODE, flags)
changes[i].Fflags = fflags
}
// Register the events.
success, err := unix.Kevent(w.kq, changes, nil, nil)
if success == -1 {
return err
}
return nil
}
// read retrieves pending events, or waits until an event occurs.
func (w *Watcher) read(events []unix.Kevent_t) ([]unix.Kevent_t, error) {
n, err := unix.Kevent(w.kq, nil, events, nil)
if err != nil {
return nil, err
}
return events[0:n], nil
}

View File

@@ -1,205 +0,0 @@
//go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
// +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify
import "errors"
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return nil, errors.New("fsnotify not supported on the current platform")
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { return nil }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return nil }
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return nil }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { return nil }

View File

@@ -1,827 +0,0 @@
//go:build windows
// +build windows
// Windows backend based on ReadDirectoryChangesW()
//
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
//
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify
import (
"errors"
"fmt"
"os"
"path/filepath"
"reflect"
"runtime"
"strings"
"sync"
"unsafe"
"golang.org/x/sys/windows"
)
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
port windows.Handle // Handle to completion port
input chan *input // Inputs to the reader are sent on this channel
quit chan chan<- error
mu sync.Mutex // Protects access to watches, closed
watches watchMap // Map of watches (key: i-number)
closed bool // Set to true when Close() is first called
}
// NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(50)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err)
}
w := &Watcher{
port: port,
watches: make(watchMap),
input: make(chan *input, 1),
Events: make(chan Event, sz),
Errors: make(chan error),
quit: make(chan chan<- error, 1),
}
go w.readEvents()
return w, nil
}
func (w *Watcher) isClosed() bool {
w.mu.Lock()
defer w.mu.Unlock()
return w.closed
}
func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 {
return false
}
event := w.newEvent(name, uint32(mask))
select {
case ch := <-w.quit:
w.quit <- ch
case w.Events <- event:
}
return true
}
// Returns true if the error was sent, or false if watcher is closed.
func (w *Watcher) sendError(err error) bool {
select {
case w.Errors <- err:
return true
case <-w.quit:
}
return false
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error {
if w.isClosed() {
return nil
}
w.mu.Lock()
w.closed = true
w.mu.Unlock()
// Send "quit" message to the reader goroutine
ch := make(chan error)
w.quit <- ch
if err := w.wakeupReader(); err != nil {
return err
}
return <-ch
}
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
with := getOptions(opts...)
if with.bufsize < 4096 {
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
}
in := &input{
op: opAddWatch,
path: filepath.Clean(name),
flags: sysFSALLEVENTS,
reply: make(chan error),
bufsize: with.bufsize,
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
in := &input{
op: opRemoveWatch,
path: filepath.Clean(name),
reply: make(chan error),
}
w.input <- in
if err := w.wakeupReader(); err != nil {
return err
}
return <-in.reply
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches))
for _, entry := range w.watches {
for _, watchEntry := range entry {
entries = append(entries, watchEntry.path)
}
}
return entries
}
// These options are from the old golang.org/x/exp/winfsnotify, where you could
// add various options to the watch. This has long since been removed.
//
// The "sys" in the name is misleading as they're not part of any "system".
//
// This should all be removed at some point, and just use windows.FILE_NOTIFY_*
const (
sysFSALLEVENTS = 0xfff
sysFSCREATE = 0x100
sysFSDELETE = 0x200
sysFSDELETESELF = 0x400
sysFSMODIFY = 0x2
sysFSMOVE = 0xc0
sysFSMOVEDFROM = 0x40
sysFSMOVEDTO = 0x80
sysFSMOVESELF = 0x800
sysFSIGNORED = 0x8000
)
func (w *Watcher) newEvent(name string, mask uint32) Event {
e := Event{Name: name}
if mask&sysFSCREATE == sysFSCREATE || mask&sysFSMOVEDTO == sysFSMOVEDTO {
e.Op |= Create
}
if mask&sysFSDELETE == sysFSDELETE || mask&sysFSDELETESELF == sysFSDELETESELF {
e.Op |= Remove
}
if mask&sysFSMODIFY == sysFSMODIFY {
e.Op |= Write
}
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
e.Op |= Rename
}
return e
}
const (
opAddWatch = iota
opRemoveWatch
)
const (
provisional uint64 = 1 << (32 + iota)
)
type input struct {
op int
path string
flags uint32
bufsize int
reply chan error
}
type inode struct {
handle windows.Handle
volume uint32
index uint64
}
type watch struct {
ov windows.Overlapped
ino *inode // i-number
recurse bool // Recursive watch?
path string // Directory path
mask uint64 // Directory itself is being watched with these notify flags
names map[string]uint64 // Map of names being watched and their notify flags
rename string // Remembers the old name while renaming a file
buf []byte // buffer, allocated later
}
type (
indexMap map[uint64]*watch
watchMap map[uint32]indexMap
)
func (w *Watcher) wakeupReader() error {
err := windows.PostQueuedCompletionStatus(w.port, 0, 0, nil)
if err != nil {
return os.NewSyscallError("PostQueuedCompletionStatus", err)
}
return nil
}
func (w *Watcher) getDir(pathname string) (dir string, err error) {
attr, err := windows.GetFileAttributes(windows.StringToUTF16Ptr(pathname))
if err != nil {
return "", os.NewSyscallError("GetFileAttributes", err)
}
if attr&windows.FILE_ATTRIBUTE_DIRECTORY != 0 {
dir = pathname
} else {
dir, _ = filepath.Split(pathname)
dir = filepath.Clean(dir)
}
return
}
func (w *Watcher) getIno(path string) (ino *inode, err error) {
h, err := windows.CreateFile(windows.StringToUTF16Ptr(path),
windows.FILE_LIST_DIRECTORY,
windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE|windows.FILE_SHARE_DELETE,
nil, windows.OPEN_EXISTING,
windows.FILE_FLAG_BACKUP_SEMANTICS|windows.FILE_FLAG_OVERLAPPED, 0)
if err != nil {
return nil, os.NewSyscallError("CreateFile", err)
}
var fi windows.ByHandleFileInformation
err = windows.GetFileInformationByHandle(h, &fi)
if err != nil {
windows.CloseHandle(h)
return nil, os.NewSyscallError("GetFileInformationByHandle", err)
}
ino = &inode{
handle: h,
volume: fi.VolumeSerialNumber,
index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow),
}
return ino, nil
}
// Must run within the I/O thread.
func (m watchMap) get(ino *inode) *watch {
if i := m[ino.volume]; i != nil {
return i[ino.index]
}
return nil
}
// Must run within the I/O thread.
func (m watchMap) set(ino *inode, watch *watch) {
i := m[ino.volume]
if i == nil {
i = make(indexMap)
m[ino.volume] = i
}
i[ino.index] = watch
}
// Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
//pathname, recurse := recursivePath(pathname)
recurse := false
dir, err := w.getDir(pathname)
if err != nil {
return err
}
ino, err := w.getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watchEntry := w.watches.get(ino)
w.mu.Unlock()
if watchEntry == nil {
_, err := windows.CreateIoCompletionPort(ino.handle, w.port, 0, 0)
if err != nil {
windows.CloseHandle(ino.handle)
return os.NewSyscallError("CreateIoCompletionPort", err)
}
watchEntry = &watch{
ino: ino,
path: dir,
names: make(map[string]uint64),
recurse: recurse,
buf: make([]byte, bufsize),
}
w.mu.Lock()
w.watches.set(ino, watchEntry)
w.mu.Unlock()
flags |= provisional
} else {
windows.CloseHandle(ino.handle)
}
if pathname == dir {
watchEntry.mask |= flags
} else {
watchEntry.names[filepath.Base(pathname)] |= flags
}
err = w.startRead(watchEntry)
if err != nil {
return err
}
if pathname == dir {
watchEntry.mask &= ^provisional
} else {
watchEntry.names[filepath.Base(pathname)] &= ^provisional
}
return nil
}
// Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error {
pathname, recurse := recursivePath(pathname)
dir, err := w.getDir(pathname)
if err != nil {
return err
}
ino, err := w.getIno(dir)
if err != nil {
return err
}
w.mu.Lock()
watch := w.watches.get(ino)
w.mu.Unlock()
if recurse && !watch.recurse {
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
}
err = windows.CloseHandle(ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
if watch == nil {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, pathname)
}
if pathname == dir {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
watch.mask = 0
} else {
name := filepath.Base(pathname)
w.sendEvent(filepath.Join(watch.path, name), watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
return w.startRead(watch)
}
// Must run within the I/O thread.
func (w *Watcher) deleteWatch(watch *watch) {
for name, mask := range watch.names {
if mask&provisional == 0 {
w.sendEvent(filepath.Join(watch.path, name), mask&sysFSIGNORED)
}
delete(watch.names, name)
}
if watch.mask != 0 {
if watch.mask&provisional == 0 {
w.sendEvent(watch.path, watch.mask&sysFSIGNORED)
}
watch.mask = 0
}
}
// Must run within the I/O thread.
func (w *Watcher) startRead(watch *watch) error {
err := windows.CancelIo(watch.ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CancelIo", err))
w.deleteWatch(watch)
}
mask := w.toWindowsFlags(watch.mask)
for _, m := range watch.names {
mask |= w.toWindowsFlags(m)
}
if mask == 0 {
err := windows.CloseHandle(watch.ino.handle)
if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err))
}
w.mu.Lock()
delete(w.watches[watch.ino.volume], watch.ino.index)
w.mu.Unlock()
return nil
}
// We need to pass the array, rather than the slice.
hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
err = nil
}
w.deleteWatch(watch)
w.startRead(watch)
return err
}
return nil
}
// readEvents reads from the I/O completion port, converts the
// received events into Event objects and sends them via the Events channel.
// Entry point to the I/O thread.
func (w *Watcher) readEvents() {
var (
n uint32
key uintptr
ov *windows.Overlapped
)
runtime.LockOSThread()
for {
// This error is handled after the watch == nil check below.
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
watch := (*watch)(unsafe.Pointer(ov))
if watch == nil {
select {
case ch := <-w.quit:
w.mu.Lock()
var indexes []indexMap
for _, index := range w.watches {
indexes = append(indexes, index)
}
w.mu.Unlock()
for _, index := range indexes {
for _, watch := range index {
w.deleteWatch(watch)
w.startRead(watch)
}
}
err := windows.CloseHandle(w.port)
if err != nil {
err = os.NewSyscallError("CloseHandle", err)
}
close(w.Events)
close(w.Errors)
ch <- err
return
case in := <-w.input:
switch in.op {
case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
case opRemoveWatch:
in.reply <- w.remWatch(in.path)
}
default:
}
continue
}
switch qErr {
case nil:
// No error
case windows.ERROR_MORE_DATA:
if watch == nil {
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
} else {
// The i/o succeeded but the buffer is full.
// In theory we should be building up a full packet.
// In practice we can get away with just carrying on.
n = uint32(unsafe.Sizeof(watch.buf))
}
case windows.ERROR_ACCESS_DENIED:
// Watched directory was probably removed
w.sendEvent(watch.path, watch.mask&sysFSDELETESELF)
w.deleteWatch(watch)
w.startRead(watch)
continue
case windows.ERROR_OPERATION_ABORTED:
// CancelIo was called on this handle
continue
default:
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
continue
}
var offset uint32
for {
if n == 0 {
w.sendError(ErrEventOverflow)
break
}
// Point "raw" to the event in the buffer
raw := (*windows.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset]))
// Create a buf that is the size of the path name
size := int(raw.FileNameLength / 2)
var buf []uint16
// TODO: Use unsafe.Slice in Go 1.17; https://stackoverflow.com/questions/51187973
sh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
sh.Data = uintptr(unsafe.Pointer(&raw.FileName))
sh.Len = size
sh.Cap = size
name := windows.UTF16ToString(buf)
fullname := filepath.Join(watch.path, name)
var mask uint64
switch raw.Action {
case windows.FILE_ACTION_REMOVED:
mask = sysFSDELETESELF
case windows.FILE_ACTION_MODIFIED:
mask = sysFSMODIFY
case windows.FILE_ACTION_RENAMED_OLD_NAME:
watch.rename = name
case windows.FILE_ACTION_RENAMED_NEW_NAME:
// Update saved path of all sub-watches.
old := filepath.Join(watch.path, watch.rename)
w.mu.Lock()
for _, watchMap := range w.watches {
for _, ww := range watchMap {
if strings.HasPrefix(ww.path, old) {
ww.path = filepath.Join(fullname, strings.TrimPrefix(ww.path, old))
}
}
}
w.mu.Unlock()
if watch.names[watch.rename] != 0 {
watch.names[name] |= watch.names[watch.rename]
delete(watch.names, watch.rename)
mask = sysFSMOVESELF
}
}
sendNameEvent := func() {
w.sendEvent(fullname, watch.names[name]&mask)
}
if raw.Action != windows.FILE_ACTION_RENAMED_NEW_NAME {
sendNameEvent()
}
if raw.Action == windows.FILE_ACTION_REMOVED {
w.sendEvent(fullname, watch.names[name]&sysFSIGNORED)
delete(watch.names, name)
}
w.sendEvent(fullname, watch.mask&w.toFSnotifyFlags(raw.Action))
if raw.Action == windows.FILE_ACTION_RENAMED_NEW_NAME {
fullname = filepath.Join(watch.path, watch.rename)
sendNameEvent()
}
// Move to the next event in the buffer
if raw.NextEntryOffset == 0 {
break
}
offset += raw.NextEntryOffset
// Error!
if offset >= n {
//lint:ignore ST1005 Windows should be capitalized
w.sendError(errors.New(
"Windows system assumed buffer larger than it is, events have likely been missed"))
break
}
}
if err := w.startRead(watch); err != nil {
w.sendError(err)
}
}
}
func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
var m uint32
if mask&sysFSMODIFY != 0 {
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
}
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
}
return m
}
func (w *Watcher) toFSnotifyFlags(action uint32) uint64 {
switch action {
case windows.FILE_ACTION_ADDED:
return sysFSCREATE
case windows.FILE_ACTION_REMOVED:
return sysFSDELETE
case windows.FILE_ACTION_MODIFIED:
return sysFSMODIFY
case windows.FILE_ACTION_RENAMED_OLD_NAME:
return sysFSMOVEDFROM
case windows.FILE_ACTION_RENAMED_NEW_NAME:
return sysFSMOVEDTO
}
return 0
}

View File

@@ -1,146 +0,0 @@
// Package fsnotify provides a cross-platform interface for file system
// notifications.
//
// Currently supported systems:
//
// Linux 2.6.32+ via inotify
// BSD, macOS via kqueue
// Windows via ReadDirectoryChangesW
// illumos via FEN
package fsnotify
import (
"errors"
"fmt"
"path/filepath"
"strings"
)
// Event represents a file system notification.
type Event struct {
// Path to the file or directory.
//
// Paths are relative to the input; for example with Add("dir") the Name
// will be set to "dir/file" if you create that file, but if you use
// Add("/path/to/dir") it will be "/path/to/dir/file".
Name string
// File operation that triggered the event.
//
// This is a bitmask and some systems may send multiple operations at once.
// Use the Event.Has() method instead of comparing with ==.
Op Op
}
// Op describes a set of file operations.
type Op uint32
// The operations fsnotify can trigger; see the documentation on [Watcher] for a
// full description, and check them with [Event.Has].
const (
// A new pathname was created.
Create Op = 1 << iota
// The pathname was written to; this does *not* mean the write has finished,
// and a write can be followed by more writes.
Write
// The path was removed; any watches on it will be removed. Some "remove"
// operations may trigger a Rename if the file is actually moved (for
// example "remove to trash" is often a rename).
Remove
// The path was renamed to something else; any watched on it will be
// removed.
Rename
// File attributes were changed.
//
// It's generally not recommended to take action on this event, as it may
// get triggered very frequently by some software. For example, Spotlight
// indexing on macOS, anti-virus software, backup software, etc.
Chmod
)
// Common errors that can be reported.
var (
ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
ErrClosed = errors.New("fsnotify: watcher already closed")
)
func (o Op) String() string {
var b strings.Builder
if o.Has(Create) {
b.WriteString("|CREATE")
}
if o.Has(Remove) {
b.WriteString("|REMOVE")
}
if o.Has(Write) {
b.WriteString("|WRITE")
}
if o.Has(Rename) {
b.WriteString("|RENAME")
}
if o.Has(Chmod) {
b.WriteString("|CHMOD")
}
if b.Len() == 0 {
return "[no events]"
}
return b.String()[1:]
}
// Has reports if this operation has the given operation.
func (o Op) Has(h Op) bool { return o&h != 0 }
// Has reports if this event has the given operation.
func (e Event) Has(op Op) bool { return e.Op.Has(op) }
// String returns a string representation of the event with their path.
func (e Event) String() string {
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
}
type (
addOpt func(opt *withOpts)
withOpts struct {
bufsize int
}
)
var defaultOpts = withOpts{
bufsize: 65536, // 64K
}
func getOptions(opts ...addOpt) withOpts {
with := defaultOpts
for _, o := range opts {
o(&with)
}
return with
}
// WithBufferSize sets the [ReadDirectoryChangesW] buffer size.
//
// This only has effect on Windows systems, and is a no-op for other backends.
//
// The default value is 64K (65536 bytes) which is the highest value that works
// on all filesystems and should be enough for most applications, but if you
// have a large burst of events it may not be enough. You can increase it if
// you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]).
//
// [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
func WithBufferSize(bytes int) addOpt {
return func(opt *withOpts) { opt.bufsize = bytes }
}
// Check if this path is recursive (ends with "/..." or "\..."), and return the
// path with the /... stripped.
func recursivePath(path string) (string, bool) {
if filepath.Base(path) == "..." {
return filepath.Dir(path), true
}
return path, false
}

View File

@@ -1,259 +0,0 @@
#!/usr/bin/env zsh
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
setopt err_exit no_unset pipefail extended_glob
# Simple script to update the godoc comments on all watchers so you don't need
# to update the same comment 5 times.
watcher=$(<<EOF
// Watcher watches a set of paths, delivering events on a channel.
//
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\\path\\to\\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
EOF
)
new=$(<<EOF
// NewWatcher creates a new Watcher.
EOF
)
newbuffered=$(<<EOF
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
EOF
)
add=$(<<EOF
// Add starts monitoring the path for changes.
//
// A path can only be watched once; watching it more than once is a no-op and will
// not return an error. Paths that do not yet exist on the filesystem cannot be
// watched.
//
// A watch will be automatically removed if the watched path is deleted or
// renamed. The exception is the Windows backend, which doesn't remove the
// watcher on renames.
//
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work.
//
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories
//
// All files in a directory are monitored, including new files that are created
// after the watcher is started. Subdirectories are not watched (i.e. it's
// non-recursive).
//
// # Watching files
//
// Watching individual files (rather than directories) is generally not
// recommended as many programs (especially editors) update files atomically: it
// will write to a temporary file which is then moved to to destination,
// overwriting the original (or some variant thereof). The watcher on the
// original file is now lost, as that no longer exists.
//
// The upshot of this is that a power failure or crash won't leave a
// half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
EOF
)
addwith=$(<<EOF
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
EOF
)
remove=$(<<EOF
// Remove stops monitoring the path for changes.
//
// Directories are always removed non-recursively. For example, if you added
// /tmp/dir and /tmp/dir/subdir then you will need to remove both.
//
// Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
EOF
)
close=$(<<EOF
// Close removes all watches and closes the Events channel.
EOF
)
watchlist=$(<<EOF
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
EOF
)
events=$(<<EOF
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
EOF
)
errors=$(<<EOF
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
EOF
)
set-cmt() {
local pat=$1
local cmt=$2
IFS=$'\n' local files=($(grep -n $pat backend_*~*_test.go))
for f in $files; do
IFS=':' local fields=($=f)
local file=$fields[1]
local end=$(( $fields[2] - 1 ))
# Find start of comment.
local start=0
IFS=$'\n' local lines=($(head -n$end $file))
for (( i = 1; i <= $#lines; i++ )); do
local line=$lines[-$i]
if ! grep -q '^[[:space:]]*//' <<<$line; then
start=$(( end - (i - 2) ))
break
fi
done
head -n $(( start - 1 )) $file >/tmp/x
print -r -- $cmt >>/tmp/x
tail -n+$(( end + 1 )) $file >>/tmp/x
mv /tmp/x $file
done
}
set-cmt '^type Watcher struct ' $watcher
set-cmt '^func NewWatcher(' $new
set-cmt '^func NewBufferedWatcher(' $newbuffered
set-cmt '^func (w \*Watcher) Add(' $add
set-cmt '^func (w \*Watcher) AddWith(' $addwith
set-cmt '^func (w \*Watcher) Remove(' $remove
set-cmt '^func (w \*Watcher) Close(' $close
set-cmt '^func (w \*Watcher) WatchList(' $watchlist
set-cmt '^[[:space:]]*Events *chan Event$' $events
set-cmt '^[[:space:]]*Errors *chan error$' $errors

View File

@@ -1,8 +0,0 @@
//go:build freebsd || openbsd || netbsd || dragonfly
// +build freebsd openbsd netbsd dragonfly
package fsnotify
import "golang.org/x/sys/unix"
const openMode = unix.O_NONBLOCK | unix.O_RDONLY | unix.O_CLOEXEC

View File

@@ -1,9 +0,0 @@
//go:build darwin
// +build darwin
package fsnotify
import "golang.org/x/sys/unix"
// note: this constant is not defined on BSD
const openMode = unix.O_EVTONLY | unix.O_CLOEXEC

View File

@@ -1,23 +0,0 @@
Copyright ©2020 The go-fonts Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of the go-fonts project nor the names of its authors and
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -1,101 +0,0 @@
Digitized data copyright (c) 2010 Google Corporation
with Reserved Font Arimo, Tinos and Cousine.
Copyright (c) 2012 Red Hat, Inc.
with Reserved Font Name Liberation.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
PREAMBLE The goals of the Open Font License (OFL) are to stimulate
worldwide development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to provide
a free and open framework in which fonts may be shared and improved in
partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves.
The fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such.
This may include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components
as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting ? in part or in whole ?
any of the components of the Original Version, by changing formats or
by porting the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical writer
or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining a
copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole, must
be distributed entirely under this license, and must not be distributed
under any other license. The requirement for fonts to remain under
this license does not apply to any document created using the Font
Software.
TERMINATION
This license becomes null and void if any of the above conditions are not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER
DEALINGS IN THE FONT SOFTWARE.

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationmonobold provides the "LiberationMono Bold" TrueType font
// from the Liberation font family.
package liberationmonobold // import "github.com/go-fonts/liberation/liberationmonobold"
import _ "embed"
// TTF is the data for the "LiberationMono Bold" TrueType font.
//
//go:embed LiberationMono-Bold.ttf
var TTF []byte

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationmonobolditalic provides the "LiberationMono BoldItalic" TrueType font
// from the Liberation font family.
package liberationmonobolditalic // import "github.com/go-fonts/liberation/liberationmonobolditalic"
import _ "embed"
// TTF is the data for the "LiberationMono BoldItalic" TrueType font.
//
//go:embed LiberationMono-BoldItalic.ttf
var TTF []byte

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationmonoitalic provides the "LiberationMono Italic" TrueType font
// from the Liberation font family.
package liberationmonoitalic // import "github.com/go-fonts/liberation/liberationmonoitalic"
import _ "embed"
// TTF is the data for the "LiberationMono Italic" TrueType font.
//
//go:embed LiberationMono-Italic.ttf
var TTF []byte

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationmonoregular provides the "LiberationMono Regular" TrueType font
// from the Liberation font family.
package liberationmonoregular // import "github.com/go-fonts/liberation/liberationmonoregular"
import _ "embed"
// TTF is the data for the "LiberationMono Regular" TrueType font.
//
//go:embed LiberationMono-Regular.ttf
var TTF []byte

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationsansbold provides the "LiberationSans Bold" TrueType font
// from the Liberation font family.
package liberationsansbold // import "github.com/go-fonts/liberation/liberationsansbold"
import _ "embed"
// TTF is the data for the "LiberationSans Bold" TrueType font.
//
//go:embed LiberationSans-Bold.ttf
var TTF []byte

View File

@@ -1,12 +0,0 @@
// generated by go run gen-fonts.go; DO NOT EDIT
// Package liberationsansbolditalic provides the "LiberationSans BoldItalic" TrueType font
// from the Liberation font family.
package liberationsansbolditalic // import "github.com/go-fonts/liberation/liberationsansbolditalic"
import _ "embed"
// TTF is the data for the "LiberationSans BoldItalic" TrueType font.
//
//go:embed LiberationSans-BoldItalic.ttf
var TTF []byte

Some files were not shown because too many files have changed in this diff Show More