From d76cacd99f9f0b76f9c259854c220e807f049e04 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 23 Oct 2025 14:17:37 +0000 Subject: [PATCH] build(deps): bump github.com/kovidgoyal/imaging from 1.6.4 to 1.7.2 Bumps [github.com/kovidgoyal/imaging](https://github.com/kovidgoyal/imaging) from 1.6.4 to 1.7.2. - [Release notes](https://github.com/kovidgoyal/imaging/releases) - [Changelog](https://github.com/kovidgoyal/imaging/blob/master/.goreleaser.yaml) - [Commits](https://github.com/kovidgoyal/imaging/compare/v1.6.4...v1.7.2) --- updated-dependencies: - dependency-name: github.com/kovidgoyal/imaging dependency-version: 1.7.2 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- go.mod | 6 +- go.sum | 12 +- .../kovidgoyal/go-parallel/.gitignore | 1 + .../kovidgoyal/go-parallel/.goreleaser.yaml | 2 + .../github.com/kovidgoyal/go-parallel/LICENSE | 28 + .../kovidgoyal/go-parallel/README.md | 5 + .../kovidgoyal/go-parallel/parallel.go | 171 +++++ .../kovidgoyal/go-parallel/publish.py | 31 + .../kovidgoyal/imaging/.goreleaser.yaml | 4 +- .../github.com/kovidgoyal/imaging/adjust.go | 57 +- .../kovidgoyal/imaging/convolution.go | 8 +- .../github.com/kovidgoyal/imaging/effects.go | 56 +- .../kovidgoyal/imaging/histogram.go | 14 +- vendor/github.com/kovidgoyal/imaging/io.go | 185 ++--- .../github.com/kovidgoyal/imaging/netpbm.go | 510 ++++++++++++++ vendor/github.com/kovidgoyal/imaging/nrgb.go | 440 ++++++++++++ .../imaging/prism/meta/autometa/autometa.go | 41 ++ .../imaging/prism/meta/autometa/doc.go | 3 + .../kovidgoyal/imaging/prism/meta/data.go | 54 ++ .../kovidgoyal/imaging/prism/meta/doc.go | 2 + .../imaging/prism/meta/icc/colorspace.go | 90 +++ .../imaging/prism/meta/icc/deviceclass.go | 36 + .../kovidgoyal/imaging/prism/meta/icc/doc.go | 2 + .../imaging/prism/meta/icc/header.go | 44 ++ .../imaging/prism/meta/icc/primaryplatform.go | 30 + .../imaging/prism/meta/icc/profile.go | 100 +++ .../imaging/prism/meta/icc/profilereader.go | 97 +++ .../imaging/prism/meta/icc/profileversion.go | 13 + .../imaging/prism/meta/icc/renderingintent.go | 27 + .../imaging/prism/meta/icc/signature.go | 209 ++++++ .../imaging/prism/meta/icc/tag_description.go | 183 +++++ .../imaging/prism/meta/icc/tags_clut.go | 140 ++++ .../imaging/prism/meta/icc/tagtable.go | 103 +++ .../imaging/prism/meta/imageformat.go | 3 + .../imaging/prism/meta/jpegmeta/doc.go | 2 + .../imaging/prism/meta/jpegmeta/jpegmeta.go | 166 +++++ .../imaging/prism/meta/jpegmeta/marker.go | 89 +++ .../imaging/prism/meta/jpegmeta/markertype.go | 115 +++ .../imaging/prism/meta/jpegmeta/segment.go | 38 + .../prism/meta/jpegmeta/segmentreader.go | 56 ++ .../imaging/prism/meta/pngmeta/chunkheader.go | 21 + .../imaging/prism/meta/pngmeta/chunktypes.go | 6 + .../imaging/prism/meta/pngmeta/doc.go | 2 + .../imaging/prism/meta/pngmeta/pngmeta.go | 163 +++++ .../prism/meta/webpmeta/chunkheader.go | 21 + .../imaging/prism/meta/webpmeta/chunktypes.go | 10 + .../imaging/prism/meta/webpmeta/doc.go | 2 + .../imaging/prism/meta/webpmeta/webpmeta.go | 213 ++++++ .../github.com/kovidgoyal/imaging/publish.py | 7 +- .../github.com/kovidgoyal/imaging/resize.go | 48 +- .../github.com/kovidgoyal/imaging/scanner.go | 41 +- .../kovidgoyal/imaging/streams/api.go | 133 ++++ vendor/github.com/kovidgoyal/imaging/tools.go | 41 +- .../kovidgoyal/imaging/transform.go | 82 ++- vendor/github.com/kovidgoyal/imaging/utils.go | 46 +- vendor/github.com/rwcarlsen/goexif/LICENSE | 24 + .../rwcarlsen/goexif/exif/README.md | 4 + .../github.com/rwcarlsen/goexif/exif/exif.go | 655 ++++++++++++++++++ .../rwcarlsen/goexif/exif/fields.go | 309 +++++++++ .../rwcarlsen/goexif/exif/sample1.jpg | Bin 0 -> 80603 bytes .../rwcarlsen/goexif/tiff/sample1.tif | Bin 0 -> 18382 bytes .../github.com/rwcarlsen/goexif/tiff/tag.go | 445 ++++++++++++ .../github.com/rwcarlsen/goexif/tiff/tiff.go | 153 ++++ vendor/modules.txt | 20 +- 64 files changed, 5289 insertions(+), 330 deletions(-) create mode 100644 vendor/github.com/kovidgoyal/go-parallel/.gitignore create mode 100644 vendor/github.com/kovidgoyal/go-parallel/.goreleaser.yaml create mode 100644 vendor/github.com/kovidgoyal/go-parallel/LICENSE create mode 100644 vendor/github.com/kovidgoyal/go-parallel/README.md create mode 100644 vendor/github.com/kovidgoyal/go-parallel/parallel.go create mode 100644 vendor/github.com/kovidgoyal/go-parallel/publish.py create mode 100644 vendor/github.com/kovidgoyal/imaging/netpbm.go create mode 100644 vendor/github.com/kovidgoyal/imaging/nrgb.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/autometa.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/autometa/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/data.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/colorspace.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/deviceclass.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/header.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/primaryplatform.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profile.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profilereader.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/profileversion.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/renderingintent.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/signature.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tag_description.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tags_clut.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/icc/tagtable.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/imageformat.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/jpegmeta.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/marker.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/markertype.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segment.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/jpegmeta/segmentreader.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunkheader.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/chunktypes.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/pngmeta/pngmeta.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunkheader.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/chunktypes.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/doc.go create mode 100644 vendor/github.com/kovidgoyal/imaging/prism/meta/webpmeta/webpmeta.go create mode 100644 vendor/github.com/kovidgoyal/imaging/streams/api.go create mode 100644 vendor/github.com/rwcarlsen/goexif/LICENSE create mode 100644 vendor/github.com/rwcarlsen/goexif/exif/README.md create mode 100644 vendor/github.com/rwcarlsen/goexif/exif/exif.go create mode 100644 vendor/github.com/rwcarlsen/goexif/exif/fields.go create mode 100644 vendor/github.com/rwcarlsen/goexif/exif/sample1.jpg create mode 100644 vendor/github.com/rwcarlsen/goexif/tiff/sample1.tif create mode 100644 vendor/github.com/rwcarlsen/goexif/tiff/tag.go create mode 100644 vendor/github.com/rwcarlsen/goexif/tiff/tiff.go diff --git a/go.mod b/go.mod index 880951bff6..4bf5c9b567 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 270baeff75..445fbec992 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 0000000000..1521c8b765 --- /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 0000000000..2bb741244f --- /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 0000000000..c17eaa8480 --- /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 0000000000..3c802e0c4a --- /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 0000000000..6da26dd376 --- /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 0000000000..aa7e866feb --- /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 feb4d664d7..4336820b52 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 971aebc6fe..8aedb1c885 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 11eddc1623..fac4972a8a 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 47316b701c..d034f362eb 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 c547fe8222..3550011e1d 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 f6c6da86ba..f9aac3f0a6 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 0000000000..91ad6d4399 --- /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 0000000000..87d71a714c --- /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 0000000000..673d555e30 --- /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 0000000000..72bd0aaac2 --- /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 0000000000..ddff91230d --- /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 0000000000..5d6c87531f --- /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 0000000000..99b5ef2deb --- /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 0000000000..1ea5110369 --- /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 0000000000..95fd943cea --- /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 0000000000..99137e4da2 --- /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 0000000000..9e439a7d9b --- /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 0000000000..3af8178e4c --- /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 0000000000..f2832e504e --- /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 0000000000..e78784f7de --- /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 0000000000..cf1193b965 --- /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 0000000000..2a63b28f24 --- /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 0000000000..24ac7550ee --- /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 0000000000..53d76cb105 --- /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 0000000000..23f5dda422 --- /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 0000000000..e9e1649aa7 --- /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 0000000000..7d80b0d243 --- /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 0000000000..53b80c30df --- /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 0000000000..bbbc06e887 --- /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 0000000000..d3ae9cfb4e --- /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 0000000000..053b16d6b2 --- /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 0000000000..141290c4c9 --- /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 0000000000..3fc53c2703 --- /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 0000000000..4d125549b5 --- /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 0000000000..2a41eda816 --- /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 0000000000..676af840cd --- /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 0000000000..47a60ab031 --- /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 0000000000..c6083474d5 --- /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 0000000000..ed7f1009af --- /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 0000000000..fd1fd44762 --- /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 29f61a8a2b..f23bfa14f8 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 963f40c482..19c939d35d 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 84de2fc90f..f72ba9cea7 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 0000000000..f71637ac3c --- /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 9591ebc5ae..78d3a77361 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 fe4a92f9d4..f7253b1ad0 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 f650c05bce..a789a78943 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 0000000000..aa6250465f --- /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 0000000000..b3bf5fa0e4 --- /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 0000000000..03afe6507a --- /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 0000000000..8b8ae0f2d3 --- /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 0000000000000000000000000000000000000000..87bcf8e33a54e6a6608c7d789175395f2b223b5a GIT binary patch literal 80603 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.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 literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..fe51399c542954e5cd837eb50626c5cd02991ebb GIT binary patch literal 18382 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 literal 0 HcmV?d00001 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 0000000000..b9ce791f54 --- /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 0000000000..771e918786 --- /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 a84773ada6..efd79837ae 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