-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathio.go
More file actions
169 lines (147 loc) · 4.78 KB
/
io.go
File metadata and controls
169 lines (147 loc) · 4.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package fennec
import (
"fmt"
"image"
"image/jpeg"
"image/png"
"io"
"os"
"path/filepath"
"strings"
)
// Open loads an image from a file path.
// If the file is a JPEG, the EXIF orientation is read (but not applied).
// Use OpenAndOrient to automatically correct orientation.
func Open(filename string) (image.Image, error) {
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("fennec: open %q: %w", filename, err)
}
defer f.Close()
img, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("fennec: decode %q: %w", filename, err)
}
return img, nil
}
// OpenAndOrient loads an image and corrects its orientation using EXIF data.
// For JPEG files with orientation metadata, the returned image will be
// rotated/flipped so that it displays correctly regardless of camera orientation.
func OpenAndOrient(filename string) (image.Image, error) {
f, err := os.Open(filename)
if err != nil {
return nil, fmt.Errorf("fennec: open %q: %w", filename, err)
}
defer f.Close()
// Read EXIF orientation first.
orient := ReadOrientation(f)
// Seek back to start for image decode.
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("fennec: seek %q: %w", filename, err)
}
img, _, err := image.Decode(f)
if err != nil {
return nil, fmt.Errorf("fennec: decode %q: %w", filename, err)
}
if orient <= OrientNormal {
return img, nil
}
// Apply orientation correction.
nrgba := toNRGBA(img)
return ApplyOrientation(nrgba, orient), nil
}
// openWithOrientation opens a file and returns the image, its EXIF orientation,
// and the file size. Used internally by CompressFile.
func openWithOrientation(filename string) (image.Image, Orientation, int64, error) {
f, err := os.Open(filename)
if err != nil {
return nil, OrientNormal, 0, fmt.Errorf("fennec: open %q: %w", filename, err)
}
defer f.Close()
stat, err := f.Stat()
if err != nil {
return nil, OrientNormal, 0, fmt.Errorf("fennec: stat %q: %w", filename, err)
}
orient := ReadOrientation(f)
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, OrientNormal, 0, fmt.Errorf("fennec: seek %q: %w", filename, err)
}
img, _, err := image.Decode(f)
if err != nil {
return nil, OrientNormal, 0, fmt.Errorf("fennec: decode %q: %w", filename, err)
}
return img, orient, stat.Size(), nil
}
// Save saves the image to a file, auto-detecting format from extension.
func Save(img image.Image, filename string, opts Options) error {
ext := strings.ToLower(filepath.Ext(filename))
var format Format
switch ext {
case ".jpg", ".jpeg":
format = JPEG
case ".png":
format = PNG
default:
return fmt.Errorf("fennec: unsupported extension %q (use .jpg or .png)", ext)
}
f, err := os.Create(filename)
if err != nil {
return fmt.Errorf("fennec: create %q: %w", filename, err)
}
defer f.Close()
return Encode(f, img, format, opts)
}
// Encode writes the image to w in the specified format with Fennec optimization.
func Encode(w io.Writer, img image.Image, format Format, opts Options) error {
src := toNRGBARef(img)
switch format {
case JPEG:
targetSSIM := opts.Quality.targetSSIM()
if opts.TargetSSIM > 0 {
targetSSIM = opts.TargetSSIM
}
_, _, _, err := compressJPEGOptimal(src, w, targetSSIM, opts)
return err
case PNG:
return compressPNG(src, w, opts)
default:
return fmt.Errorf("fennec: %w for Encode (use JPEG or PNG)", ErrUnsupportedFormat)
}
}
// encodeToBytes encodes an image to bytes in the specified format.
// Used internally when CompressedData is missing.
func encodeToBytes(img *image.NRGBA, format Format, quality int) ([]byte, error) {
var buf encodingBuffer
switch format {
case JPEG:
if err := encodeJPEG(&buf, img, quality, false); err != nil {
return nil, fmt.Errorf("fennec: JPEG encode: %w", err)
}
case PNG:
encoder := png.Encoder{CompressionLevel: png.BestCompression}
if err := encoder.Encode(&buf, img); err != nil {
return nil, fmt.Errorf("fennec: PNG encode: %w", err)
}
default:
return nil, ErrUnsupportedFormat
}
return buf.Bytes(), nil
}
// encodeJPEG handles JPEG encoding, using RGBA for opaque images (faster path).
//
// The subsample parameter is accepted for API forward-compatibility but currently
// has no effect: Go's stdlib image/jpeg encoder always uses 4:2:0 chroma
// subsampling and does not expose a toggle. When a custom encoder is added in a
// future version, this parameter will control the subsampling mode.
func encodeJPEG(w io.Writer, img *image.NRGBA, quality int, subsample bool) error {
_ = subsample // Reserved for future custom encoder; stdlib always uses 4:2:0.
if isOpaque(img) {
rgba := &image.RGBA{
Pix: img.Pix,
Stride: img.Stride,
Rect: img.Rect,
}
return jpeg.Encode(w, rgba, &jpeg.Options{Quality: quality})
}
return jpeg.Encode(w, img, &jpeg.Options{Quality: quality})
}