git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/imaging/io.go (about) 1 package imaging 2 3 import ( 4 "encoding/binary" 5 "errors" 6 "image" 7 "image/draw" 8 "image/gif" 9 "image/jpeg" 10 "image/png" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "golang.org/x/image/bmp" 18 "golang.org/x/image/tiff" 19 ) 20 21 type fileSystem interface { 22 Create(string) (io.WriteCloser, error) 23 Open(string) (io.ReadCloser, error) 24 } 25 26 type localFS struct{} 27 28 func (localFS) Create(name string) (io.WriteCloser, error) { return os.Create(name) } 29 func (localFS) Open(name string) (io.ReadCloser, error) { return os.Open(name) } 30 31 var fs fileSystem = localFS{} 32 33 type decodeConfig struct { 34 autoOrientation bool 35 } 36 37 var defaultDecodeConfig = decodeConfig{ 38 autoOrientation: false, 39 } 40 41 // DecodeOption sets an optional parameter for the Decode and Open functions. 42 type DecodeOption func(*decodeConfig) 43 44 // AutoOrientation returns a DecodeOption that sets the auto-orientation mode. 45 // If auto-orientation is enabled, the image will be transformed after decoding 46 // according to the EXIF orientation tag (if present). By default it's disabled. 47 func AutoOrientation(enabled bool) DecodeOption { 48 return func(c *decodeConfig) { 49 c.autoOrientation = enabled 50 } 51 } 52 53 // Decode reads an image from r. 54 func Decode(r io.Reader, opts ...DecodeOption) (image.Image, error) { 55 cfg := defaultDecodeConfig 56 for _, option := range opts { 57 option(&cfg) 58 } 59 60 if !cfg.autoOrientation { 61 img, _, err := image.Decode(r) 62 return img, err 63 } 64 65 var orient orientation 66 pr, pw := io.Pipe() 67 r = io.TeeReader(r, pw) 68 done := make(chan struct{}) 69 go func() { 70 defer close(done) 71 orient = readOrientation(pr) 72 io.Copy(ioutil.Discard, pr) 73 }() 74 75 img, _, err := image.Decode(r) 76 pw.Close() 77 <-done 78 if err != nil { 79 return nil, err 80 } 81 82 return fixOrientation(img, orient), nil 83 } 84 85 // Open loads an image from file. 86 // 87 // Examples: 88 // 89 // // Load an image from file. 90 // img, err := imaging.Open("test.jpg") 91 // 92 // // Load an image and transform it depending on the EXIF orientation tag (if present). 93 // img, err := imaging.Open("test.jpg", imaging.AutoOrientation(true)) 94 func Open(filename string, opts ...DecodeOption) (image.Image, error) { 95 file, err := fs.Open(filename) 96 if err != nil { 97 return nil, err 98 } 99 defer file.Close() 100 return Decode(file, opts...) 101 } 102 103 // Format is an image file format. 104 type Format int 105 106 // Image file formats. 107 const ( 108 JPEG Format = iota 109 PNG 110 GIF 111 TIFF 112 BMP 113 ) 114 115 var formatExts = map[string]Format{ 116 "jpg": JPEG, 117 "jpeg": JPEG, 118 "png": PNG, 119 "gif": GIF, 120 "tif": TIFF, 121 "tiff": TIFF, 122 "bmp": BMP, 123 } 124 125 var formatNames = map[Format]string{ 126 JPEG: "JPEG", 127 PNG: "PNG", 128 GIF: "GIF", 129 TIFF: "TIFF", 130 BMP: "BMP", 131 } 132 133 func (f Format) String() string { 134 return formatNames[f] 135 } 136 137 // ErrUnsupportedFormat means the given image format is not supported. 138 var ErrUnsupportedFormat = errors.New("imaging: unsupported image format") 139 140 // FormatFromExtension parses image format from filename extension: 141 // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. 142 func FormatFromExtension(ext string) (Format, error) { 143 if f, ok := formatExts[strings.ToLower(strings.TrimPrefix(ext, "."))]; ok { 144 return f, nil 145 } 146 return -1, ErrUnsupportedFormat 147 } 148 149 // FormatFromFilename parses image format from filename: 150 // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. 151 func FormatFromFilename(filename string) (Format, error) { 152 ext := filepath.Ext(filename) 153 return FormatFromExtension(ext) 154 } 155 156 type encodeConfig struct { 157 jpegQuality int 158 gifNumColors int 159 gifQuantizer draw.Quantizer 160 gifDrawer draw.Drawer 161 pngCompressionLevel png.CompressionLevel 162 } 163 164 var defaultEncodeConfig = encodeConfig{ 165 jpegQuality: 95, 166 gifNumColors: 256, 167 gifQuantizer: nil, 168 gifDrawer: nil, 169 pngCompressionLevel: png.DefaultCompression, 170 } 171 172 // EncodeOption sets an optional parameter for the Encode and Save functions. 173 type EncodeOption func(*encodeConfig) 174 175 // JPEGQuality returns an EncodeOption that sets the output JPEG quality. 176 // Quality ranges from 1 to 100 inclusive, higher is better. Default is 95. 177 func JPEGQuality(quality int) EncodeOption { 178 return func(c *encodeConfig) { 179 c.jpegQuality = quality 180 } 181 } 182 183 // GIFNumColors returns an EncodeOption that sets the maximum number of colors 184 // used in the GIF-encoded image. It ranges from 1 to 256. Default is 256. 185 func GIFNumColors(numColors int) EncodeOption { 186 return func(c *encodeConfig) { 187 c.gifNumColors = numColors 188 } 189 } 190 191 // GIFQuantizer returns an EncodeOption that sets the quantizer that is used to produce 192 // a palette of the GIF-encoded image. 193 func GIFQuantizer(quantizer draw.Quantizer) EncodeOption { 194 return func(c *encodeConfig) { 195 c.gifQuantizer = quantizer 196 } 197 } 198 199 // GIFDrawer returns an EncodeOption that sets the drawer that is used to convert 200 // the source image to the desired palette of the GIF-encoded image. 201 func GIFDrawer(drawer draw.Drawer) EncodeOption { 202 return func(c *encodeConfig) { 203 c.gifDrawer = drawer 204 } 205 } 206 207 // PNGCompressionLevel returns an EncodeOption that sets the compression level 208 // of the PNG-encoded image. Default is png.DefaultCompression. 209 func PNGCompressionLevel(level png.CompressionLevel) EncodeOption { 210 return func(c *encodeConfig) { 211 c.pngCompressionLevel = level 212 } 213 } 214 215 // Encode writes the image img to w in the specified format (JPEG, PNG, GIF, TIFF or BMP). 216 func Encode(w io.Writer, img image.Image, format Format, opts ...EncodeOption) error { 217 cfg := defaultEncodeConfig 218 for _, option := range opts { 219 option(&cfg) 220 } 221 222 switch format { 223 case JPEG: 224 if nrgba, ok := img.(*image.NRGBA); ok && nrgba.Opaque() { 225 rgba := &image.RGBA{ 226 Pix: nrgba.Pix, 227 Stride: nrgba.Stride, 228 Rect: nrgba.Rect, 229 } 230 return jpeg.Encode(w, rgba, &jpeg.Options{Quality: cfg.jpegQuality}) 231 } 232 return jpeg.Encode(w, img, &jpeg.Options{Quality: cfg.jpegQuality}) 233 234 case PNG: 235 encoder := png.Encoder{CompressionLevel: cfg.pngCompressionLevel} 236 return encoder.Encode(w, img) 237 238 case GIF: 239 return gif.Encode(w, img, &gif.Options{ 240 NumColors: cfg.gifNumColors, 241 Quantizer: cfg.gifQuantizer, 242 Drawer: cfg.gifDrawer, 243 }) 244 245 case TIFF: 246 return tiff.Encode(w, img, &tiff.Options{Compression: tiff.Deflate, Predictor: true}) 247 248 case BMP: 249 return bmp.Encode(w, img) 250 } 251 252 return ErrUnsupportedFormat 253 } 254 255 // Save saves the image to file with the specified filename. 256 // The format is determined from the filename extension: 257 // "jpg" (or "jpeg"), "png", "gif", "tif" (or "tiff") and "bmp" are supported. 258 // 259 // Examples: 260 // 261 // // Save the image as PNG. 262 // err := imaging.Save(img, "out.png") 263 // 264 // // Save the image as JPEG with optional quality parameter set to 80. 265 // err := imaging.Save(img, "out.jpg", imaging.JPEGQuality(80)) 266 func Save(img image.Image, filename string, opts ...EncodeOption) (err error) { 267 f, err := FormatFromFilename(filename) 268 if err != nil { 269 return err 270 } 271 file, err := fs.Create(filename) 272 if err != nil { 273 return err 274 } 275 err = Encode(file, img, f, opts...) 276 errc := file.Close() 277 if err == nil { 278 err = errc 279 } 280 return err 281 } 282 283 // orientation is an EXIF flag that specifies the transformation 284 // that should be applied to image to display it correctly. 285 type orientation int 286 287 const ( 288 orientationUnspecified = 0 289 orientationNormal = 1 290 orientationFlipH = 2 291 orientationRotate180 = 3 292 orientationFlipV = 4 293 orientationTranspose = 5 294 orientationRotate270 = 6 295 orientationTransverse = 7 296 orientationRotate90 = 8 297 ) 298 299 // readOrientation tries to read the orientation EXIF flag from image data in r. 300 // If the EXIF data block is not found or the orientation flag is not found 301 // or any other error occures while reading the data, it returns the 302 // orientationUnspecified (0) value. 303 func readOrientation(r io.Reader) orientation { 304 const ( 305 markerSOI = 0xffd8 306 markerAPP1 = 0xffe1 307 exifHeader = 0x45786966 308 byteOrderBE = 0x4d4d 309 byteOrderLE = 0x4949 310 orientationTag = 0x0112 311 ) 312 313 // Check if JPEG SOI marker is present. 314 var soi uint16 315 if err := binary.Read(r, binary.BigEndian, &soi); err != nil { 316 return orientationUnspecified 317 } 318 if soi != markerSOI { 319 return orientationUnspecified // Missing JPEG SOI marker. 320 } 321 322 // Find JPEG APP1 marker. 323 for { 324 var marker, size uint16 325 if err := binary.Read(r, binary.BigEndian, &marker); err != nil { 326 return orientationUnspecified 327 } 328 if err := binary.Read(r, binary.BigEndian, &size); err != nil { 329 return orientationUnspecified 330 } 331 if marker>>8 != 0xff { 332 return orientationUnspecified // Invalid JPEG marker. 333 } 334 if marker == markerAPP1 { 335 break 336 } 337 if size < 2 { 338 return orientationUnspecified // Invalid block size. 339 } 340 if _, err := io.CopyN(ioutil.Discard, r, int64(size-2)); err != nil { 341 return orientationUnspecified 342 } 343 } 344 345 // Check if EXIF header is present. 346 var header uint32 347 if err := binary.Read(r, binary.BigEndian, &header); err != nil { 348 return orientationUnspecified 349 } 350 if header != exifHeader { 351 return orientationUnspecified 352 } 353 if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { 354 return orientationUnspecified 355 } 356 357 // Read byte order information. 358 var ( 359 byteOrderTag uint16 360 byteOrder binary.ByteOrder 361 ) 362 if err := binary.Read(r, binary.BigEndian, &byteOrderTag); err != nil { 363 return orientationUnspecified 364 } 365 switch byteOrderTag { 366 case byteOrderBE: 367 byteOrder = binary.BigEndian 368 case byteOrderLE: 369 byteOrder = binary.LittleEndian 370 default: 371 return orientationUnspecified // Invalid byte order flag. 372 } 373 if _, err := io.CopyN(ioutil.Discard, r, 2); err != nil { 374 return orientationUnspecified 375 } 376 377 // Skip the EXIF offset. 378 var offset uint32 379 if err := binary.Read(r, byteOrder, &offset); err != nil { 380 return orientationUnspecified 381 } 382 if offset < 8 { 383 return orientationUnspecified // Invalid offset value. 384 } 385 if _, err := io.CopyN(ioutil.Discard, r, int64(offset-8)); err != nil { 386 return orientationUnspecified 387 } 388 389 // Read the number of tags. 390 var numTags uint16 391 if err := binary.Read(r, byteOrder, &numTags); err != nil { 392 return orientationUnspecified 393 } 394 395 // Find the orientation tag. 396 for i := 0; i < int(numTags); i++ { 397 var tag uint16 398 if err := binary.Read(r, byteOrder, &tag); err != nil { 399 return orientationUnspecified 400 } 401 if tag != orientationTag { 402 if _, err := io.CopyN(ioutil.Discard, r, 10); err != nil { 403 return orientationUnspecified 404 } 405 continue 406 } 407 if _, err := io.CopyN(ioutil.Discard, r, 6); err != nil { 408 return orientationUnspecified 409 } 410 var val uint16 411 if err := binary.Read(r, byteOrder, &val); err != nil { 412 return orientationUnspecified 413 } 414 if val < 1 || val > 8 { 415 return orientationUnspecified // Invalid tag value. 416 } 417 return orientation(val) 418 } 419 return orientationUnspecified // Missing orientation tag. 420 } 421 422 // fixOrientation applies a transform to img corresponding to the given orientation flag. 423 func fixOrientation(img image.Image, o orientation) image.Image { 424 switch o { 425 case orientationNormal: 426 case orientationFlipH: 427 img = FlipH(img) 428 case orientationFlipV: 429 img = FlipV(img) 430 case orientationRotate90: 431 img = Rotate90(img) 432 case orientationRotate180: 433 img = Rotate180(img) 434 case orientationRotate270: 435 img = Rotate270(img) 436 case orientationTranspose: 437 img = Transpose(img) 438 case orientationTransverse: 439 img = Transverse(img) 440 } 441 return img 442 }