diff --git a/go.mod b/go.mod index 880951bff..4bf5c9b56 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/jellydator/ttlcache/v3 v3.4.0 github.com/jinzhu/now v1.1.5 github.com/justinas/alice v1.2.0 - github.com/kovidgoyal/imaging v1.6.4 + github.com/kovidgoyal/imaging v1.7.2 github.com/leonelquinteros/gotext v1.7.2 github.com/libregraph/idm v0.5.0 github.com/libregraph/lico v0.66.0 @@ -104,7 +104,7 @@ require ( go.opentelemetry.io/otel/trace v1.38.0 golang.org/x/crypto v0.43.0 golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac - golang.org/x/image v0.31.0 + golang.org/x/image v0.32.0 golang.org/x/net v0.46.0 golang.org/x/oauth2 v0.32.0 golang.org/x/sync v0.17.0 @@ -257,6 +257,7 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/klauspost/cpuid/v2 v2.2.11 // indirect + github.com/kovidgoyal/go-parallel v1.0.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/dsig v1.0.0 // indirect @@ -327,6 +328,7 @@ require ( github.com/rs/xid v1.6.0 // indirect github.com/russellhaering/goxmldsig v1.5.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd // indirect github.com/segmentio/asm v1.2.0 // indirect github.com/segmentio/kafka-go v0.4.49 // indirect github.com/segmentio/ksuid v1.0.4 // indirect diff --git a/go.sum b/go.sum index 270baeff7..445fbec99 100644 --- a/go.sum +++ b/go.sum @@ -729,8 +729,10 @@ github.com/kolo/xmlrpc v0.0.0-20200310150728-e0350524596b/go.mod h1:o03bZfuBwAXH github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kovidgoyal/imaging v1.6.4 h1:K0idhRPXnRrJBKnBYcTfI1HTWSNDeAn7hYDvf9I0dCk= -github.com/kovidgoyal/imaging v1.6.4/go.mod h1:bEIgsaZmXlvFfkv/CUxr9rJook6AQkJnpB5EPosRfRY= +github.com/kovidgoyal/go-parallel v1.0.1 h1:nYUjN+EdpbmQjTg3N5eTUInuXTB3/1oD2vHdaMfuHoI= +github.com/kovidgoyal/go-parallel v1.0.1/go.mod h1:BJNIbe6+hxyFWv7n6oEDPj3PA5qSw5OCtf0hcVxWJiw= +github.com/kovidgoyal/imaging v1.7.2 h1:mmT6k6Az3mC6dbqdZ6Q9KQCdZFWTAQ+q97NyGZgJ/2c= +github.com/kovidgoyal/imaging v1.7.2/go.mod h1:GdkCORjfZMMGFY0Pb7TDmRhj7PDhxF/QShKukSCj0VU= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -1079,6 +1081,8 @@ github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvD github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc= +github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sacloud/libsacloud v1.36.2/go.mod h1:P7YAOVmnIn3DKHqCZcUKYUXmSwGBm3yS7IBEjKVSrjg= github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8= @@ -1360,8 +1364,8 @@ golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScy golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= -golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA= -golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA= +golang.org/x/image v0.32.0 h1:6lZQWq75h7L5IWNk0r+SCpUJ6tUVd3v4ZHnbRKLkUDQ= +golang.org/x/image v0.32.0/go.mod h1:/R37rrQmKXtO6tYXAjtDLwQgFLHmhW+V6ayXlxzP2Pc= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= diff --git a/vendor/github.com/kovidgoyal/go-parallel/.gitignore b/vendor/github.com/kovidgoyal/go-parallel/.gitignore new file mode 100644 index 000000000..1521c8b76 --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/.gitignore @@ -0,0 +1 @@ +dist diff --git a/vendor/github.com/kovidgoyal/go-parallel/.goreleaser.yaml b/vendor/github.com/kovidgoyal/go-parallel/.goreleaser.yaml new file mode 100644 index 000000000..2bb741244 --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/.goreleaser.yaml @@ -0,0 +1,2 @@ +builds: + - skip: true diff --git a/vendor/github.com/kovidgoyal/go-parallel/LICENSE b/vendor/github.com/kovidgoyal/go-parallel/LICENSE new file mode 100644 index 000000000..c17eaa848 --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/LICENSE @@ -0,0 +1,28 @@ +BSD 3-Clause License + +Copyright (c) 2025, Kovid Goyal + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. 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. + +3. Neither the name of the copyright holder 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 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. diff --git a/vendor/github.com/kovidgoyal/go-parallel/README.md b/vendor/github.com/kovidgoyal/go-parallel/README.md new file mode 100644 index 000000000..3c802e0c4 --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/README.md @@ -0,0 +1,5 @@ +# go-parallel + +Utility functions to make running code in parallel easier and safer. +Panics in go routines are turned into regular errors, instead of crashing +the program. diff --git a/vendor/github.com/kovidgoyal/go-parallel/parallel.go b/vendor/github.com/kovidgoyal/go-parallel/parallel.go new file mode 100644 index 000000000..6da26dd37 --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/parallel.go @@ -0,0 +1,171 @@ +package parallel + +import ( + "fmt" + "iter" + "runtime" + "slices" + "strings" + "sync" +) + +var _ = fmt.Print + +type PanicError struct { + frames []runtime.Frame + panic_value any +} + +const indent_lead = " " + +func format_frame_line(frame runtime.Frame) string { + return fmt.Sprintf("\r\n%s%s%s:%d", indent_lead, frame.Function, frame.File, frame.Line) +} + +func (e *PanicError) walk(level int, yield func(string) bool) bool { + s := "Panic" + cause := fmt.Sprintf("%v", e.panic_value) + if _, ok := e.panic_value.(*PanicError); ok { + cause = "sub-panic (see below)" + } + if level > 0 { + s = "\r\n--> Sub-panic" + } + if !yield(fmt.Sprintf("%s caused by: %s\r\nStack trace (most recent call first):", s, cause)) { + return false + } + for _, f := range e.frames { + if !yield(format_frame_line(f)) { + return false + } + } + if sp, ok := e.panic_value.(*PanicError); ok { + return sp.walk(level+1, yield) + } + return true +} + +func (e *PanicError) lines() iter.Seq[string] { + return func(yield func(string) bool) { + e.walk(0, yield) + } +} + +func (e *PanicError) Error() string { + return strings.Join(slices.Collect(e.lines()), "") +} + +func (e *PanicError) Unwrap() error { + if ans, ok := e.panic_value.(*PanicError); ok { + return ans + } + return nil +} + +// Format a stack trace on panic and return it as an error +func Format_stacktrace_on_panic(r any, skip_frames int) (err *PanicError) { + pcs := make([]uintptr, 512) + n := runtime.Callers(2+skip_frames, pcs) + var ans []runtime.Frame + frames := runtime.CallersFrames(pcs[:n]) + found_first_frame := false + for frame, more := frames.Next(); more; frame, more = frames.Next() { + if !found_first_frame { + if strings.HasPrefix(frame.Function, "runtime.") { + continue + } + found_first_frame = true + } + ans = append(ans, frame) + } + return &PanicError{frames: ans, panic_value: r} +} + +// Run the specified function in parallel over chunks from the specified range. +// If the function panics, it is turned into a regular error. If multiple function calls panic, +// any one of the panics will be returned. +func Run_in_parallel_over_range(num_procs int, f func(int, int), start, limit int) (err error) { + num_items := limit - start + if num_procs <= 0 { + num_procs = runtime.GOMAXPROCS(0) + } + num_procs = max(1, min(num_procs, num_items)) + if num_procs < 2 { + defer func() { + if r := recover(); r != nil { + err = Format_stacktrace_on_panic(r, 1) + } + }() + f(start, limit) + return + } + chunk_sz := max(1, num_items/num_procs) + var wg sync.WaitGroup + echan := make(chan error, num_items/chunk_sz+1) + for start < limit { + end := min(start+chunk_sz, limit) + wg.Add(1) + go func(start, end int) { + defer func() { + if r := recover(); r != nil { + echan <- Format_stacktrace_on_panic(r, 1) + } + wg.Done() + }() + f(start, end) + }(start, end) + start = end + } + wg.Wait() + close(echan) + for qerr := range echan { + return qerr + } + return +} + +// Run the specified function in parallel over chunks from the specified range. +// If the function panics, it is turned into a regular error. If the function +// returns an error it is returned. If multiple function calls panic or return errors, +// any one of them will be returned. +func Run_in_parallel_over_range_with_error(num_procs int, f func(int, int) error, start, limit int) (err error) { + num_items := limit - start + if num_procs <= 0 { + num_procs = runtime.GOMAXPROCS(0) + } + num_procs = max(1, min(num_procs, num_items)) + if num_procs < 2 { + defer func() { + if r := recover(); r != nil { + err = Format_stacktrace_on_panic(r, 1) + } + }() + err = f(start, limit) + return + } + chunk_sz := max(1, num_items/num_procs) + var wg sync.WaitGroup + echan := make(chan error, num_items/chunk_sz+1) + for start < limit { + end := min(start+chunk_sz, limit) + wg.Add(1) + go func(start, end int) { + defer func() { + if r := recover(); r != nil { + echan <- Format_stacktrace_on_panic(r, 1) + } + wg.Done() + }() + if cerr := f(start, end); cerr != nil { + echan <- cerr + } + }(start, end) + start = end + } + wg.Wait() + close(echan) + for qerr := range echan { + return qerr + } + return +} diff --git a/vendor/github.com/kovidgoyal/go-parallel/publish.py b/vendor/github.com/kovidgoyal/go-parallel/publish.py new file mode 100644 index 000000000..aa7e866fe --- /dev/null +++ b/vendor/github.com/kovidgoyal/go-parallel/publish.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# License: GPLv3 Copyright: 2024, Kovid Goyal + +import os +import subprocess + + +VERSION = '1.0.1' + + +def run(*args: str): + cp = subprocess.run(args) + if cp.returncode != 0: + raise SystemExit(cp.returncode) + + +def main(): + try: + ans = input(f'Publish version \033[91m{VERSION}\033[m (y/n): ') + except KeyboardInterrupt: + ans = 'n' + if ans.lower() != 'y': + return + os.environ['GITHUB_TOKEN'] = open(os.path.join(os.environ['PENV'], 'github-token')).read().strip().partition(':')[2] + run('git', 'tag', '-a', 'v' + VERSION, '-m', f'version {VERSION}') + run('git', 'push') + run('goreleaser', 'release', '--clean') + + +if __name__ == '__main__': + main() diff --git a/vendor/github.com/kovidgoyal/imaging/.goreleaser.yaml b/vendor/github.com/kovidgoyal/imaging/.goreleaser.yaml index feb4d664d..4336820b5 100644 --- a/vendor/github.com/kovidgoyal/imaging/.goreleaser.yaml +++ b/vendor/github.com/kovidgoyal/imaging/.goreleaser.yaml @@ -19,7 +19,7 @@ builds: - skip: true archives: - - format: tar.gz + - formats: [ 'tar.gz' ] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ @@ -31,7 +31,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: [ 'zip' ] changelog: disable: true diff --git a/vendor/github.com/kovidgoyal/imaging/adjust.go b/vendor/github.com/kovidgoyal/imaging/adjust.go index 971aebc6f..8aedb1c88 100644 --- a/vendor/github.com/kovidgoyal/imaging/adjust.go +++ b/vendor/github.com/kovidgoyal/imaging/adjust.go @@ -10,10 +10,10 @@ import ( func Grayscale(img image.Image) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) + src.Scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) for x := 0; x < src.w; x++ { d := dst.Pix[i : i+3 : i+3] r := d[0] @@ -27,7 +27,9 @@ func Grayscale(img image.Image) *image.NRGBA { i += 4 } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -35,10 +37,10 @@ func Grayscale(img image.Image) *image.NRGBA { func Invert(img image.Image) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) + src.Scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) for x := 0; x < src.w; x++ { d := dst.Pix[i : i+3 : i+3] d[0] = 255 - d[0] @@ -47,7 +49,9 @@ func Invert(img image.Image) *image.NRGBA { i += 4 } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -58,9 +62,9 @@ func Invert(img image.Image) *image.NRGBA { // The percentage = -100 gives the image with the saturation value zeroed for each pixel (grayscale). // // Examples: -// dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%. -// dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%. // +// dstImage = imaging.AdjustSaturation(srcImage, 25) // Increase image saturation by 25%. +// dstImage = imaging.AdjustSaturation(srcImage, -10) // Decrease image saturation by 10%. func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA { if percentage == 0 { return Clone(img) @@ -85,9 +89,9 @@ func AdjustSaturation(img image.Image, percentage float64) *image.NRGBA { // The shift = 180 (or -180) corresponds to a 180° degree rotation of the color wheel and thus gives the image with its hue inverted for each pixel. // // Examples: -// dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°. -// dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°. // +// dstImage = imaging.AdjustHue(srcImage, 90) // Shift Hue by 90°. +// dstImage = imaging.AdjustHue(srcImage, -30) // Shift Hue by -30°. func AdjustHue(img image.Image, shift float64) *image.NRGBA { if math.Mod(shift, 360) == 0 { return Clone(img) @@ -116,7 +120,6 @@ func AdjustHue(img image.Image, shift float64) *image.NRGBA { // // dstImage = imaging.AdjustContrast(srcImage, -10) // Decrease image contrast by 10%. // dstImage = imaging.AdjustContrast(srcImage, 20) // Increase image contrast by 20%. -// func AdjustContrast(img image.Image, percentage float64) *image.NRGBA { if percentage == 0 { return Clone(img) @@ -148,7 +151,6 @@ func AdjustContrast(img image.Image, percentage float64) *image.NRGBA { // // dstImage = imaging.AdjustBrightness(srcImage, -15) // Decrease image brightness by 15%. // dstImage = imaging.AdjustBrightness(srcImage, 10) // Increase image brightness by 10%. -// func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA { if percentage == 0 { return Clone(img) @@ -172,7 +174,6 @@ func AdjustBrightness(img image.Image, percentage float64) *image.NRGBA { // Example: // // dstImage = imaging.AdjustGamma(srcImage, 0.7) -// func AdjustGamma(img image.Image, gamma float64) *image.NRGBA { if gamma == 1 { return Clone(img) @@ -198,7 +199,6 @@ func AdjustGamma(img image.Image, gamma float64) *image.NRGBA { // // dstImage = imaging.AdjustSigmoid(srcImage, 0.5, 3.0) // Increase the contrast. // dstImage = imaging.AdjustSigmoid(srcImage, 0.5, -3.0) // Decrease the contrast. -// func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA { if factor == 0 { return Clone(img) @@ -212,14 +212,14 @@ func AdjustSigmoid(img image.Image, midpoint, factor float64) *image.NRGBA { e := 1.0e-6 if factor > 0 { - for i := 0; i < 256; i++ { + for i := range 256 { x := float64(i) / 255.0 sigX := sigmoid(a, b, x) f := (sigX - sig0) / (sig1 - sig0) lut[i] = clamp(f * 255.0) } } else { - for i := 0; i < 256; i++ { + for i := range 256 { x := float64(i) / 255.0 arg := math.Min(math.Max((sig1-sig0)*x+sig0, e), 1.0-e) f := a - math.Log(1.0/arg-1.0)/b @@ -239,10 +239,10 @@ func adjustLUT(img image.Image, lut []uint8) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) lut = lut[0:256] - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) + src.Scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) for x := 0; x < src.w; x++ { d := dst.Pix[i : i+3 : i+3] d[0] = lut[d[0]] @@ -251,7 +251,9 @@ func adjustLUT(img image.Image, lut []uint8) *image.NRGBA { i += 4 } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -270,14 +272,13 @@ func adjustLUT(img image.Image, lut []uint8) *image.NRGBA { // return color.NRGBA{uint8(r), c.G, c.B, c.A} // } // ) -// func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) + src.Scan(0, y, src.w, y+1, dst.Pix[i:i+src.w*4]) for x := 0; x < src.w; x++ { d := dst.Pix[i : i+4 : i+4] r := d[0] @@ -292,6 +293,8 @@ func AdjustFunc(img image.Image, fn func(c color.NRGBA) color.NRGBA) *image.NRGB i += 4 } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } diff --git a/vendor/github.com/kovidgoyal/imaging/convolution.go b/vendor/github.com/kovidgoyal/imaging/convolution.go index 11eddc162..fac4972a8 100644 --- a/vendor/github.com/kovidgoyal/imaging/convolution.go +++ b/vendor/github.com/kovidgoyal/imaging/convolution.go @@ -70,8 +70,8 @@ func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *imag } } - parallel(0, h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { for x := 0; x < w; x++ { var r, g, b float64 for _, c := range coefs { @@ -123,7 +123,9 @@ func convolve(img image.Image, kernel []float64, options *ConvolveOptions) *imag d[3] = src.Pix[srcOff+3] } } - }) + }, 0, h); err != nil { + panic(err) + } return dst } diff --git a/vendor/github.com/kovidgoyal/imaging/effects.go b/vendor/github.com/kovidgoyal/imaging/effects.go index 47316b701..d034f362e 100644 --- a/vendor/github.com/kovidgoyal/imaging/effects.go +++ b/vendor/github.com/kovidgoyal/imaging/effects.go @@ -15,7 +15,6 @@ func gaussianBlurKernel(x, sigma float64) float64 { // Example: // // dstImage := imaging.Blur(srcImage, 3.5) -// func Blur(img image.Image, sigma float64) *image.NRGBA { if sigma <= 0 { return Clone(img) @@ -36,25 +35,19 @@ func blurHorizontal(img image.Image, kernel []float64) *image.NRGBA { dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) radius := len(kernel) - 1 - parallel(0, src.h, func(ys <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, src.w*4) scanLineF := make([]float64, len(scanLine)) - for y := range ys { - src.scan(0, y, src.w, y+1, scanLine) + for y := start; y < limit; y++ { + src.Scan(0, y, src.w, y+1, scanLine) for i, v := range scanLine { scanLineF[i] = float64(v) } for x := 0; x < src.w; x++ { - min := x - radius - if min < 0 { - min = 0 - } - max := x + radius - if max > src.w-1 { - max = src.w - 1 - } + minv := max(0, x-radius) + maxv := min(x+radius, src.w-1) var r, g, b, a, wsum float64 - for ix := min; ix <= max; ix++ { + for ix := minv; ix <= maxv; ix++ { i := ix * 4 weight := kernel[absint(x-ix)] wsum += weight @@ -76,7 +69,9 @@ func blurHorizontal(img image.Image, kernel []float64) *image.NRGBA { } } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -86,25 +81,19 @@ func blurVertical(img image.Image, kernel []float64) *image.NRGBA { dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) radius := len(kernel) - 1 - parallel(0, src.w, func(xs <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, src.h*4) scanLineF := make([]float64, len(scanLine)) - for x := range xs { - src.scan(x, 0, x+1, src.h, scanLine) + for x := start; x < limit; x++ { + src.Scan(x, 0, x+1, src.h, scanLine) for i, v := range scanLine { scanLineF[i] = float64(v) } for y := 0; y < src.h; y++ { - min := y - radius - if min < 0 { - min = 0 - } - max := y + radius - if max > src.h-1 { - max = src.h - 1 - } + minv := max(0, y-radius) + maxv := min(y+radius, src.h-1) var r, g, b, a, wsum float64 - for iy := min; iy <= max; iy++ { + for iy := minv; iy <= maxv; iy++ { i := iy * 4 weight := kernel[absint(y-iy)] wsum += weight @@ -126,7 +115,9 @@ func blurVertical(img image.Image, kernel []float64) *image.NRGBA { } } } - }) + }, 0, src.w); err != nil { + panic(err) + } return dst } @@ -137,7 +128,6 @@ func blurVertical(img image.Image, kernel []float64) *image.NRGBA { // Example: // // dstImage := imaging.Sharpen(srcImage, 3.5) -// func Sharpen(img image.Image, sigma float64) *image.NRGBA { if sigma <= 0 { return Clone(img) @@ -147,10 +137,10 @@ func Sharpen(img image.Image, sigma float64) *image.NRGBA { dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) blurred := Blur(img, sigma) - parallel(0, src.h, func(ys <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, src.w*4) - for y := range ys { - src.scan(0, y, src.w, y+1, scanLine) + for y := start; y < limit; y++ { + src.Scan(0, y, src.w, y+1, scanLine) j := y * dst.Stride for i := 0; i < src.w*4; i++ { val := int(scanLine[i])<<1 - int(blurred.Pix[j]) @@ -163,7 +153,9 @@ func Sharpen(img image.Image, sigma float64) *image.NRGBA { j++ } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } diff --git a/vendor/github.com/kovidgoyal/imaging/histogram.go b/vendor/github.com/kovidgoyal/imaging/histogram.go index c547fe822..3550011e1 100644 --- a/vendor/github.com/kovidgoyal/imaging/histogram.go +++ b/vendor/github.com/kovidgoyal/imaging/histogram.go @@ -19,12 +19,12 @@ func Histogram(img image.Image) [256]float64 { return histogram } - parallel(0, src.h, func(ys <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { var tmpHistogram [256]float64 var tmpTotal float64 scanLine := make([]uint8, src.w*4) - for y := range ys { - src.scan(0, y, src.w, y+1, scanLine) + for y := start; y < limit; y++ { + src.Scan(0, y, src.w, y+1, scanLine) i := 0 for x := 0; x < src.w; x++ { s := scanLine[i : i+3 : i+3] @@ -38,14 +38,16 @@ func Histogram(img image.Image) [256]float64 { } } mu.Lock() - for i := 0; i < 256; i++ { + for i := range 256 { histogram[i] += tmpHistogram[i] } total += tmpTotal mu.Unlock() - }) + }, 0, src.h); err != nil { + panic(err) + } - for i := 0; i < 256; i++ { + for i := range 256 { histogram[i] = histogram[i] / total } return histogram diff --git a/vendor/github.com/kovidgoyal/imaging/io.go b/vendor/github.com/kovidgoyal/imaging/io.go index f6c6da86b..f9aac3f0a 100644 --- a/vendor/github.com/kovidgoyal/imaging/io.go +++ b/vendor/github.com/kovidgoyal/imaging/io.go @@ -1,7 +1,7 @@ package imaging import ( - "encoding/binary" + "bytes" "errors" "image" "image/draw" @@ -9,11 +9,14 @@ import ( "image/jpeg" "image/png" "io" - "io/ioutil" "os" "path/filepath" + "strconv" "strings" + "github.com/kovidgoyal/imaging/prism/meta/autometa" + "github.com/rwcarlsen/goexif/exif" + "golang.org/x/image/bmp" "golang.org/x/image/tiff" ) @@ -35,7 +38,7 @@ type decodeConfig struct { } var defaultDecodeConfig = decodeConfig{ - autoOrientation: false, + autoOrientation: true, } // DecodeOption sets an optional parameter for the Decode and Open functions. @@ -43,7 +46,7 @@ type DecodeOption func(*decodeConfig) // AutoOrientation returns a DecodeOption that sets the auto-orientation mode. // If auto-orientation is enabled, the image will be transformed after decoding -// according to the EXIF orientation tag (if present). By default it's disabled. +// according to the EXIF orientation tag (if present). By default it's enabled. func AutoOrientation(enabled bool) DecodeOption { return func(c *decodeConfig) { c.autoOrientation = enabled @@ -53,6 +56,7 @@ func AutoOrientation(enabled bool) DecodeOption { // Decode reads an image from r. func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { cfg := defaultDecodeConfig + for _, option := range opts { option(&cfg) } @@ -61,25 +65,27 @@ func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { img, _, err := image.Decode(r) return img, err } - - var orient orientation - pr, pw := io.Pipe() - r = io.TeeReader(r, pw) - done := make(chan struct{}) - go func() { - defer close(done) - orient = readOrientation(pr) - io.Copy(ioutil.Discard, pr) - }() + md, r, err := autometa.Load(r) + var oval orientation = orientationUnspecified + if err == nil && md != nil && len(md.ExifData) > 6 { + exif_data, err := exif.Decode(bytes.NewReader(md.ExifData)) + if err == nil { + orient, err := exif_data.Get(exif.Orientation) + if err == nil && orient != nil { + x, err := strconv.ParseUint(orient.String(), 10, 0) + if err == nil && x > 0 && x < 9 { + oval = orientation(int(x)) + } + } + } + } img, _, err := image.Decode(r) - pw.Close() - <-done if err != nil { return nil, err } - return fixOrientation(img, orient), nil + return fixOrientation(img, oval), nil } // Open loads an image from file. @@ -91,7 +97,6 @@ func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { // // // Load an image and transform it depending on the EXIF orientation tag (if present). // img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true)) -// func Open(filename string, opts ...DecodeOption) (image.Image, error) { file, err := fs.Open(filename) if err != nil { @@ -101,6 +106,15 @@ func Open(filename string, opts ...DecodeOption) (image.Image, error) { return Decode(file, opts...) } +func OpenConfig(filename string) (ans image.Config, format_name string, err error) { + file, err := fs.Open(filename) + if err != nil { + return ans, "", err + } + defer file.Close() + return image.DecodeConfig(file) +} + // Format is an image file format. type Format int @@ -111,6 +125,10 @@ const ( GIF TIFF BMP + PBM + PGM + PPM + PAM ) var formatExts = map[string]Format{ @@ -121,6 +139,10 @@ var formatExts = map[string]Format{ "tif": TIFF, "tiff": TIFF, "bmp": BMP, + "pbm": PBM, + "pgm": PGM, + "ppm": PPM, + "pam": PAM, } var formatNames = map[Format]string{ @@ -129,6 +151,9 @@ var formatNames = map[Format]string{ GIF: "GIF", TIFF: "TIFF", BMP: "BMP", + PBM: "PBM", + PGM: "PGM", + PAM: "PAM", } func (f Format) String() string { @@ -264,7 +289,6 @@ func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) e // // // Save the image as JPEG with optional quality parameter set to 80. // err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80)) -// func Save(img image.Image, filename string, opts ...EncodeOption) (err error) { f, err := FormatFromFilename(filename) if err != nil { @@ -298,129 +322,6 @@ const ( orientationRotate90 = 8 ) -// readOrientation tries to read the orientation EXIF flag from image data in r. -// If the EXIF data block is not found or the orientation flag is not found -// or any other error occures while reading the data, it returns the -// orientationUnspecified (0) value. -func readOrientation(r io.Reader) orientation { - const ( - markerSOI = 0xffd8 - markerAPP1 = 0xffe1 - exifHeader = 0x45786966 - byteOrderBE = 0x4d4d - byteOrderLE = 0x4949 - orientationTag = 0x0112 - ) - - // Check if JPEG SOI marker is present. - var soi uint16 - if err := binary.Read(r, binary.BigEndian, &soi); err != nil { - return orientationUnspecified - } - if soi != markerSOI { - return orientationUnspecified // Missing JPEG SOI marker. - } - - // Find JPEG APP1 marker. - for { - var marker, size uint16 - if err := binary.Read(r, binary.BigEndian, &marker); err != nil { - return orientationUnspecified - } - if err := binary.Read(r, binary.BigEndian, &size); err != nil { - return orientationUnspecified - } - if marker>>8 != 0xff { - return orientationUnspecified // Invalid JPEG marker. - } - if marker == markerAPP1 { - break - } - if size < 2 { - return orientationUnspecified // Invalid block size. - } - if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil { - return orientationUnspecified - } - } - - // Check if EXIF header is present. - var header uint32 - if err := binary.Read(r, binary.BigEndian, &header); err != nil { - return orientationUnspecified - } - if header != exifHeader { - return orientationUnspecified - } - if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { - return orientationUnspecified - } - - // Read byte order information. - var ( - byteOrderTag uint16 - byteOrder binary.ByteOrder - ) - if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil { - return orientationUnspecified - } - switch byteOrderTag { - case byteOrderBE: - byteOrder = binary.BigEndian - case byteOrderLE: - byteOrder = binary.LittleEndian - default: - return orientationUnspecified // Invalid byte order flag. - } - if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { - return orientationUnspecified - } - - // Skip the EXIF offset. - var offset uint32 - if err := binary.Read(r, byteOrder, &offset); err != nil { - return orientationUnspecified - } - if offset < 8 { - return orientationUnspecified // Invalid offset value. - } - if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil { - return orientationUnspecified - } - - // Read the number of tags. - var numTags uint16 - if err := binary.Read(r, byteOrder, &numTags); err != nil { - return orientationUnspecified - } - - // Find the orientation tag. - for i := 0; i < int(numTags); i++ { - var tag uint16 - if err := binary.Read(r, byteOrder, &tag); err != nil { - return orientationUnspecified - } - if tag != orientationTag { - if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil { - return orientationUnspecified - } - continue - } - if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil { - return orientationUnspecified - } - var val uint16 - if err := binary.Read(r, byteOrder, &val); err != nil { - return orientationUnspecified - } - if val < 1 || val > 8 { - return orientationUnspecified // Invalid tag value. - } - return orientation(val) - } - return orientationUnspecified // Missing orientation tag. -} - // fixOrientation applies a transform to img corresponding to the given orientation flag. func fixOrientation(img image.Image, o orientation) image.Image { switch o { diff --git a/vendor/github.com/kovidgoyal/imaging/netpbm.go b/vendor/github.com/kovidgoyal/imaging/netpbm.go new file mode 100644 index 000000000..91ad6d439 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/netpbm.go @@ -0,0 +1,510 @@ +package imaging + +import ( + "bufio" + "errors" + "fmt" + "image" + "image/color" + "io" + "strconv" + "strings" +) + +var _ = fmt.Print + +// skip_comments reads ahead past any comment lines (starting with #) and returns the first non-comment, non-empty line. +func skip_comments(br *bufio.Reader) (string, error) { + for { + line, err := br.ReadString('\n') + if err != nil { + return "", err + } + line = strings.TrimSpace(line) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + return line, nil + } +} + +type data_type int + +const ( + rgb data_type = iota + blackwhite + grayscale +) + +type header struct { + format string + width, height, num_channels uint + maxval uint32 + has_alpha bool + data_type data_type +} + +func (h header) bytes_per_channel() uint { + if h.maxval > 255 { + return 2 + } + return 1 +} + +func (h header) num_bytes_per_pixel() uint { + return h.num_channels * h.bytes_per_channel() +} + +func read_ppm_header(br *bufio.Reader, magic string) (ans header, err error) { + ans.format = magic + required_num_fields := 3 + switch magic { + case "P1", "P4": + ans.data_type = blackwhite + ans.num_channels = 1 + ans.maxval = 1 + required_num_fields = 2 + case "P2", "P5": + ans.data_type = grayscale + ans.num_channels = 1 + default: + ans.data_type = rgb + ans.num_channels = 3 + } + var fields []uint + for len(fields) < required_num_fields { + var line string + if line, err = skip_comments(br); err != nil { + return + } + for x := range strings.FieldsSeq(line) { + var val uint64 + if val, err = strconv.ParseUint(x, 10, 0); err != nil { + return + } + fields = append(fields, uint(val)) + } + } + ans.width = fields[0] + ans.height = fields[1] + if required_num_fields > 2 { + ans.maxval = uint32(fields[2]) + } + if ans.maxval > 65535 { + return ans, fmt.Errorf("header specifies a maximum value %d larger than 65535", ans.maxval) + } + return +} + +func read_pam_header(br *bufio.Reader) (ans header, err error) { + ans.format = "P7" + ans.data_type = rgb + ans.num_channels = 3 + for { + line, err := skip_comments(br) + if err != nil { + return ans, err + } + if line == "ENDHDR" { + break + } + prefix, payload, found := strings.Cut(line, " ") + if !found { + return ans, fmt.Errorf("invalid line in header: %#v", line) + } + switch prefix { + case "WIDTH": + w, err := strconv.ParseUint(payload, 10, 0) + if err != nil { + return ans, fmt.Errorf("invalid width %#v in header: %w", payload, err) + } + ans.width = uint(w) + case "HEIGHT": + w, err := strconv.ParseUint(payload, 10, 0) + if err != nil { + return ans, fmt.Errorf("invalid height %#v in header: %w", payload, err) + } + ans.height = uint(w) + case "MAXVAL": + w, err := strconv.ParseUint(payload, 10, 0) + if err != nil { + return ans, fmt.Errorf("invalid maxval %#v in header: %w", payload, err) + } + ans.maxval = uint32(w) + case "DEPTH": + w, err := strconv.ParseUint(payload, 10, 0) + if err != nil { + return ans, fmt.Errorf("invalid depth %#v in header: %w", payload, err) + } + if w == 0 || w > 4 { + return ans, fmt.Errorf("invalid depth %d in header", w) + } + ans.num_channels = uint(w) + case "TUPLTYPE": + switch payload { + case "BLACKANDWHITE": + ans.data_type = blackwhite + case "BLACKANDWHITE_ALPHA": + ans.has_alpha = true + ans.data_type = blackwhite + case "GRAYSCALE": + ans.data_type = grayscale + case "GRAYSCALE_ALPHA": + ans.has_alpha = true + ans.data_type = grayscale + case "RGB": + case "RGB_ALPHA": + ans.has_alpha = true + default: + return ans, fmt.Errorf("invalid TUPLTYPE in header: %#v", payload) + } + } + } + if ans.width == 0 || ans.height == 0 || ans.maxval == 0 { + return ans, fmt.Errorf("header does not specify width, height and maximum value") + } + ok := true + switch ans.data_type { + case rgb: + ok = (!ans.has_alpha && ans.num_channels == 3) || (ans.has_alpha && ans.num_channels == 4) + case blackwhite, grayscale: + ok = (!ans.has_alpha && ans.num_channels == 1) || (ans.has_alpha && ans.num_channels == 2) + } + if !ok { + return ans, fmt.Errorf("header specified depth: %d does not match TUPLTYPE", ans.num_channels) + } + return +} + +func read_header(br *bufio.Reader) (ans header, err error) { + b := []byte{0, 0} + if _, err = io.ReadFull(br, b); err != nil { + return ans, err + } + magic := string(b) + switch magic { + case "P1", "P2", "P3", "P4", "P5", "P6": + return read_ppm_header(br, magic) + case "P7": + return read_pam_header(br) + default: + err = fmt.Errorf("unsupported netPBM format: %#v", magic) + return + } +} + +func ascii_range_over_values(br *bufio.Reader, h header, callback func(uint32, []uint8) []uint8) (ans []uint8, err error) { + anssz := h.width * h.height * h.num_bytes_per_pixel() + ans = make([]uint8, 0, anssz) + for uint(len(ans)) < anssz { + token, err := br.ReadString(' ') + if err != nil && err != io.EOF { + return nil, err + } + for field := range strings.FieldsSeq(token) { + if val, perr := strconv.ParseUint(field, 10, 16); perr == nil { + ans = callback(uint32(val), ans) + } + } + if err == io.EOF { + break + } + } + return +} + +func decode_rgb_ascii(br *bufio.Reader, h header) (ans []byte, err error) { + mult := uint32(255) + if h.maxval > 255 { + mult = 65535 + } + anssz := h.width * h.height * h.num_bytes_per_pixel() + if mult == 255 { + ans, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 { + ch := (uint32(val) * mult) / h.maxval + return append(ans, uint8(ch)) + }) + } else { + ans, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 { + ch := (uint32(val) * mult) / h.maxval + ans = append(ans, uint8(ch)) + if len(ans)%6 == 0 { // alpha is always 255 + ans = append(ans, 255, 255) + } + return ans + }) + } + if err != nil { + return nil, err + } + if uint(len(ans)) < anssz { + return nil, errors.New("insufficient color data present in PPM file") + } + return +} + +func DecodeNetPBMConfig(r io.Reader) (cfg image.Config, err error) { + br := bufio.NewReader(r) + h, err := read_header(br) + if err != nil { + return cfg, err + } + cfg.Width = int(h.width) + cfg.Height = int(h.height) + cfg.ColorModel = NRGBModel + switch h.data_type { + case blackwhite, grayscale: + if h.has_alpha { + if h.maxval > 255 { + cfg.ColorModel = color.NRGBA64Model + } else { + cfg.ColorModel = color.NRGBAModel + } + } else { + if h.maxval > 255 { + cfg.ColorModel = color.Gray16Model + } else { + cfg.ColorModel = color.GrayModel + } + } + default: + if h.has_alpha { + if h.maxval > 255 { + cfg.ColorModel = color.NRGBA64Model + } else { + cfg.ColorModel = color.NRGBAModel + } + } else { + if h.maxval > 255 { + cfg.ColorModel = color.NRGBA64Model + } else { + cfg.ColorModel = NRGBModel + } + } + } + return +} + +func decode_black_white_ascii(br *bufio.Reader, h header) (img image.Image, err error) { + r := image.Rect(0, 0, int(h.width), int(h.height)) + g := &image.Gray{Stride: r.Dx(), Rect: r} + g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 { + var c uint8 = 255 * uint8(1-(val&1)) + return append(ans, c) + }) + return g, err +} + +func decode_grayscale_ascii(br *bufio.Reader, h header) (img image.Image, err error) { + r := image.Rect(0, 0, int(h.width), int(h.height)) + if h.maxval > 255 { + g := &image.Gray16{Stride: 2 * r.Dx(), Rect: r} + g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 { + c := uint16(val * 65535 / h.maxval) + return append(ans, uint8(c>>8), uint8(c)) + }) + return g, err + } else { + g := &image.Gray{Stride: r.Dx(), Rect: r} + g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 { + c := uint8(val * 255 / h.maxval) + return append(ans, c) + }) + return g, err + } +} + +// Consume whitespace after header (per spec, it's a single whitespace, but can be more) +func skip_whitespace_before_pixel_data(br *bufio.Reader, num_of_bytes_needed uint) ([]uint8, error) { + for { + b, err := br.Peek(1) + if err != nil { + return nil, err + } + if b[0] == '\n' || b[0] == '\r' || b[0] == '\t' || b[0] == ' ' { + br.ReadByte() + } else { + break + } + } + ans := make([]byte, num_of_bytes_needed) + _, err := io.ReadFull(br, ans) + return ans, err +} + +func rescale(v uint32, num, den uint32) uint32 { + return (v * num) / den +} + +func rescale_binary_data(b []uint8, num, den uint32) error { + return run_in_parallel_over_range(0, func(start, end int) { + for i := start; i < end; i++ { + b[i] = uint8(rescale(uint32(b[i]), num, den)) + } + }, 0, len(b)) +} + +func rescale_binary_data16(b []uint8, num, den uint32) error { + if len(b)&1 != 0 { + return fmt.Errorf("pixel data is not a multiple of two but uses 16 bits per channel") + } + return run_in_parallel_over_range(0, func(start, end int) { + start *= 2 + end *= 2 + for i := start; i < end; i += 2 { + v := uint32((uint16(b[i]) << 8) | uint16(b[i+1])) + v = rescale(v, num, den) + b[i] = uint8(v >> 8) + b[i+1] = uint8(v) + } + }, 0, len(b)/2) +} + +func decode_binary_data(br *bufio.Reader, h header) (ans image.Image, err error) { + var binary_data []uint8 + if binary_data, err = skip_whitespace_before_pixel_data(br, h.width*h.height*h.num_bytes_per_pixel()); err != nil { + return + } + if n := h.num_bytes_per_pixel() * h.width * h.height; uint(len(binary_data)) < n { + return nil, fmt.Errorf( + "insufficient pixel data for image area and num_channels (%d): %f < %d", + h.num_channels, float64(len(binary_data))/float64(h.width*h.height), n/(h.width*h.height)) + } + switch { + case h.maxval < 255: + if err = rescale_binary_data(binary_data, 255, h.maxval); err != nil { + return nil, err + } + case 255 < h.maxval && h.maxval < 65535: + if err = rescale_binary_data16(binary_data, 65535, h.maxval); err != nil { + return nil, err + } + } + + r := image.Rect(0, 0, int(h.width), int(h.height)) + switch h.num_channels { + case 1: + // bw or gray without alpha + if h.maxval > 255 { + return &image.Gray16{Rect: r, Stride: r.Dx() * 2, Pix: binary_data}, nil + } + return &image.Gray{Rect: r, Stride: r.Dx(), Pix: binary_data}, nil + case 2: + // bw or gray with alpha + if h.maxval > 255 { + g := image.NewNRGBA64(r) + b := g.Pix + if err = run_in_parallel_over_range(0, func(start, end int) { + for i := start; i < end; i++ { + src := binary_data[i*4 : i*4+4] + dest := b[i*8 : i*8+8] + gray1, gray2 := src[0], src[1] + dest[0], dest[1], dest[2], dest[3], dest[4], dest[5] = gray1, gray2, gray1, gray2, gray1, gray2 + dest[6], dest[7] = src[2], src[3] + } + }, 0, int(h.width*h.height)); err != nil { + return nil, err + } + } + g := image.NewNRGBA(r) + b := g.Pix + if err = run_in_parallel_over_range(0, func(start, end int) { + for i := start; i < end; i++ { + src := binary_data[i*2 : i*2+2] + dest := b[i*4 : i*4+4] + dest[0], dest[1], dest[2], dest[3] = src[0], src[0], src[0], src[1] + } + }, 0, int(h.width*h.height)); err != nil { + return nil, err + } + return g, nil + case 3: + // RGB without alpha + if h.maxval > 255 { + g := image.NewNRGBA64(r) + b := g.Pix + if err = run_in_parallel_over_range(0, func(start, end int) { + for i := start; i < end; i++ { + src := binary_data[i*6 : i*6+6] + dest := b[i*8 : i*8+8] + copy(dest[:6], src) + dest[6], dest[7] = 255, 255 + } + }, 0, int(h.width*h.height)); err != nil { + return nil, err + } + return g, nil + } + return NewNRGBWithContiguousRGBPixels(binary_data, 0, 0, r.Dx(), r.Dy()) + case 4: + // RGB with alpha + if h.maxval <= 255 { + return &image.NRGBA{Rect: r, Stride: r.Dx() * int(h.num_bytes_per_pixel()), Pix: binary_data}, nil + } + return &image.NRGBA64{Rect: r, Stride: r.Dx() * int(h.num_bytes_per_pixel()), Pix: binary_data}, nil + default: + return nil, fmt.Errorf("unsupported number of channels: %d", h.num_channels) + } +} + +// Decode decodes a PPM image from r and returns it as an image.Image. +// Supports both P3 (ASCII) and P6 (binary) variants. +func DecodeNetPBM(r io.Reader) (img image.Image, err error) { + br := bufio.NewReader(r) + h, err := read_header(br) + if err != nil { + return nil, err + } + var binary_data []uint8 + switch h.format { + case "P1": + return decode_black_white_ascii(br, h) + case "P2": + return decode_grayscale_ascii(br, h) + case "P3": + vals, err := decode_rgb_ascii(br, h) + if err != nil { + return nil, err + } + if h.maxval <= 255 { + return NewNRGBWithContiguousRGBPixels(vals, 0, 0, int(h.width), int(h.height)) + } + return &image.NRGBA64{Pix: vals, Stride: int(h.width) * 8, Rect: image.Rect(0, 0, int(h.width), int(h.height))}, nil + case "P4": + bytes_per_row := (h.width + 7) / 8 + if binary_data, err = skip_whitespace_before_pixel_data(br, h.height*bytes_per_row); err != nil { + return nil, err + } + ans := image.NewGray(image.Rect(0, 0, int(h.width), int(h.height))) + i := 0 + for range h.height { + for x := range h.width { + byteIdx := x / 8 + bitIdx := 7 - uint(x%8) + bit := (binary_data[byteIdx] >> bitIdx) & 1 + ans.Pix[i] = (1 - bit) * 255 + i++ + } + binary_data = binary_data[bytes_per_row:] + } + if len(binary_data) > 0 { + return nil, fmt.Errorf("insufficient color data in netPBM file, need %d more bytes", len(binary_data)) + } + return ans, nil + case "P5", "P6", "P7": + return decode_binary_data(br, h) + default: + return nil, fmt.Errorf("invalid format for PPM: %#v", h.format) + } +} + +// Register this decoder with Go's image package +func init() { + image.RegisterFormat("pbm", "P1", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("pgm", "P2", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("ppm", "P3", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("pbm", "P4", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("pgm", "P5", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("ppm", "P6", DecodeNetPBM, DecodeNetPBMConfig) + image.RegisterFormat("pam", "P7", DecodeNetPBM, DecodeNetPBMConfig) +} diff --git a/vendor/github.com/kovidgoyal/imaging/nrgb.go b/vendor/github.com/kovidgoyal/imaging/nrgb.go new file mode 100644 index 000000000..87d71a714 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/nrgb.go @@ -0,0 +1,440 @@ +package imaging + +import ( + "fmt" + "image" + "image/color" +) + +var _ = fmt.Print + +type NRGBColor struct { + R, G, B uint8 +} + +func (c NRGBColor) AsSharp() string { + return fmt.Sprintf("#%02X%02X%02X", c.R, c.G, c.B) +} + +func (c NRGBColor) String() string { + return fmt.Sprintf("NRGBColor{%02X %02X %02X}", c.R, c.G, c.B) +} + +func (c NRGBColor) RGBA() (r, g, b, a uint32) { + r = uint32(c.R) + r |= r << 8 + g = uint32(c.G) + g |= g << 8 + b = uint32(c.B) + b |= b << 8 + a = 65535 // (255 << 8 | 255) + return +} + +// NRGB is an in-memory image whose At method returns NRGBColor values. +type NRGB struct { + // Pix holds the image's pixels, in R, G, B order. The pixel at + // (x, y) starts at Pix[(y-Rect.Min.Y)*Stride + (x-Rect.Min.X)*3]. + Pix []uint8 + // Stride is the Pix stride (in bytes) between vertically adjacent pixels. + Stride int + // Rect is the image's bounds. + Rect image.Rectangle +} + +func nrgbModel(c color.Color) color.Color { + if _, ok := c.(NRGBColor); ok { + return c + } + r, g, b, a := c.RGBA() + switch a { + case 0xffff: + return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)} + case 0: + return NRGBColor{0, 0, 0} + default: + // Since Color.RGBA returns an alpha-premultiplied color, we should have r <= a && g <= a && b <= a. + r = (r * 0xffff) / a + g = (g * 0xffff) / a + b = (b * 0xffff) / a + return NRGBColor{uint8(r >> 8), uint8(g >> 8), uint8(b >> 8)} + } +} + +var NRGBModel color.Model = color.ModelFunc(nrgbModel) + +func (p *NRGB) ColorModel() color.Model { return NRGBModel } + +func (p *NRGB) Bounds() image.Rectangle { return p.Rect } + +func (p *NRGB) At(x, y int) color.Color { + return p.NRGBAt(x, y) +} + +func (p *NRGB) NRGBAt(x, y int) NRGBColor { + if !(image.Point{x, y}.In(p.Rect)) { + return NRGBColor{} + } + i := p.PixOffset(x, y) + s := p.Pix[i : i+3 : i+3] // Small cap improves performance, see https://golang.org/issue/27857 + return NRGBColor{s[0], s[1], s[2]} +} + +// PixOffset returns the index of the first element of Pix that corresponds to +// the pixel at (x, y). +func (p *NRGB) PixOffset(x, y int) int { + return (y-p.Rect.Min.Y)*p.Stride + (x-p.Rect.Min.X)*3 +} + +func (p *NRGB) Set(x, y int, c color.Color) { + if !(image.Point{x, y}.In(p.Rect)) { + return + } + i := p.PixOffset(x, y) + c1 := NRGBModel.Convert(c).(NRGBColor) + s := p.Pix[i : i+3 : i+3] // Small cap improves performance, see https://golang.org/issue/27857 + s[0] = c1.R + s[1] = c1.G + s[2] = c1.B +} + +func (p *NRGB) SetRGBA64(x, y int, c color.RGBA64) { + if !(image.Point{x, y}.In(p.Rect)) { + return + } + r, g, b, a := uint32(c.R), uint32(c.G), uint32(c.B), uint32(c.A) + if (a != 0) && (a != 0xffff) { + r = (r * 0xffff) / a + g = (g * 0xffff) / a + b = (b * 0xffff) / a + } + i := p.PixOffset(x, y) + s := p.Pix[i : i+3 : i+3] // Small cap improves performance, see https://golang.org/issue/27857 + s[0] = uint8(r >> 8) + s[1] = uint8(g >> 8) + s[2] = uint8(b >> 8) +} + +func (p *NRGB) SetNRGBA(x, y int, c color.NRGBA) { + if !(image.Point{x, y}.In(p.Rect)) { + return + } + i := p.PixOffset(x, y) + s := p.Pix[i : i+3 : i+3] // Small cap improves performance, see https://golang.org/issue/27857 + s[0] = c.R + s[1] = c.G + s[2] = c.B +} + +// SubImage returns an image representing the portion of the image p visible +// through r. The returned value shares pixels with the original image. +func (p *NRGB) SubImage(r image.Rectangle) image.Image { + r = r.Intersect(p.Rect) + // If r1 and r2 are Rectangles, r1.Intersect(r2) is not guaranteed to be inside + // either r1 or r2 if the intersection is empty. Without explicitly checking for + // this, the Pix[i:] expression below can panic. + if r.Empty() { + return &NRGB{} + } + i := p.PixOffset(r.Min.X, r.Min.Y) + return &NRGB{ + Pix: p.Pix[i:], + Stride: p.Stride, + Rect: r, + } +} + +// Opaque scans the entire image and reports whether it is fully opaque. +func (p *NRGB) Opaque() bool { return true } + +type scanner_rgb struct { + image image.Image + w, h int + palette []NRGBColor + opaque_base []float64 + opaque_base_uint []uint8 +} + +func (s scanner_rgb) Bytes_per_channel() int { return 1 } +func (s scanner_rgb) Num_of_channels() int { return 3 } +func (s scanner_rgb) Bounds() image.Rectangle { return s.image.Bounds() } + +func blend(dest []uint8, base []float64, r, g, b, a uint8) { + alpha := float64(a) / 255.0 + dest[0] = uint8(alpha*float64(r) + (1.0-alpha)*base[0]) + dest[1] = uint8(alpha*float64(g) + (1.0-alpha)*base[1]) + dest[2] = uint8(alpha*float64(b) + (1.0-alpha)*base[2]) +} + +func newScannerRGB(img image.Image, opaque_base NRGBColor) *scanner_rgb { + s := &scanner_rgb{ + image: img, w: img.Bounds().Dx(), h: img.Bounds().Dy(), + opaque_base: []float64{float64(opaque_base.R), float64(opaque_base.G), float64(opaque_base.B)}[0:3:3], + opaque_base_uint: []uint8{opaque_base.R, opaque_base.G, opaque_base.B}[0:3:3], + } + if img, ok := img.(*image.Paletted); ok { + s.palette = make([]NRGBColor, max(256, len(img.Palette))) + d := [3]uint8{0, 0, 0} + ds := d[:] + for i := 0; i < len(img.Palette); i++ { + r, g, b, a := img.Palette[i].RGBA() + switch a { + case 0: + s.palette[i] = opaque_base + case 0xffff: + s.palette[i] = NRGBColor{R: uint8(r >> 8), G: uint8(g >> 8), B: uint8(b >> 8)} + default: + blend(ds, s.opaque_base, uint8((r*0xffff/a)>>8), uint8((g*0xffff/a)>>8), uint8((b*0xffff/a)>>8), uint8(a>>8)) + s.palette[i] = NRGBColor{R: d[0], G: d[1], B: d[2]} + } + } + } + return s +} + +// scan scans the given rectangular region of the image into dst. +func (s *scanner_rgb) Scan(x1, y1, x2, y2 int, dst []uint8) { + switch img := s.image.(type) { + case *image.NRGBA: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1*4 + for x := x1; x < x2; x++ { + blend(dst[j:j+3:j+3], s.opaque_base, img.Pix[i], img.Pix[i+1], img.Pix[i+2], img.Pix[i+3]) + j += 3 + i += 4 + } + } + + case *image.NRGBA64: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1*8 + for x := x1; x < x2; x++ { + blend(dst[j:j+3:j+3], s.opaque_base, img.Pix[i], img.Pix[i+2], img.Pix[i+4], img.Pix[i+6]) + j += 3 + i += 8 + } + } + + case *image.RGBA: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1*4 + for x := x1; x < x2; x++ { + d := dst[j : j+3 : j+3] + a := img.Pix[i+3] + switch a { + case 0: + d[0] = s.opaque_base_uint[0] + d[1] = s.opaque_base_uint[1] + d[2] = s.opaque_base_uint[2] + case 0xff: + s := img.Pix[i : i+3 : i+3] + d[0] = s[0] + d[1] = s[1] + d[2] = s[2] + default: + r16 := uint16(img.Pix[i]) + g16 := uint16(img.Pix[i+1]) + b16 := uint16(img.Pix[i+2]) + a16 := uint16(a) + blend(d, s.opaque_base, uint8(r16*0xff/a16), uint8(g16*0xff/a16), uint8(b16*0xff/a16), a) + } + j += 3 + i += 4 + } + } + + case *image.RGBA64: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1*8 + for x := x1; x < x2; x++ { + src := img.Pix[i : i+8 : i+8] + d := dst[j : j+3 : j+3] + a := src[6] + switch a { + case 0: + d[0] = s.opaque_base_uint[0] + d[1] = s.opaque_base_uint[1] + d[2] = s.opaque_base_uint[2] + case 0xff: + d[0] = src[0] + d[1] = src[2] + d[2] = src[4] + default: + r32 := uint32(src[0])<<8 | uint32(src[1]) + g32 := uint32(src[2])<<8 | uint32(src[3]) + b32 := uint32(src[4])<<8 | uint32(src[5]) + a32 := uint32(src[6])<<8 | uint32(src[7]) + blend(d, s.opaque_base, uint8((r32*0xffff/a32)>>8), uint8((g32*0xffff/a32)>>8), uint8((b32*0xffff/a32)>>8), a) + } + j += 3 + i += 8 + } + } + + case *image.Gray: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1 + for x := x1; x < x2; x++ { + c := img.Pix[i] + d := dst[j : j+3 : j+3] + d[0] = c + d[1] = c + d[2] = c + j += 3 + i++ + } + } + + case *image.Gray16: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1*2 + for x := x1; x < x2; x++ { + c := img.Pix[i] + d := dst[j : j+3 : j+3] + d[0] = c + d[1] = c + d[2] = c + j += 3 + i += 2 + } + } + + case *image.YCbCr: + j := 0 + x1 += img.Rect.Min.X + x2 += img.Rect.Min.X + y1 += img.Rect.Min.Y + y2 += img.Rect.Min.Y + + hy := img.Rect.Min.Y / 2 + hx := img.Rect.Min.X / 2 + for y := y1; y < y2; y++ { + iy := (y-img.Rect.Min.Y)*img.YStride + (x1 - img.Rect.Min.X) + + var yBase int + switch img.SubsampleRatio { + case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio422: + yBase = (y - img.Rect.Min.Y) * img.CStride + case image.YCbCrSubsampleRatio420, image.YCbCrSubsampleRatio440: + yBase = (y/2 - hy) * img.CStride + } + + for x := x1; x < x2; x++ { + var ic int + switch img.SubsampleRatio { + case image.YCbCrSubsampleRatio444, image.YCbCrSubsampleRatio440: + ic = yBase + (x - img.Rect.Min.X) + case image.YCbCrSubsampleRatio422, image.YCbCrSubsampleRatio420: + ic = yBase + (x/2 - hx) + default: + ic = img.COffset(x, y) + } + + yy1 := int32(img.Y[iy]) * 0x10101 + cb1 := int32(img.Cb[ic]) - 128 + cr1 := int32(img.Cr[ic]) - 128 + + r := yy1 + 91881*cr1 + if uint32(r)&0xff000000 == 0 { + r >>= 16 + } else { + r = ^(r >> 31) + } + + g := yy1 - 22554*cb1 - 46802*cr1 + if uint32(g)&0xff000000 == 0 { + g >>= 16 + } else { + g = ^(g >> 31) + } + + b := yy1 + 116130*cb1 + if uint32(b)&0xff000000 == 0 { + b >>= 16 + } else { + b = ^(b >> 31) + } + + d := dst[j : j+3 : j+3] + d[0] = uint8(r) + d[1] = uint8(g) + d[2] = uint8(b) + + iy++ + j += 3 + } + } + + case *image.Paletted: + j := 0 + for y := y1; y < y2; y++ { + i := y*img.Stride + x1 + for x := x1; x < x2; x++ { + c := s.palette[img.Pix[i]] + d := dst[j : j+3 : j+3] + d[0] = c.R + d[1] = c.G + d[2] = c.B + j += 3 + i++ + } + } + + default: + j := 0 + b := s.image.Bounds() + x1 += b.Min.X + x2 += b.Min.X + y1 += b.Min.Y + y2 += b.Min.Y + for y := y1; y < y2; y++ { + for x := x1; x < x2; x++ { + r16, g16, b16, a16 := s.image.At(x, y).RGBA() + d := dst[j : j+3 : j+3] + switch a16 { + case 0xffff: + d[0] = uint8(r16 >> 8) + d[1] = uint8(g16 >> 8) + d[2] = uint8(b16 >> 8) + case 0: + d[0] = s.opaque_base_uint[0] + d[1] = s.opaque_base_uint[1] + d[2] = s.opaque_base_uint[2] + default: + blend(d, s.opaque_base, uint8(((r16*0xffff)/a16)>>8), uint8(((g16*0xffff)/a16)>>8), uint8(((b16*0xffff)/a16)>>8), uint8(a16>>8)) + } + j += 3 + } + } + } +} + +func NewNRGB(r image.Rectangle) *NRGB { + return &NRGB{ + Pix: make([]uint8, 3*r.Dx()*r.Dy()), + Stride: 3 * r.Dx(), + Rect: r, + } +} + +func NewNRGBWithContiguousRGBPixels(p []byte, left, top, width, height int) (*NRGB, error) { + const bpp = 3 + if expected := bpp * width * height; expected != len(p) { + return nil, fmt.Errorf("the image width and height dont match the size of the specified pixel data: width=%d height=%d sz=%d != %d", width, height, len(p), expected) + } + return &NRGB{ + Pix: p, + Stride: bpp * width, + Rect: image.Rectangle{image.Point{left, top}, image.Point{left + width, top + height}}, + }, nil +} + +func NewNRGBScanner(source_image image.Image, opaque_base NRGBColor) Scanner { + return newScannerRGB(source_image, opaque_base) +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/autometa.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/autometa.go new file mode 100644 index 000000000..673d555e3 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/autometa.go @@ -0,0 +1,41 @@ +package autometa + +import ( + "fmt" + "io" + + "github.com/kovidgoyal/imaging/prism/meta" + "github.com/kovidgoyal/imaging/prism/meta/jpegmeta" + "github.com/kovidgoyal/imaging/prism/meta/pngmeta" + "github.com/kovidgoyal/imaging/prism/meta/webpmeta" + "github.com/kovidgoyal/imaging/streams" +) + +// Load loads the metadata for an image stream, which may be one of the +// supported image formats. +// +// Only as much of the stream is consumed as necessary to extract the metadata; +// the returned stream contains a buffered copy of the consumed data such that +// reading from it will produce the same results as fully reading the input +// stream. This provides a convenient way to load the full image after loading +// the metadata. +// +// An error is returned if basic metadata could not be extracted. The returned +// stream still provides the full image data. +func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) { + loaders := []func(io.Reader) (*meta.Data, error){ + pngmeta.ExtractMetadata, + jpegmeta.ExtractMetadata, + webpmeta.ExtractMetadata, + } + for _, loader := range loaders { + r, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) { + md, err = loader(r) + return + }) + if err == nil { + return md, r, nil + } + } + return nil, r, fmt.Errorf("unrecognised image format") +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/doc.go new file mode 100644 index 000000000..72bd0aaac --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/doc.go @@ -0,0 +1,3 @@ +// Package autometa provides support for embedded metadata and automatic +// detection of image formats. +package autometa diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/data.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/data.go new file mode 100644 index 000000000..ddff91230 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/data.go @@ -0,0 +1,54 @@ +package meta + +import ( + "bytes" + "fmt" + + "github.com/kovidgoyal/imaging/prism/meta/icc" +) + +var _ = fmt.Println + +// Data represents the metadata for an image. +type Data struct { + Format ImageFormat + PixelWidth uint32 + PixelHeight uint32 + BitsPerComponent uint32 + ExifData []byte + iccProfileData []byte + iccProfileErr error +} + +// ICCProfile returns an extracted ICC profile from this metadata. +// +// An error is returned if the ICC profile could not be correctly parsed. +// +// If no profile data was found, nil is returned without an error. +func (md *Data) ICCProfile() (*icc.Profile, error) { + if md.iccProfileData == nil { + return nil, md.iccProfileErr + } + + return icc.NewProfileReader(bytes.NewReader(md.iccProfileData)).ReadProfile() +} + +// ICCProfile returns the raw ICC profile data from this metadata. +// +// An error is returned if the ICC profile could not be correctly extracted from +// the image. +// +// If no profile data was found, nil is returned without an error. +func (md *Data) ICCProfileData() ([]byte, error) { + return md.iccProfileData, md.iccProfileErr +} + +func (md *Data) SetICCProfileData(data []byte) { + md.iccProfileData = data + md.iccProfileErr = nil +} + +func (md *Data) SetICCProfileError(err error) { + md.iccProfileData = nil + md.iccProfileErr = err +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/doc.go new file mode 100644 index 000000000..5d6c87531 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/doc.go @@ -0,0 +1,2 @@ +// Package meta and its subpackages provide support for embedded image metadata. +package meta diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/colorspace.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/colorspace.go new file mode 100644 index 000000000..99b5ef2de --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/colorspace.go @@ -0,0 +1,90 @@ +package icc + +import "fmt" + +type ColorSpace uint32 + +const ( + ColorSpaceXYZ ColorSpace = 0x58595A20 // 'XYZ ' + ColorSpaceLab ColorSpace = 0x4C616220 // 'Lab ' + ColorSpaceLuv ColorSpace = 0x4C757620 // 'Luv ' + ColorSpaceYCbCr ColorSpace = 0x59436272 // 'YCbr' + ColorSpaceYxy ColorSpace = 0x59787920 // 'Yxy ' + ColorSpaceRGB ColorSpace = 0x52474220 // 'RGB ' + ColorSpaceGray ColorSpace = 0x47524159 // 'Gray' + ColorSpaceHSV ColorSpace = 0x48535620 // 'HSV ' + ColorSpaceHLS ColorSpace = 0x484C5320 // 'HLS ' + ColorSpaceCMYK ColorSpace = 0x434D594B // 'CMYK' + ColorSpaceCMY ColorSpace = 0x434D5920 // 'CMY ' + ColorSpace2Color ColorSpace = 0x32434C52 // '2CLR' + ColorSpace3Color ColorSpace = 0x33434C52 // '3CLR' + ColorSpace4Color ColorSpace = 0x34434C52 // '4CLR' + ColorSpace5Color ColorSpace = 0x35434C52 // '5CLR' + ColorSpace6Color ColorSpace = 0x36434C52 // '6CLR' + ColorSpace7Color ColorSpace = 0x37434C52 // '7CLR' + ColorSpace8Color ColorSpace = 0x38434C52 // '8CLR' + ColorSpace9Color ColorSpace = 0x39434C52 // '9CLR' + ColorSpace10Color ColorSpace = 0x41434C52 // 'ACLR' + ColorSpace11Color ColorSpace = 0x42434C52 // 'BCLR' + ColorSpace12Color ColorSpace = 0x43434C52 // 'CCLR' + ColorSpace13Color ColorSpace = 0x44434C52 // 'DCLR' + ColorSpace14Color ColorSpace = 0x45434C52 // 'ECLR' + ColorSpace15Color ColorSpace = 0x46434C52 // 'FCLR' +) + +func (cs ColorSpace) String() string { + switch cs { + case ColorSpaceXYZ: + return "XYZ" + case ColorSpaceLab: + return "Lab" + case ColorSpaceLuv: + return "Luv" + case ColorSpaceYCbCr: + return "YCbCr" + case ColorSpaceYxy: + return "Yxy" + case ColorSpaceRGB: + return "RGB" + case ColorSpaceGray: + return "Gray" + case ColorSpaceHSV: + return "HSV" + case ColorSpaceHLS: + return "HLS" + case ColorSpaceCMYK: + return "CMYK" + case ColorSpaceCMY: + return "CMY" + case ColorSpace2Color: + return "2 color" + case ColorSpace3Color: + return "3 color" + case ColorSpace4Color: + return "4 color" + case ColorSpace5Color: + return "5 color" + case ColorSpace6Color: + return "6 color" + case ColorSpace7Color: + return "7 color" + case ColorSpace8Color: + return "8 color" + case ColorSpace9Color: + return "9 color" + case ColorSpace10Color: + return "10 color" + case ColorSpace11Color: + return "11 color" + case ColorSpace12Color: + return "12 color" + case ColorSpace13Color: + return "13 color" + case ColorSpace14Color: + return "14 color" + case ColorSpace15Color: + return "15 color" + default: + return fmt.Sprintf("Unknown (%s)", Signature(cs)) + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/deviceclass.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/deviceclass.go new file mode 100644 index 000000000..1ea511036 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/deviceclass.go @@ -0,0 +1,36 @@ +package icc + +import "fmt" + +type DeviceClass uint32 + +const ( + DeviceClassInput DeviceClass = 0x73636E72 // 'scnr' + DeviceClassDisplay DeviceClass = 0x6D6E7472 // 'mntr' + DeviceClassOutput DeviceClass = 0x70727472 // 'prtr' + DeviceClassLink DeviceClass = 0x6C696E6B // 'link' + DeviceClassColorSpace DeviceClass = 0x73706163 // 'spac' + DeviceClassAbstract DeviceClass = 0x61627374 // 'abst' + DeviceClassNamedColor DeviceClass = 0x6E6D636C // 'nmcl' +) + +func (dc DeviceClass) String() string { + switch dc { + case DeviceClassInput: + return "Input" + case DeviceClassDisplay: + return "Display" + case DeviceClassOutput: + return "Output" + case DeviceClassLink: + return "Device link" + case DeviceClassColorSpace: + return "Color space" + case DeviceClassAbstract: + return "Abstract" + case DeviceClassNamedColor: + return "Named color" + default: + return fmt.Sprintf("Unknown (%s)", Signature(dc)) + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/doc.go new file mode 100644 index 000000000..95fd943ce --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/doc.go @@ -0,0 +1,2 @@ +// Package icc provides support for working with ICC colour profile data. +package icc diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/header.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/header.go new file mode 100644 index 000000000..99137e4da --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/header.go @@ -0,0 +1,44 @@ +package icc + +import ( + "fmt" + "time" +) + +type Header struct { + ProfileSize uint32 + PreferredCMM Signature + Version Version + DeviceClass DeviceClass + DataColorSpace ColorSpace + ProfileConnectionSpace ColorSpace + CreatedAtRaw [6]uint16 + FileSignature Signature + PrimaryPlatform PrimaryPlatform + Flags uint32 + DeviceManufacturer Signature + DeviceModel Signature + DeviceAttributes uint64 + RenderingIntent RenderingIntent + PCSIlluminant [3]uint32 + ProfileCreator Signature + ProfileID [16]byte + Reserved [28]byte +} + +func (h Header) CreatedAt() time.Time { + b := h.CreatedAtRaw + return time.Date(int(b[0]), time.Month(b[1]), int(b[2]), int(b[3]), int(b[4]), int(b[5]), 0, time.UTC) +} + +func (h Header) Embedded() bool { + return (h.Flags >> 31) != 0 +} + +func (h Header) DependsOnEmbeddedData() bool { + return (h.Flags>>30)&1 != 0 +} + +func (h Header) String() string { + return fmt.Sprintf("Header{PreferredCMM: %s, Version: %s, DeviceManufacturer: %s, DeviceModel: %s, ProfileCreator: %s, RenderingIntent: %s, CreatedAt: %v}", h.PreferredCMM, h.Version, h.DeviceManufacturer, h.DeviceModel, h.ProfileCreator, h.RenderingIntent, h.CreatedAt()) +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/primaryplatform.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/primaryplatform.go new file mode 100644 index 000000000..9e439a7d9 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/primaryplatform.go @@ -0,0 +1,30 @@ +package icc + +import "fmt" + +type PrimaryPlatform uint32 + +const ( + PrimaryPlatformNone PrimaryPlatform = 0x00000000 + PrimaryPlatformApple PrimaryPlatform = 0x4150504C // 'AAPL' + PrimaryPlatformMicrosoft PrimaryPlatform = 0x4D534654 // 'MSFT' + PrimaryPlatformSGI PrimaryPlatform = 0x53474920 // 'SGI ' + PrimaryPlatformSun PrimaryPlatform = 0x53554E57 // 'SUNW' +) + +func (pp PrimaryPlatform) String() string { + switch pp { + case PrimaryPlatformNone: + return "None" + case PrimaryPlatformApple: + return "Apple Computer, Inc." + case PrimaryPlatformMicrosoft: + return "Microsoft Corporation" + case PrimaryPlatformSGI: + return "Silicon Graphics, Inc." + case PrimaryPlatformSun: + return "Sun Microsystems, Inc." + default: + return fmt.Sprintf("Unknown (%d)", Signature(pp)) + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go new file mode 100644 index 000000000..3af8178e4 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go @@ -0,0 +1,100 @@ +package icc + +type WellKnownProfile int + +const ( + UnknownProfile WellKnownProfile = iota + SRGBProfile + AdobeRGBProfile + PhotoProProfile + DisplayP3Profile +) + +func WellKnownProfileFromDescription(x string) WellKnownProfile { + switch x { + case "sRGB IEC61966-2.1", "sRGB_ICC_v4_Appearance.icc": + return SRGBProfile + case "Adobe RGB (1998)": + return AdobeRGBProfile + case "Display P3": + return DisplayP3Profile + case "ProPhoto RGB": + return PhotoProProfile + default: + return UnknownProfile + } +} + +func (p WellKnownProfile) String() string { + switch p { + case SRGBProfile: + return "sRGB IEC61966-2.1" + case AdobeRGBProfile: + return "Adobe RGB (1998)" + case PhotoProProfile: + return "ProPhoto RGB" + case DisplayP3Profile: + return "Display P3" + default: + return "Unknown Profile" + } +} + +type Profile struct { + Header Header + TagTable TagTable +} + +func (p *Profile) Description() (string, error) { + return p.TagTable.getProfileDescription() +} + +func (p *Profile) DeviceManufacturerDescription() (string, error) { + return p.TagTable.getDeviceManufacturerDescription() +} + +func (p *Profile) DeviceModelDescription() (string, error) { + return p.TagTable.getDeviceModelDescription() +} + +func (p *Profile) WellKnownProfile() WellKnownProfile { + model, err := p.DeviceModelDescription() + if err == nil { + switch model { + case "IEC 61966-2-1 Default RGB Colour Space - sRGB": + return SRGBProfile + } + } + d, err := p.Description() + if err == nil { + if ans := WellKnownProfileFromDescription(d); ans != UnknownProfile { + return ans + } + } + switch p.Header.DeviceManufacturer { + case IECManufacturerSignature: + switch p.Header.DeviceModel { + case SRGBModelSignature: + return SRGBProfile + } + case AdobeManufacturerSignature: + switch p.Header.DeviceModel { + case AdobeRGBModelSignature: + return AdobeRGBProfile + case PhotoProModelSignature: + return PhotoProProfile + } + case AppleManufacturerSignature, AppleUpperManufacturerSignature: + switch p.Header.DeviceModel { + case DisplayP3ModelSignature: + return DisplayP3Profile + } + } + return UnknownProfile +} + +func newProfile() *Profile { + return &Profile{ + TagTable: emptyTagTable(), + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go new file mode 100644 index 000000000..f2832e504 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go @@ -0,0 +1,97 @@ +package icc + +import ( + "encoding/binary" + "fmt" + "io" + "os" + + "github.com/kovidgoyal/go-parallel" +) + +var _ = fmt.Println +var _ = os.Stderr + +type ProfileReader struct { + reader io.Reader +} + +func (pr *ProfileReader) ReadProfile() (p *Profile, err error) { + defer func() { + if r := recover(); r != nil { + p = nil + err = parallel.Format_stacktrace_on_panic(r, 1) + } + }() + + profile := newProfile() + + err = pr.readHeader(&profile.Header) + if err != nil { + return nil, fmt.Errorf("failed to reader header from ICC profile: %w", err) + } + + err = pr.readTagTable(&profile.TagTable) + if err != nil { + return nil, fmt.Errorf("failed to read tag table from ICC profile: %w", err) + } + + return profile, nil +} + +func (pr *ProfileReader) readHeader(header *Header) (err error) { + var data [128]byte + if _, err = io.ReadFull(pr.reader, data[:]); err == nil { + var n int + n, err = binary.Decode(data[:], binary.BigEndian, header) + if err == nil { + if header.FileSignature != ProfileFileSignature { + return fmt.Errorf("ICC header has invalid signature: %s", header.FileSignature) + } + if n != len(data) { + return fmt.Errorf("decoding header consumed %d instead of %d bytes", n, len(data)) + } + } + } + return +} + +func (pr *ProfileReader) readTagTable(tagTable *TagTable) (err error) { + var tagCount uint32 + if err = binary.Read(pr.reader, binary.BigEndian, &tagCount); err != nil { + return + } + type tagIndexEntry struct { + Sig uint32 + Offset uint32 + Size uint32 + } + endOfTagData := uint32(0) + tag_indices := make([]tagIndexEntry, tagCount) + if err = binary.Read(pr.reader, binary.BigEndian, tag_indices); err != nil { + return fmt.Errorf("failed to read tag indices from ICC profile: %w", err) + } + for _, t := range tag_indices { + endOfTagData = max(endOfTagData, t.Offset+t.Size) + } + tagDataOffset := 132 + tagCount*12 + if endOfTagData > tagDataOffset { + tagData := make([]byte, endOfTagData-tagDataOffset) + if _, err = io.ReadFull(pr.reader, tagData); err != nil { + return fmt.Errorf("failed to read tag data from ICC profile: %w", err) + } + for _, t := range tag_indices { + startOffset := t.Offset - tagDataOffset + endOffset := startOffset + t.Size + tagTable.add(Signature(t.Sig), tagData[startOffset:endOffset]) + } + } + + return nil +} + +func NewProfileReader(r io.Reader) *ProfileReader { + return &ProfileReader{ + reader: r, + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profileversion.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profileversion.go new file mode 100644 index 000000000..e78784f7d --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profileversion.go @@ -0,0 +1,13 @@ +package icc + +import "fmt" + +type Version struct { + Major byte + MinorAndRev byte + Reserved1, Reserved2 byte +} + +func (pv Version) String() string { + return fmt.Sprintf("%d.%d.%d", pv.Major, pv.MinorAndRev>>4, pv.MinorAndRev&3) +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/renderingintent.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/renderingintent.go new file mode 100644 index 000000000..cf1193b96 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/renderingintent.go @@ -0,0 +1,27 @@ +package icc + +import "fmt" + +const ( + PerceptualRenderingIntent RenderingIntent = 0 + RelativeColorimetricRenderingIntent RenderingIntent = 1 + SaturationRenderingIntent RenderingIntent = 2 + AbsoluteColorimetricRenderingIntent RenderingIntent = 3 +) + +type RenderingIntent uint32 + +func (ri RenderingIntent) String() string { + switch ri { + case PerceptualRenderingIntent: + return "Perceptual" + case RelativeColorimetricRenderingIntent: + return "Relative colorimetric" + case SaturationRenderingIntent: + return "Saturation" + case AbsoluteColorimetricRenderingIntent: + return "Absolute colorimetric" + default: + return fmt.Sprintf("Unknown (%d)", ri) + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/signature.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/signature.go new file mode 100644 index 000000000..2a63b28f2 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/signature.go @@ -0,0 +1,209 @@ +package icc + +type Signature uint32 + +const ( + ProfileFileSignature Signature = 0x61637370 // 'acsp' + TextTagSignature Signature = 0x74657874 // 'text' + SignateTagSignature Signature = 0x73696720 // 'sig ' + + DescSignature Signature = 0x64657363 // 'desc' + MultiLocalisedUnicodeSignature Signature = 0x6D6C7563 // 'mluc' + DeviceManufacturerDescriptionSignature Signature = 0x646d6e64 // 'dmnd' + DeviceModelDescriptionSignature Signature = 0x646d6464 // 'dmdd' + + AdobeManufacturerSignature Signature = 0x41444245 // 'ADBE' + AppleManufacturerSignature Signature = 0x6170706c // 'appl' + AppleUpperManufacturerSignature Signature = 0x4150504c // 'APPL' + IECManufacturerSignature Signature = 0x49454320 // 'IEC ' + + AdobeRGBModelSignature Signature = 0x52474220 // 'RGB ' + SRGBModelSignature Signature = 0x73524742 // 'sRGB' + PhotoProModelSignature Signature = 0x50525452 // 'PTPR' + DisplayP3ModelSignature Signature = 0x70332020 // 'p3 ' + + ChromaticityTypeSignature Signature = 0x6368726D /* 'chrm' */ + ColorantOrderTypeSignature Signature = 0x636C726F /* 'clro' */ + ColorantTableTypeSignature Signature = 0x636C7274 /* 'clrt' */ + CrdInfoTypeSignature Signature = 0x63726469 /* 'crdi' Removed in V4 */ + CurveTypeSignature Signature = 0x63757276 /* 'curv' */ + DataTypeSignature Signature = 0x64617461 /* 'data' */ + DictTypeSignature Signature = 0x64696374 /* 'dict' */ + DateTimeTypeSignature Signature = 0x6474696D /* 'dtim' */ + DeviceSettingsTypeSignature Signature = 0x64657673 /* 'devs' Removed in V4 */ + Lut16TypeSignature Signature = 0x6d667432 /* 'mft2' */ + Lut8TypeSignature Signature = 0x6d667431 /* 'mft1' */ + LutAtoBTypeSignature Signature = 0x6d414220 /* 'mAB ' */ + LutBtoATypeSignature Signature = 0x6d424120 /* 'mBA ' */ + MeasurementTypeSignature Signature = 0x6D656173 /* 'meas' */ + MultiLocalizedUnicodeTypeSignature Signature = 0x6D6C7563 /* 'mluc' */ + MultiProcessElementTypeSignature Signature = 0x6D706574 /* 'mpet' */ + NamedColorTypeSignature Signature = 0x6E636f6C /* 'ncol' OBSOLETE use ncl2 */ + NamedColor2TypeSignature Signature = 0x6E636C32 /* 'ncl2' */ + ParametricCurveTypeSignature Signature = 0x70617261 /* 'para' */ + ProfileSequenceDescTypeSignature Signature = 0x70736571 /* 'pseq' */ + ProfileSequceIdTypeSignature Signature = 0x70736964 /* 'psid' */ + ResponseCurveSet16TypeSignature Signature = 0x72637332 /* 'rcs2' */ + S15Fixed16ArrayTypeSignature Signature = 0x73663332 /* 'sf32' */ + ScreeningTypeSignature Signature = 0x7363726E /* 'scrn' Removed in V4 */ + SignatureTypeSignature Signature = 0x73696720 /* 'sig ' */ + TextTypeSignature Signature = 0x74657874 /* 'text' */ + TextDescriptionTypeSignature Signature = 0x64657363 /* 'desc' Removed in V4 */ + U16Fixed16ArrayTypeSignature Signature = 0x75663332 /* 'uf32' */ + UcrBgTypeSignature Signature = 0x62666420 /* 'bfd ' Removed in V4 */ + UInt16ArrayTypeSignature Signature = 0x75693136 /* 'ui16' */ + UInt32ArrayTypeSignature Signature = 0x75693332 /* 'ui32' */ + UInt64ArrayTypeSignature Signature = 0x75693634 /* 'ui64' */ + UInt8ArrayTypeSignature Signature = 0x75693038 /* 'ui08' */ + ViewingConditionsTypeSignature Signature = 0x76696577 /* 'view' */ + XYZTypeSignature Signature = 0x58595A20 /* 'XYZ ' */ + XYZArrayTypeSignature Signature = 0x58595A20 /* 'XYZ ' */ + + XYZSignature Signature = 0x58595A20 /* 'XYZ ' */ + LabSignature Signature = 0x4C616220 /* 'Lab ' */ + LUVSignature Signature = 0x4C757620 /* 'Luv ' */ + YCbrSignature Signature = 0x59436272 /* 'YCbr' */ + YxySignature Signature = 0x59787920 /* 'Yxy ' */ + RGBSignature Signature = 0x52474220 /* 'RGB ' */ + GraySignature Signature = 0x47524159 /* 'GRAY' */ + HSVSignature Signature = 0x48535620 /* 'HSV ' */ + HLSSignature Signature = 0x484C5320 /* 'HLS ' */ + CMYKSignature Signature = 0x434D594B /* 'CMYK' */ + CMYSignature Signature = 0x434D5920 /* 'CMY ' */ + + MCH2Signature Signature = 0x32434C52 /* '2CLR' */ + MCH3Signature Signature = 0x33434C52 /* '3CLR' */ + MCH4Signature Signature = 0x34434C52 /* '4CLR' */ + MCH5Signature Signature = 0x35434C52 /* '5CLR' */ + MCH6Signature Signature = 0x36434C52 /* '6CLR' */ + MCH7Signature Signature = 0x37434C52 /* '7CLR' */ + MCH8Signature Signature = 0x38434C52 /* '8CLR' */ + MCH9Signature Signature = 0x39434C52 /* '9CLR' */ + MCHASignature Signature = 0x41434C52 /* 'ACLR' */ + MCHBSignature Signature = 0x42434C52 /* 'BCLR' */ + MCHCSignature Signature = 0x43434C52 /* 'CCLR' */ + MCHDSignature Signature = 0x44434C52 /* 'DCLR' */ + MCHESignature Signature = 0x45434C52 /* 'ECLR' */ + MCHFSignature Signature = 0x46434C52 /* 'FCLR' */ + NamedSignature Signature = 0x6e6d636c /* 'nmcl' */ + + Color2Signature Signature = 0x32434C52 /* '2CLR' */ + Color3Signature Signature = 0x33434C52 /* '3CLR' */ + Color4Signature Signature = 0x34434C52 /* '4CLR' */ + Color5Signature Signature = 0x35434C52 /* '5CLR' */ + Color6Signature Signature = 0x36434C52 /* '6CLR' */ + Color7Signature Signature = 0x37434C52 /* '7CLR' */ + Color8Signature Signature = 0x38434C52 /* '8CLR' */ + Color9Signature Signature = 0x39434C52 /* '9CLR' */ + Color10Signature Signature = 0x41434C52 /* 'ACLR' */ + Color11Signature Signature = 0x42434C52 /* 'BCLR' */ + Color12Signature Signature = 0x43434C52 /* 'CCLR' */ + Color13Signature Signature = 0x44434C52 /* 'DCLR' */ + Color14Signature Signature = 0x45434C52 /* 'ECLR' */ + Color15Signature Signature = 0x46434C52 /* 'FCLR' */ + + AToB0TagSignature Signature = 0x41324230 /* 'A2B0' */ + AToB1TagSignature Signature = 0x41324231 /* 'A2B1' */ + AToB2TagSignature Signature = 0x41324232 /* 'A2B2' */ + BlueColorantTagSignature Signature = 0x6258595A /* 'bXYZ' */ + BlueMatrixColumnTagSignature Signature = 0x6258595A /* 'bXYZ' */ + BlueTRCTagSignature Signature = 0x62545243 /* 'bTRC' */ + BToA0TagSignature Signature = 0x42324130 /* 'B2A0' */ + BToA1TagSignature Signature = 0x42324131 /* 'B2A1' */ + BToA2TagSignature Signature = 0x42324132 /* 'B2A2' */ + CalibrationDateTimeTagSignature Signature = 0x63616C74 /* 'calt' */ + CharTargetTagSignature Signature = 0x74617267 /* 'targ' */ + ChromaticAdaptationTagSignature Signature = 0x63686164 /* 'chad' */ + ChromaticityTagSignature Signature = 0x6368726D /* 'chrm' */ + ColorantOrderTagSignature Signature = 0x636C726F /* 'clro' */ + ColorantTableTagSignature Signature = 0x636C7274 /* 'clrt' */ + ColorantTableOutTagSignature Signature = 0x636C6F74 /* 'clot' */ + ColorimetricIntentImageStateTagSignature Signature = 0x63696973 /* 'ciis' */ + CopyrightTagSignature Signature = 0x63707274 /* 'cprt' */ + CrdInfoTagSignature Signature = 0x63726469 /* 'crdi' Removed in V4 */ + DataTagSignature Signature = 0x64617461 /* 'data' Removed in V4 */ + DateTimeTagSignature Signature = 0x6474696D /* 'dtim' Removed in V4 */ + DeviceMfgDescTagSignature Signature = 0x646D6E64 /* 'dmnd' */ + DeviceModelDescTagSignature Signature = 0x646D6464 /* 'dmdd' */ + DeviceSettingsTagSignature Signature = 0x64657673 /* 'devs' Removed in V4 */ + DToB0TagSignature Signature = 0x44324230 /* 'D2B0' */ + DToB1TagSignature Signature = 0x44324231 /* 'D2B1' */ + DToB2TagSignature Signature = 0x44324232 /* 'D2B2' */ + DToB3TagSignature Signature = 0x44324233 /* 'D2B3' */ + BToD0TagSignature Signature = 0x42324430 /* 'B2D0' */ + BToD1TagSignature Signature = 0x42324431 /* 'B2D1' */ + BToD2TagSignature Signature = 0x42324432 /* 'B2D2' */ + BToD3TagSignature Signature = 0x42324433 /* 'B2D3' */ + GamutTagSignature Signature = 0x67616D74 /* 'gamt' */ + GrayTRCTagSignature Signature = 0x6b545243 /* 'kTRC' */ + GreenColorantTagSignature Signature = 0x6758595A /* 'gXYZ' */ + GreenMatrixColumnTagSignature Signature = 0x6758595A /* 'gXYZ' */ + GreenTRCTagSignature Signature = 0x67545243 /* 'gTRC' */ + LuminanceTagSignature Signature = 0x6C756d69 /* 'lumi' */ + MeasurementTagSignature Signature = 0x6D656173 /* 'meas' */ + MediaBlackPointTagSignature Signature = 0x626B7074 /* 'bkpt' */ + MediaWhitePointTagSignature Signature = 0x77747074 /* 'wtpt' */ + MetaDataTagSignature Signature = 0x6D657461 /* 'meta' */ + NamedColorTagSignature Signature = 0x6E636f6C /* 'ncol' OBSOLETE use ncl2 */ + NamedColor2TagSignature Signature = 0x6E636C32 /* 'ncl2' */ + OutputResponseTagSignature Signature = 0x72657370 /* 'resp' */ + PerceptualRenderingIntentGamutTagSignature Signature = 0x72696730 /* 'rig0' */ + Preview0TagSignature Signature = 0x70726530 /* 'pre0' */ + Preview1TagSignature Signature = 0x70726531 /* 'pre1' */ + Preview2TagSignature Signature = 0x70726532 /* 'pre2' */ + PrintConditionTagSignature Signature = 0x7074636e /* 'ptcn' */ + ProfileDescriptionTagSignature Signature = 0x64657363 /* 'desc' */ + ProfileSequenceDescTagSignature Signature = 0x70736571 /* 'pseq' */ + ProfileSequceIdTagSignature Signature = 0x70736964 /* 'psid' */ + Ps2CRD0TagSignature Signature = 0x70736430 /* 'psd0' Removed in V4 */ + Ps2CRD1TagSignature Signature = 0x70736431 /* 'psd1' Removed in V4 */ + Ps2CRD2TagSignature Signature = 0x70736432 /* 'psd2' Removed in V4 */ + Ps2CRD3TagSignature Signature = 0x70736433 /* 'psd3' Removed in V4 */ + Ps2CSATagSignature Signature = 0x70733273 /* 'ps2s' Removed in V4 */ + Ps2RenderingIntentTagSignature Signature = 0x70733269 /* 'ps2i' Removed in V4 */ + RedColorantTagSignature Signature = 0x7258595A /* 'rXYZ' */ + RedMatrixColumnTagSignature Signature = 0x7258595A /* 'rXYZ' */ + RedTRCTagSignature Signature = 0x72545243 /* 'rTRC' */ + SaturationRenderingIntentGamutTagSignature Signature = 0x72696732 /* 'rig2' */ + ScreeningDescTagSignature Signature = 0x73637264 /* 'scrd' Removed in V4 */ + ScreeningTagSignature Signature = 0x7363726E /* 'scrn' Removed in V4 */ + TechnologyTagSignature Signature = 0x74656368 /* 'tech' */ + UcrBgTagSignature Signature = 0x62666420 /* 'bfd ' Removed in V4 */ + ViewingCondDescTagSignature Signature = 0x76756564 /* 'vued' */ + ViewingConditionsTagSignature Signature = 0x76696577 /* 'view' */ + + CurveSetElemTypeSignature Signature = 0x63767374 /* 'cvst' */ + MatrixElemTypeSignature Signature = 0x6D617466 /* 'matf' */ + CLutElemTypeSignature Signature = 0x636C7574 /* 'clut' */ + BAcsElemTypeSignature Signature = 0x62414353 /* 'bACS' */ + EAcsElemTypeSignature Signature = 0x65414353 /* 'eACS' */ +) + +func maskNull(b byte) byte { + switch b { + case 0: + return ' ' + default: + return b + } +} + +func signature(b []byte) Signature { + return Signature(uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])) +} + +func SignatureFromString(sig string) Signature { + var b []byte = []byte{0x20, 0x20, 0x20, 0x20} + copy(b, sig) + return signature(b) +} + +func (s Signature) String() string { + v := []byte{ + (maskNull(byte((s >> 24) & 0xff))), + (maskNull(byte((s >> 16) & 0xff))), + (maskNull(byte((s >> 8) & 0xff))), + (maskNull(byte(s & 0xff))), + } + return "'" + string(v) + "'" +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tag_description.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tag_description.go new file mode 100644 index 000000000..24ac7550e --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tag_description.go @@ -0,0 +1,183 @@ +package icc + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "unicode/utf16" +) + +var _ = fmt.Print + +func parse_text_tag(data []byte) (any, error) { + var tag_type Signature + _, err := binary.Decode(data, binary.BigEndian, &tag_type) + if err != nil { + return nil, err + } + switch tag_type { + case TextTagSignature: + return textDecoder(data) + case DescSignature: + return descDecoder(data) + default: + return mlucDecoder(data) + } +} + +type TextTag interface { + BestGuessValue() string +} + +type DescriptionTag struct { + ASCII string + Unicode string + Script string +} + +func (d DescriptionTag) BestGuessValue() string { + if d.ASCII != "" { + return d.ASCII + } + return d.Unicode +} + +var _ TextTag = (*DescriptionTag)(nil) + +func descDecoder(raw []byte) (any, error) { + if len(raw) < 12 { + return nil, errors.New("desc tag too short") + } + asciiLen := int(binary.BigEndian.Uint32(raw[8:12])) + if asciiLen < 1 || 12+asciiLen > len(raw) { + return nil, errors.New("invalid ASCII length in desc tag") + } + ascii := raw[12 : 12+asciiLen] + if i := bytes.IndexByte(ascii, 0); i >= 0 { + ascii = ascii[:i] + } + + offset := 12 + asciiLen + if len(raw) < offset+4 { + return &DescriptionTag{ASCII: string(ascii)}, nil // ASCII-only, no Unicode + } + + unicodeCount := int(binary.BigEndian.Uint32(raw[offset : offset+4])) + offset += 4 + if len(raw) < offset+(unicodeCount*2) { + return nil, errors.New("desc tag truncated: missing UTF-16 data") + } + unicodeData := raw[offset : offset+(unicodeCount*2)] + offset += unicodeCount * 2 + unicode := decodeUTF16BE(unicodeData) + + if len(raw) <= offset { + return &DescriptionTag{ + ASCII: string(ascii), + Unicode: unicode, + }, nil + } + + scriptCount := int(raw[offset]) + offset++ + if len(raw) < offset+scriptCount { + return nil, errors.New("desc tag truncated: missing ScriptCode data") + } + script := string(raw[offset : offset+scriptCount]) + + return &DescriptionTag{ + ASCII: string(ascii), + Unicode: unicode, + Script: script, + }, nil +} + +type PlainText struct { + val string +} + +var _ TextTag = (*PlainText)(nil) + +func (p PlainText) BestGuessValue() string { return p.val } + +func textDecoder(raw []byte) (any, error) { + if len(raw) < 8 { + return nil, errors.New("text tag too short") + } + text := raw[8:] + text = bytes.TrimRight(text, "\x00") + return &PlainText{string(text)}, nil +} + +type MultiLocalizedTag struct { + Strings []LocalizedString +} + +func (p MultiLocalizedTag) BestGuessValue() string { + for _, t := range p.Strings { + if t.Value != "" && (t.Language == "en" || t.Language == "eng") { + return t.Value + } + } + for _, t := range p.Strings { + if t.Value != "" { + return t.Value + } + } + return "" +} + +type LocalizedString struct { + Language string // e.g. "en" + Country string // e.g. "US" + Value string +} + +func mlucDecoder(raw []byte) (any, error) { + if len(raw) < 16 { + return nil, errors.New("mluc tag too short") + } + count := int(binary.BigEndian.Uint32(raw[8:12])) + recordSize := int(binary.BigEndian.Uint32(raw[12:16])) + if recordSize != 12 { + return nil, fmt.Errorf("unexpected mluc record size: %d", recordSize) + } + if len(raw) < 16+(count*recordSize) { + return nil, fmt.Errorf("mluc tag too small for %d records", count) + } + tag := &MultiLocalizedTag{Strings: make([]LocalizedString, 0, count)} + for i := 0; i < count; i++ { + base := 16 + i*recordSize + langCode := string(raw[base : base+2]) + countryCode := string(raw[base+2 : base+4]) + strLen := int(binary.BigEndian.Uint32(raw[base+4 : base+8])) + strOffset := int(binary.BigEndian.Uint32(raw[base+8 : base+12])) + + if strOffset+strLen > len(raw) || strLen%2 != 0 { + return nil, fmt.Errorf("invalid string offset/length in mluc record %d", i) + } + + strData := raw[strOffset : strOffset+strLen] + decoded := decodeUTF16BE(strData) + tag.Strings = append(tag.Strings, LocalizedString{ + Language: langCode, + Country: countryCode, + Value: decoded, + }) + } + return tag, nil +} + +func decodeUTF16BE(data []byte) string { + codeUnits := make([]uint16, len(data)/2) + _, _ = binary.Decode(data, binary.BigEndian, codeUnits) + return string(utf16.Decode(codeUnits)) +} + +func sigDecoder(raw []byte) (any, error) { + if len(raw) < 12 { + return nil, errors.New("sig tag too short") + } + return signature(raw[8:12]), nil +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go new file mode 100644 index 000000000..53d76cb10 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go @@ -0,0 +1,140 @@ +package icc + +import ( + "encoding/binary" + "errors" + "fmt" +) + +// CLUTTag represents a color lookup table tag (TagColorLookupTable) +type CLUTTag struct { + GridPoints []uint8 // e.g., [17,17,17] for 3D CLUT + InputChannels int + OutputChannels int + Values []float64 // flattened [in1, in2, ..., out1, out2, ...] +} + +var _ ChannelTransformer = (*CLUTTag)(nil) + +// section 10.12.3 (CLUT) in ICC.1-2202-05.pdf +func embeddedClutDecoder(raw []byte, InputChannels, OutputChannels int) (any, error) { + if len(raw) < 20 { + return nil, errors.New("clut tag too short") + } + gridPoints := make([]uint8, InputChannels) + copy(gridPoints, raw[:InputChannels]) + bytes_per_channel := raw[16] + raw = raw[20:] + // expected size: (product of grid points) * output channels * bytes_per_channel + expected_num_of_values := expectedValues(gridPoints, OutputChannels) + values := make([]float64, expected_num_of_values) + if len(values)*int(bytes_per_channel) > len(raw) { + return nil, fmt.Errorf("CLUT unexpected body length: expected %d, got %d", expected_num_of_values*int(bytes_per_channel), len(raw)) + } + + switch bytes_per_channel { + case 1: + for i, b := range raw[:len(values)] { + values[i] = float64(b) / 255 + } + case 2: + for i := range len(values) { + values[i] = float64(binary.BigEndian.Uint16(raw[i*2:i*2+2])) / 65535 + } + } + ans := &CLUTTag{ + GridPoints: gridPoints, + InputChannels: InputChannels, + OutputChannels: OutputChannels, + Values: values, + } + if ans.InputChannels > 6 { + return nil, fmt.Errorf("unsupported num of CLUT input channels: %d", ans.InputChannels) + } + return ans, nil +} + +func expectedValues(gridPoints []uint8, outputChannels int) int { + expectedPoints := 1 + for _, g := range gridPoints { + expectedPoints *= int(g) + } + return expectedPoints * outputChannels +} + +func (c *CLUTTag) WorkspaceSize() int { return 16 } + +func (c *CLUTTag) IsSuitableFor(num_input_channels, num_output_channels int) bool { + return num_input_channels == int(c.InputChannels) && num_output_channels == c.OutputChannels +} + +func (c *CLUTTag) Transform(output, workspace []float64, inputs ...float64) error { + return c.Lookup(output, workspace, inputs) +} + +func (c *CLUTTag) Lookup(output, workspace, inputs []float64) error { + // clamp input values to 0-1... + clamped := workspace[:len(inputs)] + for i, v := range inputs { + clamped[i] = clamp01(v) + } + // find the grid positions and interpolation factors... + gridFrac := workspace[len(clamped) : 2*len(clamped)] + var buf [4]int + gridPos := buf[:] + for i, v := range clamped { + nPoints := int(c.GridPoints[i]) + if nPoints < 2 { + return fmt.Errorf("CLUT input channel %d has invalid grid points: %d", i, nPoints) + } + pos := v * float64(nPoints-1) + gridPos[i] = int(pos) + if gridPos[i] >= nPoints-1 { + gridPos[i] = nPoints - 2 // clamp + gridFrac[i] = 1.0 + } else { + gridFrac[i] = pos - float64(gridPos[i]) + } + } + // perform multi-dimensional interpolation (recursive)... + return c.triLinearInterpolate(output[:c.OutputChannels], gridPos, gridFrac) +} + +func (c *CLUTTag) triLinearInterpolate(out []float64, gridPos []int, gridFrac []float64) error { + numCorners := 1 << c.InputChannels // 2^inputs + for o := range c.OutputChannels { + out[o] = 0 + } + // walk all corners of the hypercube + for corner := range numCorners { + weight := 1.0 + idx := 0 + stride := 1 + for dim := c.InputChannels - 1; dim >= 0; dim-- { + bit := (corner >> dim) & 1 + pos := gridPos[dim] + bit + if pos >= int(c.GridPoints[dim]) { + return fmt.Errorf("CLUT corner position out of bounds at dimension %d", dim) + } + idx += pos * stride + stride *= int(c.GridPoints[dim]) + if bit == 0 { + weight *= 1 - gridFrac[dim] + } else { + weight *= gridFrac[dim] + } + } + base := idx * c.OutputChannels + if base+c.OutputChannels > len(c.Values) { + return errors.New("CLUT value index out of bounds") + } + for o := range c.OutputChannels { + out[o] += weight * c.Values[base+o] + } + } + return nil +} + +func clamp01(v float64) float64 { + return max(0, min(v, 1)) +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go new file mode 100644 index 000000000..23f5dda42 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go @@ -0,0 +1,103 @@ +package icc + +import ( + "fmt" + "sync" +) + +type not_found struct { + sig Signature +} + +func (e *not_found) Error() string { + return fmt.Sprintf("no tag for signature: %s found in this ICC profile", e.sig) +} + +type unsupported struct { + sig Signature +} + +func (e *unsupported) Error() string { + return fmt.Sprintf("the tag: %s is not supported", e.sig) +} + +func parse_tag(sig Signature, data []byte) (result any, err error) { + if len(data) == 0 { + return nil, ¬_found{sig} + } + switch sig { + default: + return nil, &unsupported{sig} + case DescSignature, DeviceManufacturerDescriptionSignature, DeviceModelDescriptionSignature: + return parse_text_tag(data) + case SignateTagSignature: + return sigDecoder(data) + } +} + +type parsed_tag struct { + tag any + err error +} + +type TagTable struct { + entries map[Signature][]byte + lock sync.Mutex + parsed map[Signature]parsed_tag +} + +func (t *TagTable) add(sig Signature, data []byte) { + t.entries[sig] = data +} + +func (t *TagTable) get_parsed(sig Signature) (ans any, err error) { + t.lock.Lock() + defer t.lock.Unlock() + existing, found := t.parsed[sig] + if found { + return existing.tag, existing.err + } + if t.parsed == nil { + t.parsed = make(map[Signature]parsed_tag) + } + defer func() { + t.parsed[sig] = parsed_tag{ans, err} + }() + return parse_tag(sig, t.entries[sig]) +} + +func (t *TagTable) getDescription(s Signature) (string, error) { + q, err := t.get_parsed(s) + if err != nil { + return "", fmt.Errorf("could not get description for %s with error: %w", s, err) + } + if t, ok := q.(TextTag); ok { + return t.BestGuessValue(), nil + } else { + return "", fmt.Errorf("tag for %s is not a text tag", s) + } +} + +func (t *TagTable) getProfileDescription() (string, error) { + return t.getDescription(DescSignature) +} + +func (t *TagTable) getDeviceManufacturerDescription() (string, error) { + return t.getDescription(DeviceManufacturerDescriptionSignature) +} + +func (t *TagTable) getDeviceModelDescription() (string, error) { + return t.getDescription(DeviceModelDescriptionSignature) +} + +func emptyTagTable() TagTable { + return TagTable{ + entries: make(map[Signature][]byte), + } +} + +type ChannelTransformer interface { + Transform(output, workspace []float64, input ...float64) error + IsSuitableFor(num_input_channels int, num_output_channels int) bool + WorkspaceSize() int +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/imageformat.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/imageformat.go new file mode 100644 index 000000000..e9e1649aa --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/imageformat.go @@ -0,0 +1,3 @@ +package meta + +type ImageFormat string diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/doc.go new file mode 100644 index 000000000..7d80b0d24 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/doc.go @@ -0,0 +1,2 @@ +// Package jpegmeta provides support for working with embedded JPEG metadata. +package jpegmeta diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/jpegmeta.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/jpegmeta.go new file mode 100644 index 000000000..53b80c30d --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/jpegmeta.go @@ -0,0 +1,166 @@ +package jpegmeta + +import ( + "bytes" + "fmt" + "io" + + "github.com/kovidgoyal/go-parallel" + "github.com/kovidgoyal/imaging/prism/meta" + "github.com/kovidgoyal/imaging/streams" +) + +// Format specifies the image format handled by this package +var Format = meta.ImageFormat("JPEG") + +const exifSignature = "Exif\x00\x00" + +var iccProfileIdentifier = []byte("ICC_PROFILE\x00") + +// Load loads the metadata for a JPEG image stream. +// +// Only as much of the stream is consumed as necessary to extract the metadata; +// the returned stream contains a buffered copy of the consumed data such that +// reading from it will produce the same results as fully reading the input +// stream. This provides a convenient way to load the full image after loading +// the metadata. +// +// An error is returned if basic metadata could not be extracted. The returned +// stream still provides the full image data. +func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) { + imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) { + md, err = ExtractMetadata(r) + return + }) + return +} + +// Same as Load() except that no new stream is provided +func ExtractMetadata(r io.Reader) (md *meta.Data, err error) { + metadataExtracted := false + md = &meta.Data{Format: Format} + segReader := NewSegmentReader(r) + + defer func() { + if r := recover(); r != nil { + if !metadataExtracted { + md = nil + } + err = parallel.Format_stacktrace_on_panic(r, 1) + } + }() + + var iccProfileChunks [][]byte + var iccProfileChunksExtracted int + var exif []byte + + allMetadataExtracted := func() bool { + return metadataExtracted && + iccProfileChunks != nil && + iccProfileChunksExtracted == len(iccProfileChunks) && + exif != nil + } + + soiSegment, err := segReader.ReadSegment() + if err != nil { + return nil, err + } + if soiSegment.Marker.Type != markerTypeStartOfImage { + return nil, fmt.Errorf("stream does not begin with start-of-image") + } + +parseSegments: + for { + segment, err := segReader.ReadSegment() + if err != nil { + if err == io.EOF { + return nil, fmt.Errorf("unexpected EOF") + } + return nil, err + } + + switch segment.Marker.Type { + + case markerTypeStartOfFrameBaseline, + markerTypeStartOfFrameProgressive: + md.BitsPerComponent = uint32(segment.Data[0]) + md.PixelHeight = uint32(segment.Data[1])<<8 | uint32(segment.Data[2]) + md.PixelWidth = uint32(segment.Data[3])<<8 | uint32(segment.Data[4]) + metadataExtracted = true + + if allMetadataExtracted() { + break parseSegments + } + + case markerTypeStartOfScan, + markerTypeEndOfImage: + break parseSegments + + case markerTypeApp1: + if bytes.HasPrefix(segment.Data, []byte(exifSignature)) { + exif = segment.Data + } + case markerTypeApp2: + if len(segment.Data) < len(iccProfileIdentifier)+2 { + continue + } + + for i := range iccProfileIdentifier { + if segment.Data[i] != iccProfileIdentifier[i] { + continue parseSegments + } + } + + iccData, iccErr := md.ICCProfileData() + if iccData != nil || iccErr != nil { + continue + } + + chunkTotal := segment.Data[len(iccProfileIdentifier)+1] + if iccProfileChunks == nil { + iccProfileChunks = make([][]byte, chunkTotal) + } else if int(chunkTotal) != len(iccProfileChunks) { + md.SetICCProfileError(fmt.Errorf("inconsistent ICC profile chunk count")) + continue + } + + chunkNum := segment.Data[len(iccProfileIdentifier)] + if chunkNum == 0 || int(chunkNum) > len(iccProfileChunks) { + md.SetICCProfileError(fmt.Errorf("invalid ICC profile chunk number")) + continue + } + if iccProfileChunks[chunkNum-1] != nil { + md.SetICCProfileError(fmt.Errorf("duplicated ICC profile chunk")) + continue + } + iccProfileChunksExtracted++ + iccProfileChunks[chunkNum-1] = segment.Data[len(iccProfileIdentifier)+2:] + + if allMetadataExtracted() { + break parseSegments + } + } + } + + if !metadataExtracted { + return nil, fmt.Errorf("no metadata found") + } + md.ExifData = exif + + // Incomplete or missing ICC profile + if len(iccProfileChunks) != iccProfileChunksExtracted { + _, iccErr := md.ICCProfileData() + if iccErr == nil { + md.SetICCProfileError(fmt.Errorf("incomplete ICC profile data")) + } + return md, nil + } + + iccProfileData := bytes.Buffer{} + for i := range iccProfileChunks { + iccProfileData.Write(iccProfileChunks[i]) + } + md.SetICCProfileData(iccProfileData.Bytes()) + + return md, nil +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/marker.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/marker.go new file mode 100644 index 000000000..bbbc06e88 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/marker.go @@ -0,0 +1,89 @@ +package jpegmeta + +import ( + "encoding/binary" + "fmt" + "io" + + "github.com/kovidgoyal/imaging/streams" +) + +var invalidMarker = marker{Type: markerTypeInvalid} + +type marker struct { + Type markerType + DataLength int +} + +func makeMarker(mType byte, r io.Reader) (marker, error) { + var length uint16 + switch mType { + + case + byte(markerTypeRestart0), + byte(markerTypeRestart1), + byte(markerTypeRestart2), + byte(markerTypeRestart3), + byte(markerTypeRestart4), + byte(markerTypeRestart5), + byte(markerTypeRestart6), + byte(markerTypeRestart7), + byte(markerTypeStartOfImage), + byte(markerTypeEndOfImage): + + length = 2 + + case byte(markerTypeStartOfFrameBaseline), + byte(markerTypeStartOfFrameProgressive), + byte(markerTypeDefineHuffmanTable), + byte(markerTypeStartOfScan), + byte(markerTypeDefineQuantisationTable), + byte(markerTypeDefineRestartInterval), + byte(markerTypeApp0), + byte(markerTypeApp1), + byte(markerTypeApp2), + byte(markerTypeApp3), + byte(markerTypeApp4), + byte(markerTypeApp5), + byte(markerTypeApp6), + byte(markerTypeApp7), + byte(markerTypeApp8), + byte(markerTypeApp9), + byte(markerTypeApp10), + byte(markerTypeApp11), + byte(markerTypeApp12), + byte(markerTypeApp13), + byte(markerTypeApp14), + byte(markerTypeApp15), + byte(markerTypeComment): + + var err error + if err = binary.Read(r, binary.BigEndian, &length); err != nil { + return invalidMarker, err + } + + default: + return invalidMarker, fmt.Errorf("unrecognised marker type %0x", mType) + } + + return marker{ + Type: markerType(mType), + DataLength: int(length) - 2, + }, nil +} + +func readMarker(r io.Reader) (marker, error) { + b, err := streams.ReadByte(r) + if err != nil { + return invalidMarker, err + } + + if b != 0xff { + return invalidMarker, fmt.Errorf("invalid marker identifier %0x", b) + } + if b, err = streams.ReadByte(r); err != nil { + return invalidMarker, err + } + + return makeMarker(b, r) +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/markertype.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/markertype.go new file mode 100644 index 000000000..d3ae9cfb4 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/markertype.go @@ -0,0 +1,115 @@ +package jpegmeta + +import "fmt" + +type markerType int + +const ( + markerTypeInvalid markerType = 0x00 + markerTypeStartOfFrameBaseline markerType = 0xc0 + markerTypeStartOfFrameProgressive markerType = 0xc2 + markerTypeDefineHuffmanTable markerType = 0xc4 + markerTypeRestart0 markerType = 0xd0 + markerTypeRestart1 markerType = 0xd1 + markerTypeRestart2 markerType = 0xd2 + markerTypeRestart3 markerType = 0xd3 + markerTypeRestart4 markerType = 0xd4 + markerTypeRestart5 markerType = 0xd5 + markerTypeRestart6 markerType = 0xd6 + markerTypeRestart7 markerType = 0xd7 + markerTypeStartOfImage markerType = 0xd8 + markerTypeEndOfImage markerType = 0xd9 + markerTypeStartOfScan markerType = 0xda + markerTypeDefineQuantisationTable markerType = 0xdb + markerTypeDefineRestartInterval markerType = 0xdd + markerTypeApp0 markerType = 0xe0 + markerTypeApp1 markerType = 0xe1 + markerTypeApp2 markerType = 0xe2 + markerTypeApp3 markerType = 0xe3 + markerTypeApp4 markerType = 0xe4 + markerTypeApp5 markerType = 0xe5 + markerTypeApp6 markerType = 0xe6 + markerTypeApp7 markerType = 0xe7 + markerTypeApp8 markerType = 0xe8 + markerTypeApp9 markerType = 0xe9 + markerTypeApp10 markerType = 0xea + markerTypeApp11 markerType = 0xeb + markerTypeApp12 markerType = 0xec + markerTypeApp13 markerType = 0xed + markerTypeApp14 markerType = 0xee + markerTypeApp15 markerType = 0xef + markerTypeComment markerType = 0xfe +) + +func (mt markerType) String() string { + switch mt { + case markerTypeStartOfFrameBaseline: + return "SOF0" + case markerTypeStartOfFrameProgressive: + return "SOF2" + case markerTypeDefineHuffmanTable: + return "DHT" + case markerTypeRestart0: + return "RST0" + case markerTypeRestart1: + return "RST1" + case markerTypeRestart2: + return "RST2" + case markerTypeRestart3: + return "RST3" + case markerTypeRestart4: + return "RST4" + case markerTypeRestart5: + return "RST5" + case markerTypeRestart6: + return "RST6" + case markerTypeRestart7: + return "RST7" + case markerTypeStartOfImage: + return "SOI" + case markerTypeEndOfImage: + return "EOI" + case markerTypeStartOfScan: + return "SOS" + case markerTypeDefineQuantisationTable: + return "DQT" + case markerTypeDefineRestartInterval: + return "DRI" + case markerTypeApp0: + return "APP0" + case markerTypeApp1: + return "APP1" + case markerTypeApp2: + return "APP2" + case markerTypeApp3: + return "APP3" + case markerTypeApp4: + return "APP4" + case markerTypeApp5: + return "APP5" + case markerTypeApp6: + return "APP6" + case markerTypeApp7: + return "APP7" + case markerTypeApp8: + return "APP8" + case markerTypeApp9: + return "APP9" + case markerTypeApp10: + return "APP10" + case markerTypeApp11: + return "APP11" + case markerTypeApp12: + return "APP12" + case markerTypeApp13: + return "APP13" + case markerTypeApp14: + return "APP14" + case markerTypeApp15: + return "APP15" + case markerTypeComment: + return "COM" + default: + return fmt.Sprintf("Unknown (%0x)", byte(mt)) + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segment.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segment.go new file mode 100644 index 000000000..053b16d6b --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segment.go @@ -0,0 +1,38 @@ +package jpegmeta + +import ( + "io" +) + +var invalidSegment = segment{Marker: invalidMarker} + +type segment struct { + Marker marker + Data []byte +} + +func makeSegment(markerType byte, r io.Reader) (segment, error) { + m, err := makeMarker(markerType, r) + return segment{Marker: m}, err +} + +func readSegment(r io.Reader) (segment, error) { + m, err := readMarker(r) + if err != nil { + return invalidSegment, err + } + + seg := segment{ + Marker: m, + } + if m.DataLength > 0 { + seg.Data = make([]byte, m.DataLength) + + _, err := io.ReadFull(r, seg.Data) + if err != nil { + return invalidSegment, err + } + } + + return seg, nil +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segmentreader.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segmentreader.go new file mode 100644 index 000000000..141290c4c --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segmentreader.go @@ -0,0 +1,56 @@ +package jpegmeta + +import ( + "io" + + "github.com/kovidgoyal/imaging/streams" +) + +type segmentReader struct { + reader io.Reader + inEntropyCodedData bool +} + +func (sr *segmentReader) ReadSegment() (segment, error) { + if sr.inEntropyCodedData { + for { + b, err := streams.ReadByte(sr.reader) + if err != nil { + return segment{}, err + } + + if b == 0xFF { + if b, err = streams.ReadByte(sr.reader); err != nil { + return segment{}, err + } + + if b != 0x00 { + seg, err := makeSegment(b, sr.reader) + if err != nil { + return segment{}, err + } + + sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan || + (seg.Marker.Type >= markerTypeRestart0 && seg.Marker.Type <= markerTypeRestart7) + + return seg, err + } + } + } + } + + seg, err := readSegment(sr.reader) + if err != nil { + return seg, err + } + + sr.inEntropyCodedData = seg.Marker.Type == markerTypeStartOfScan + + return seg, nil +} + +func NewSegmentReader(r io.Reader) *segmentReader { + return &segmentReader{ + reader: r, + } +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunkheader.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunkheader.go new file mode 100644 index 000000000..3fc53c270 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunkheader.go @@ -0,0 +1,21 @@ +package pngmeta + +import ( + "encoding/binary" + "fmt" + "io" +) + +type chunkHeader struct { + Length uint32 + ChunkType [4]byte +} + +func (ch chunkHeader) String() string { + return fmt.Sprintf("%c%c%c%c(%d)", ch.ChunkType[0], ch.ChunkType[1], ch.ChunkType[2], ch.ChunkType[3], ch.Length) +} + +func readChunkHeader(r io.Reader) (ch chunkHeader, err error) { + err = binary.Read(r, binary.BigEndian, &ch) + return +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunktypes.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunktypes.go new file mode 100644 index 000000000..4d125549b --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunktypes.go @@ -0,0 +1,6 @@ +package pngmeta + +var chunkTypeiCCP = [4]byte{'i', 'C', 'C', 'P'} +var chunkTypeIDAT = [4]byte{'I', 'D', 'A', 'T'} +var chunkTypeIEND = [4]byte{'I', 'E', 'N', 'D'} +var chunkTypeIHDR = [4]byte{'I', 'H', 'D', 'R'} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/doc.go new file mode 100644 index 000000000..2a41eda81 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/doc.go @@ -0,0 +1,2 @@ +// Package pngmeta provides support for working with embedded PNG metadata. +package pngmeta diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/pngmeta.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/pngmeta.go new file mode 100644 index 000000000..676af840c --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/pngmeta.go @@ -0,0 +1,163 @@ +package pngmeta + +import ( + "bytes" + "compress/zlib" + "encoding/binary" + "errors" + "fmt" + "io" + + "github.com/kovidgoyal/imaging/prism/meta" + "github.com/kovidgoyal/imaging/streams" +) + +// Format specifies the image format handled by this package +var Format = meta.ImageFormat("PNG") + +var pngSignature = [8]byte{0x89, 'P', 'N', 'G', 0x0D, 0x0A, 0x1A, 0x0A} + +// Load loads the metadata for a PNG image stream. +// +// Only as much of the stream is consumed as necessary to extract the metadata; +// the returned stream contains a buffered copy of the consumed data such that +// reading from it will produce the same results as fully reading the input +// stream. This provides a convenient way to load the full image after loading +// the metadata. +// +// An error is returned if basic metadata could not be extracted. The returned +// stream still provides the full image data. +func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) { + imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) { + md, err = ExtractMetadata(r) + return + }) + return +} + +func read_chunk(r io.Reader, length uint32) (ans []byte, err error) { + ans = make([]byte, length+4) + _, err = io.ReadFull(r, ans) + ans = ans[:len(ans)-4] // we dont care about the chunk CRC + return +} + +func skip_chunk(r io.Reader, length uint32) (err error) { + return streams.Skip(r, int64(length)+4) +} + +// Same as Load() except that no new stream is provided +func ExtractMetadata(r io.Reader) (md *meta.Data, err error) { + metadataExtracted := false + md = &meta.Data{Format: Format} + + defer func() { + if r := recover(); r != nil { + if !metadataExtracted { + md = nil + } + err = fmt.Errorf("panic while extracting image metadata: %v", r) + } + }() + + allMetadataExtracted := func() bool { + iccData, iccErr := md.ICCProfileData() + return metadataExtracted && (iccData != nil || iccErr != nil) + } + + pngSig := [8]byte{} + if _, err := io.ReadFull(r, pngSig[:]); err != nil { + return nil, err + } + if pngSig != pngSignature { + return nil, fmt.Errorf("invalid PNG signature") + } + var chunk []byte + + decode := func(target any) error { + if n, err := binary.Decode(chunk, binary.BigEndian, target); err == nil { + chunk = chunk[n:] + return nil + } else { + return err + } + } + +parseChunks: + for { + ch, err := readChunkHeader(r) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + + switch ch.ChunkType { + + case chunkTypeIHDR: + if chunk, err = read_chunk(r, ch.Length); err != nil { + return nil, err + } + if err = decode(&md.PixelWidth); err != nil { + return nil, err + } + if err = decode(&md.PixelHeight); err != nil { + return nil, err + } + md.BitsPerComponent = uint32(chunk[0]) + metadataExtracted = true + if allMetadataExtracted() { + break parseChunks + } + + case chunkTypeiCCP: + if chunk, err = read_chunk(r, ch.Length); err != nil { + return nil, err + } + idx := bytes.IndexByte(chunk, 0) + if idx < 0 || idx > 80 { + return nil, fmt.Errorf("null terminator not found reading ICC profile name") + } + chunk = chunk[idx+1:] + if len(chunk) < 1 { + return nil, fmt.Errorf("incomplete ICCP chunk in PNG file") + } + if compressionMethod := chunk[0]; compressionMethod != 0x00 { + return nil, fmt.Errorf("unknown compression method (%d)", compressionMethod) + } + chunk = chunk[1:] + // Decompress ICC profile data + zReader, err := zlib.NewReader(bytes.NewReader(chunk)) + if err != nil { + md.SetICCProfileError(err) + break + } + defer zReader.Close() + profileData := &bytes.Buffer{} + _, err = io.Copy(profileData, zReader) + if err == nil { + md.SetICCProfileData(profileData.Bytes()) + if allMetadataExtracted() { + break parseChunks + } + } else { + md.SetICCProfileError(err) + } + + case chunkTypeIDAT, chunkTypeIEND: + break parseChunks + + default: + if err = skip_chunk(r, ch.Length); err != nil { + return nil, err + } + } + } + + if !metadataExtracted { + return nil, fmt.Errorf("no metadata found") + } + + return md, nil +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunkheader.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunkheader.go new file mode 100644 index 000000000..47a60ab03 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunkheader.go @@ -0,0 +1,21 @@ +package webpmeta + +import ( + "encoding/binary" + "fmt" + "io" +) + +type chunkHeader struct { + ChunkType [4]byte + Length uint32 +} + +func (ch chunkHeader) String() string { + return fmt.Sprintf("%c%c%c%c(%d)", ch.ChunkType[0], ch.ChunkType[1], ch.ChunkType[2], ch.ChunkType[3], ch.Length) +} + +func readChunkHeader(r io.Reader) (ch chunkHeader, err error) { + err = binary.Read(r, binary.LittleEndian, &ch) + return +} diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunktypes.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunktypes.go new file mode 100644 index 000000000..c6083474d --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunktypes.go @@ -0,0 +1,10 @@ +package webpmeta + +var ( + chunkTypeRIFF = [4]byte{'R', 'I', 'F', 'F'} + chunkTypeWEBP = [4]byte{'W', 'E', 'B', 'P'} + chunkTypeVP8 = [4]byte{'V', 'P', '8', ' '} + chunkTypeVP8L = [4]byte{'V', 'P', '8', 'L'} + chunkTypeVP8X = [4]byte{'V', 'P', '8', 'X'} + chunkTypeICCP = [4]byte{'I', 'C', 'C', 'P'} +) diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/doc.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/doc.go new file mode 100644 index 000000000..ed7f1009a --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/doc.go @@ -0,0 +1,2 @@ +// Package webpmeta provides support for working with embedded WebP metadata. +package webpmeta diff --git a/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/webpmeta.go b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/webpmeta.go new file mode 100644 index 000000000..fd1fd4476 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/webpmeta.go @@ -0,0 +1,213 @@ +package webpmeta + +import ( + "errors" + "fmt" + "io" + + "github.com/kovidgoyal/imaging/prism/meta" + "github.com/kovidgoyal/imaging/streams" +) + +// Format specifies the image format handled by this package +var Format = meta.ImageFormat("WebP") + +// Signature is FourCC bytes in the RIFF chunk, "RIFF????WEBP" +var webpSignature = [4]byte{'W', 'E', 'B', 'P'} + +type webpFormat int + +const ( + webpFormatSimple = webpFormat(iota) + webpFormatLossless + webpFormatExtended +) + +// Bits per component is fixed in WebP +const bitsPerComponent = 8 + +// Load loads the metadata for a WebP image stream. +// +// Only as much of the stream is consumed as necessary to extract the metadata; +// the returned stream contains a buffered copy of the consumed data such that +// reading from it will produce the same results as fully reading the input +// stream. This provides a convenient way to load the full image after loading +// the metadata. +// +// An error is returned if basic metadata could not be extracted. The returned +// stream still provides the full image data. +func Load(r io.Reader) (md *meta.Data, imgStream io.Reader, err error) { + imgStream, err = streams.CallbackWithSeekable(r, func(r io.Reader) (err error) { + md, err = ExtractMetadata(r) + return + }) + return +} + +// Same as Load() except that no new stream is provided +func ExtractMetadata(r io.Reader) (md *meta.Data, err error) { + md = &meta.Data{Format: Format} + + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("panic while extracting image metadata: %v", r) + } + }() + + if err := verifySignature(r); err != nil { + return nil, err + } + format, chunkLen, err := readWebPFormat(r) + if err != nil { + return nil, err + } + err = parseFormat(r, md, format, chunkLen) + if err != nil { + return nil, err + } + return md, nil +} + +func parseFormat(r io.Reader, md *meta.Data, format webpFormat, chunkLen uint32) error { + switch format { + case webpFormatExtended: + return parseWebpExtended(r, md, chunkLen) + case webpFormatSimple: + return parseWebpSimple(r, md, chunkLen) + case webpFormatLossless: + return parseWebpLossless(r, md, chunkLen) + default: + return errors.New("unknown WebP format") + } +} + +func parseWebpSimple(r io.Reader, md *meta.Data, chunkLen uint32) error { + var buf [10]byte + b := buf[:] + if _, err := io.ReadFull(r, b); err != nil { + return err + } + b = b[3:] + if b[0] != 0x9d || b[1] != 0x01 || b[2] != 0x2a { + return errors.New("corrupted WebP VP8 frame") + } + md.PixelWidth = uint32(b[4]&((1<<6)-1))<<8 | uint32(b[3]) + md.PixelWidth = uint32(b[6]&((1<<6)-1))<<8 | uint32(b[5]) + md.BitsPerComponent = bitsPerComponent + return nil +} + +func parseWebpLossless(r io.Reader, md *meta.Data, chunkLen uint32) error { + var b [5]byte + if _, err := io.ReadFull(r, b[:]); err != nil { + return err + } + if b[0] != 0x2f { + return errors.New("corrupted lossless WebP") + } + // Next 28 bits are width-1 and height-1. + w := uint32(b[1]) + w |= uint32(b[2]&((1<<6)-1)) << 8 + w &= 0x3FFF + + h := uint32((b[2] >> 6) & ((1 << 2) - 1)) + h |= uint32(b[3]) << 2 + h |= uint32(b[4]&((1<<4)-1)) << 10 + h &= 0x3FFF + + md.PixelWidth = w + 1 + md.PixelHeight = h + 1 + md.BitsPerComponent = bitsPerComponent + return nil +} + +func parseWebpExtended(r io.Reader, md *meta.Data, chunkLen uint32) error { + if chunkLen != 10 { + return fmt.Errorf("unexpected VP8X chunk length: %d", chunkLen) + } + var hb [10]byte + h := hb[:] + if _, err := io.ReadFull(r, h); err != nil { + return err + } + hasProfile := h[0]&(1<<5) != 0 + h = h[4:] + w := uint32(h[0]) | uint32(h[1])<<8 | uint32(h[2])<<16 + ht := uint32(h[3]) | uint32(h[4])<<8 | uint32(h[5])<<16 + md.PixelWidth = w + 1 + md.PixelHeight = ht + 1 + md.BitsPerComponent = bitsPerComponent + + if hasProfile { + data, err := readICCP(r, chunkLen) + if err != nil { + md.SetICCProfileError(err) + } else { + md.SetICCProfileData(data) + } + } + + return nil +} + +func readICCP(r io.Reader, chunkLen uint32) ([]byte, error) { + // Skip to the end of the chunk. + if err := skip(r, chunkLen-10); err != nil { + return nil, err + } + + // ICCP _must_ be the next chunk. + ch, err := readChunkHeader(r) + if err != nil { + return nil, err + } + if ch.ChunkType != chunkTypeICCP { + return nil, errors.New("no expected ICCP chunk") + } + + // Extract ICCP. + data := make([]byte, ch.Length) + if _, err := io.ReadFull(r, data); err != nil { + return nil, err + } + return data, nil +} + +func verifySignature(r io.Reader) error { + ch, err := readChunkHeader(r) + if err != nil { + return err + } + if ch.ChunkType != chunkTypeRIFF { + return errors.New("missing RIFF header") + } + var fourcc [4]byte + if _, err := io.ReadFull(r, fourcc[:]); err != nil { + return err + } + if fourcc != webpSignature { + return errors.New("not a WEBP file") + } + return nil +} + +func readWebPFormat(r io.Reader) (format webpFormat, length uint32, err error) { + ch, err := readChunkHeader(r) + if err != nil { + return 0, 0, err + } + switch ch.ChunkType { + case chunkTypeVP8: + return webpFormatSimple, ch.Length, nil + case chunkTypeVP8L: + return webpFormatLossless, ch.Length, nil + case chunkTypeVP8X: + return webpFormatExtended, ch.Length, nil + default: + return 0, 0, fmt.Errorf("unexpected WEBP format: %s", string(ch.ChunkType[:])) + } +} + +func skip(r io.Reader, length uint32) error { + return streams.Skip(r, int64(length)) +} diff --git a/vendor/github.com/kovidgoyal/imaging/publish.py b/vendor/github.com/kovidgoyal/imaging/publish.py index 29f61a8a2..f23bfa14f 100644 --- a/vendor/github.com/kovidgoyal/imaging/publish.py +++ b/vendor/github.com/kovidgoyal/imaging/publish.py @@ -5,6 +5,9 @@ import os import subprocess +VERSION = "1.7.2" + + def run(*args: str): cp = subprocess.run(args) if cp.returncode != 0: @@ -12,14 +15,14 @@ def run(*args: str): def main(): - version = input('Enter the version to publish: ') + version = VERSION try: ans = input(f'Publish version \033[91m{version}\033[m (y/n): ') except KeyboardInterrupt: ans = 'n' if ans.lower() != 'y': return - os.environ['GITHUB_TOKEN'] = open(os.path.join(os.environ['PENV'], 'github-token')).read().strip() + os.environ['GITHUB_TOKEN'] = open(os.path.join(os.environ['PENV'], 'github-token')).read().strip().partition(':')[2] run('git', 'tag', '-a', 'v' + version, '-m', f'version {version}') run('git', 'push') run('goreleaser', 'release', '--clean') diff --git a/vendor/github.com/kovidgoyal/imaging/resize.go b/vendor/github.com/kovidgoyal/imaging/resize.go index 963f40c48..19c939d35 100644 --- a/vendor/github.com/kovidgoyal/imaging/resize.go +++ b/vendor/github.com/kovidgoyal/imaging/resize.go @@ -61,7 +61,6 @@ func precomputeWeights(dstSize, srcSize int, filter ResampleFilter) [][]indexWei // Example: // // dstImage := imaging.Resize(srcImage, 800, 600, imaging.Lanczos) -// func Resize(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA { dstW, dstH := width, height if dstW < 0 || dstH < 0 { @@ -110,10 +109,10 @@ func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image. src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, width, src.h)) weights := precomputeWeights(width, src.w, filter) - parallel(0, src.h, func(ys <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, src.w*4) - for y := range ys { - src.scan(0, y, src.w, y+1, scanLine) + for y := start; y < limit; y++ { + src.Scan(0, y, src.w, y+1, scanLine) j0 := y * dst.Stride for x := range weights { var r, g, b, a float64 @@ -137,7 +136,9 @@ func resizeHorizontal(img image.Image, width int, filter ResampleFilter) *image. } } } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -145,10 +146,10 @@ func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.N src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, height)) weights := precomputeWeights(height, src.h, filter) - parallel(0, src.w, func(xs <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, src.h*4) - for x := range xs { - src.scan(x, 0, x+1, src.h, scanLine) + for x := start; x < limit; x++ { + src.Scan(x, 0, x+1, src.h, scanLine) for y := range weights { var r, g, b, a float64 for _, w := range weights[y] { @@ -171,7 +172,9 @@ func resizeVertical(img image.Image, height int, filter ResampleFilter) *image.N } } } - }) + }, 0, src.w); err != nil { + panic(err) + } return dst } @@ -183,34 +186,37 @@ func resizeNearest(img image.Image, width, height int) *image.NRGBA { if dx > 1 && dy > 1 { src := newScanner(img) - parallel(0, height, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { srcY := int((float64(y) + 0.5) * dy) dstOff := y * dst.Stride - for x := 0; x < width; x++ { + for x := range width { srcX := int((float64(x) + 0.5) * dx) - src.scan(srcX, srcY, srcX+1, srcY+1, dst.Pix[dstOff:dstOff+4]) + src.Scan(srcX, srcY, srcX+1, srcY+1, dst.Pix[dstOff:dstOff+4]) dstOff += 4 } } - }) + }, 0, height); err != nil { + panic(err) + } } else { src := toNRGBA(img) - parallel(0, height, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { srcY := int((float64(y) + 0.5) * dy) srcOff0 := srcY * src.Stride dstOff := y * dst.Stride - for x := 0; x < width; x++ { + for x := range width { srcX := int((float64(x) + 0.5) * dx) srcOff := srcOff0 + srcX*4 copy(dst.Pix[dstOff:dstOff+4], src.Pix[srcOff:srcOff+4]) dstOff += 4 } } - }) + }, 0, height); err != nil { + panic(err) + } } - return dst } @@ -220,7 +226,6 @@ func resizeNearest(img image.Image, width, height int) *image.NRGBA { // Example: // // dstImage := imaging.Fit(srcImage, 800, 600, imaging.Lanczos) -// func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA { maxW, maxH := width, height @@ -261,7 +266,6 @@ func Fit(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA // Example: // // dstImage := imaging.Fill(srcImage, 800, 600, imaging.Center, imaging.Lanczos) -// func Fill(img image.Image, width, height int, anchor Anchor, filter ResampleFilter) *image.NRGBA { dstW, dstH := width, height @@ -340,7 +344,6 @@ func resizeAndCrop(img image.Image, width, height int, anchor Anchor, filter Res // Example: // // dstImage := imaging.Thumbnail(srcImage, 100, 100, imaging.Lanczos) -// func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image.NRGBA { return Fill(img, width, height, Center, filter) } @@ -367,7 +370,6 @@ func Thumbnail(img image.Image, width, height int, filter ResampleFilter) *image // // - NearestNeighbor // Fastest resampling filter, no antialiasing. -// type ResampleFilter struct { Support float64 Kernel func(float64) float64 diff --git a/vendor/github.com/kovidgoyal/imaging/scanner.go b/vendor/github.com/kovidgoyal/imaging/scanner.go index 84de2fc90..f72ba9cea 100644 --- a/vendor/github.com/kovidgoyal/imaging/scanner.go +++ b/vendor/github.com/kovidgoyal/imaging/scanner.go @@ -11,6 +11,10 @@ type scanner struct { palette []color.NRGBA } +func (s scanner) Bytes_per_channel() int { return 1 } +func (s scanner) Num_of_channels() int { return 4 } +func (s scanner) Bounds() image.Rectangle { return s.image.Bounds() } + func newScanner(img image.Image) *scanner { s := &scanner{ image: img, @@ -27,8 +31,32 @@ func newScanner(img image.Image) *scanner { } // scan scans the given rectangular region of the image into dst. -func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) { +func (s *scanner) Scan(x1, y1, x2, y2 int, dst []uint8) { switch img := s.image.(type) { + case *NRGB: + j := 0 + if x2 == x1+1 { + i := y1*img.Stride + x1*3 + for y := y1; y < y2; y++ { + d := dst[j : j+4 : j+4] + s := img.Pix[i : i+3 : i+3] + d[0] = s[0] + d[1] = s[1] + d[2] = s[2] + d[3] = 255 + j += 4 + i += img.Stride + } + } else { + d := dst + for y := y1; y < y2; y++ { + s := img.Pix[y*img.Stride+x1*3:] + for range x2 - x1 { + d[0], d[1], d[2], d[3] = s[0], s[1], s[2], 255 + d, s = d[4:], s[3:] + } + } + } case *image.NRGBA: size := (x2 - x1) * 4 j := 0 @@ -283,3 +311,14 @@ func (s *scanner) scan(x1, y1, x2, y2 int, dst []uint8) { } } } + +type Scanner interface { + Scan(x1, y1, x2, y2 int, dst []uint8) + Bytes_per_channel() int + Num_of_channels() int + Bounds() image.Rectangle +} + +func NewNRGBAScanner(source_image image.Image) Scanner { + return newScanner(source_image) +} diff --git a/vendor/github.com/kovidgoyal/imaging/streams/api.go b/vendor/github.com/kovidgoyal/imaging/streams/api.go new file mode 100644 index 000000000..f71637ac3 --- /dev/null +++ b/vendor/github.com/kovidgoyal/imaging/streams/api.go @@ -0,0 +1,133 @@ +package streams + +import ( + "bufio" + "bytes" + "fmt" + "io" +) + +// BufferedReadSeeker wraps an io.ReadSeeker to provide buffering. +// It implements the io.ReadSeeker interface. +type BufferedReadSeeker struct { + reader *bufio.Reader + seeker io.ReadSeeker +} + +// NewBufferedReadSeeker creates a new BufferedReadSeeker with a default buffer size. +func NewBufferedReadSeeker(rs io.ReadSeeker) *BufferedReadSeeker { + return &BufferedReadSeeker{ + reader: bufio.NewReader(rs), + seeker: rs, + } +} + +// Read reads data into p. It reads from the underlying buffered reader. +func (brs *BufferedReadSeeker) Read(p []byte) (n int, err error) { + return brs.reader.Read(p) +} + +// Seek sets the offset for the next Read. It is optimized to use the +// buffer for seeks that land within the buffered data range. +func (brs *BufferedReadSeeker) Seek(offset int64, whence int) (int64, error) { + // Determine the current position (where the next Read would start) + underlyingPos, err := brs.seeker.Seek(0, io.SeekCurrent) + if err != nil { + return 0, err + } + // The position of the stream as seen by clients + logicalPos := underlyingPos - int64(brs.reader.Buffered()) + + // 2. Calculate the absolute target position for the seek + var absTargetPos int64 + switch whence { + case io.SeekStart: + absTargetPos = offset + case io.SeekCurrent: + absTargetPos = logicalPos + offset + case io.SeekEnd: + // Seeking from the end requires a fallback, as we don't know the end + // position without invalidating the buffer's state relative to the seeker. + return brs.fallbackSeek(offset, whence) + default: + return 0, fmt.Errorf("invalid whence: %d", whence) + } + + // 3. Check if the target position is within the current buffer + if absTargetPos >= logicalPos && absTargetPos < underlyingPos { + // The target is within the buffer. Calculate how many bytes to discard. + bytesToDiscard := absTargetPos - logicalPos + _, err := brs.reader.Discard(int(bytesToDiscard)) + if err != nil { + // This is unlikely, but if Discard fails, fall back to a full seek + return brs.fallbackSeek(offset, whence) + } + return absTargetPos, nil + } + + // 4. If the target is outside the buffer, perform a fallback seek + return brs.fallbackSeek(absTargetPos, io.SeekStart) +} + +// fallbackSeek performs a seek on the underlying seeker and resets the buffer. +func (brs *BufferedReadSeeker) fallbackSeek(offset int64, whence int) (int64, error) { + newOffset, err := brs.seeker.Seek(offset, whence) + if err != nil { + return 0, err + } + brs.reader.Reset(brs.seeker) + return newOffset, nil +} + +// Run the callback function with a buffered reader that supports Seek() and +// Read(). Return an io.Reader that represents all content from the original +// io.Reader. +func CallbackWithSeekable(r io.Reader, callback func(io.Reader) error) (stream io.Reader, err error) { + switch s := r.(type) { + case io.ReadSeeker: + pos, err := s.Seek(0, io.SeekCurrent) + if err == nil { + defer func() { + _, serr := s.Seek(pos, io.SeekStart) + if err == nil { + err = serr + } + }() + // Add bufferring to s for efficiency + bs := s + switch r.(type) { + case *BufferedReadSeeker, *bytes.Reader: + default: + bs = NewBufferedReadSeeker(s) + } + err = callback(bs) + return s, err + } + case *bytes.Buffer: + err = callback(bytes.NewReader(s.Bytes())) + return s, err + } + rewindBuffer := &bytes.Buffer{} + tee := io.TeeReader(r, rewindBuffer) + err = callback(bufio.NewReader(tee)) + return io.MultiReader(rewindBuffer, r), err +} + +// Skip reading the specified number of bytes efficiently +func Skip(r io.Reader, amt int64) (err error) { + if s, ok := r.(io.Seeker); ok { + if _, serr := s.Seek(amt, io.SeekCurrent); serr == nil { + return + } + } + _, err = io.CopyN(io.Discard, r, amt) + return +} + +// Read a single byte from the reader +func ReadByte(r io.Reader) (ans byte, err error) { + var v [1]byte + _, err = io.ReadFull(r, v[:]) + ans = v[0] + return +} diff --git a/vendor/github.com/kovidgoyal/imaging/tools.go b/vendor/github.com/kovidgoyal/imaging/tools.go index 9591ebc5a..78d3a7736 100644 --- a/vendor/github.com/kovidgoyal/imaging/tools.go +++ b/vendor/github.com/kovidgoyal/imaging/tools.go @@ -30,12 +30,14 @@ func Clone(img image.Image) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, src.w, src.h)) size := src.w * 4 - parallel(0, src.h, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := y * dst.Stride - src.scan(0, y, src.w, y+1, dst.Pix[i:i+size]) + src.Scan(0, y, src.w, y+1, dst.Pix[i:i+size]) } - }) + }, 0, src.h); err != nil { + panic(err) + } return dst } @@ -103,12 +105,14 @@ func Crop(img image.Image, rect image.Rectangle) *image.NRGBA { src := newScanner(img) dst := image.NewNRGBA(image.Rect(0, 0, r.Dx(), r.Dy())) rowSize := r.Dx() * 4 - parallel(r.Min.Y, r.Max.Y, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { i := (y - r.Min.Y) * dst.Stride - src.scan(r.Min.X, y, r.Max.X, y+1, dst.Pix[i:i+rowSize]) + src.Scan(r.Min.X, y, r.Max.X, y+1, dst.Pix[i:i+rowSize]) } - }) + }, r.Min.Y, r.Max.Y); err != nil { + panic(err) + } return dst } @@ -142,17 +146,19 @@ func Paste(background, img image.Image, pos image.Point) *image.NRGBA { } src := newScanner(img) - parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) { - for y := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for y := start; y < limit; y++ { x1 := interRect.Min.X - pasteRect.Min.X x2 := interRect.Max.X - pasteRect.Min.X y1 := y - pasteRect.Min.Y y2 := y1 + 1 i1 := y*dst.Stride + interRect.Min.X*4 i2 := i1 + interRect.Dx()*4 - src.scan(x1, y1, x2, y2, dst.Pix[i1:i2]) + src.Scan(x1, y1, x2, y2, dst.Pix[i1:i2]) } - }) + }, interRect.Min.Y, interRect.Max.Y); err != nil { + panic(err) + } return dst } @@ -184,7 +190,6 @@ func PasteCenter(background, img image.Image) *image.NRGBA { // // // Blend two opaque images of the same size. // dstImage := imaging.Overlay(imageOne, imageTwo, image.Pt(0, 0), 0.5) -// func Overlay(background, img image.Image, pos image.Point, opacity float64) *image.NRGBA { opacity = math.Min(math.Max(opacity, 0.0), 1.0) // Ensure 0.0 <= opacity <= 1.0. dst := Clone(background) @@ -195,14 +200,14 @@ func Overlay(background, img image.Image, pos image.Point, opacity float64) *ima return dst } src := newScanner(img) - parallel(interRect.Min.Y, interRect.Max.Y, func(ys <-chan int) { + if err := run_in_parallel_over_range(0, func(start, limit int) { scanLine := make([]uint8, interRect.Dx()*4) - for y := range ys { + for y := start; y < limit; y++ { x1 := interRect.Min.X - pasteRect.Min.X x2 := interRect.Max.X - pasteRect.Min.X y1 := y - pasteRect.Min.Y y2 := y1 + 1 - src.scan(x1, y1, x2, y2, scanLine) + src.Scan(x1, y1, x2, y2, scanLine) i := y*dst.Stride + interRect.Min.X*4 j := 0 for x := interRect.Min.X; x < interRect.Max.X; x++ { @@ -233,7 +238,9 @@ func Overlay(background, img image.Image, pos image.Point, opacity float64) *ima j += 4 } } - }) + }, interRect.Min.Y, interRect.Max.Y); err != nil { + panic(err) + } return dst } diff --git a/vendor/github.com/kovidgoyal/imaging/transform.go b/vendor/github.com/kovidgoyal/imaging/transform.go index fe4a92f9d..f7253b1ad 100644 --- a/vendor/github.com/kovidgoyal/imaging/transform.go +++ b/vendor/github.com/kovidgoyal/imaging/transform.go @@ -13,14 +13,16 @@ func FlipH(img image.Image) *image.NRGBA { dstH := src.h rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcY := dstY - src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) + src.Scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) reverse(dst.Pix[i : i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -31,13 +33,15 @@ func FlipV(img image.Image) *image.NRGBA { dstH := src.h rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcY := dstH - dstY - 1 - src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) + src.Scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -48,13 +52,15 @@ func Transpose(img image.Image) *image.NRGBA { dstH := src.w rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcX := dstY - src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) + src.Scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -65,14 +71,16 @@ func Transverse(img image.Image) *image.NRGBA { dstH := src.w rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcX := dstH - dstY - 1 - src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) + src.Scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) reverse(dst.Pix[i : i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -83,13 +91,15 @@ func Rotate90(img image.Image) *image.NRGBA { dstH := src.w rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcX := dstH - dstY - 1 - src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) + src.Scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -100,14 +110,16 @@ func Rotate180(img image.Image) *image.NRGBA { dstH := src.h rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcY := dstH - dstY - 1 - src.scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) + src.Scan(0, srcY, src.w, srcY+1, dst.Pix[i:i+rowSize]) reverse(dst.Pix[i : i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -118,14 +130,16 @@ func Rotate270(img image.Image) *image.NRGBA { dstH := src.w rowSize := dstW * 4 dst := image.NewNRGBA(image.Rect(0, 0, dstW, dstH)) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { i := dstY * dst.Stride srcX := dstY - src.scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) + src.Scan(srcX, 0, srcX+1, src.h, dst.Pix[i:i+rowSize]) reverse(dst.Pix[i : i+rowSize]) } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -164,15 +178,17 @@ func Rotate(img image.Image, angle float64, bgColor color.Color) *image.NRGBA { bgColorNRGBA := color.NRGBAModel.Convert(bgColor).(color.NRGBA) sin, cos := math.Sincos(math.Pi * angle / 180) - parallel(0, dstH, func(ys <-chan int) { - for dstY := range ys { - for dstX := 0; dstX < dstW; dstX++ { + if err := run_in_parallel_over_range(0, func(start, limit int) { + for dstY := start; dstY < limit; dstY++ { + for dstX := range dstW { xf, yf := rotatePoint(float64(dstX)-dstXOff, float64(dstY)-dstYOff, sin, cos) xf, yf = xf+srcXOff, yf+srcYOff interpolatePoint(dst, dstX, dstY, src, xf, yf, bgColorNRGBA) } } - }) + }, 0, dstH); err != nil { + panic(err) + } return dst } @@ -239,7 +255,7 @@ func interpolatePoint(dst *image.NRGBA, dstX, dstY int, src *image.NRGBA, xf, yf } var r, g, b, a float64 - for i := 0; i < 4; i++ { + for i := range 4 { p := points[i] w := weights[i] if p.In(bounds) { diff --git a/vendor/github.com/kovidgoyal/imaging/utils.go b/vendor/github.com/kovidgoyal/imaging/utils.go index f650c05bc..a789a7894 100644 --- a/vendor/github.com/kovidgoyal/imaging/utils.go +++ b/vendor/github.com/kovidgoyal/imaging/utils.go @@ -4,49 +4,29 @@ import ( "image" "math" "runtime" - "sync" "sync/atomic" + + "github.com/kovidgoyal/go-parallel" ) -var maxProcs int64 +var max_procs atomic.Int64 // SetMaxProcs limits the number of concurrent processing goroutines to the given value. // A value <= 0 clears the limit. func SetMaxProcs(value int) { - atomic.StoreInt64(&maxProcs, int64(value)) + max_procs.Store(int64(value)) } -// parallel processes the data in separate goroutines. -func parallel(start, stop int, fn func(<-chan int)) { - count := stop - start - if count < 1 { - return +// Run the specified function in parallel over chunks from the specified range. +// If the function panics, it is turned into a regular error. +func run_in_parallel_over_range(num_procs int, f func(int, int), start, limit int) (err error) { + if num_procs <= 0 { + num_procs = runtime.GOMAXPROCS(0) + if mp := int(max_procs.Load()); mp > 0 { + num_procs = min(num_procs, mp) + } } - - procs := runtime.GOMAXPROCS(0) - limit := int(atomic.LoadInt64(&maxProcs)) - if procs > limit && limit > 0 { - procs = limit - } - if procs > count { - procs = count - } - - c := make(chan int, count) - for i := start; i < stop; i++ { - c <- i - } - close(c) - - var wg sync.WaitGroup - for i := 0; i < procs; i++ { - wg.Add(1) - go func() { - defer wg.Done() - fn(c) - }() - } - wg.Wait() + return parallel.Run_in_parallel_over_range(num_procs, f, start, limit) } // absint returns the absolute value of i. diff --git a/vendor/github.com/rwcarlsen/goexif/LICENSE b/vendor/github.com/rwcarlsen/goexif/LICENSE new file mode 100644 index 000000000..aa6250465 --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/LICENSE @@ -0,0 +1,24 @@ + +Copyright (c) 2012, Robert Carlsen & Contributors +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. + +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. diff --git a/vendor/github.com/rwcarlsen/goexif/exif/README.md b/vendor/github.com/rwcarlsen/goexif/exif/README.md new file mode 100644 index 000000000..b3bf5fa0e --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/exif/README.md @@ -0,0 +1,4 @@ + +To regenerate the regression test data, run `go generate` inside the exif +package directory and commit the changes to *regress_expected_test.go*. + diff --git a/vendor/github.com/rwcarlsen/goexif/exif/exif.go b/vendor/github.com/rwcarlsen/goexif/exif/exif.go new file mode 100644 index 000000000..03afe6507 --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/exif/exif.go @@ -0,0 +1,655 @@ +// Package exif implements decoding of EXIF data as defined in the EXIF 2.2 +// specification (http://www.exif.org/Exif2-2.PDF). +package exif + +import ( + "bufio" + "bytes" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "math" + "strconv" + "strings" + "time" + + "github.com/rwcarlsen/goexif/tiff" +) + +const ( + jpeg_APP1 = 0xE1 + + exifPointer = 0x8769 + gpsPointer = 0x8825 + interopPointer = 0xA005 +) + +// A decodeError is returned when the image cannot be decoded as a tiff image. +type decodeError struct { + cause error +} + +func (de decodeError) Error() string { + return fmt.Sprintf("exif: decode failed (%v) ", de.cause.Error()) +} + +// IsShortReadTagValueError identifies a ErrShortReadTagValue error. +func IsShortReadTagValueError(err error) bool { + de, ok := err.(decodeError) + if ok { + return de.cause == tiff.ErrShortReadTagValue + } + return false +} + +// A TagNotPresentError is returned when the requested field is not +// present in the EXIF. +type TagNotPresentError FieldName + +func (tag TagNotPresentError) Error() string { + return fmt.Sprintf("exif: tag %q is not present", string(tag)) +} + +func IsTagNotPresentError(err error) bool { + _, ok := err.(TagNotPresentError) + return ok +} + +// Parser allows the registration of custom parsing and field loading +// in the Decode function. +type Parser interface { + // Parse should read data from x and insert parsed fields into x via + // LoadTags. + Parse(x *Exif) error +} + +var parsers []Parser + +func init() { + RegisterParsers(&parser{}) +} + +// RegisterParsers registers one or more parsers to be automatically called +// when decoding EXIF data via the Decode function. +func RegisterParsers(ps ...Parser) { + parsers = append(parsers, ps...) +} + +type parser struct{} + +type tiffErrors map[tiffError]string + +func (te tiffErrors) Error() string { + var allErrors []string + for k, v := range te { + allErrors = append(allErrors, fmt.Sprintf("%s: %v\n", stagePrefix[k], v)) + } + return strings.Join(allErrors, "\n") +} + +// IsCriticalError, given the error returned by Decode, reports whether the +// returned *Exif may contain usable information. +func IsCriticalError(err error) bool { + _, ok := err.(tiffErrors) + return !ok +} + +// IsExifError reports whether the error happened while decoding the EXIF +// sub-IFD. +func IsExifError(err error) bool { + if te, ok := err.(tiffErrors); ok { + _, isExif := te[loadExif] + return isExif + } + return false +} + +// IsGPSError reports whether the error happened while decoding the GPS sub-IFD. +func IsGPSError(err error) bool { + if te, ok := err.(tiffErrors); ok { + _, isGPS := te[loadExif] + return isGPS + } + return false +} + +// IsInteroperabilityError reports whether the error happened while decoding the +// Interoperability sub-IFD. +func IsInteroperabilityError(err error) bool { + if te, ok := err.(tiffErrors); ok { + _, isInterop := te[loadInteroperability] + return isInterop + } + return false +} + +type tiffError int + +const ( + loadExif tiffError = iota + loadGPS + loadInteroperability +) + +var stagePrefix = map[tiffError]string{ + loadExif: "loading EXIF sub-IFD", + loadGPS: "loading GPS sub-IFD", + loadInteroperability: "loading Interoperability sub-IFD", +} + +// Parse reads data from the tiff data in x and populates the tags +// in x. If parsing a sub-IFD fails, the error is recorded and +// parsing continues with the remaining sub-IFDs. +func (p *parser) Parse(x *Exif) error { + if len(x.Tiff.Dirs) == 0 { + return errors.New("Invalid exif data") + } + x.LoadTags(x.Tiff.Dirs[0], exifFields, false) + + // thumbnails + if len(x.Tiff.Dirs) >= 2 { + x.LoadTags(x.Tiff.Dirs[1], thumbnailFields, false) + } + + te := make(tiffErrors) + + // recurse into exif, gps, and interop sub-IFDs + if err := loadSubDir(x, ExifIFDPointer, exifFields); err != nil { + te[loadExif] = err.Error() + } + if err := loadSubDir(x, GPSInfoIFDPointer, gpsFields); err != nil { + te[loadGPS] = err.Error() + } + + if err := loadSubDir(x, InteroperabilityIFDPointer, interopFields); err != nil { + te[loadInteroperability] = err.Error() + } + if len(te) > 0 { + return te + } + return nil +} + +func loadSubDir(x *Exif, ptr FieldName, fieldMap map[uint16]FieldName) error { + r := bytes.NewReader(x.Raw) + + tag, err := x.Get(ptr) + if err != nil { + return nil + } + offset, err := tag.Int64(0) + if err != nil { + return nil + } + + _, err = r.Seek(offset, 0) + if err != nil { + return fmt.Errorf("exif: seek to sub-IFD %s failed: %v", ptr, err) + } + subDir, _, err := tiff.DecodeDir(r, x.Tiff.Order) + if err != nil { + return fmt.Errorf("exif: sub-IFD %s decode failed: %v", ptr, err) + } + x.LoadTags(subDir, fieldMap, false) + return nil +} + +// Exif provides access to decoded EXIF metadata fields and values. +type Exif struct { + Tiff *tiff.Tiff + main map[FieldName]*tiff.Tag + Raw []byte +} + +// Decode parses EXIF data from r (a TIFF, JPEG, or raw EXIF block) +// and returns a queryable Exif object. After the EXIF data section is +// called and the TIFF structure is decoded, each registered parser is +// called (in order of registration). If one parser returns an error, +// decoding terminates and the remaining parsers are not called. +// +// The error can be inspected with functions such as IsCriticalError +// to determine whether the returned object might still be usable. +func Decode(r io.Reader) (*Exif, error) { + + // EXIF data in JPEG is stored in the APP1 marker. EXIF data uses the TIFF + // format to store data. + // If we're parsing a TIFF image, we don't need to strip away any data. + // If we're parsing a JPEG image, we need to strip away the JPEG APP1 + // marker and also the EXIF header. + + header := make([]byte, 4) + n, err := io.ReadFull(r, header) + if err != nil { + return nil, fmt.Errorf("exif: error reading 4 byte header, got %d, %v", n, err) + } + + var isTiff bool + var isRawExif bool + var assumeJPEG bool + switch string(header) { + case "II*\x00": + // TIFF - Little endian (Intel) + isTiff = true + case "MM\x00*": + // TIFF - Big endian (Motorola) + isTiff = true + case "Exif": + isRawExif = true + default: + // Not TIFF, assume JPEG + assumeJPEG = true + } + + // Put the header bytes back into the reader. + r = io.MultiReader(bytes.NewReader(header), r) + var ( + er *bytes.Reader + tif *tiff.Tiff + sec *appSec + ) + + switch { + case isRawExif: + var header [6]byte + if _, err := io.ReadFull(r, header[:]); err != nil { + return nil, fmt.Errorf("exif: unexpected raw exif header read error") + } + if got, want := string(header[:]), "Exif\x00\x00"; got != want { + return nil, fmt.Errorf("exif: unexpected raw exif header; got %q, want %q", got, want) + } + fallthrough + case isTiff: + // Functions below need the IFDs from the TIFF data to be stored in a + // *bytes.Reader. We use TeeReader to get a copy of the bytes as a + // side-effect of tiff.Decode() doing its work. + b := &bytes.Buffer{} + tr := io.TeeReader(r, b) + tif, err = tiff.Decode(tr) + er = bytes.NewReader(b.Bytes()) + case assumeJPEG: + // Locate the JPEG APP1 header. + sec, err = newAppSec(jpeg_APP1, r) + if err != nil { + return nil, err + } + // Strip away EXIF header. + er, err = sec.exifReader() + if err != nil { + return nil, err + } + tif, err = tiff.Decode(er) + } + + if err != nil { + return nil, decodeError{cause: err} + } + + er.Seek(0, 0) + raw, err := ioutil.ReadAll(er) + if err != nil { + return nil, decodeError{cause: err} + } + + // build an exif structure from the tiff + x := &Exif{ + main: map[FieldName]*tiff.Tag{}, + Tiff: tif, + Raw: raw, + } + + for i, p := range parsers { + if err := p.Parse(x); err != nil { + if _, ok := err.(tiffErrors); ok { + return x, err + } + // This should never happen, as Parse always returns a tiffError + // for now, but that could change. + return x, fmt.Errorf("exif: parser %v failed (%v)", i, err) + } + } + + return x, nil +} + +// LoadTags loads tags into the available fields from the tiff Directory +// using the given tagid-fieldname mapping. Used to load makernote and +// other meta-data. If showMissing is true, tags in d that are not in the +// fieldMap will be loaded with the FieldName UnknownPrefix followed by the +// tag ID (in hex format). +func (x *Exif) LoadTags(d *tiff.Dir, fieldMap map[uint16]FieldName, showMissing bool) { + for _, tag := range d.Tags { + name := fieldMap[tag.Id] + if name == "" { + if !showMissing { + continue + } + name = FieldName(fmt.Sprintf("%v%x", UnknownPrefix, tag.Id)) + } + x.main[name] = tag + } +} + +// Get retrieves the EXIF tag for the given field name. +// +// If the tag is not known or not present, an error is returned. If the +// tag name is known, the error will be a TagNotPresentError. +func (x *Exif) Get(name FieldName) (*tiff.Tag, error) { + if tg, ok := x.main[name]; ok { + return tg, nil + } + return nil, TagNotPresentError(name) +} + +// Walker is the interface used to traverse all fields of an Exif object. +type Walker interface { + // Walk is called for each non-nil EXIF field. Returning a non-nil + // error aborts the walk/traversal. + Walk(name FieldName, tag *tiff.Tag) error +} + +// Walk calls the Walk method of w with the name and tag for every non-nil +// EXIF field. If w aborts the walk with an error, that error is returned. +func (x *Exif) Walk(w Walker) error { + for name, tag := range x.main { + if err := w.Walk(name, tag); err != nil { + return err + } + } + return nil +} + +// DateTime returns the EXIF's "DateTimeOriginal" field, which +// is the creation time of the photo. If not found, it tries +// the "DateTime" (which is meant as the modtime) instead. +// The error will be TagNotPresentErr if none of those tags +// were found, or a generic error if the tag value was +// not a string, or the error returned by time.Parse. +// +// If the EXIF lacks timezone information or GPS time, the returned +// time's Location will be time.Local. +func (x *Exif) DateTime() (time.Time, error) { + var dt time.Time + tag, err := x.Get(DateTimeOriginal) + if err != nil { + tag, err = x.Get(DateTime) + if err != nil { + return dt, err + } + } + if tag.Format() != tiff.StringVal { + return dt, errors.New("DateTime[Original] not in string format") + } + exifTimeLayout := "2006:01:02 15:04:05" + dateStr := strings.TrimRight(string(tag.Val), "\x00") + // TODO(bradfitz,mpl): look for timezone offset, GPS time, etc. + timeZone := time.Local + if tz, _ := x.TimeZone(); tz != nil { + timeZone = tz + } + return time.ParseInLocation(exifTimeLayout, dateStr, timeZone) +} + +func (x *Exif) TimeZone() (*time.Location, error) { + // TODO: parse more timezone fields (e.g. Nikon WorldTime). + timeInfo, err := x.Get("Canon.TimeInfo") + if err != nil { + return nil, err + } + if timeInfo.Count < 2 { + return nil, errors.New("Canon.TimeInfo does not contain timezone") + } + offsetMinutes, err := timeInfo.Int(1) + if err != nil { + return nil, err + } + return time.FixedZone("", offsetMinutes*60), nil +} + +func ratFloat(num, dem int64) float64 { + return float64(num) / float64(dem) +} + +// Tries to parse a Geo degrees value from a string as it was found in some +// EXIF data. +// Supported formats so far: +// - "52,00000,50,00000,34,01180" ==> 52 deg 50'34.0118" +// Probably due to locale the comma is used as decimal mark as well as the +// separator of three floats (degrees, minutes, seconds) +// http://en.wikipedia.org/wiki/Decimal_mark#Hindu.E2.80.93Arabic_numeral_system +// - "52.0,50.0,34.01180" ==> 52deg50'34.0118" +// - "52,50,34.01180" ==> 52deg50'34.0118" +func parseTagDegreesString(s string) (float64, error) { + const unparsableErrorFmt = "Unknown coordinate format: %s" + isSplitRune := func(c rune) bool { + return c == ',' || c == ';' + } + parts := strings.FieldsFunc(s, isSplitRune) + var degrees, minutes, seconds float64 + var err error + switch len(parts) { + case 6: + degrees, err = strconv.ParseFloat(parts[0]+"."+parts[1], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + minutes, err = strconv.ParseFloat(parts[2]+"."+parts[3], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + minutes = math.Copysign(minutes, degrees) + seconds, err = strconv.ParseFloat(parts[4]+"."+parts[5], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + seconds = math.Copysign(seconds, degrees) + case 3: + degrees, err = strconv.ParseFloat(parts[0], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + minutes, err = strconv.ParseFloat(parts[1], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + minutes = math.Copysign(minutes, degrees) + seconds, err = strconv.ParseFloat(parts[2], 64) + if err != nil { + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + seconds = math.Copysign(seconds, degrees) + default: + return 0.0, fmt.Errorf(unparsableErrorFmt, s) + } + return degrees + minutes/60.0 + seconds/3600.0, nil +} + +func parse3Rat2(tag *tiff.Tag) ([3]float64, error) { + v := [3]float64{} + for i := range v { + num, den, err := tag.Rat2(i) + if err != nil { + return v, err + } + v[i] = ratFloat(num, den) + if tag.Count < uint32(i+2) { + break + } + } + return v, nil +} + +func tagDegrees(tag *tiff.Tag) (float64, error) { + switch tag.Format() { + case tiff.RatVal: + // The usual case, according to the Exif spec + // (http://www.kodak.com/global/plugins/acrobat/en/service/digCam/exifStandard2.pdf, + // sec 4.6.6, p. 52 et seq.) + v, err := parse3Rat2(tag) + if err != nil { + return 0.0, err + } + return v[0] + v[1]/60 + v[2]/3600.0, nil + case tiff.StringVal: + // Encountered this weird case with a panorama picture taken with a HTC phone + s, err := tag.StringVal() + if err != nil { + return 0.0, err + } + return parseTagDegreesString(s) + default: + // don't know how to parse value, give up + return 0.0, fmt.Errorf("Malformed EXIF Tag Degrees") + } +} + +// LatLong returns the latitude and longitude of the photo and +// whether it was present. +func (x *Exif) LatLong() (lat, long float64, err error) { + // All calls of x.Get might return an TagNotPresentError + longTag, err := x.Get(FieldName("GPSLongitude")) + if err != nil { + return + } + ewTag, err := x.Get(FieldName("GPSLongitudeRef")) + if err != nil { + return + } + latTag, err := x.Get(FieldName("GPSLatitude")) + if err != nil { + return + } + nsTag, err := x.Get(FieldName("GPSLatitudeRef")) + if err != nil { + return + } + if long, err = tagDegrees(longTag); err != nil { + return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err) + } + if lat, err = tagDegrees(latTag); err != nil { + return 0, 0, fmt.Errorf("Cannot parse latitude: %v", err) + } + ew, err := ewTag.StringVal() + if err == nil && ew == "W" { + long *= -1.0 + } else if err != nil { + return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err) + } + ns, err := nsTag.StringVal() + if err == nil && ns == "S" { + lat *= -1.0 + } else if err != nil { + return 0, 0, fmt.Errorf("Cannot parse longitude: %v", err) + } + return lat, long, nil +} + +// String returns a pretty text representation of the decoded exif data. +func (x *Exif) String() string { + var buf bytes.Buffer + for name, tag := range x.main { + fmt.Fprintf(&buf, "%s: %s\n", name, tag) + } + return buf.String() +} + +// JpegThumbnail returns the jpeg thumbnail if it exists. If it doesn't exist, +// TagNotPresentError will be returned +func (x *Exif) JpegThumbnail() ([]byte, error) { + offset, err := x.Get(ThumbJPEGInterchangeFormat) + if err != nil { + return nil, err + } + start, err := offset.Int(0) + if err != nil { + return nil, err + } + + length, err := x.Get(ThumbJPEGInterchangeFormatLength) + if err != nil { + return nil, err + } + l, err := length.Int(0) + if err != nil { + return nil, err + } + + return x.Raw[start : start+l], nil +} + +// MarshalJson implements the encoding/json.Marshaler interface providing output of +// all EXIF fields present (names and values). +func (x Exif) MarshalJSON() ([]byte, error) { + return json.Marshal(x.main) +} + +type appSec struct { + marker byte + data []byte +} + +// newAppSec finds marker in r and returns the corresponding application data +// section. +func newAppSec(marker byte, r io.Reader) (*appSec, error) { + br := bufio.NewReader(r) + app := &appSec{marker: marker} + var dataLen int + + // seek to marker + for dataLen == 0 { + if _, err := br.ReadBytes(0xFF); err != nil { + return nil, err + } + c, err := br.ReadByte() + if err != nil { + return nil, err + } else if c != marker { + continue + } + + dataLenBytes := make([]byte, 2) + for k, _ := range dataLenBytes { + c, err := br.ReadByte() + if err != nil { + return nil, err + } + dataLenBytes[k] = c + } + dataLen = int(binary.BigEndian.Uint16(dataLenBytes)) - 2 + } + + // read section data + nread := 0 + for nread < dataLen { + s := make([]byte, dataLen-nread) + n, err := br.Read(s) + nread += n + if err != nil && nread < dataLen { + return nil, err + } + app.data = append(app.data, s[:n]...) + } + return app, nil +} + +// reader returns a reader on this appSec. +func (app *appSec) reader() *bytes.Reader { + return bytes.NewReader(app.data) +} + +// exifReader returns a reader on this appSec with the read cursor advanced to +// the start of the exif's tiff encoded portion. +func (app *appSec) exifReader() (*bytes.Reader, error) { + if len(app.data) < 6 { + return nil, errors.New("exif: failed to find exif intro marker") + } + + // read/check for exif special mark + exif := app.data[:6] + if !bytes.Equal(exif, append([]byte("Exif"), 0x00, 0x00)) { + return nil, errors.New("exif: failed to find exif intro marker") + } + return bytes.NewReader(app.data[6:]), nil +} diff --git a/vendor/github.com/rwcarlsen/goexif/exif/fields.go b/vendor/github.com/rwcarlsen/goexif/exif/fields.go new file mode 100644 index 000000000..8b8ae0f2d --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/exif/fields.go @@ -0,0 +1,309 @@ +package exif + +type FieldName string + +// UnknownPrefix is used as the first part of field names for decoded tags for +// which there is no known/supported EXIF field. +const UnknownPrefix = "UnknownTag_" + +// Primary EXIF fields +const ( + ImageWidth FieldName = "ImageWidth" + ImageLength FieldName = "ImageLength" // Image height called Length by EXIF spec + BitsPerSample FieldName = "BitsPerSample" + Compression FieldName = "Compression" + PhotometricInterpretation FieldName = "PhotometricInterpretation" + Orientation FieldName = "Orientation" + SamplesPerPixel FieldName = "SamplesPerPixel" + PlanarConfiguration FieldName = "PlanarConfiguration" + YCbCrSubSampling FieldName = "YCbCrSubSampling" + YCbCrPositioning FieldName = "YCbCrPositioning" + XResolution FieldName = "XResolution" + YResolution FieldName = "YResolution" + ResolutionUnit FieldName = "ResolutionUnit" + DateTime FieldName = "DateTime" + ImageDescription FieldName = "ImageDescription" + Make FieldName = "Make" + Model FieldName = "Model" + Software FieldName = "Software" + Artist FieldName = "Artist" + Copyright FieldName = "Copyright" + ExifIFDPointer FieldName = "ExifIFDPointer" + GPSInfoIFDPointer FieldName = "GPSInfoIFDPointer" + InteroperabilityIFDPointer FieldName = "InteroperabilityIFDPointer" + ExifVersion FieldName = "ExifVersion" + FlashpixVersion FieldName = "FlashpixVersion" + ColorSpace FieldName = "ColorSpace" + ComponentsConfiguration FieldName = "ComponentsConfiguration" + CompressedBitsPerPixel FieldName = "CompressedBitsPerPixel" + PixelXDimension FieldName = "PixelXDimension" + PixelYDimension FieldName = "PixelYDimension" + MakerNote FieldName = "MakerNote" + UserComment FieldName = "UserComment" + RelatedSoundFile FieldName = "RelatedSoundFile" + DateTimeOriginal FieldName = "DateTimeOriginal" + DateTimeDigitized FieldName = "DateTimeDigitized" + SubSecTime FieldName = "SubSecTime" + SubSecTimeOriginal FieldName = "SubSecTimeOriginal" + SubSecTimeDigitized FieldName = "SubSecTimeDigitized" + ImageUniqueID FieldName = "ImageUniqueID" + ExposureTime FieldName = "ExposureTime" + FNumber FieldName = "FNumber" + ExposureProgram FieldName = "ExposureProgram" + SpectralSensitivity FieldName = "SpectralSensitivity" + ISOSpeedRatings FieldName = "ISOSpeedRatings" + OECF FieldName = "OECF" + ShutterSpeedValue FieldName = "ShutterSpeedValue" + ApertureValue FieldName = "ApertureValue" + BrightnessValue FieldName = "BrightnessValue" + ExposureBiasValue FieldName = "ExposureBiasValue" + MaxApertureValue FieldName = "MaxApertureValue" + SubjectDistance FieldName = "SubjectDistance" + MeteringMode FieldName = "MeteringMode" + LightSource FieldName = "LightSource" + Flash FieldName = "Flash" + FocalLength FieldName = "FocalLength" + SubjectArea FieldName = "SubjectArea" + FlashEnergy FieldName = "FlashEnergy" + SpatialFrequencyResponse FieldName = "SpatialFrequencyResponse" + FocalPlaneXResolution FieldName = "FocalPlaneXResolution" + FocalPlaneYResolution FieldName = "FocalPlaneYResolution" + FocalPlaneResolutionUnit FieldName = "FocalPlaneResolutionUnit" + SubjectLocation FieldName = "SubjectLocation" + ExposureIndex FieldName = "ExposureIndex" + SensingMethod FieldName = "SensingMethod" + FileSource FieldName = "FileSource" + SceneType FieldName = "SceneType" + CFAPattern FieldName = "CFAPattern" + CustomRendered FieldName = "CustomRendered" + ExposureMode FieldName = "ExposureMode" + WhiteBalance FieldName = "WhiteBalance" + DigitalZoomRatio FieldName = "DigitalZoomRatio" + FocalLengthIn35mmFilm FieldName = "FocalLengthIn35mmFilm" + SceneCaptureType FieldName = "SceneCaptureType" + GainControl FieldName = "GainControl" + Contrast FieldName = "Contrast" + Saturation FieldName = "Saturation" + Sharpness FieldName = "Sharpness" + DeviceSettingDescription FieldName = "DeviceSettingDescription" + SubjectDistanceRange FieldName = "SubjectDistanceRange" + LensMake FieldName = "LensMake" + LensModel FieldName = "LensModel" +) + +// Windows-specific tags +const ( + XPTitle FieldName = "XPTitle" + XPComment FieldName = "XPComment" + XPAuthor FieldName = "XPAuthor" + XPKeywords FieldName = "XPKeywords" + XPSubject FieldName = "XPSubject" +) + +// thumbnail fields +const ( + ThumbJPEGInterchangeFormat FieldName = "ThumbJPEGInterchangeFormat" // offset to thumb jpeg SOI + ThumbJPEGInterchangeFormatLength FieldName = "ThumbJPEGInterchangeFormatLength" // byte length of thumb +) + +// GPS fields +const ( + GPSVersionID FieldName = "GPSVersionID" + GPSLatitudeRef FieldName = "GPSLatitudeRef" + GPSLatitude FieldName = "GPSLatitude" + GPSLongitudeRef FieldName = "GPSLongitudeRef" + GPSLongitude FieldName = "GPSLongitude" + GPSAltitudeRef FieldName = "GPSAltitudeRef" + GPSAltitude FieldName = "GPSAltitude" + GPSTimeStamp FieldName = "GPSTimeStamp" + GPSSatelites FieldName = "GPSSatelites" + GPSStatus FieldName = "GPSStatus" + GPSMeasureMode FieldName = "GPSMeasureMode" + GPSDOP FieldName = "GPSDOP" + GPSSpeedRef FieldName = "GPSSpeedRef" + GPSSpeed FieldName = "GPSSpeed" + GPSTrackRef FieldName = "GPSTrackRef" + GPSTrack FieldName = "GPSTrack" + GPSImgDirectionRef FieldName = "GPSImgDirectionRef" + GPSImgDirection FieldName = "GPSImgDirection" + GPSMapDatum FieldName = "GPSMapDatum" + GPSDestLatitudeRef FieldName = "GPSDestLatitudeRef" + GPSDestLatitude FieldName = "GPSDestLatitude" + GPSDestLongitudeRef FieldName = "GPSDestLongitudeRef" + GPSDestLongitude FieldName = "GPSDestLongitude" + GPSDestBearingRef FieldName = "GPSDestBearingRef" + GPSDestBearing FieldName = "GPSDestBearing" + GPSDestDistanceRef FieldName = "GPSDestDistanceRef" + GPSDestDistance FieldName = "GPSDestDistance" + GPSProcessingMethod FieldName = "GPSProcessingMethod" + GPSAreaInformation FieldName = "GPSAreaInformation" + GPSDateStamp FieldName = "GPSDateStamp" + GPSDifferential FieldName = "GPSDifferential" +) + +// interoperability fields +const ( + InteroperabilityIndex FieldName = "InteroperabilityIndex" +) + +var exifFields = map[uint16]FieldName{ + ///////////////////////////////////// + ////////// IFD 0 //////////////////// + ///////////////////////////////////// + + // image data structure for the thumbnail + 0x0100: ImageWidth, + 0x0101: ImageLength, + 0x0102: BitsPerSample, + 0x0103: Compression, + 0x0106: PhotometricInterpretation, + 0x0112: Orientation, + 0x0115: SamplesPerPixel, + 0x011C: PlanarConfiguration, + 0x0212: YCbCrSubSampling, + 0x0213: YCbCrPositioning, + 0x011A: XResolution, + 0x011B: YResolution, + 0x0128: ResolutionUnit, + + // Other tags + 0x0132: DateTime, + 0x010E: ImageDescription, + 0x010F: Make, + 0x0110: Model, + 0x0131: Software, + 0x013B: Artist, + 0x8298: Copyright, + + // Windows-specific tags + 0x9c9b: XPTitle, + 0x9c9c: XPComment, + 0x9c9d: XPAuthor, + 0x9c9e: XPKeywords, + 0x9c9f: XPSubject, + + // private tags + exifPointer: ExifIFDPointer, + + ///////////////////////////////////// + ////////// Exif sub IFD ///////////// + ///////////////////////////////////// + + gpsPointer: GPSInfoIFDPointer, + interopPointer: InteroperabilityIFDPointer, + + 0x9000: ExifVersion, + 0xA000: FlashpixVersion, + + 0xA001: ColorSpace, + + 0x9101: ComponentsConfiguration, + 0x9102: CompressedBitsPerPixel, + 0xA002: PixelXDimension, + 0xA003: PixelYDimension, + + 0x927C: MakerNote, + 0x9286: UserComment, + + 0xA004: RelatedSoundFile, + 0x9003: DateTimeOriginal, + 0x9004: DateTimeDigitized, + 0x9290: SubSecTime, + 0x9291: SubSecTimeOriginal, + 0x9292: SubSecTimeDigitized, + + 0xA420: ImageUniqueID, + + // picture conditions + 0x829A: ExposureTime, + 0x829D: FNumber, + 0x8822: ExposureProgram, + 0x8824: SpectralSensitivity, + 0x8827: ISOSpeedRatings, + 0x8828: OECF, + 0x9201: ShutterSpeedValue, + 0x9202: ApertureValue, + 0x9203: BrightnessValue, + 0x9204: ExposureBiasValue, + 0x9205: MaxApertureValue, + 0x9206: SubjectDistance, + 0x9207: MeteringMode, + 0x9208: LightSource, + 0x9209: Flash, + 0x920A: FocalLength, + 0x9214: SubjectArea, + 0xA20B: FlashEnergy, + 0xA20C: SpatialFrequencyResponse, + 0xA20E: FocalPlaneXResolution, + 0xA20F: FocalPlaneYResolution, + 0xA210: FocalPlaneResolutionUnit, + 0xA214: SubjectLocation, + 0xA215: ExposureIndex, + 0xA217: SensingMethod, + 0xA300: FileSource, + 0xA301: SceneType, + 0xA302: CFAPattern, + 0xA401: CustomRendered, + 0xA402: ExposureMode, + 0xA403: WhiteBalance, + 0xA404: DigitalZoomRatio, + 0xA405: FocalLengthIn35mmFilm, + 0xA406: SceneCaptureType, + 0xA407: GainControl, + 0xA408: Contrast, + 0xA409: Saturation, + 0xA40A: Sharpness, + 0xA40B: DeviceSettingDescription, + 0xA40C: SubjectDistanceRange, + 0xA433: LensMake, + 0xA434: LensModel, +} + +var gpsFields = map[uint16]FieldName{ + ///////////////////////////////////// + //// GPS sub-IFD //////////////////// + ///////////////////////////////////// + 0x0: GPSVersionID, + 0x1: GPSLatitudeRef, + 0x2: GPSLatitude, + 0x3: GPSLongitudeRef, + 0x4: GPSLongitude, + 0x5: GPSAltitudeRef, + 0x6: GPSAltitude, + 0x7: GPSTimeStamp, + 0x8: GPSSatelites, + 0x9: GPSStatus, + 0xA: GPSMeasureMode, + 0xB: GPSDOP, + 0xC: GPSSpeedRef, + 0xD: GPSSpeed, + 0xE: GPSTrackRef, + 0xF: GPSTrack, + 0x10: GPSImgDirectionRef, + 0x11: GPSImgDirection, + 0x12: GPSMapDatum, + 0x13: GPSDestLatitudeRef, + 0x14: GPSDestLatitude, + 0x15: GPSDestLongitudeRef, + 0x16: GPSDestLongitude, + 0x17: GPSDestBearingRef, + 0x18: GPSDestBearing, + 0x19: GPSDestDistanceRef, + 0x1A: GPSDestDistance, + 0x1B: GPSProcessingMethod, + 0x1C: GPSAreaInformation, + 0x1D: GPSDateStamp, + 0x1E: GPSDifferential, +} + +var interopFields = map[uint16]FieldName{ + ///////////////////////////////////// + //// Interoperability sub-IFD /////// + ///////////////////////////////////// + 0x1: InteroperabilityIndex, +} + +var thumbnailFields = map[uint16]FieldName{ + 0x0201: ThumbJPEGInterchangeFormat, + 0x0202: ThumbJPEGInterchangeFormatLength, +} diff --git a/vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg b/vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg new file mode 100644 index 000000000..87bcf8e33 Binary files /dev/null and b/vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg differ diff --git a/vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif b/vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif new file mode 100644 index 000000000..fe51399c5 Binary files /dev/null and b/vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif differ diff --git a/vendor/github.com/rwcarlsen/goexif/tiff/tag.go b/vendor/github.com/rwcarlsen/goexif/tiff/tag.go new file mode 100644 index 000000000..b9ce791f5 --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/tiff/tag.go @@ -0,0 +1,445 @@ +package tiff + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "math/big" + "strings" + "unicode" + "unicode/utf8" +) + +// Format specifies the Go type equivalent used to represent the basic +// tiff data types. +type Format int + +const ( + IntVal Format = iota + FloatVal + RatVal + StringVal + UndefVal + OtherVal +) + +var ErrShortReadTagValue = errors.New("tiff: short read of tag value") + +var formatNames = map[Format]string{ + IntVal: "int", + FloatVal: "float", + RatVal: "rational", + StringVal: "string", + UndefVal: "undefined", + OtherVal: "other", +} + +// DataType represents the basic tiff tag data types. +type DataType uint16 + +const ( + DTByte DataType = 1 + DTAscii DataType = 2 + DTShort DataType = 3 + DTLong DataType = 4 + DTRational DataType = 5 + DTSByte DataType = 6 + DTUndefined DataType = 7 + DTSShort DataType = 8 + DTSLong DataType = 9 + DTSRational DataType = 10 + DTFloat DataType = 11 + DTDouble DataType = 12 +) + +var typeNames = map[DataType]string{ + DTByte: "byte", + DTAscii: "ascii", + DTShort: "short", + DTLong: "long", + DTRational: "rational", + DTSByte: "signed byte", + DTUndefined: "undefined", + DTSShort: "signed short", + DTSLong: "signed long", + DTSRational: "signed rational", + DTFloat: "float", + DTDouble: "double", +} + +// typeSize specifies the size in bytes of each type. +var typeSize = map[DataType]uint32{ + DTByte: 1, + DTAscii: 1, + DTShort: 2, + DTLong: 4, + DTRational: 8, + DTSByte: 1, + DTUndefined: 1, + DTSShort: 2, + DTSLong: 4, + DTSRational: 8, + DTFloat: 4, + DTDouble: 8, +} + +// Tag reflects the parsed content of a tiff IFD tag. +type Tag struct { + // Id is the 2-byte tiff tag identifier. + Id uint16 + // Type is an integer (1 through 12) indicating the tag value's data type. + Type DataType + // Count is the number of type Type stored in the tag's value (i.e. the + // tag's value is an array of type Type and length Count). + Count uint32 + // Val holds the bytes that represent the tag's value. + Val []byte + // ValOffset holds byte offset of the tag value w.r.t. the beginning of the + // reader it was decoded from. Zero if the tag value fit inside the offset + // field. + ValOffset uint32 + + order binary.ByteOrder + intVals []int64 + floatVals []float64 + ratVals [][]int64 + strVal string + format Format +} + +// DecodeTag parses a tiff-encoded IFD tag from r and returns a Tag object. The +// first read from r should be the first byte of the tag. ReadAt offsets should +// generally be relative to the beginning of the tiff structure (not relative +// to the beginning of the tag). +func DecodeTag(r ReadAtReader, order binary.ByteOrder) (*Tag, error) { + t := new(Tag) + t.order = order + + err := binary.Read(r, order, &t.Id) + if err != nil { + return nil, errors.New("tiff: tag id read failed: " + err.Error()) + } + + err = binary.Read(r, order, &t.Type) + if err != nil { + return nil, errors.New("tiff: tag type read failed: " + err.Error()) + } + + err = binary.Read(r, order, &t.Count) + if err != nil { + return nil, errors.New("tiff: tag component count read failed: " + err.Error()) + } + + // There seems to be a relatively common corrupt tag which has a Count of + // MaxUint32. This is probably not a valid value, so return early. + if t.Count == 1<<32-1 { + return t, errors.New("invalid Count offset in tag") + } + + valLen := typeSize[t.Type] * t.Count + if valLen == 0 { + return t, errors.New("zero length tag value") + } + + if valLen > 4 { + binary.Read(r, order, &t.ValOffset) + + // Use a bytes.Buffer so we don't allocate a huge slice if the tag + // is corrupt. + var buff bytes.Buffer + sr := io.NewSectionReader(r, int64(t.ValOffset), int64(valLen)) + n, err := io.Copy(&buff, sr) + if err != nil { + return t, errors.New("tiff: tag value read failed: " + err.Error()) + } else if n != int64(valLen) { + return t, ErrShortReadTagValue + } + t.Val = buff.Bytes() + + } else { + val := make([]byte, valLen) + if _, err = io.ReadFull(r, val); err != nil { + return t, errors.New("tiff: tag offset read failed: " + err.Error()) + } + // ignore padding. + if _, err = io.ReadFull(r, make([]byte, 4-valLen)); err != nil { + return t, errors.New("tiff: tag offset read failed: " + err.Error()) + } + + t.Val = val + } + + return t, t.convertVals() +} + +func (t *Tag) convertVals() error { + r := bytes.NewReader(t.Val) + + switch t.Type { + case DTAscii: + if len(t.Val) <= 0 { + break + } + nullPos := bytes.IndexByte(t.Val, 0) + if nullPos == -1 { + t.strVal = string(t.Val) + } else { + // ignore all trailing NULL bytes, in case of a broken t.Count + t.strVal = string(t.Val[:nullPos]) + } + case DTByte: + var v uint8 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTShort: + var v uint16 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTLong: + var v uint32 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTSByte: + var v int8 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTSShort: + var v int16 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTSLong: + var v int32 + t.intVals = make([]int64, int(t.Count)) + for i := range t.intVals { + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.intVals[i] = int64(v) + } + case DTRational: + t.ratVals = make([][]int64, int(t.Count)) + for i := range t.ratVals { + var n, d uint32 + err := binary.Read(r, t.order, &n) + if err != nil { + return err + } + err = binary.Read(r, t.order, &d) + if err != nil { + return err + } + t.ratVals[i] = []int64{int64(n), int64(d)} + } + case DTSRational: + t.ratVals = make([][]int64, int(t.Count)) + for i := range t.ratVals { + var n, d int32 + err := binary.Read(r, t.order, &n) + if err != nil { + return err + } + err = binary.Read(r, t.order, &d) + if err != nil { + return err + } + t.ratVals[i] = []int64{int64(n), int64(d)} + } + case DTFloat: // float32 + t.floatVals = make([]float64, int(t.Count)) + for i := range t.floatVals { + var v float32 + err := binary.Read(r, t.order, &v) + if err != nil { + return err + } + t.floatVals[i] = float64(v) + } + case DTDouble: + t.floatVals = make([]float64, int(t.Count)) + for i := range t.floatVals { + var u float64 + err := binary.Read(r, t.order, &u) + if err != nil { + return err + } + t.floatVals[i] = u + } + } + + switch t.Type { + case DTByte, DTShort, DTLong, DTSByte, DTSShort, DTSLong: + t.format = IntVal + case DTRational, DTSRational: + t.format = RatVal + case DTFloat, DTDouble: + t.format = FloatVal + case DTAscii: + t.format = StringVal + case DTUndefined: + t.format = UndefVal + default: + t.format = OtherVal + } + + return nil +} + +// Format returns a value indicating which method can be called to retrieve the +// tag's value properly typed (e.g. integer, rational, etc.). +func (t *Tag) Format() Format { return t.format } + +func (t *Tag) typeErr(to Format) error { + return &wrongFmtErr{typeNames[t.Type], formatNames[to]} +} + +// Rat returns the tag's i'th value as a rational number. It returns a nil and +// an error if this tag's Format is not RatVal. It panics for zero deminators +// or if i is out of range. +func (t *Tag) Rat(i int) (*big.Rat, error) { + n, d, err := t.Rat2(i) + if err != nil { + return nil, err + } + return big.NewRat(n, d), nil +} + +// Rat2 returns the tag's i'th value as a rational number represented by a +// numerator-denominator pair. It returns an error if the tag's Format is not +// RatVal. It panics if i is out of range. +func (t *Tag) Rat2(i int) (num, den int64, err error) { + if t.format != RatVal { + return 0, 0, t.typeErr(RatVal) + } + return t.ratVals[i][0], t.ratVals[i][1], nil +} + +// Int64 returns the tag's i'th value as an integer. It returns an error if the +// tag's Format is not IntVal. It panics if i is out of range. +func (t *Tag) Int64(i int) (int64, error) { + if t.format != IntVal { + return 0, t.typeErr(IntVal) + } + return t.intVals[i], nil +} + +// Int returns the tag's i'th value as an integer. It returns an error if the +// tag's Format is not IntVal. It panics if i is out of range. +func (t *Tag) Int(i int) (int, error) { + if t.format != IntVal { + return 0, t.typeErr(IntVal) + } + return int(t.intVals[i]), nil +} + +// Float returns the tag's i'th value as a float. It returns an error if the +// tag's Format is not IntVal. It panics if i is out of range. +func (t *Tag) Float(i int) (float64, error) { + if t.format != FloatVal { + return 0, t.typeErr(FloatVal) + } + return t.floatVals[i], nil +} + +// StringVal returns the tag's value as a string. It returns an error if the +// tag's Format is not StringVal. It panics if i is out of range. +func (t *Tag) StringVal() (string, error) { + if t.format != StringVal { + return "", t.typeErr(StringVal) + } + return t.strVal, nil +} + +// String returns a nicely formatted version of the tag. +func (t *Tag) String() string { + data, err := t.MarshalJSON() + if err != nil { + return "ERROR: " + err.Error() + } + + if t.Count == 1 { + return strings.Trim(fmt.Sprintf("%s", data), "[]") + } + return fmt.Sprintf("%s", data) +} + +func (t *Tag) MarshalJSON() ([]byte, error) { + switch t.format { + case StringVal, UndefVal: + return nullString(t.Val), nil + case OtherVal: + return []byte(fmt.Sprintf("unknown tag type '%v'", t.Type)), nil + } + + rv := []string{} + for i := 0; i < int(t.Count); i++ { + switch t.format { + case RatVal: + n, d, _ := t.Rat2(i) + rv = append(rv, fmt.Sprintf(`"%v/%v"`, n, d)) + case FloatVal: + v, _ := t.Float(i) + rv = append(rv, fmt.Sprintf("%v", v)) + case IntVal: + v, _ := t.Int(i) + rv = append(rv, fmt.Sprintf("%v", v)) + } + } + return []byte(fmt.Sprintf(`[%s]`, strings.Join(rv, ","))), nil +} + +func nullString(in []byte) []byte { + rv := bytes.Buffer{} + rv.WriteByte('"') + for _, b := range in { + if unicode.IsPrint(rune(b)) { + rv.WriteByte(b) + } + } + rv.WriteByte('"') + rvb := rv.Bytes() + if utf8.Valid(rvb) { + return rvb + } + return []byte(`""`) +} + +type wrongFmtErr struct { + From, To string +} + +func (e *wrongFmtErr) Error() string { + return fmt.Sprintf("cannot convert tag type '%v' into '%v'", e.From, e.To) +} diff --git a/vendor/github.com/rwcarlsen/goexif/tiff/tiff.go b/vendor/github.com/rwcarlsen/goexif/tiff/tiff.go new file mode 100644 index 000000000..771e91878 --- /dev/null +++ b/vendor/github.com/rwcarlsen/goexif/tiff/tiff.go @@ -0,0 +1,153 @@ +// Package tiff implements TIFF decoding as defined in TIFF 6.0 specification at +// http://partners.adobe.com/public/developer/en/tiff/TIFF6.pdf +package tiff + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" +) + +// ReadAtReader is used when decoding Tiff tags and directories +type ReadAtReader interface { + io.Reader + io.ReaderAt +} + +// Tiff provides access to a decoded tiff data structure. +type Tiff struct { + // Dirs is an ordered slice of the tiff's Image File Directories (IFDs). + // The IFD at index 0 is IFD0. + Dirs []*Dir + // The tiff's byte-encoding (i.e. big/little endian). + Order binary.ByteOrder +} + +// Decode parses tiff-encoded data from r and returns a Tiff struct that +// reflects the structure and content of the tiff data. The first read from r +// should be the first byte of the tiff-encoded data and not necessarily the +// first byte of an os.File object. +func Decode(r io.Reader) (*Tiff, error) { + data, err := ioutil.ReadAll(r) + if err != nil { + return nil, errors.New("tiff: could not read data") + } + buf := bytes.NewReader(data) + + t := new(Tiff) + + // read byte order + bo := make([]byte, 2) + if _, err = io.ReadFull(buf, bo); err != nil { + return nil, errors.New("tiff: could not read tiff byte order") + } + if string(bo) == "II" { + t.Order = binary.LittleEndian + } else if string(bo) == "MM" { + t.Order = binary.BigEndian + } else { + return nil, errors.New("tiff: could not read tiff byte order") + } + + // check for special tiff marker + var sp int16 + err = binary.Read(buf, t.Order, &sp) + if err != nil || 42 != sp { + return nil, errors.New("tiff: could not find special tiff marker") + } + + // load offset to first IFD + var offset int32 + err = binary.Read(buf, t.Order, &offset) + if err != nil { + return nil, errors.New("tiff: could not read offset to first IFD") + } + + // load IFD's + var d *Dir + prev := offset + for offset != 0 { + // seek to offset + _, err := buf.Seek(int64(offset), 0) + if err != nil { + return nil, errors.New("tiff: seek to IFD failed") + } + + if buf.Len() == 0 { + return nil, errors.New("tiff: seek offset after EOF") + } + + // load the dir + d, offset, err = DecodeDir(buf, t.Order) + if err != nil { + return nil, err + } + + if offset == prev { + return nil, errors.New("tiff: recursive IFD") + } + prev = offset + + t.Dirs = append(t.Dirs, d) + } + + return t, nil +} + +func (tf *Tiff) String() string { + var buf bytes.Buffer + fmt.Fprint(&buf, "Tiff{") + for _, d := range tf.Dirs { + fmt.Fprintf(&buf, "%s, ", d.String()) + } + fmt.Fprintf(&buf, "}") + return buf.String() +} + +// Dir provides access to the parsed content of a tiff Image File Directory (IFD). +type Dir struct { + Tags []*Tag +} + +// DecodeDir parses a tiff-encoded IFD from r and returns a Dir object. offset +// is the offset to the next IFD. The first read from r should be at the first +// byte of the IFD. ReadAt offsets should generally be relative to the +// beginning of the tiff structure (not relative to the beginning of the IFD). +func DecodeDir(r ReadAtReader, order binary.ByteOrder) (d *Dir, offset int32, err error) { + d = new(Dir) + + // get num of tags in ifd + var nTags int16 + err = binary.Read(r, order, &nTags) + if err != nil { + return nil, 0, errors.New("tiff: failed to read IFD tag count: " + err.Error()) + } + + // load tags + for n := 0; n < int(nTags); n++ { + t, err := DecodeTag(r, order) + if err != nil { + return nil, 0, err + } + d.Tags = append(d.Tags, t) + } + + // get offset to next ifd + err = binary.Read(r, order, &offset) + if err != nil { + return nil, 0, errors.New("tiff: falied to read offset to next IFD: " + err.Error()) + } + + return d, offset, nil +} + +func (d *Dir) String() string { + s := "Dir{" + for _, t := range d.Tags { + s += t.String() + ", " + } + return s + "}" +} diff --git a/vendor/modules.txt b/vendor/modules.txt index a84773ada..efd79837a 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -876,9 +876,19 @@ github.com/klauspost/compress/zstd/internal/xxhash # github.com/klauspost/cpuid/v2 v2.2.11 ## explicit; go 1.22 github.com/klauspost/cpuid/v2 -# github.com/kovidgoyal/imaging v1.6.4 -## explicit; go 1.21 +# github.com/kovidgoyal/go-parallel v1.0.1 +## explicit; go 1.23 +github.com/kovidgoyal/go-parallel +# github.com/kovidgoyal/imaging v1.7.2 +## explicit; go 1.24.0 github.com/kovidgoyal/imaging +github.com/kovidgoyal/imaging/prism/meta +github.com/kovidgoyal/imaging/prism/meta/autometa +github.com/kovidgoyal/imaging/prism/meta/icc +github.com/kovidgoyal/imaging/prism/meta/jpegmeta +github.com/kovidgoyal/imaging/prism/meta/pngmeta +github.com/kovidgoyal/imaging/prism/meta/webpmeta +github.com/kovidgoyal/imaging/streams # github.com/leodido/go-urn v1.4.0 ## explicit; go 1.18 github.com/leodido/go-urn @@ -1865,6 +1875,10 @@ github.com/russellhaering/goxmldsig/types # github.com/russross/blackfriday/v2 v2.1.0 ## explicit github.com/russross/blackfriday/v2 +# github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd +## explicit +github.com/rwcarlsen/goexif/exif +github.com/rwcarlsen/goexif/tiff # github.com/segmentio/asm v1.2.0 ## explicit; go 1.18 github.com/segmentio/asm/base64 @@ -2383,7 +2397,7 @@ golang.org/x/exp/slices golang.org/x/exp/slog golang.org/x/exp/slog/internal golang.org/x/exp/slog/internal/buffer -# golang.org/x/image v0.31.0 +# golang.org/x/image v0.32.0 ## explicit; go 1.24.0 golang.org/x/image/bmp golang.org/x/image/ccitt