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] <support@github.com>
This commit is contained in:
dependabot[bot]
2025-10-23 17:48:43 +02:00
committed by Ralf Haferkamp
parent fb94f34a1f
commit d76cacd99f
64 changed files with 5289 additions and 330 deletions
+2 -2
View File
@@ -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
+30 -27
View File
@@ -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
}
+5 -3
View File
@@ -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
}
+24 -32
View File
@@ -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
}
+8 -6
View File
@@ -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
+43 -142
View File
@@ -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 {
+510
View File
@@ -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)
}
+440
View File
@@ -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)
}
+41
View File
@@ -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")
}
+3
View File
@@ -0,0 +1,3 @@
// Package autometa provides support for embedded metadata and automatic
// detection of image formats.
package autometa
+54
View File
@@ -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
}
+2
View File
@@ -0,0 +1,2 @@
// Package meta and its subpackages provide support for embedded image metadata.
package meta
+90
View File
@@ -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))
}
}
+36
View File
@@ -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))
}
}
+2
View File
@@ -0,0 +1,2 @@
// Package icc provides support for working with ICC colour profile data.
package icc
+44
View File
@@ -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())
}
+30
View File
@@ -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))
}
}
+100
View File
@@ -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(),
}
}
+97
View File
@@ -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,
}
}
+13
View File
@@ -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)
}
+27
View File
@@ -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)
}
}
+209
View File
@@ -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) + "'"
}
+183
View File
@@ -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
}
+140
View File
@@ -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))
}
+103
View File
@@ -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, &not_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
}
+3
View File
@@ -0,0 +1,3 @@
package meta
type ImageFormat string
+2
View File
@@ -0,0 +1,2 @@
// Package jpegmeta provides support for working with embedded JPEG metadata.
package jpegmeta
+166
View File
@@ -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
}
+89
View File
@@ -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)
}
+115
View File
@@ -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))
}
}
+38
View File
@@ -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
}
@@ -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,
}
}
+21
View File
@@ -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
}
@@ -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'}
+2
View File
@@ -0,0 +1,2 @@
// Package pngmeta provides support for working with embedded PNG metadata.
package pngmeta
+163
View File
@@ -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
}
@@ -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
}
+10
View File
@@ -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'}
)
+2
View File
@@ -0,0 +1,2 @@
// Package webpmeta provides support for working with embedded WebP metadata.
package webpmeta
+213
View File
@@ -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))
}
+5 -2
View File
@@ -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')
+25 -23
View File
@@ -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
+40 -1
View File
@@ -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)
}
+133
View File
@@ -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
}
+24 -17
View File
@@ -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
}
+49 -33
View File
@@ -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) {
+13 -33
View File
@@ -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.