├── .gitignore ├── imaging ├── resize │ ├── sinc_test.go │ ├── LICENSE │ ├── resize_test.go │ ├── sinc.go │ ├── converter.go │ ├── README.md │ ├── resize.go │ └── filters.go └── optimalResize │ ├── optimalResize.go │ └── optimalCrop.go ├── LICENSE ├── README.md ├── fsnotify ├── fsnotify.go ├── fsnotify_linux.go ├── fsnotify_bsd.go └── fsnotify_windows.go ├── imageResizeDaemon └── imageResizeDaemon.go └── imageResizer └── imageResizer.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /imaging/resize/sinc_test.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | const limit = 1e-12 10 | 11 | func Test_SincOne(t *testing.T) { 12 | zero := Sinc(1) 13 | if zero >= limit { 14 | t.Error("Sinc(1) != 0") 15 | } 16 | } 17 | 18 | func Test_SincZero(t *testing.T) { 19 | one := Sinc(0) 20 | if math.Abs(one-1) >= limit { 21 | t.Error("Sinc(0) != 1") 22 | } 23 | } 24 | 25 | func Test_SincDotOne(t *testing.T) { 26 | res := Sinc(0.1) 27 | if math.Abs(res-0.983631643083466) >= limit { 28 | t.Error("Sinc(0.1) wrong") 29 | } 30 | } 31 | 32 | func Test_SincNearZero(t *testing.T) { 33 | res := Sinc(0.000001) 34 | if math.Abs(res-0.9999999999983551) >= limit { 35 | fmt.Println(res) 36 | t.Error("Sinc near zero not stable") 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /imaging/resize/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Jan Schlicht 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Ghassen Hamrouni 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /imaging/resize/resize_test.go: -------------------------------------------------------------------------------- 1 | package resize 2 | 3 | import ( 4 | "image" 5 | "image/color" 6 | "runtime" 7 | "testing" 8 | ) 9 | 10 | var img = image.NewGray16(image.Rect(0, 0, 3, 3)) 11 | 12 | func init() { 13 | runtime.GOMAXPROCS(runtime.NumCPU()) 14 | img.Set(1, 1, color.White) 15 | } 16 | 17 | func Test_Nearest(t *testing.T) { 18 | m := Resize(6, 0, img, NearestNeighbor) 19 | if m.At(2, 2) == m.At(3, 3) { 20 | t.Fail() 21 | } 22 | } 23 | 24 | func Test_Param1(t *testing.T) { 25 | m := Resize(0, 0, img, NearestNeighbor) 26 | if m.Bounds() != img.Bounds() { 27 | t.Fail() 28 | } 29 | } 30 | 31 | func Test_Param2(t *testing.T) { 32 | m := Resize(100, 0, img, NearestNeighbor) 33 | if m.Bounds() != image.Rect(0, 0, 100, 100) { 34 | t.Fail() 35 | } 36 | } 37 | 38 | func Test_ZeroImg(t *testing.T) { 39 | zeroImg := image.NewGray16(image.Rect(0, 0, 0, 0)) 40 | 41 | m := Resize(0, 0, zeroImg, NearestNeighbor) 42 | if m.Bounds() != zeroImg.Bounds() { 43 | t.Fail() 44 | } 45 | } 46 | 47 | func Benchmark_BigResize(b *testing.B) { 48 | var m image.Image 49 | for i := 0; i < b.N; i++ { 50 | m = Resize(1000, 1000, img, Lanczos3) 51 | } 52 | m.At(0, 0) 53 | } 54 | 55 | func Benchmark_Reduction(b *testing.B) { 56 | largeImg := image.NewRGBA(image.Rect(0, 0, 1000, 1000)) 57 | 58 | var m image.Image 59 | for i := 0; i < b.N; i++ { 60 | m = Resize(300, 300, largeImg, Bicubic) 61 | } 62 | m.At(0, 0) 63 | } 64 | -------------------------------------------------------------------------------- /imaging/resize/sinc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Jan Schlicht 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package resize 18 | 19 | import ( 20 | "math" 21 | ) 22 | 23 | var ( 24 | epsilon = math.Nextafter(1.0, 2.0) - 1.0 // machine epsilon 25 | taylor2bound = math.Sqrt(epsilon) 26 | taylorNbound = math.Sqrt(taylor2bound) 27 | ) 28 | 29 | // unnormalized sinc function 30 | func Sinc1(x float64) (y float64) { 31 | if math.Abs(x) >= taylorNbound { 32 | y = math.Sin(x) / x 33 | } else { 34 | y = 1.0 35 | if math.Abs(x) >= epsilon { 36 | x2 := x * x 37 | y -= x2 / 6.0 38 | if math.Abs(x) >= taylor2bound { 39 | y += (x2 * x2) / 120.0 40 | } 41 | } 42 | } 43 | return 44 | } 45 | 46 | // normalized sinc function 47 | func Sinc(x float64) float64 { 48 | return Sinc1(x * math.Pi) 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## OptimalCrop -- Go library for intelligent image re-sizing and cropping 2 | 3 | 4 | OptimalCrop is a Go library for croping and re-sizing the image by locating the interesting parts. 5 | 6 | ### Testing 7 | 8 | Once you build the project you can use imageResizer: 9 | 10 | imageResizer -in inputDir -out outputDir -width 128 -height 128 11 | 12 | ### How It Works 13 | 14 | The interestingness of an image is subjective and may vary from one person to another. One way to quantitatively measure the interestingness is to measure the informations contained in that image. I thought that an interesting region of an image is a zone that carries a lot of informations. We need to be able to calculate the information at each individual pixel of the image to find out the information of a particular region. 15 | One could calculate the information of the pixel based on information theoretic definition: 16 | 17 | I = -log(p(i)) 18 | where i is the pixel, I is the self information and p(i) is the probability of occurrence of the pixel i in our image. 19 | 20 | The probability of occurrence is simply the frequency of that particular pixel. An efficient way to calculate the probability is using a normalized histogram. The histogram stores the frequency of an intensity measure of the pixel. In our case we convert the RGB image to the CIELAB color space. A color space invented by the CIE (Commission internationale de l'éclairage). It describes all the colors visible to the human eye. 21 | The problem is reduced to maximizing the total information in a region R(h,w). Or equivalently to find a region of width w and height h with max information. In order to find that region we compute the information per line (i.e. the sum of the info of the pixels in that line) and the information per column. 22 | For this reason you need only to know how to find the maximum sum subsequence for the lines and the columns. Fortunately this is a well known problem that can be solved in linear time O(n). 23 | -------------------------------------------------------------------------------- /fsnotify/fsnotify.go: -------------------------------------------------------------------------------- 1 | package fsnotify 2 | 3 | import "fmt" 4 | 5 | const ( 6 | FSN_CREATE = 1 7 | FSN_MODIFY = 2 8 | FSN_DELETE = 4 9 | FSN_RENAME = 8 10 | 11 | FSN_ALL = FSN_MODIFY | FSN_DELETE | FSN_RENAME | FSN_CREATE 12 | ) 13 | 14 | // Purge events from interal chan to external chan if passes filter 15 | func (w *Watcher) purgeEvents() { 16 | for ev := range w.internalEvent { 17 | sendEvent := false 18 | fsnFlags := w.fsnFlags[ev.Name] 19 | 20 | if (fsnFlags&FSN_CREATE == FSN_CREATE) && ev.IsCreate() { 21 | sendEvent = true 22 | } 23 | 24 | if (fsnFlags&FSN_MODIFY == FSN_MODIFY) && ev.IsModify() { 25 | sendEvent = true 26 | } 27 | 28 | if (fsnFlags&FSN_DELETE == FSN_DELETE) && ev.IsDelete() { 29 | sendEvent = true 30 | } 31 | 32 | if (fsnFlags&FSN_RENAME == FSN_RENAME) && ev.IsRename() { 33 | //w.RemoveWatch(ev.Name) 34 | sendEvent = true 35 | } 36 | 37 | if sendEvent { 38 | w.Event <- ev 39 | } 40 | } 41 | 42 | close(w.Event) 43 | } 44 | 45 | // Watch a given file path 46 | func (w *Watcher) Watch(path string) error { 47 | w.fsnFlags[path] = FSN_ALL 48 | return w.watch(path) 49 | } 50 | 51 | // Watch a given file path for a particular set of notifications (FSN_MODIFY etc.) 52 | func (w *Watcher) WatchFlags(path string, flags uint32) error { 53 | w.fsnFlags[path] = flags 54 | return w.watch(path) 55 | } 56 | 57 | // Remove a watch on a file 58 | func (w *Watcher) RemoveWatch(path string) error { 59 | delete(w.fsnFlags, path) 60 | return w.removeWatch(path) 61 | } 62 | 63 | // String formats the event e in the form 64 | // "filename: DELETE|MODIFY|..." 65 | func (e *FileEvent) String() string { 66 | var events string = "" 67 | 68 | if e.IsCreate() { 69 | events += "|" + "CREATE" 70 | } 71 | 72 | if e.IsDelete() { 73 | events += "|" + "DELETE" 74 | } 75 | 76 | if e.IsModify() { 77 | events += "|" + "MODIFY" 78 | } 79 | 80 | if e.IsRename() { 81 | events += "|" + "RENAME" 82 | } 83 | 84 | if len(events) > 0 { 85 | events = events[1:] 86 | } 87 | 88 | return fmt.Sprintf("%q: %s", e.Name, events) 89 | } 90 | -------------------------------------------------------------------------------- /imaging/optimalResize/optimalResize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Ghassen Hamrouni 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package optimalResize 18 | 19 | import ( 20 | "image" 21 | "imaging/resize" 22 | "math" 23 | ) 24 | 25 | import _ "image/jpeg" 26 | 27 | func OptimalResize(b image.Image, rectX int, rectY int, maxIter int) image.Image { 28 | 29 | bounds := b.Bounds() 30 | width, height := bounds.Max.X, bounds.Max.Y 31 | 32 | aspect := float64(rectX) / float64(rectY) 33 | 34 | minInfoRatio := 0.85 35 | 36 | var W, H float64 37 | 38 | W = float64(width) 39 | H = float64(height) 40 | 41 | // Match the aspect ratio 42 | if aspect < 1.0 { 43 | H = aspect * W 44 | } else if aspect > 1 { 45 | W = (1.0 / aspect) * H 46 | } else { 47 | H = math.Min(H, W) 48 | W = H 49 | } 50 | 51 | // Crop the image to match the aspect ratio 52 | cropRegion := FindOptimalCropRegion(&b, int(H), int(W)) 53 | 54 | // If initial cropping incur an information loss > 20% 55 | // crop the image to match the aspect ratio but don t 56 | // proceed to successive cropping 57 | if cropRegion.confidenceX < minInfoRatio || cropRegion.confidenceY < minInfoRatio { 58 | 59 | mpix := b.(*image.NRGBA) 60 | b = mpix.SubImage(cropRegion.region) 61 | } 62 | 63 | // Perform successive cropping if there is enough information left 64 | for maxIter > 0 && cropRegion.confidenceX > minInfoRatio && 65 | cropRegion.confidenceY > minInfoRatio { 66 | 67 | mpix := b.(*image.NRGBA) 68 | b = mpix.SubImage(cropRegion.region) 69 | 70 | var w, h float64 71 | w, h = float64(b.Bounds().Max.X), float64(b.Bounds().Max.Y) 72 | 73 | cropRegion = FindOptimalCropRegion(&b, int(w*0.7), int(h*0.7)) 74 | 75 | maxIter-- 76 | } 77 | 78 | b = resize.Resize(uint(rectX), uint(rectY), b, resize.MitchellNetravali) 79 | return b 80 | } 81 | -------------------------------------------------------------------------------- /imaging/resize/converter.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Jan Schlicht 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package resize 18 | 19 | import ( 20 | "image" 21 | "image/color" 22 | ) 23 | 24 | type colorArray [4]float32 25 | 26 | // converter allows to retrieve 27 | // a colorArray for points of an image 28 | type converter interface { 29 | at(x, y int) colorArray 30 | } 31 | 32 | type genericConverter struct { 33 | src image.Image 34 | } 35 | 36 | func replicateBorder1d(x, min, max int) int { 37 | if x < min { 38 | x = min 39 | } else if x >= max { 40 | x = max - 1 41 | } 42 | 43 | return x 44 | } 45 | 46 | func replicateBorder(x, y int, rect image.Rectangle) (xx, yy int) { 47 | xx = replicateBorder1d(x, rect.Min.X, rect.Max.X) 48 | yy = replicateBorder1d(y, rect.Min.Y, rect.Max.Y) 49 | return 50 | } 51 | 52 | func (c *genericConverter) at(x, y int) colorArray { 53 | r, g, b, a := c.src.At(replicateBorder(x, y, c.src.Bounds())).RGBA() 54 | return colorArray{ 55 | float32(r), 56 | float32(g), 57 | float32(b), 58 | float32(a), 59 | } 60 | } 61 | 62 | type rgbaConverter struct { 63 | src *image.RGBA 64 | } 65 | 66 | func (c *rgbaConverter) at(x, y int) colorArray { 67 | i := c.src.PixOffset(replicateBorder(x, y, c.src.Rect)) 68 | return colorArray{ 69 | float32(uint16(c.src.Pix[i+0])<<8 | uint16(c.src.Pix[i+0])), 70 | float32(uint16(c.src.Pix[i+1])<<8 | uint16(c.src.Pix[i+1])), 71 | float32(uint16(c.src.Pix[i+2])<<8 | uint16(c.src.Pix[i+2])), 72 | float32(uint16(c.src.Pix[i+3])<<8 | uint16(c.src.Pix[i+3])), 73 | } 74 | } 75 | 76 | type rgba64Converter struct { 77 | src *image.RGBA64 78 | } 79 | 80 | func (c *rgba64Converter) at(x, y int) colorArray { 81 | i := c.src.PixOffset(replicateBorder(x, y, c.src.Rect)) 82 | return colorArray{ 83 | float32(uint16(c.src.Pix[i+0])<<8 | uint16(c.src.Pix[i+1])), 84 | float32(uint16(c.src.Pix[i+2])<<8 | uint16(c.src.Pix[i+3])), 85 | float32(uint16(c.src.Pix[i+4])<<8 | uint16(c.src.Pix[i+5])), 86 | float32(uint16(c.src.Pix[i+6])<<8 | uint16(c.src.Pix[i+7])), 87 | } 88 | } 89 | 90 | type grayConverter struct { 91 | src *image.Gray 92 | } 93 | 94 | func (c *grayConverter) at(x, y int) colorArray { 95 | i := c.src.PixOffset(replicateBorder(x, y, c.src.Rect)) 96 | g := float32(uint16(c.src.Pix[i])<<8 | uint16(c.src.Pix[i])) 97 | return colorArray{ 98 | g, 99 | g, 100 | g, 101 | float32(0xffff), 102 | } 103 | } 104 | 105 | type gray16Converter struct { 106 | src *image.Gray16 107 | } 108 | 109 | func (c *gray16Converter) at(x, y int) colorArray { 110 | i := c.src.PixOffset(replicateBorder(x, y, c.src.Rect)) 111 | g := float32(uint16(c.src.Pix[i+0])<<8 | uint16(c.src.Pix[i+1])) 112 | return colorArray{ 113 | g, 114 | g, 115 | g, 116 | float32(0xffff), 117 | } 118 | } 119 | 120 | type ycbcrConverter struct { 121 | src *image.YCbCr 122 | } 123 | 124 | func (c *ycbcrConverter) at(x, y int) colorArray { 125 | xx, yy := replicateBorder(x, y, c.src.Rect) 126 | yi := c.src.YOffset(xx, yy) 127 | ci := c.src.COffset(xx, yy) 128 | r, g, b := color.YCbCrToRGB(c.src.Y[yi], c.src.Cb[ci], c.src.Cr[ci]) 129 | return colorArray{ 130 | float32(uint16(r) * 0x101), 131 | float32(uint16(g) * 0x101), 132 | float32(uint16(b) * 0x101), 133 | float32(0xffff), 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /imaging/resize/README.md: -------------------------------------------------------------------------------- 1 | Resize 2 | ====== 3 | 4 | Image resizing for the [Go programming language](http://golang.org) that includes a few interpolation methods. 5 | 6 | Installation 7 | ------------ 8 | 9 | ```bash 10 | $ go get github.com/nfnt/resize 11 | ``` 12 | 13 | It's that easy! 14 | 15 | Usage 16 | ----- 17 | 18 | Import package with 19 | 20 | ```go 21 | import "github.com/nfnt/resize" 22 | ``` 23 | 24 | Resize creates a scaled image with new dimensions (`width`, `height`) using the interpolation function `interp`. 25 | If either `width` or `height` is set to 0, it will be set to an aspect ratio preserving value. 26 | 27 | ```go 28 | resize.Resize(width, height uint, img image.Image, interp resize.InterpolationFunction) image.Image 29 | ``` 30 | 31 | The provided interpolation functions are 32 | 33 | - `NearestNeighbor`: [Nearest-neighbor interpolation](http://en.wikipedia.org/wiki/Nearest-neighbor_interpolation) 34 | - `Bilinear`: [Bilinear interpolation](http://en.wikipedia.org/wiki/Bilinear_interpolation) 35 | - `Bicubic`: [Bicubic interpolation](http://en.wikipedia.org/wiki/Bicubic_interpolation) 36 | - `MitchellNetravali`: [Mitchell-Netravali interpolation](http://dl.acm.org/citation.cfm?id=378514) 37 | - `Lanczos2`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=2 38 | - `Lanczos3`: [Lanczos resampling](http://en.wikipedia.org/wiki/Lanczos_resampling) with a=3 39 | 40 | Sample usage: 41 | 42 | ```go 43 | package main 44 | 45 | import ( 46 | "github.com/nfnt/resize" 47 | "image/jpeg" 48 | "log" 49 | "os" 50 | ) 51 | 52 | func main() { 53 | // open "test.jpg" 54 | file, err := os.Open("test.jpg") 55 | if err != nil { 56 | log.Fatal(err) 57 | } 58 | 59 | // decode jpeg into image.Image 60 | img, err := jpeg.Decode(file) 61 | if err != nil { 62 | log.Fatal(err) 63 | } 64 | file.Close() 65 | 66 | // resize to width 1000 using Lanczos resampling 67 | // and preserve aspect ratio 68 | m := resize.Resize(1000, 0, img, resize.Lanczos3) 69 | 70 | out, err := os.Create("test_resized.jpg") 71 | if err != nil { 72 | log.Fatal(err) 73 | } 74 | defer out.Close() 75 | 76 | // write new image to file 77 | jpeg.Encode(out, m, nil) 78 | } 79 | ``` 80 | 81 | Downsizing Samples 82 | ------- 83 | 84 | Downsizing is not as simple as it might look like. Images have to be filtered before they are scaled down, otherwise aliasing might occur. 85 | Filtering is highly subjective: Applying too much will blur the whole image, too little will make aliasing become apparent. 86 | Resize tries to provide sane defaults that should suffice in most cases. 87 | 88 | ### Artificial sample 89 | 90 | Original image 91 | ![Rings](http://nfnt.github.com/img/rings_lg_orig.png) 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 |

Nearest-Neighbor

Bilinear

Bicubic

Mitchell-Netravali

Lanczos2

Lanczos3
107 | 108 | ### Real-Life sample 109 | 110 | Original image 111 | ![Original](http://nfnt.github.com/img/IMG_3694_720.jpg) 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |

Nearest-Neighbor

Bilinear

Bicubic

Mitchell-Netravali

Lanczos2

Lanczos3
127 | 128 | 129 | License 130 | ------- 131 | 132 | Copyright (c) 2012 Jan Schlicht 133 | Resize is released under an MIT style license. 134 | -------------------------------------------------------------------------------- /imageResizeDaemon/imageResizeDaemon.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Ghassen Hamrouni 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "flag" 22 | "fmt" 23 | "fsnotify" 24 | "image" 25 | "image/draw" 26 | "image/png" 27 | "imaging/optimalResize" 28 | "log" 29 | "os" 30 | "path/filepath" 31 | "time" 32 | ) 33 | 34 | import _ "image/jpeg" 35 | import _ "image/gif" 36 | 37 | // Resize and crop an image 38 | func resizeImage(inputFile string, outputFile string, width int, height int) { 39 | 40 | nTrials := 4 41 | 42 | success := false 43 | var imagefile *os.File 44 | 45 | // Only three image formats are supported (png, jpg, gif) 46 | if filepath.Ext(inputFile) == ".png" || 47 | filepath.Ext(inputFile) == ".jpg" || 48 | filepath.Ext(inputFile) == ".gif" { 49 | 50 | for nTrials > 0 && !success { 51 | 52 | success = true 53 | file, err := os.Open(inputFile) 54 | 55 | // TODO: implement a saner way to handle 56 | // oprn errors 57 | if err != nil { 58 | fmt.Printf("Open error \n") 59 | log.Println(err) // Don't use log.Fatal to exit 60 | 61 | success = false 62 | 63 | // give the system time to sync write change 64 | // sleep for some time before retry 65 | time.Sleep(500 * time.Millisecond) 66 | } 67 | 68 | nTrials-- 69 | imagefile = file 70 | } 71 | 72 | // After multiple trials the system is unable 73 | // to access the file. 74 | if !success { 75 | return 76 | } 77 | 78 | defer imagefile.Close() 79 | 80 | // Decode the image. 81 | m, _, err := image.Decode(imagefile) 82 | if err != nil { 83 | fmt.Printf("Decode error \n") 84 | log.Println(err) // Don't use log.Fatal to exit 85 | 86 | return 87 | } 88 | 89 | b := m.Bounds() 90 | 91 | // All images are converted to the NRGBA type 92 | rgbaImage := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) 93 | draw.Draw(rgbaImage, rgbaImage.Bounds(), m, b.Min, draw.Src) 94 | 95 | // Perform an optimal resize with 4 iterations 96 | m2 := optimalResize.OptimalResize(rgbaImage, width, height, 4) 97 | 98 | fo, err := os.Create(outputFile) 99 | 100 | if err != nil { 101 | panic(err) 102 | } 103 | 104 | defer fo.Close() 105 | w := bufio.NewWriter(fo) 106 | 107 | defer w.Flush() 108 | png.Encode(w, m2) 109 | } 110 | } 111 | 112 | func main() { 113 | 114 | // Read the CMD options 115 | 116 | inDir := flag.String("in", "", "input directory") // input directory 117 | outDir := flag.String("out", "", "output directory") // output directory 118 | width := flag.Int("width", 128, "the new width") // width 119 | height := flag.Int("height", 128, "the new height") // height 120 | 121 | flag.Parse() 122 | 123 | if *inDir == "" || *outDir == "" { 124 | log.Fatal("usage: \n imageResizer -in inputDir -out outputDir -width 128 -height 128") 125 | } 126 | 127 | // Print the cmd options 128 | 129 | fmt.Printf("image resize daemon \n") 130 | 131 | fmt.Printf("Input: %s \n", *inDir) 132 | fmt.Printf("Output: %s \n", *outDir) 133 | fmt.Printf("Width: %d \n", *width) 134 | fmt.Printf("Height: %d \n", *height) 135 | 136 | // Create a channel to use it to handle the idle state 137 | c := make(chan int) 138 | 139 | watcher, err := fsnotify.NewWatcher() 140 | if err != nil { 141 | log.Fatal(err) 142 | } 143 | 144 | defer watcher.Close() 145 | 146 | // Process file events 147 | go func() { 148 | for { 149 | select { 150 | case ev := <-watcher.Event: 151 | 152 | log.Println("event:", ev) 153 | 154 | if ev.IsCreate() { 155 | // Combine the directory path with the filename to get 156 | // the full path of the image 157 | fullImagePath := ev.Name 158 | fullImageOutPath := filepath.Join(*outDir, filepath.Base(ev.Name)) 159 | 160 | // Launch asychronously a resize operation 161 | go resizeImage(fullImagePath, fullImageOutPath, *width, *height) 162 | } 163 | case err := <-watcher.Error: 164 | log.Println("error:", err) 165 | } 166 | } 167 | }() 168 | 169 | err = watcher.Watch(*inDir) 170 | 171 | if err != nil { 172 | log.Fatal(err) 173 | } 174 | 175 | <-c 176 | } 177 | -------------------------------------------------------------------------------- /imaging/optimalResize/optimalCrop.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Ghassen Hamrouni 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package optimalResize 18 | 19 | import ( 20 | "image" 21 | "math" 22 | ) 23 | 24 | import _ "image/jpeg" 25 | 26 | // Represents a region to be cropped in the input image 27 | type croppingArea struct { 28 | region image.Rectangle 29 | /// Represents the preserved information (on x-axis) When 30 | /// the image is cropped 31 | confidenceX float64 32 | /// Represents the preserved information (on y-axis) When 33 | // the image is cropped 34 | confidenceY float64 35 | } 36 | 37 | // Find the max subinterval with length = intervalSize 38 | // It takes O(n) operations to find the interval 39 | func FindMaxSubInterval(data []float64, intervalSize int) (T int, preservedInfo float64) { 40 | 41 | n := len(data) 42 | cumulativeData := make([]float64, n) 43 | 44 | cumulativeData[0] = data[0] 45 | 46 | // Compute the cumulative information 47 | // this data structure is used to accelerate 48 | // computations 49 | for i := 1; i < n; i++ { 50 | cumulativeData[i] = cumulativeData[i-1] + data[i] 51 | } 52 | 53 | T = 0 54 | 55 | var maxSum int64 56 | var depth int 57 | 58 | maxSum = 0 59 | depth = 0 60 | 61 | // Find an optimal interval in O(n) 62 | for i := intervalSize; i < n; i++ { 63 | var sum int64 64 | sum = 0 65 | 66 | if (i - intervalSize) == 0 { 67 | sum = int64(cumulativeData[i]) 68 | } else { 69 | sum = int64(cumulativeData[i] - cumulativeData[i-intervalSize-1]) 70 | } 71 | 72 | if sum > maxSum { 73 | maxSum = sum 74 | T = i - intervalSize 75 | depth = 0 76 | } else if sum == maxSum { 77 | depth++ 78 | } else { 79 | if depth > 0 { 80 | T = T + depth/2 81 | depth = 0 82 | } 83 | } 84 | } 85 | 86 | if depth > 0 { 87 | T = T + depth/2 88 | } 89 | 90 | preservedInfo = float64(maxSum) / cumulativeData[n-1] 91 | 92 | return 93 | } 94 | 95 | func CalulatePixelIntensity(m *image.Image, x int, y int) int { 96 | r, g, b, _ := (*m).At(x, y).RGBA() 97 | 98 | // I is the intensity of the pixel 99 | // measured with Manhattan distance 100 | return int(255.0 * (r + g + b) / (65535.0 * 3.0)) 101 | } 102 | 103 | func FindOptimalCropRegion(m *image.Image, rectX int, rectY int) croppingArea { 104 | 105 | bounds := (*m).Bounds() 106 | 107 | width, height := bounds.Max.X, bounds.Max.Y 108 | 109 | // The histogram of the image 110 | var H [256]float64 111 | 112 | // The self-informations of lines/columns 113 | Hx := make([]float64, width) 114 | Hy := make([]float64, height) 115 | 116 | for y := 0; y < 256; y++ { 117 | H[y] = 0 118 | } 119 | 120 | for i := 0; i < width; i++ { 121 | Hx[i] = 0 122 | } 123 | 124 | for i := 0; i < height; i++ { 125 | Hy[i] = 0 126 | } 127 | 128 | // Compute the histogram of the image 129 | for x := 0; x < width; x++ { 130 | for y := 0; y < height; y++ { 131 | 132 | // I is the intensity of the pixel 133 | // measured with Manhattan distance 134 | I := CalulatePixelIntensity(m, x, y) 135 | 136 | // Increment the histogram 137 | H[I] += 1 138 | } 139 | } 140 | 141 | // 142 | // Normalize the histogram 143 | // 144 | sum := 0.0 145 | 146 | for y := 0; y < 256; y++ { 147 | sum += H[y] 148 | } 149 | 150 | for y := 0; y < 256; y++ { 151 | H[y] = H[y] / sum 152 | } 153 | 154 | // Compute the self-information for line/columns 155 | for x := 0; x < width; x++ { 156 | for y := 0; y < height; y++ { 157 | 158 | I := CalulatePixelIntensity(m, x, y) 159 | 160 | // H[I] = the probability of the pixel 161 | // -log(p) = the information in the pixel 162 | Hx[x] += -math.Log(H[I]) 163 | Hy[y] += -math.Log(H[I]) 164 | } 165 | } 166 | 167 | // The x-coordinate of the optimal 168 | // cropping region 169 | Tx, preservedInfoX := FindMaxSubInterval(Hx, rectX) 170 | 171 | // The y-coordinate of the optimal 172 | // cropping region 173 | Ty, preservedInfoY := FindMaxSubInterval(Hy, rectY) 174 | 175 | if rectX >= width { 176 | preservedInfoX = 1 177 | } 178 | 179 | if rectY >= height { 180 | preservedInfoY = 1 181 | } 182 | 183 | // Return the solution 184 | return croppingArea{ 185 | region: image.Rect(Tx, Ty, Tx + rectY, Ty + rectX), 186 | confidenceX: preservedInfoX, 187 | confidenceY: preservedInfoY} 188 | } 189 | -------------------------------------------------------------------------------- /imaging/resize/resize.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Jan Schlicht 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | // Package resize implements various image resizing methods. 18 | // 19 | // The package works with the Image interface described in the image package. 20 | // Various interpolation methods are provided and multiple processors may be 21 | // utilized in the computations. 22 | // 23 | // Example: 24 | // imgResized := resize.Resize(1000, 0, imgOld, resize.MitchellNetravali) 25 | package resize 26 | 27 | import ( 28 | "image" 29 | "image/color" 30 | "runtime" 31 | ) 32 | 33 | // Trans2 is a 2-dimensional linear transformation. 34 | type Trans2 [6]float32 35 | 36 | // Apply the transformation to a point (x,y). 37 | func (t *Trans2) Eval(x, y float32) (u, v float32) { 38 | u = t[0]*x + t[1]*y + t[2] 39 | v = t[3]*x + t[4]*y + t[5] 40 | return 41 | } 42 | 43 | // Filter can interpolate at points (x,y) 44 | type Filter interface { 45 | Interpolate(x, y float32) color.RGBA64 46 | } 47 | 48 | // InterpolationFunction return a Filter implementation 49 | // that operates on an image. Two factors 50 | // allow to scale the filter kernels in x- and y-direction 51 | // to prevent moire patterns. 52 | type InterpolationFunction func(image.Image, [2]float32) Filter 53 | 54 | // Resize an image to new width and height using the interpolation function interp. 55 | // A new image with the given dimensions will be returned. 56 | // If one of the parameters width or height is set to 0, its size will be calculated so that 57 | // the aspect ratio is that of the originating image. 58 | // The resizing algorithm uses channels for parallel computation. 59 | func Resize(width, height uint, img image.Image, interp InterpolationFunction) image.Image { 60 | oldBounds := img.Bounds() 61 | oldWidth := float32(oldBounds.Dx()) 62 | oldHeight := float32(oldBounds.Dy()) 63 | 64 | scaleX, scaleY := calcFactors(width, height, oldWidth, oldHeight) 65 | t := Trans2{scaleX, 0, float32(oldBounds.Min.X), 0, scaleY, float32(oldBounds.Min.Y)} 66 | 67 | resizedImg := image.NewRGBA64(image.Rect(0, 0, int(oldWidth/scaleX), int(oldHeight/scaleY))) 68 | b := resizedImg.Bounds() 69 | 70 | n := numJobs(b.Dy()) 71 | c := make(chan int, n) 72 | for i := 0; i < n; i++ { 73 | go func(b image.Rectangle, c chan int) { 74 | filter := interp(img, [2]float32{clampFactor(scaleX), clampFactor(scaleY)}) 75 | var u, v float32 76 | var color color.RGBA64 77 | for y := b.Min.Y; y < b.Max.Y; y++ { 78 | for x := b.Min.X; x < b.Max.X; x++ { 79 | u, v = t.Eval(float32(x), float32(y)) 80 | color = filter.Interpolate(u, v) 81 | 82 | i := resizedImg.PixOffset(x, y) 83 | resizedImg.Pix[i+0] = uint8(color.R >> 8) 84 | resizedImg.Pix[i+1] = uint8(color.R) 85 | resizedImg.Pix[i+2] = uint8(color.G >> 8) 86 | resizedImg.Pix[i+3] = uint8(color.G) 87 | resizedImg.Pix[i+4] = uint8(color.B >> 8) 88 | resizedImg.Pix[i+5] = uint8(color.B) 89 | resizedImg.Pix[i+6] = uint8(color.A >> 8) 90 | resizedImg.Pix[i+7] = uint8(color.A) 91 | } 92 | } 93 | c <- 1 94 | }(image.Rect(b.Min.X, b.Min.Y+i*(b.Dy())/n, b.Max.X, b.Min.Y+(i+1)*(b.Dy())/n), c) 95 | } 96 | 97 | for i := 0; i < n; i++ { 98 | <-c 99 | } 100 | 101 | return resizedImg 102 | } 103 | 104 | // Calculate scaling factors using old and new image dimensions. 105 | func calcFactors(width, height uint, oldWidth, oldHeight float32) (scaleX, scaleY float32) { 106 | if width == 0 { 107 | if height == 0 { 108 | scaleX = 1.0 109 | scaleY = 1.0 110 | } else { 111 | scaleY = oldHeight / float32(height) 112 | scaleX = scaleY 113 | } 114 | } else { 115 | scaleX = oldWidth / float32(width) 116 | if height == 0 { 117 | scaleY = scaleX 118 | } else { 119 | scaleY = oldHeight / float32(height) 120 | } 121 | } 122 | return 123 | } 124 | 125 | // Set filter scaling factor to avoid moire patterns. 126 | // This is only useful in case of downscaling (factor>1). 127 | func clampFactor(factor float32) float32 { 128 | if factor < 1 { 129 | factor = 1 130 | } 131 | return factor 132 | } 133 | 134 | // Set number of parallel jobs 135 | // but prevent resize from doing too much work 136 | // if #CPUs > width 137 | func numJobs(d int) (n int) { 138 | n = runtime.NumCPU() 139 | if n > d { 140 | n = d 141 | } 142 | return 143 | } 144 | -------------------------------------------------------------------------------- /imageResizer/imageResizer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Ghassen Hamrouni 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bufio" 21 | "flag" 22 | "fmt" 23 | "image" 24 | "image/png" 25 | "image/draw" 26 | "imaging/optimalResize" 27 | "log" 28 | "os" 29 | "os/exec" 30 | "path/filepath" 31 | "time" 32 | ) 33 | 34 | import _ "image/jpeg" 35 | import _ "image/gif" 36 | 37 | // Resize and crop an image 38 | func resizeImage(inputFile string, outputFile string, width int, height int) { 39 | 40 | nTrials := 4 41 | 42 | success := false 43 | var imagefile *os.File 44 | 45 | // Only three image formats are supported (png, jpg, gif) 46 | if filepath.Ext(inputFile) == ".png" || 47 | filepath.Ext(inputFile) == ".jpg" || 48 | filepath.Ext(inputFile) == ".gif" { 49 | 50 | for nTrials > 0 && !success { 51 | 52 | success = true 53 | file, err := os.Open(inputFile) 54 | 55 | // TODO: implement a saner way to handle 56 | // oprn errors 57 | if err != nil { 58 | fmt.Printf("Open error \n") 59 | log.Println(err) // Don't use log.Fatal to exit 60 | 61 | success = false 62 | 63 | // give the system time to sync write change 64 | // sleep for some time before retry 65 | time.Sleep(500 * time.Millisecond) 66 | } 67 | 68 | nTrials-- 69 | imagefile = file 70 | } 71 | 72 | // After multiple trials the system is unable 73 | // to access the file. 74 | if !success { 75 | return 76 | } 77 | 78 | defer imagefile.Close() 79 | 80 | // Decode the image. 81 | m, _, err := image.Decode(imagefile) 82 | if err != nil { 83 | fmt.Printf("Decode error \n") 84 | log.Println(err) 85 | 86 | // If we fail at decoding the image this is probably 87 | // caused by an unsupported jpeg features: 88 | // 1) Progressive mode 89 | // 2) Wrong SOS marker 90 | // Invoke the cjpeg command 91 | cmd := exec.Command("djpeg", inputFile, "-gif", inputFile + ".gif") 92 | err = cmd.Run() 93 | 94 | if err != nil { 95 | log.Println(err) 96 | return 97 | } else { 98 | imagefile, _ := os.Open(inputFile + ".gif") 99 | m, _, err = image.Decode(imagefile) 100 | 101 | if err != nil { 102 | fmt.Printf("Decode error \n") 103 | log.Println(err) 104 | 105 | return 106 | } 107 | } 108 | } 109 | 110 | b := m.Bounds() 111 | 112 | // All images are converted to the NRGBA type 113 | rgbaImage := image.NewNRGBA(image.Rect(0, 0, b.Dx(), b.Dy())) 114 | draw.Draw(rgbaImage, rgbaImage.Bounds(), m, b.Min, draw.Src) 115 | 116 | // Perform an optimal resize with 4 iterations 117 | m2 := optimalResize.OptimalResize(rgbaImage, width, height, 4) 118 | 119 | fo, err := os.Create(outputFile) 120 | 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | defer fo.Close() 126 | w := bufio.NewWriter(fo) 127 | 128 | defer w.Flush() 129 | png.Encode(w, m2) 130 | } 131 | } 132 | 133 | func main() { 134 | 135 | t0 := time.Now() 136 | 137 | // Read the CMD options 138 | 139 | inDir := flag.String("in", "", "input directory") // input directory 140 | outDir := flag.String("out", "", "output directory") // output directory 141 | width := flag.Int("width", 128, "the new width") // width 142 | height := flag.Int("height", 128, "the new height") // height 143 | 144 | flag.Parse() 145 | 146 | if *inDir == "" || *outDir == "" { 147 | log.Fatal("usage: \n imageResizer -in inputDir -out outputDir -width 128 -height 128") 148 | } 149 | 150 | // Print the cmd options 151 | 152 | fmt.Printf("image resize daemon \n") 153 | 154 | fmt.Printf("Input: %s \n", *inDir) 155 | fmt.Printf("Output: %s \n", *outDir) 156 | fmt.Printf("Width: %d \n", *width) 157 | fmt.Printf("Height: %d \n", *height) 158 | 159 | d, err := os.Open(*inDir) 160 | if err != nil { 161 | fmt.Println(err) 162 | os.Exit(1) 163 | } 164 | 165 | fi, err := d.Readdir(-1) 166 | if err != nil { 167 | fmt.Println(err) 168 | os.Exit(1) 169 | } 170 | 171 | n := 0 172 | 173 | for _, file := range fi { 174 | if !file.IsDir() { 175 | if filepath.Ext(file.Name()) == ".png" { 176 | n++ 177 | } else if filepath.Ext(file.Name()) == ".jpg" { 178 | n++ 179 | } else if filepath.Ext(file.Name()) == ".gif" { 180 | n++ 181 | } 182 | } 183 | } 184 | 185 | c := make(chan int, n) 186 | 187 | for _, file := range fi { 188 | if !file.IsDir() { 189 | if filepath.Ext(file.Name()) == ".png" || 190 | filepath.Ext(file.Name()) == ".jpg" || 191 | filepath.Ext(file.Name()) == ".gif" { 192 | 193 | go func(filename string) { 194 | // Combine the directory path with the filename to get 195 | // the full path of the image 196 | fullImagePath := filepath.Join(*inDir, filename) 197 | fullImageOutPath := filepath.Join(*outDir, filepath.Base(filename)) 198 | 199 | resizeImage(fullImagePath, fullImageOutPath, *width, *height) 200 | c <- 1 201 | }(file.Name()) 202 | 203 | } 204 | } 205 | } 206 | 207 | for i := 0; i < n; i++ { 208 | <-c 209 | } 210 | 211 | t1 := time.Now() 212 | fmt.Printf("Processing time : %v \n", t1.Sub(t0)) 213 | } 214 | -------------------------------------------------------------------------------- /imaging/resize/filters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2012, Jan Schlicht 3 | 4 | Permission to use, copy, modify, and/or distribute this software for any purpose 5 | with or without fee is hereby granted, provided that the above copyright notice 6 | and this permission notice appear in all copies. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 10 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 12 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 13 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 14 | THIS SOFTWARE. 15 | */ 16 | 17 | package resize 18 | 19 | import ( 20 | "image" 21 | "image/color" 22 | "math" 23 | ) 24 | 25 | // restrict an input float32 to the 26 | // range of uint16 values 27 | func clampToUint16(x float32) (y uint16) { 28 | y = uint16(x) 29 | if x < 0 { 30 | y = 0 31 | } else if x > float32(0xfffe) { 32 | // "else if x > float32(0xffff)" will cause overflows! 33 | y = 0xffff 34 | } 35 | return 36 | } 37 | 38 | type filterModel struct { 39 | converter 40 | factor [2]float32 41 | kernel func(float32) float32 42 | tempRow, tempCol []colorArray 43 | } 44 | 45 | func (f *filterModel) convolution1d(x float32, p []colorArray, isRow bool) colorArray { 46 | var k float32 47 | var sum float32 = 0 48 | c := colorArray{0.0, 0.0, 0.0, 0.0} 49 | 50 | var index uint 51 | if isRow { 52 | index = 0 53 | } else { 54 | index = 1 55 | } 56 | 57 | for j := range p { 58 | k = f.kernel((x - float32(j)) / f.factor[index]) 59 | sum += k 60 | for i := range c { 61 | c[i] += p[j][i] * k 62 | } 63 | } 64 | 65 | // normalize values 66 | for i := range c { 67 | c[i] = c[i] / sum 68 | } 69 | return c 70 | } 71 | 72 | func (f *filterModel) Interpolate(x, y float32) color.RGBA64 { 73 | xf, yf := int(x)-len(f.tempRow)/2+1, int(y)-len(f.tempCol)/2+1 74 | x -= float32(xf) 75 | y -= float32(yf) 76 | 77 | for i := 0; i < len(f.tempCol); i++ { 78 | for j := 0; j < len(f.tempRow); j++ { 79 | f.tempRow[j] = f.at(xf+j, yf+i) 80 | } 81 | f.tempCol[i] = f.convolution1d(x, f.tempRow, true) 82 | } 83 | 84 | c := f.convolution1d(y, f.tempCol, false) 85 | return color.RGBA64{ 86 | clampToUint16(c[0]), 87 | clampToUint16(c[1]), 88 | clampToUint16(c[2]), 89 | clampToUint16(c[3]), 90 | } 91 | } 92 | 93 | // createFilter tries to find an optimized converter for the given input image 94 | // and initializes all filterModel members to their defaults 95 | func createFilter(img image.Image, factor [2]float32, size int, kernel func(float32) float32) (f Filter) { 96 | sizeX := size * (int(math.Ceil(float64(factor[0])))) 97 | sizeY := size * (int(math.Ceil(float64(factor[1])))) 98 | 99 | switch img.(type) { 100 | default: 101 | f = &filterModel{ 102 | &genericConverter{img}, 103 | factor, kernel, 104 | make([]colorArray, sizeX), make([]colorArray, sizeY), 105 | } 106 | case *image.RGBA: 107 | f = &filterModel{ 108 | &rgbaConverter{img.(*image.RGBA)}, 109 | factor, kernel, 110 | make([]colorArray, sizeX), make([]colorArray, sizeY), 111 | } 112 | case *image.RGBA64: 113 | f = &filterModel{ 114 | &rgba64Converter{img.(*image.RGBA64)}, 115 | factor, kernel, 116 | make([]colorArray, sizeX), make([]colorArray, sizeY), 117 | } 118 | case *image.Gray: 119 | f = &filterModel{ 120 | &grayConverter{img.(*image.Gray)}, 121 | factor, kernel, 122 | make([]colorArray, sizeX), make([]colorArray, sizeY), 123 | } 124 | case *image.Gray16: 125 | f = &filterModel{ 126 | &gray16Converter{img.(*image.Gray16)}, 127 | factor, kernel, 128 | make([]colorArray, sizeX), make([]colorArray, sizeY), 129 | } 130 | case *image.YCbCr: 131 | f = &filterModel{ 132 | &ycbcrConverter{img.(*image.YCbCr)}, 133 | factor, kernel, 134 | make([]colorArray, sizeX), make([]colorArray, sizeY), 135 | } 136 | } 137 | return 138 | } 139 | 140 | // Nearest-neighbor interpolation 141 | func NearestNeighbor(img image.Image, factor [2]float32) Filter { 142 | return createFilter(img, factor, 2, func(x float32) (y float32) { 143 | if x >= -0.5 && x < 0.5 { 144 | y = 1 145 | } else { 146 | y = 0 147 | } 148 | return 149 | }) 150 | } 151 | 152 | // Bilinear interpolation 153 | func Bilinear(img image.Image, factor [2]float32) Filter { 154 | return createFilter(img, factor, 2, func(x float32) (y float32) { 155 | absX := float32(math.Abs(float64(x))) 156 | if absX <= 1 { 157 | y = 1 - absX 158 | } else { 159 | y = 0 160 | } 161 | return 162 | }) 163 | } 164 | 165 | // Bicubic interpolation (with cubic hermite spline) 166 | func Bicubic(img image.Image, factor [2]float32) Filter { 167 | return createFilter(img, factor, 4, func(x float32) (y float32) { 168 | absX := float32(math.Abs(float64(x))) 169 | if absX <= 1 { 170 | y = absX*absX*(1.5*absX-2.5) + 1 171 | } else if absX <= 2 { 172 | y = absX*(absX*(2.5-0.5*absX)-4) + 2 173 | } else { 174 | y = 0 175 | } 176 | return 177 | }) 178 | } 179 | 180 | // Mitchell-Netravali interpolation 181 | func MitchellNetravali(img image.Image, factor [2]float32) Filter { 182 | return createFilter(img, factor, 4, func(x float32) (y float32) { 183 | absX := float32(math.Abs(float64(x))) 184 | if absX <= 1 { 185 | y = absX*absX*(7*absX-12) + 16.0/3 186 | } else if absX <= 2 { 187 | y = -(absX - 2) * (absX - 2) / 3 * (7*absX - 8) 188 | } else { 189 | y = 0 190 | } 191 | return 192 | }) 193 | } 194 | 195 | func lanczosKernel(a uint) func(float32) float32 { 196 | return func(x float32) (y float32) { 197 | if x > -float32(a) && x < float32(a) { 198 | y = float32(Sinc(float64(x))) * float32(Sinc(float64(x/float32(a)))) 199 | } else { 200 | y = 0 201 | } 202 | return 203 | } 204 | } 205 | 206 | // Lanczos interpolation (a=2) 207 | func Lanczos2(img image.Image, factor [2]float32) Filter { 208 | return createFilter(img, factor, 4, lanczosKernel(2)) 209 | } 210 | 211 | // Lanczos interpolation (a=3) 212 | func Lanczos3(img image.Image, factor [2]float32) Filter { 213 | return createFilter(img, factor, 6, lanczosKernel(3)) 214 | } 215 | -------------------------------------------------------------------------------- /fsnotify/fsnotify_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build linux 6 | 7 | // Package fsnotify implements a wrapper for the Linux inotify system. 8 | package fsnotify 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "os" 14 | "strings" 15 | "sync" 16 | "syscall" 17 | "unsafe" 18 | ) 19 | 20 | type FileEvent struct { 21 | mask uint32 // Mask of events 22 | cookie uint32 // Unique cookie associating related events (for rename(2)) 23 | Name string // File name (optional) 24 | } 25 | 26 | // IsCreate reports whether the FileEvent was triggerd by a creation 27 | func (e *FileEvent) IsCreate() bool { 28 | return (e.mask&IN_CREATE) == IN_CREATE || (e.mask&IN_MOVED_TO) == IN_MOVED_TO 29 | } 30 | 31 | // IsDelete reports whether the FileEvent was triggerd by a delete 32 | func (e *FileEvent) IsDelete() bool { 33 | return (e.mask&IN_DELETE_SELF) == IN_DELETE_SELF || (e.mask&IN_DELETE) == IN_DELETE 34 | } 35 | 36 | // IsModify reports whether the FileEvent was triggerd by a file modification or attribute change 37 | func (e *FileEvent) IsModify() bool { 38 | return ((e.mask&IN_MODIFY) == IN_MODIFY || (e.mask&IN_ATTRIB) == IN_ATTRIB) 39 | } 40 | 41 | // IsRename reports whether the FileEvent was triggerd by a change name 42 | func (e *FileEvent) IsRename() bool { 43 | return ((e.mask&IN_MOVE_SELF) == IN_MOVE_SELF || (e.mask&IN_MOVED_FROM) == IN_MOVED_FROM) 44 | } 45 | 46 | type watch struct { 47 | wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall) 48 | flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags) 49 | } 50 | 51 | type Watcher struct { 52 | mu sync.Mutex // Map access 53 | fd int // File descriptor (as returned by the inotify_init() syscall) 54 | watches map[string]*watch // Map of inotify watches (key: path) 55 | fsnFlags map[string]uint32 // Map of watched files to flags used for filter 56 | paths map[int]string // Map of watched paths (key: watch descriptor) 57 | Error chan error // Errors are sent on this channel 58 | internalEvent chan *FileEvent // Events are queued on this channel 59 | Event chan *FileEvent // Events are returned on this channel 60 | done chan bool // Channel for sending a "quit message" to the reader goroutine 61 | isClosed bool // Set to true when Close() is first called 62 | } 63 | 64 | // NewWatcher creates and returns a new inotify instance using inotify_init(2) 65 | func NewWatcher() (*Watcher, error) { 66 | fd, errno := syscall.InotifyInit() 67 | if fd == -1 { 68 | return nil, os.NewSyscallError("inotify_init", errno) 69 | } 70 | w := &Watcher{ 71 | fd: fd, 72 | watches: make(map[string]*watch), 73 | fsnFlags: make(map[string]uint32), 74 | paths: make(map[int]string), 75 | internalEvent: make(chan *FileEvent), 76 | Event: make(chan *FileEvent), 77 | Error: make(chan error), 78 | done: make(chan bool, 1), 79 | } 80 | 81 | go w.readEvents() 82 | go w.purgeEvents() 83 | return w, nil 84 | } 85 | 86 | // Close closes an inotify watcher instance 87 | // It sends a message to the reader goroutine to quit and removes all watches 88 | // associated with the inotify instance 89 | func (w *Watcher) Close() error { 90 | if w.isClosed { 91 | return nil 92 | } 93 | w.isClosed = true 94 | 95 | // Remove all watches 96 | for path := range w.watches { 97 | w.RemoveWatch(path) 98 | } 99 | 100 | // Send "quit" message to the reader goroutine 101 | w.done <- true 102 | 103 | return nil 104 | } 105 | 106 | // AddWatch adds path to the watched file set. 107 | // The flags are interpreted as described in inotify_add_watch(2). 108 | func (w *Watcher) addWatch(path string, flags uint32) error { 109 | if w.isClosed { 110 | return errors.New("inotify instance already closed") 111 | } 112 | 113 | watchEntry, found := w.watches[path] 114 | if found { 115 | watchEntry.flags |= flags 116 | flags |= syscall.IN_MASK_ADD 117 | } 118 | wd, errno := syscall.InotifyAddWatch(w.fd, path, flags) 119 | if wd == -1 { 120 | return errno 121 | } 122 | 123 | w.mu.Lock() 124 | w.watches[path] = &watch{wd: uint32(wd), flags: flags} 125 | w.paths[wd] = path 126 | w.mu.Unlock() 127 | 128 | return nil 129 | } 130 | 131 | // Watch adds path to the watched file set, watching all events. 132 | func (w *Watcher) watch(path string) error { 133 | return w.addWatch(path, OS_AGNOSTIC_EVENTS) 134 | } 135 | 136 | // RemoveWatch removes path from the watched file set. 137 | func (w *Watcher) removeWatch(path string) error { 138 | watch, ok := w.watches[path] 139 | if !ok { 140 | return errors.New(fmt.Sprintf("can't remove non-existent inotify watch for: %s", path)) 141 | } 142 | success, errno := syscall.InotifyRmWatch(w.fd, watch.wd) 143 | if success == -1 { 144 | return os.NewSyscallError("inotify_rm_watch", errno) 145 | } 146 | delete(w.watches, path) 147 | return nil 148 | } 149 | 150 | // readEvents reads from the inotify file descriptor, converts the 151 | // received events into Event objects and sends them via the Event channel 152 | func (w *Watcher) readEvents() { 153 | var ( 154 | buf [syscall.SizeofInotifyEvent * 4096]byte // Buffer for a maximum of 4096 raw events 155 | n int // Number of bytes read with read() 156 | errno error // Syscall errno 157 | ) 158 | 159 | for { 160 | n, errno = syscall.Read(w.fd, buf[0:]) 161 | // See if there is a message on the "done" channel 162 | var done bool 163 | select { 164 | case done = <-w.done: 165 | default: 166 | } 167 | 168 | // If EOF or a "done" message is received 169 | if n == 0 || done { 170 | syscall.Close(w.fd) 171 | close(w.internalEvent) 172 | close(w.Error) 173 | return 174 | } 175 | 176 | if n < 0 { 177 | w.Error <- os.NewSyscallError("read", errno) 178 | continue 179 | } 180 | if n < syscall.SizeofInotifyEvent { 181 | w.Error <- errors.New("inotify: short read in readEvents()") 182 | continue 183 | } 184 | 185 | var offset uint32 = 0 186 | // We don't know how many events we just read into the buffer 187 | // While the offset points to at least one whole event... 188 | for offset <= uint32(n-syscall.SizeofInotifyEvent) { 189 | // Point "raw" to the event in the buffer 190 | raw := (*syscall.InotifyEvent)(unsafe.Pointer(&buf[offset])) 191 | event := new(FileEvent) 192 | event.mask = uint32(raw.Mask) 193 | event.cookie = uint32(raw.Cookie) 194 | nameLen := uint32(raw.Len) 195 | // If the event happened to the watched directory or the watched file, the kernel 196 | // doesn't append the filename to the event, but we would like to always fill the 197 | // the "Name" field with a valid filename. We retrieve the path of the watch from 198 | // the "paths" map. 199 | w.mu.Lock() 200 | event.Name = w.paths[int(raw.Wd)] 201 | w.mu.Unlock() 202 | watchedName := event.Name 203 | if nameLen > 0 { 204 | // Point "bytes" at the first byte of the filename 205 | bytes := (*[syscall.PathMax]byte)(unsafe.Pointer(&buf[offset+syscall.SizeofInotifyEvent])) 206 | // The filename is padded with NUL bytes. TrimRight() gets rid of those. 207 | event.Name += "/" + strings.TrimRight(string(bytes[0:nameLen]), "\000") 208 | } 209 | 210 | // Setup FSNotify flags (inherit from directory watch) 211 | fsnFlags := w.fsnFlags[watchedName] 212 | _, fsnFound := w.fsnFlags[event.Name] 213 | if !fsnFound { 214 | w.fsnFlags[event.Name] = fsnFlags 215 | } 216 | 217 | // Send the events that are not ignored on the events channel 218 | if (event.mask & IN_IGNORED) == 0 { 219 | w.internalEvent <- event 220 | } 221 | 222 | // Move to the next event in the buffer 223 | offset += syscall.SizeofInotifyEvent + nameLen 224 | } 225 | } 226 | } 227 | 228 | const ( 229 | // Options for inotify_init() are not exported 230 | // IN_CLOEXEC uint32 = syscall.IN_CLOEXEC 231 | // IN_NONBLOCK uint32 = syscall.IN_NONBLOCK 232 | 233 | // Options for AddWatch 234 | IN_DONT_FOLLOW uint32 = syscall.IN_DONT_FOLLOW 235 | IN_ONESHOT uint32 = syscall.IN_ONESHOT 236 | IN_ONLYDIR uint32 = syscall.IN_ONLYDIR 237 | 238 | // The "IN_MASK_ADD" option is not exported, as AddWatch 239 | // adds it automatically, if there is already a watch for the given path 240 | // IN_MASK_ADD uint32 = syscall.IN_MASK_ADD 241 | 242 | // Events 243 | IN_ACCESS uint32 = syscall.IN_ACCESS 244 | IN_ALL_EVENTS uint32 = syscall.IN_ALL_EVENTS 245 | IN_ATTRIB uint32 = syscall.IN_ATTRIB 246 | IN_CLOSE uint32 = syscall.IN_CLOSE 247 | IN_CLOSE_NOWRITE uint32 = syscall.IN_CLOSE_NOWRITE 248 | IN_CLOSE_WRITE uint32 = syscall.IN_CLOSE_WRITE 249 | IN_CREATE uint32 = syscall.IN_CREATE 250 | IN_DELETE uint32 = syscall.IN_DELETE 251 | IN_DELETE_SELF uint32 = syscall.IN_DELETE_SELF 252 | IN_MODIFY uint32 = syscall.IN_MODIFY 253 | IN_MOVE uint32 = syscall.IN_MOVE 254 | IN_MOVED_FROM uint32 = syscall.IN_MOVED_FROM 255 | IN_MOVED_TO uint32 = syscall.IN_MOVED_TO 256 | IN_MOVE_SELF uint32 = syscall.IN_MOVE_SELF 257 | IN_OPEN uint32 = syscall.IN_OPEN 258 | 259 | OS_AGNOSTIC_EVENTS = IN_MOVED_TO | IN_MOVED_FROM | IN_CREATE | IN_ATTRIB | IN_MODIFY | IN_MOVE_SELF | IN_DELETE | IN_DELETE_SELF 260 | 261 | // Special events 262 | IN_ISDIR uint32 = syscall.IN_ISDIR 263 | IN_IGNORED uint32 = syscall.IN_IGNORED 264 | IN_Q_OVERFLOW uint32 = syscall.IN_Q_OVERFLOW 265 | IN_UNMOUNT uint32 = syscall.IN_UNMOUNT 266 | ) 267 | -------------------------------------------------------------------------------- /fsnotify/fsnotify_bsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2010 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build freebsd openbsd netbsd darwin 6 | 7 | //Package fsnotify implements filesystem notification. 8 | package fsnotify 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "io/ioutil" 14 | "os" 15 | "path/filepath" 16 | "syscall" 17 | ) 18 | 19 | type FileEvent struct { 20 | mask uint32 // Mask of events 21 | Name string // File name (optional) 22 | create bool // set by fsnotify package if found new file 23 | } 24 | 25 | // IsCreate reports whether the FileEvent was triggerd by a creation 26 | func (e *FileEvent) IsCreate() bool { return e.create } 27 | 28 | // IsDelete reports whether the FileEvent was triggerd by a delete 29 | func (e *FileEvent) IsDelete() bool { return (e.mask & NOTE_DELETE) == NOTE_DELETE } 30 | 31 | // IsModify reports whether the FileEvent was triggerd by a file modification 32 | func (e *FileEvent) IsModify() bool { 33 | return ((e.mask&NOTE_WRITE) == NOTE_WRITE || (e.mask&NOTE_ATTRIB) == NOTE_ATTRIB) 34 | } 35 | 36 | // IsRename reports whether the FileEvent was triggerd by a change name 37 | func (e *FileEvent) IsRename() bool { return (e.mask & NOTE_RENAME) == NOTE_RENAME } 38 | 39 | type Watcher struct { 40 | kq int // File descriptor (as returned by the kqueue() syscall) 41 | watches map[string]int // Map of watched file diescriptors (key: path) 42 | fsnFlags map[string]uint32 // Map of watched files to flags used for filter 43 | enFlags map[string]uint32 // Map of watched files to evfilt note flags used in kqueue 44 | paths map[int]string // Map of watched paths (key: watch descriptor) 45 | finfo map[int]os.FileInfo // Map of file information (isDir, isReg; key: watch descriptor) 46 | fileExists map[string]bool // Keep track of if we know this file exists (to stop duplicate create events) 47 | Error chan error // Errors are sent on this channel 48 | internalEvent chan *FileEvent // Events are queued on this channel 49 | Event chan *FileEvent // Events are returned on this channel 50 | done chan bool // Channel for sending a "quit message" to the reader goroutine 51 | isClosed bool // Set to true when Close() is first called 52 | kbuf [1]syscall.Kevent_t // An event buffer for Add/Remove watch 53 | } 54 | 55 | // NewWatcher creates and returns a new kevent instance using kqueue(2) 56 | func NewWatcher() (*Watcher, error) { 57 | fd, errno := syscall.Kqueue() 58 | if fd == -1 { 59 | return nil, os.NewSyscallError("kqueue", errno) 60 | } 61 | w := &Watcher{ 62 | kq: fd, 63 | watches: make(map[string]int), 64 | fsnFlags: make(map[string]uint32), 65 | enFlags: make(map[string]uint32), 66 | paths: make(map[int]string), 67 | finfo: make(map[int]os.FileInfo), 68 | fileExists: make(map[string]bool), 69 | internalEvent: make(chan *FileEvent), 70 | Event: make(chan *FileEvent), 71 | Error: make(chan error), 72 | done: make(chan bool, 1), 73 | } 74 | 75 | go w.readEvents() 76 | go w.purgeEvents() 77 | return w, nil 78 | } 79 | 80 | // Close closes a kevent watcher instance 81 | // It sends a message to the reader goroutine to quit and removes all watches 82 | // associated with the kevent instance 83 | func (w *Watcher) Close() error { 84 | if w.isClosed { 85 | return nil 86 | } 87 | w.isClosed = true 88 | 89 | // Send "quit" message to the reader goroutine 90 | w.done <- true 91 | for path := range w.watches { 92 | w.removeWatch(path) 93 | } 94 | 95 | return nil 96 | } 97 | 98 | // AddWatch adds path to the watched file set. 99 | // The flags are interpreted as described in kevent(2). 100 | func (w *Watcher) addWatch(path string, flags uint32) error { 101 | if w.isClosed { 102 | return errors.New("kevent instance already closed") 103 | } 104 | 105 | watchDir := false 106 | 107 | watchfd, found := w.watches[path] 108 | if !found { 109 | fi, errstat := os.Lstat(path) 110 | if errstat != nil { 111 | return errstat 112 | } 113 | 114 | // don't watch socket 115 | if fi.Mode()&os.ModeSocket == os.ModeSocket { 116 | return nil 117 | } 118 | 119 | // Follow Symlinks 120 | // Unfortunately, Linux can add bogus symlinks to watch list without 121 | // issue, and Windows can't do symlinks period (AFAIK). To maintain 122 | // consistency, we will act like everything is fine. There will simply 123 | // be no file events for broken symlinks. 124 | // Hence the returns of nil on errors. 125 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 126 | path, err := filepath.EvalSymlinks(path) 127 | if err != nil { 128 | return nil 129 | } 130 | 131 | fi, errstat = os.Lstat(path) 132 | if errstat != nil { 133 | return nil 134 | } 135 | } 136 | 137 | fd, errno := syscall.Open(path, syscall.O_NONBLOCK|syscall.O_RDONLY, 0700) 138 | if fd == -1 { 139 | return errno 140 | } 141 | watchfd = fd 142 | 143 | w.watches[path] = watchfd 144 | w.paths[watchfd] = path 145 | 146 | w.finfo[watchfd] = fi 147 | } 148 | // Watch the directory if it has not been watched before. 149 | if w.finfo[watchfd].IsDir() && 150 | (flags&NOTE_WRITE) == NOTE_WRITE && 151 | (!found || (w.enFlags[path]&NOTE_WRITE) != NOTE_WRITE) { 152 | watchDir = true 153 | } 154 | 155 | w.enFlags[path] = flags 156 | watchEntry := &w.kbuf[0] 157 | watchEntry.Fflags = flags 158 | syscall.SetKevent(watchEntry, watchfd, syscall.EVFILT_VNODE, syscall.EV_ADD|syscall.EV_CLEAR) 159 | 160 | wd, errno := syscall.Kevent(w.kq, w.kbuf[:], nil, nil) 161 | if wd == -1 { 162 | return errno 163 | } else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR { 164 | return errors.New("kevent add error") 165 | } 166 | 167 | if watchDir { 168 | errdir := w.watchDirectoryFiles(path) 169 | if errdir != nil { 170 | return errdir 171 | } 172 | } 173 | return nil 174 | } 175 | 176 | // Watch adds path to the watched file set, watching all events. 177 | func (w *Watcher) watch(path string) error { 178 | return w.addWatch(path, NOTE_ALLEVENTS) 179 | } 180 | 181 | // RemoveWatch removes path from the watched file set. 182 | func (w *Watcher) removeWatch(path string) error { 183 | watchfd, ok := w.watches[path] 184 | if !ok { 185 | return errors.New(fmt.Sprintf("can't remove non-existent kevent watch for: %s", path)) 186 | } 187 | watchEntry := &w.kbuf[0] 188 | syscall.SetKevent(watchEntry, w.watches[path], syscall.EVFILT_VNODE, syscall.EV_DELETE) 189 | success, errno := syscall.Kevent(w.kq, w.kbuf[:], nil, nil) 190 | if success == -1 { 191 | return os.NewSyscallError("kevent_rm_watch", errno) 192 | } else if (watchEntry.Flags & syscall.EV_ERROR) == syscall.EV_ERROR { 193 | return errors.New("kevent rm error") 194 | } 195 | syscall.Close(watchfd) 196 | delete(w.watches, path) 197 | return nil 198 | } 199 | 200 | // readEvents reads from the kqueue file descriptor, converts the 201 | // received events into Event objects and sends them via the Event channel 202 | func (w *Watcher) readEvents() { 203 | var ( 204 | eventbuf [10]syscall.Kevent_t // Event buffer 205 | events []syscall.Kevent_t // Received events 206 | twait *syscall.Timespec // Time to block waiting for events 207 | n int // Number of events returned from kevent 208 | errno error // Syscall errno 209 | ) 210 | events = eventbuf[0:0] 211 | twait = new(syscall.Timespec) 212 | *twait = syscall.NsecToTimespec(keventWaitTime) 213 | 214 | for { 215 | // See if there is a message on the "done" channel 216 | var done bool 217 | select { 218 | case done = <-w.done: 219 | default: 220 | } 221 | 222 | // If "done" message is received 223 | if done { 224 | errno := syscall.Close(w.kq) 225 | if errno != nil { 226 | w.Error <- os.NewSyscallError("close", errno) 227 | } 228 | close(w.internalEvent) 229 | close(w.Error) 230 | return 231 | } 232 | 233 | // Get new events 234 | if len(events) == 0 { 235 | n, errno = syscall.Kevent(w.kq, nil, eventbuf[:], twait) 236 | 237 | // EINTR is okay, basically the syscall was interrupted before 238 | // timeout expired. 239 | if errno != nil && errno != syscall.EINTR { 240 | w.Error <- os.NewSyscallError("kevent", errno) 241 | continue 242 | } 243 | 244 | // Received some events 245 | if n > 0 { 246 | events = eventbuf[0:n] 247 | } 248 | } 249 | 250 | // Flush the events we recieved to the events channel 251 | for len(events) > 0 { 252 | fileEvent := new(FileEvent) 253 | watchEvent := &events[0] 254 | fileEvent.mask = uint32(watchEvent.Fflags) 255 | fileEvent.Name = w.paths[int(watchEvent.Ident)] 256 | 257 | fileInfo := w.finfo[int(watchEvent.Ident)] 258 | if fileInfo.IsDir() && !fileEvent.IsDelete() { 259 | // Double check to make sure the directory exist. This can happen when 260 | // we do a rm -fr on a recursively watched folders and we receive a 261 | // modification event first but the folder has been deleted and later 262 | // receive the delete event 263 | if _, err := os.Lstat(fileEvent.Name); os.IsNotExist(err) { 264 | // mark is as delete event 265 | fileEvent.mask |= NOTE_DELETE 266 | } 267 | } 268 | 269 | if fileInfo.IsDir() && fileEvent.IsModify() && !fileEvent.IsDelete() { 270 | w.sendDirectoryChangeEvents(fileEvent.Name) 271 | } else { 272 | // Send the event on the events channel 273 | w.internalEvent <- fileEvent 274 | } 275 | 276 | // Move to next event 277 | events = events[1:] 278 | 279 | if fileEvent.IsRename() { 280 | w.removeWatch(fileEvent.Name) 281 | delete(w.fileExists, fileEvent.Name) 282 | } 283 | if fileEvent.IsDelete() { 284 | w.removeWatch(fileEvent.Name) 285 | delete(w.fileExists, fileEvent.Name) 286 | 287 | // Look for a file that may have overwritten this 288 | // (ie mv f1 f2 will delete f2 then create f2) 289 | fileDir, _ := filepath.Split(fileEvent.Name) 290 | fileDir = filepath.Clean(fileDir) 291 | if _, found := w.watches[fileDir]; found { 292 | // make sure the directory exist before we watch for changes. When we 293 | // do a recursive watch and perform rm -fr, the parent directory might 294 | // have gone missing, ignore the missing directory and let the 295 | // upcoming delete event remove the watch form the parent folder 296 | if _, err := os.Lstat(fileDir); !os.IsNotExist(err) { 297 | w.sendDirectoryChangeEvents(fileDir) 298 | } 299 | } 300 | } 301 | } 302 | } 303 | } 304 | 305 | func (w *Watcher) watchDirectoryFiles(dirPath string) error { 306 | // Get all files 307 | files, err := ioutil.ReadDir(dirPath) 308 | if err != nil { 309 | return err 310 | } 311 | 312 | // Search for new files 313 | for _, fileInfo := range files { 314 | filePath := filepath.Join(dirPath, fileInfo.Name()) 315 | if fileInfo.IsDir() == false { 316 | // Watch file to mimic linux fsnotify 317 | e := w.addWatch(filePath, NOTE_DELETE|NOTE_WRITE|NOTE_RENAME) 318 | w.fsnFlags[filePath] = FSN_ALL 319 | if e != nil { 320 | return e 321 | } 322 | } else { 323 | // If the user is currently waching directory 324 | // we want to preserve the flags used 325 | currFlags, found := w.enFlags[filePath] 326 | var newFlags uint32 = NOTE_DELETE 327 | if found { 328 | newFlags |= currFlags 329 | } 330 | 331 | // Linux gives deletes if not explicitly watching 332 | e := w.addWatch(filePath, newFlags) 333 | w.fsnFlags[filePath] = FSN_ALL 334 | if e != nil { 335 | return e 336 | } 337 | } 338 | w.fileExists[filePath] = true 339 | } 340 | 341 | return nil 342 | } 343 | 344 | // sendDirectoryEvents searches the directory for newly created files 345 | // and sends them over the event channel. This functionality is to have 346 | // the BSD version of fsnotify mach linux fsnotify which provides a 347 | // create event for files created in a watched directory. 348 | func (w *Watcher) sendDirectoryChangeEvents(dirPath string) { 349 | // Get all files 350 | files, err := ioutil.ReadDir(dirPath) 351 | if err != nil { 352 | w.Error <- err 353 | } 354 | 355 | // Search for new files 356 | for _, fileInfo := range files { 357 | filePath := filepath.Join(dirPath, fileInfo.Name()) 358 | _, doesExist := w.fileExists[filePath] 359 | if doesExist == false { 360 | w.fsnFlags[filePath] = FSN_ALL 361 | // Send create event 362 | fileEvent := new(FileEvent) 363 | fileEvent.Name = filePath 364 | fileEvent.create = true 365 | w.internalEvent <- fileEvent 366 | } 367 | w.fileExists[filePath] = true 368 | } 369 | w.watchDirectoryFiles(dirPath) 370 | } 371 | 372 | const ( 373 | // Flags (from ) 374 | NOTE_DELETE = 0x0001 /* vnode was removed */ 375 | NOTE_WRITE = 0x0002 /* data contents changed */ 376 | NOTE_EXTEND = 0x0004 /* size increased */ 377 | NOTE_ATTRIB = 0x0008 /* attributes changed */ 378 | NOTE_LINK = 0x0010 /* link count changed */ 379 | NOTE_RENAME = 0x0020 /* vnode was renamed */ 380 | NOTE_REVOKE = 0x0040 /* vnode access was revoked */ 381 | 382 | // Watch all events 383 | NOTE_ALLEVENTS = NOTE_DELETE | NOTE_WRITE | NOTE_ATTRIB | NOTE_RENAME 384 | 385 | // Block for 100 ms on each call to kevent 386 | keventWaitTime = 100e6 387 | ) 388 | -------------------------------------------------------------------------------- /fsnotify/fsnotify_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // +build windows 6 | 7 | // Package fsnotify allows the user to receive 8 | // file system event notifications on Windows. 9 | package fsnotify 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "os" 15 | "path/filepath" 16 | "runtime" 17 | "syscall" 18 | "unsafe" 19 | ) 20 | 21 | // Event is the type of the notification messages 22 | // received on the watcher's Event channel. 23 | type FileEvent struct { 24 | mask uint32 // Mask of events 25 | cookie uint32 // Unique cookie associating related events (for rename) 26 | Name string // File name (optional) 27 | } 28 | 29 | // IsCreate reports whether the FileEvent was triggerd by a creation 30 | func (e *FileEvent) IsCreate() bool { return (e.mask & FS_CREATE) == FS_CREATE } 31 | 32 | // IsDelete reports whether the FileEvent was triggerd by a delete 33 | func (e *FileEvent) IsDelete() bool { 34 | return ((e.mask&FS_DELETE) == FS_DELETE || (e.mask&FS_DELETE_SELF) == FS_DELETE_SELF) 35 | } 36 | 37 | // IsModify reports whether the FileEvent was triggerd by a file modification or attribute change 38 | func (e *FileEvent) IsModify() bool { 39 | return ((e.mask&FS_MODIFY) == FS_MODIFY || (e.mask&FS_ATTRIB) == FS_ATTRIB) 40 | } 41 | 42 | // IsRename reports whether the FileEvent was triggerd by a change name 43 | func (e *FileEvent) IsRename() bool { 44 | return ((e.mask&FS_MOVE) == FS_MOVE || (e.mask&FS_MOVE_SELF) == FS_MOVE_SELF || (e.mask&FS_MOVED_FROM) == FS_MOVED_FROM || (e.mask&FS_MOVED_TO) == FS_MOVED_TO) 45 | } 46 | 47 | const ( 48 | opAddWatch = iota 49 | opRemoveWatch 50 | ) 51 | 52 | const ( 53 | provisional uint64 = 1 << (32 + iota) 54 | ) 55 | 56 | type input struct { 57 | op int 58 | path string 59 | flags uint32 60 | reply chan error 61 | } 62 | 63 | type inode struct { 64 | handle syscall.Handle 65 | volume uint32 66 | index uint64 67 | } 68 | 69 | type watch struct { 70 | ov syscall.Overlapped 71 | ino *inode // i-number 72 | path string // Directory path 73 | mask uint64 // Directory itself is being watched with these notify flags 74 | names map[string]uint64 // Map of names being watched and their notify flags 75 | rename string // Remembers the old name while renaming a file 76 | buf [4096]byte 77 | } 78 | 79 | type indexMap map[uint64]*watch 80 | type watchMap map[uint32]indexMap 81 | 82 | // A Watcher waits for and receives event notifications 83 | // for a specific set of files and directories. 84 | type Watcher struct { 85 | port syscall.Handle // Handle to completion port 86 | watches watchMap // Map of watches (key: i-number) 87 | fsnFlags map[string]uint32 // Map of watched files to flags used for filter 88 | input chan *input // Inputs to the reader are sent on this channel 89 | internalEvent chan *FileEvent // Events are queued on this channel 90 | Event chan *FileEvent // Events are returned on this channel 91 | Error chan error // Errors are sent on this channel 92 | isClosed bool // Set to true when Close() is first called 93 | quit chan chan<- error 94 | cookie uint32 95 | } 96 | 97 | // NewWatcher creates and returns a Watcher. 98 | func NewWatcher() (*Watcher, error) { 99 | port, e := syscall.CreateIoCompletionPort(syscall.InvalidHandle, 0, 0, 0) 100 | if e != nil { 101 | return nil, os.NewSyscallError("CreateIoCompletionPort", e) 102 | } 103 | w := &Watcher{ 104 | port: port, 105 | watches: make(watchMap), 106 | fsnFlags: make(map[string]uint32), 107 | input: make(chan *input, 1), 108 | Event: make(chan *FileEvent, 50), 109 | internalEvent: make(chan *FileEvent), 110 | Error: make(chan error), 111 | quit: make(chan chan<- error, 1), 112 | } 113 | go w.readEvents() 114 | go w.purgeEvents() 115 | return w, nil 116 | } 117 | 118 | // Close closes a Watcher. 119 | // It sends a message to the reader goroutine to quit and removes all watches 120 | // associated with the watcher. 121 | func (w *Watcher) Close() error { 122 | if w.isClosed { 123 | return nil 124 | } 125 | w.isClosed = true 126 | 127 | // Send "quit" message to the reader goroutine 128 | ch := make(chan error) 129 | w.quit <- ch 130 | if err := w.wakeupReader(); err != nil { 131 | return err 132 | } 133 | return <-ch 134 | } 135 | 136 | // AddWatch adds path to the watched file set. 137 | func (w *Watcher) AddWatch(path string, flags uint32) error { 138 | if w.isClosed { 139 | return errors.New("watcher already closed") 140 | } 141 | in := &input{ 142 | op: opAddWatch, 143 | path: filepath.Clean(path), 144 | flags: flags, 145 | reply: make(chan error), 146 | } 147 | w.input <- in 148 | if err := w.wakeupReader(); err != nil { 149 | return err 150 | } 151 | return <-in.reply 152 | } 153 | 154 | // Watch adds path to the watched file set, watching all events. 155 | func (w *Watcher) watch(path string) error { 156 | return w.AddWatch(path, FS_ALL_EVENTS) 157 | } 158 | 159 | // RemoveWatch removes path from the watched file set. 160 | func (w *Watcher) removeWatch(path string) error { 161 | in := &input{ 162 | op: opRemoveWatch, 163 | path: filepath.Clean(path), 164 | reply: make(chan error), 165 | } 166 | w.input <- in 167 | if err := w.wakeupReader(); err != nil { 168 | return err 169 | } 170 | return <-in.reply 171 | } 172 | 173 | func (w *Watcher) wakeupReader() error { 174 | e := syscall.PostQueuedCompletionStatus(w.port, 0, 0, nil) 175 | if e != nil { 176 | return os.NewSyscallError("PostQueuedCompletionStatus", e) 177 | } 178 | return nil 179 | } 180 | 181 | func getDir(pathname string) (dir string, err error) { 182 | attr, e := syscall.GetFileAttributes(syscall.StringToUTF16Ptr(pathname)) 183 | if e != nil { 184 | return "", os.NewSyscallError("GetFileAttributes", e) 185 | } 186 | if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 { 187 | dir = pathname 188 | } else { 189 | dir, _ = filepath.Split(pathname) 190 | dir = filepath.Clean(dir) 191 | } 192 | return 193 | } 194 | 195 | func getIno(path string) (ino *inode, err error) { 196 | h, e := syscall.CreateFile(syscall.StringToUTF16Ptr(path), 197 | syscall.FILE_LIST_DIRECTORY, 198 | syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, 199 | nil, syscall.OPEN_EXISTING, 200 | syscall.FILE_FLAG_BACKUP_SEMANTICS|syscall.FILE_FLAG_OVERLAPPED, 0) 201 | if e != nil { 202 | return nil, os.NewSyscallError("CreateFile", e) 203 | } 204 | var fi syscall.ByHandleFileInformation 205 | if e = syscall.GetFileInformationByHandle(h, &fi); e != nil { 206 | syscall.CloseHandle(h) 207 | return nil, os.NewSyscallError("GetFileInformationByHandle", e) 208 | } 209 | ino = &inode{ 210 | handle: h, 211 | volume: fi.VolumeSerialNumber, 212 | index: uint64(fi.FileIndexHigh)<<32 | uint64(fi.FileIndexLow), 213 | } 214 | return ino, nil 215 | } 216 | 217 | // Must run within the I/O thread. 218 | func (m watchMap) get(ino *inode) *watch { 219 | if i := m[ino.volume]; i != nil { 220 | return i[ino.index] 221 | } 222 | return nil 223 | } 224 | 225 | // Must run within the I/O thread. 226 | func (m watchMap) set(ino *inode, watch *watch) { 227 | i := m[ino.volume] 228 | if i == nil { 229 | i = make(indexMap) 230 | m[ino.volume] = i 231 | } 232 | i[ino.index] = watch 233 | } 234 | 235 | // Must run within the I/O thread. 236 | func (w *Watcher) addWatch(pathname string, flags uint64) error { 237 | dir, err := getDir(pathname) 238 | if err != nil { 239 | return err 240 | } 241 | if flags&FS_ONLYDIR != 0 && pathname != dir { 242 | return nil 243 | } 244 | ino, err := getIno(dir) 245 | if err != nil { 246 | return err 247 | } 248 | watchEntry := w.watches.get(ino) 249 | if watchEntry == nil { 250 | if _, e := syscall.CreateIoCompletionPort(ino.handle, w.port, 0, 0); e != nil { 251 | syscall.CloseHandle(ino.handle) 252 | return os.NewSyscallError("CreateIoCompletionPort", e) 253 | } 254 | watchEntry = &watch{ 255 | ino: ino, 256 | path: dir, 257 | names: make(map[string]uint64), 258 | } 259 | w.watches.set(ino, watchEntry) 260 | flags |= provisional 261 | } else { 262 | syscall.CloseHandle(ino.handle) 263 | } 264 | if pathname == dir { 265 | watchEntry.mask |= flags 266 | } else { 267 | watchEntry.names[filepath.Base(pathname)] |= flags 268 | } 269 | if err = w.startRead(watchEntry); err != nil { 270 | return err 271 | } 272 | if pathname == dir { 273 | watchEntry.mask &= ^provisional 274 | } else { 275 | watchEntry.names[filepath.Base(pathname)] &= ^provisional 276 | } 277 | return nil 278 | } 279 | 280 | // Must run within the I/O thread. 281 | func (w *Watcher) remWatch(pathname string) error { 282 | dir, err := getDir(pathname) 283 | if err != nil { 284 | return err 285 | } 286 | ino, err := getIno(dir) 287 | if err != nil { 288 | return err 289 | } 290 | watch := w.watches.get(ino) 291 | if watch == nil { 292 | return fmt.Errorf("can't remove non-existent watch for: %s", pathname) 293 | } 294 | if pathname == dir { 295 | w.sendEvent(watch.path, watch.mask&FS_IGNORED) 296 | watch.mask = 0 297 | } else { 298 | name := filepath.Base(pathname) 299 | w.sendEvent(watch.path+"/"+name, watch.names[name]&FS_IGNORED) 300 | delete(watch.names, name) 301 | } 302 | return w.startRead(watch) 303 | } 304 | 305 | // Must run within the I/O thread. 306 | func (w *Watcher) deleteWatch(watch *watch) { 307 | for name, mask := range watch.names { 308 | if mask&provisional == 0 { 309 | w.sendEvent(watch.path+"/"+name, mask&FS_IGNORED) 310 | } 311 | delete(watch.names, name) 312 | } 313 | if watch.mask != 0 { 314 | if watch.mask&provisional == 0 { 315 | w.sendEvent(watch.path, watch.mask&FS_IGNORED) 316 | } 317 | watch.mask = 0 318 | } 319 | } 320 | 321 | // Must run within the I/O thread. 322 | func (w *Watcher) startRead(watch *watch) error { 323 | if e := syscall.CancelIo(watch.ino.handle); e != nil { 324 | w.Error <- os.NewSyscallError("CancelIo", e) 325 | w.deleteWatch(watch) 326 | } 327 | mask := toWindowsFlags(watch.mask) 328 | for _, m := range watch.names { 329 | mask |= toWindowsFlags(m) 330 | } 331 | if mask == 0 { 332 | if e := syscall.CloseHandle(watch.ino.handle); e != nil { 333 | w.Error <- os.NewSyscallError("CloseHandle", e) 334 | } 335 | delete(w.watches[watch.ino.volume], watch.ino.index) 336 | return nil 337 | } 338 | e := syscall.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], 339 | uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) 340 | if e != nil { 341 | err := os.NewSyscallError("ReadDirectoryChanges", e) 342 | if e == syscall.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { 343 | // Watched directory was probably removed 344 | if w.sendEvent(watch.path, watch.mask&FS_DELETE_SELF) { 345 | if watch.mask&FS_ONESHOT != 0 { 346 | watch.mask = 0 347 | } 348 | } 349 | err = nil 350 | } 351 | w.deleteWatch(watch) 352 | w.startRead(watch) 353 | return err 354 | } 355 | return nil 356 | } 357 | 358 | // readEvents reads from the I/O completion port, converts the 359 | // received events into Event objects and sends them via the Event channel. 360 | // Entry point to the I/O thread. 361 | func (w *Watcher) readEvents() { 362 | var ( 363 | n, key uint32 364 | ov *syscall.Overlapped 365 | ) 366 | runtime.LockOSThread() 367 | 368 | for { 369 | e := syscall.GetQueuedCompletionStatus(w.port, &n, &key, &ov, syscall.INFINITE) 370 | watch := (*watch)(unsafe.Pointer(ov)) 371 | 372 | if watch == nil { 373 | select { 374 | case ch := <-w.quit: 375 | for _, index := range w.watches { 376 | for _, watch := range index { 377 | w.deleteWatch(watch) 378 | w.startRead(watch) 379 | } 380 | } 381 | var err error 382 | if e := syscall.CloseHandle(w.port); e != nil { 383 | err = os.NewSyscallError("CloseHandle", e) 384 | } 385 | close(w.internalEvent) 386 | close(w.Error) 387 | ch <- err 388 | return 389 | case in := <-w.input: 390 | switch in.op { 391 | case opAddWatch: 392 | in.reply <- w.addWatch(in.path, uint64(in.flags)) 393 | case opRemoveWatch: 394 | in.reply <- w.remWatch(in.path) 395 | } 396 | default: 397 | } 398 | continue 399 | } 400 | 401 | switch e { 402 | case syscall.ERROR_ACCESS_DENIED: 403 | // Watched directory was probably removed 404 | w.sendEvent(watch.path, watch.mask&FS_DELETE_SELF) 405 | w.deleteWatch(watch) 406 | w.startRead(watch) 407 | continue 408 | case syscall.ERROR_OPERATION_ABORTED: 409 | // CancelIo was called on this handle 410 | continue 411 | default: 412 | w.Error <- os.NewSyscallError("GetQueuedCompletionPort", e) 413 | continue 414 | case nil: 415 | } 416 | 417 | var offset uint32 418 | for { 419 | if n == 0 { 420 | w.internalEvent <- &FileEvent{mask: FS_Q_OVERFLOW} 421 | w.Error <- errors.New("short read in readEvents()") 422 | break 423 | } 424 | 425 | // Point "raw" to the event in the buffer 426 | raw := (*syscall.FileNotifyInformation)(unsafe.Pointer(&watch.buf[offset])) 427 | buf := (*[syscall.MAX_PATH]uint16)(unsafe.Pointer(&raw.FileName)) 428 | name := syscall.UTF16ToString(buf[:raw.FileNameLength/2]) 429 | fullname := watch.path + "/" + name 430 | 431 | var mask uint64 432 | switch raw.Action { 433 | case syscall.FILE_ACTION_REMOVED: 434 | mask = FS_DELETE_SELF 435 | case syscall.FILE_ACTION_MODIFIED: 436 | mask = FS_MODIFY 437 | case syscall.FILE_ACTION_RENAMED_OLD_NAME: 438 | watch.rename = name 439 | case syscall.FILE_ACTION_RENAMED_NEW_NAME: 440 | if watch.names[watch.rename] != 0 { 441 | watch.names[name] |= watch.names[watch.rename] 442 | delete(watch.names, watch.rename) 443 | mask = FS_MOVE_SELF 444 | } 445 | } 446 | 447 | sendNameEvent := func() { 448 | if w.sendEvent(fullname, watch.names[name]&mask) { 449 | if watch.names[name]&FS_ONESHOT != 0 { 450 | delete(watch.names, name) 451 | } 452 | } 453 | } 454 | if raw.Action != syscall.FILE_ACTION_RENAMED_NEW_NAME { 455 | sendNameEvent() 456 | } 457 | if raw.Action == syscall.FILE_ACTION_REMOVED { 458 | w.sendEvent(fullname, watch.names[name]&FS_IGNORED) 459 | delete(watch.names, name) 460 | } 461 | if w.sendEvent(fullname, watch.mask&toFSnotifyFlags(raw.Action)) { 462 | if watch.mask&FS_ONESHOT != 0 { 463 | watch.mask = 0 464 | } 465 | } 466 | if raw.Action == syscall.FILE_ACTION_RENAMED_NEW_NAME { 467 | fullname = watch.path + "/" + watch.rename 468 | sendNameEvent() 469 | } 470 | 471 | // Move to the next event in the buffer 472 | if raw.NextEntryOffset == 0 { 473 | break 474 | } 475 | offset += raw.NextEntryOffset 476 | } 477 | 478 | if err := w.startRead(watch); err != nil { 479 | w.Error <- err 480 | } 481 | } 482 | } 483 | 484 | func (w *Watcher) sendEvent(name string, mask uint64) bool { 485 | if mask == 0 { 486 | return false 487 | } 488 | event := &FileEvent{mask: uint32(mask), Name: name} 489 | if mask&FS_MOVE != 0 { 490 | if mask&FS_MOVED_FROM != 0 { 491 | w.cookie++ 492 | } 493 | event.cookie = w.cookie 494 | } 495 | select { 496 | case ch := <-w.quit: 497 | w.quit <- ch 498 | case w.Event <- event: 499 | } 500 | return true 501 | } 502 | 503 | func toWindowsFlags(mask uint64) uint32 { 504 | var m uint32 505 | if mask&FS_ACCESS != 0 { 506 | m |= syscall.FILE_NOTIFY_CHANGE_LAST_ACCESS 507 | } 508 | if mask&FS_MODIFY != 0 { 509 | m |= syscall.FILE_NOTIFY_CHANGE_LAST_WRITE 510 | } 511 | if mask&FS_ATTRIB != 0 { 512 | m |= syscall.FILE_NOTIFY_CHANGE_ATTRIBUTES 513 | } 514 | if mask&(FS_MOVE|FS_CREATE|FS_DELETE) != 0 { 515 | m |= syscall.FILE_NOTIFY_CHANGE_FILE_NAME | syscall.FILE_NOTIFY_CHANGE_DIR_NAME 516 | } 517 | return m 518 | } 519 | 520 | func toFSnotifyFlags(action uint32) uint64 { 521 | switch action { 522 | case syscall.FILE_ACTION_ADDED: 523 | return FS_CREATE 524 | case syscall.FILE_ACTION_REMOVED: 525 | return FS_DELETE 526 | case syscall.FILE_ACTION_MODIFIED: 527 | return FS_MODIFY 528 | case syscall.FILE_ACTION_RENAMED_OLD_NAME: 529 | return FS_MOVED_FROM 530 | case syscall.FILE_ACTION_RENAMED_NEW_NAME: 531 | return FS_MOVED_TO 532 | } 533 | return 0 534 | } 535 | 536 | const ( 537 | // Options for AddWatch 538 | FS_ONESHOT = 0x80000000 539 | FS_ONLYDIR = 0x1000000 540 | 541 | // Events 542 | FS_ACCESS = 0x1 543 | FS_ALL_EVENTS = 0xfff 544 | FS_ATTRIB = 0x4 545 | FS_CLOSE = 0x18 546 | FS_CREATE = 0x100 547 | FS_DELETE = 0x200 548 | FS_DELETE_SELF = 0x400 549 | FS_MODIFY = 0x2 550 | FS_MOVE = 0xc0 551 | FS_MOVED_FROM = 0x40 552 | FS_MOVED_TO = 0x80 553 | FS_MOVE_SELF = 0x800 554 | 555 | // Special events 556 | FS_IGNORED = 0x8000 557 | FS_Q_OVERFLOW = 0x4000 558 | ) 559 | 560 | var eventBits = []struct { 561 | Value uint32 562 | Name string 563 | }{ 564 | {FS_ACCESS, "FS_ACCESS"}, 565 | {FS_ATTRIB, "FS_ATTRIB"}, 566 | {FS_CREATE, "FS_CREATE"}, 567 | {FS_DELETE, "FS_DELETE"}, 568 | {FS_DELETE_SELF, "FS_DELETE_SELF"}, 569 | {FS_MODIFY, "FS_MODIFY"}, 570 | {FS_MOVED_FROM, "FS_MOVED_FROM"}, 571 | {FS_MOVED_TO, "FS_MOVED_TO"}, 572 | {FS_MOVE_SELF, "FS_MOVE_SELF"}, 573 | {FS_IGNORED, "FS_IGNORED"}, 574 | {FS_Q_OVERFLOW, "FS_Q_OVERFLOW"}, 575 | } 576 | --------------------------------------------------------------------------------