-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathavif.go
More file actions
1732 lines (1654 loc) · 52.7 KB
/
Copy pathavif.go
File metadata and controls
1732 lines (1654 loc) · 52.7 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
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
package goavif
import (
"bytes"
"errors"
"fmt"
"image"
"image/color"
"io"
"github.com/KarpelesLab/goavif/av1/decoder"
"github.com/KarpelesLab/goavif/av1/encoder"
"github.com/KarpelesLab/goavif/av1/obu"
"github.com/KarpelesLab/goavif/colorspace"
"github.com/KarpelesLab/goavif/isobmff"
)
// ErrUnsupported is returned by entry points that are not yet implemented.
// It will be removed once the matching codec path lands.
var ErrUnsupported = errors.New("goavif: not yet implemented")
// Options tunes encoding behavior. It is not used by decoding.
type Options struct {
// Quality 0..100, where 100 is highest quality. Ignored if Lossless.
Quality int
// Speed 0..10, where 10 is fastest. Trades encoding time for compression.
Speed int
// Lossless forces a lossless bitstream; overrides Quality.
Lossless bool
// BitDepth is one of 8, 10 or 12. Defaults to 8 when zero.
BitDepth int
// ChromaSubsampling selects the output chroma format.
ChromaSubsampling ChromaSubsampling
// Alpha, if true, includes the image's alpha channel as an auxiliary item.
Alpha bool
// InterEnabled, if true, enables inter-frame prediction for AVIS
// image sequences — frames other than keyframes are coded as
// INTER_FRAME against the previously decoded frame. Currently
// restricted to 8-bit 4:2:0 color sequences; monochrome / HBD
// fall back to the intra-only path. No effect on still-image
// encoding.
InterEnabled bool
// KeyFrameInterval is the number of frames between keyframes
// when InterEnabled is true. 0 or 1 means "every frame is a
// keyframe" (intra-only behavior). A value of N means frames
// 0, N, 2N, ... are keyframes. No effect when InterEnabled is
// false.
KeyFrameInterval int
// TargetBytes enables target-size rate control: when non-zero,
// [Encode] runs a Q-bisection loop and returns the best bitstream
// within ±10% of the target (or the tightest quality-bounded
// result if the target can't be hit). Overrides [Options.Quality]
// when set. No effect on [EncodeAll] / [EncodeGrid] currently.
TargetBytes int
// FilmGrainStrength in [0, 255]. When > 0, [Encode] emits a
// film_grain_params block in the frame header so the decoder
// overlays synthetic grain on the output. 0 disables grain
// emission. Typical "subtle" values are 8..32.
FilmGrainStrength uint8
}
// ChromaSubsampling identifies a YUV chroma sampling configuration.
type ChromaSubsampling int
const (
// ChromaUnspecified lets the encoder pick based on the input image.
ChromaUnspecified ChromaSubsampling = 0
// Chroma420 = 4:2:0 (horizontal + vertical subsampling).
Chroma420 ChromaSubsampling = 420
// Chroma422 = 4:2:2 (horizontal subsampling only).
Chroma422 ChromaSubsampling = 422
// Chroma444 = 4:4:4 (no subsampling).
Chroma444 ChromaSubsampling = 444
// Chroma400 = monochrome.
Chroma400 ChromaSubsampling = 400
)
// Decode reads an AVIF image from r and returns it as an [image.Image].
//
// The container and AV1 header parsing are implemented today. Pixel
// reconstruction is still landing; callers that hit an unimplemented code
// path receive an error wrapping [ErrUnsupported] or
// [decoder.ErrPixelDecodeUnimplemented].
func Decode(r io.Reader) (image.Image, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
ct, err := isobmff.ParseContainer(data)
if err != nil {
return nil, err
}
if !ct.Ftyp.HasBrand("avif") && !ct.Ftyp.HasBrand("avis") {
return nil, fmt.Errorf("goavif: ftyp has no avif/avis brand")
}
primaryID := ct.PrimaryItemID()
if primaryID == 0 {
return nil, fmt.Errorf("goavif: no primary item")
}
// Grid items (HEIF §6.6.2) split a large image into tile items
// referenced via a dimg iref. Decode each tile and composite.
if ct.ItemType(primaryID) == isobmff.TypeGridItem {
img, err := decodeGridPrimary(ct, primaryID)
if err != nil {
return nil, err
}
return applyPrimaryPostTransforms(ct, primaryID, img), nil
}
seq, err := extractSequenceHeader(ct, primaryID)
if err != nil {
return nil, err
}
itemBytes, err := ct.ItemData(primaryID)
if err != nil {
return nil, err
}
frame, err := decoder.Decode(itemBytes, seq)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrUnsupported, err)
}
// Alpha auxiliary: if the container signals an alpha item via auxl
// iref + auxC alpha URN, decode it and composite into NRGBA /
// NRGBA64. No alpha channel → passthrough.
var img image.Image
if alphaID := findAlphaItemID(ct, primaryID); alphaID != 0 {
alpha, err := decodeAlphaFrame(ct, alphaID)
if err != nil {
return nil, fmt.Errorf("goavif: alpha decode: %w", err)
}
if frame.BitDepth > 8 {
img, err = compositeNRGBA64(frame, alpha)
} else {
img, err = compositeNRGBA(frame, alpha)
}
if err != nil {
return nil, err
}
} else {
img, err = frameToImage(frame)
if err != nil {
return nil, err
}
}
return applyPrimaryPostTransforms(ct, primaryID, img), nil
}
// applyPrimaryPostTransforms honors the primary item's ispe cropping,
// clap (clean-aperture) cropping, and irot / imir transform
// properties. Shared by the single-tile and grid decode paths.
//
// Per HEIF §6.5.10, the canonical application order is: clap → irot
// → imir. ispe-based cropping is applied first since it represents
// the author's declared size vs. our encoder's padded coded frame.
func applyPrimaryPostTransforms(ct *isobmff.Container, primaryID uint32, img image.Image) image.Image {
if iw, ih, ok := primarySpatialExtents(ct, primaryID); ok {
if int(iw) < img.Bounds().Dx() || int(ih) < img.Bounds().Dy() {
img = cropToRect(img, image.Rect(0, 0, int(iw), int(ih)))
}
}
if clap := primaryClap(ct, primaryID); clap != nil {
img = applyClap(img, clap)
}
if props := primaryTransformProps(ct, primaryID); len(props) > 0 {
img = applyTransforms(img, props)
}
return img
}
// primaryClap returns the clap (clean-aperture) property associated
// with itemID, or nil.
func primaryClap(ct *isobmff.Container, itemID uint32) *isobmff.Clap {
iprp := findIprp(ct)
if iprp == nil {
return nil
}
for _, m := range iprp.Ipma {
for _, e := range m.Entries {
if e.ItemID != itemID {
continue
}
for _, a := range e.Associations {
if a.PropertyIndex == 0 || int(a.PropertyIndex) > len(iprp.Ipco.Properties) {
continue
}
if c, ok := iprp.Ipco.Properties[a.PropertyIndex-1].(*isobmff.Clap); ok {
return c
}
}
}
}
return nil
}
// applyClap crops img per HEIF §6.5.10. clap carries rational values
// for crop width / height / horizontal-offset / vertical-offset; we
// evaluate them on pixel coordinates and return the resulting
// sub-image. Fractional results are rounded to the nearest integer.
func applyClap(img image.Image, clap *isobmff.Clap) image.Image {
if clap.CleanApertureWidthD == 0 || clap.CleanApertureHeightD == 0 ||
clap.HorizOffD == 0 || clap.VertOffD == 0 {
return img
}
b := img.Bounds()
W := b.Dx()
H := b.Dy()
// Crop width / height (rounded).
cw := int((int64(clap.CleanApertureWidthN) + int64(clap.CleanApertureWidthD)/2) / int64(clap.CleanApertureWidthD))
ch := int((int64(clap.CleanApertureHeightN) + int64(clap.CleanApertureHeightD)/2) / int64(clap.CleanApertureHeightD))
if cw <= 0 || ch <= 0 || cw > W || ch > H {
return img
}
// Crop center: (W-1)/2 + horizOff, (H-1)/2 + vertOff.
centerX := (float64(W-1))/2.0 + float64(clap.HorizOffN)/float64(clap.HorizOffD)
centerY := (float64(H-1))/2.0 + float64(clap.VertOffN)/float64(clap.VertOffD)
x0 := int(centerX - float64(cw-1)/2.0 + 0.5)
y0 := int(centerY - float64(ch-1)/2.0 + 0.5)
if x0 < 0 {
x0 = 0
}
if y0 < 0 {
y0 = 0
}
if x0+cw > W {
x0 = W - cw
}
if y0+ch > H {
y0 = H - ch
}
return cropToRect(img, image.Rect(b.Min.X+x0, b.Min.Y+y0, b.Min.X+x0+cw, b.Min.Y+y0+ch))
}
// decodeGridPrimary decodes a grid-type primary item by decoding each
// referenced tile (via dimg iref) and pasting them into an output
// image of size output_width × output_height. All tiles must share
// dimensions — the final row / column are cropped to fit output
// dimensions when tileW × columns > output_width.
func decodeGridPrimary(ct *isobmff.Container, gridID uint32) (image.Image, error) {
gridBytes, err := ct.ItemData(gridID)
if err != nil {
return nil, fmt.Errorf("goavif: grid item data: %w", err)
}
grid, err := isobmff.ParseImageGrid(gridBytes)
if err != nil {
return nil, err
}
tileIDs := ct.FindDimgTargets(gridID)
expected := int(grid.Rows) * int(grid.Columns)
if len(tileIDs) != expected {
return nil, fmt.Errorf("goavif: grid has %d tiles, dimg references %d", expected, len(tileIDs))
}
// Decode each tile via the single-item path, then paste into
// the output canvas at its row/column offset.
tiles := make([]image.Image, len(tileIDs))
var tileW, tileH int
for i, tid := range tileIDs {
seq, err := extractSequenceHeader(ct, tid)
if err != nil {
return nil, fmt.Errorf("goavif: grid tile %d seq: %w", i, err)
}
itemBytes, err := ct.ItemData(tid)
if err != nil {
return nil, fmt.Errorf("goavif: grid tile %d item: %w", i, err)
}
frame, err := decoder.Decode(itemBytes, seq)
if err != nil {
return nil, fmt.Errorf("goavif: grid tile %d decode: %w", i, err)
}
img, err := frameToImage(frame)
if err != nil {
return nil, err
}
tiles[i] = img
if i == 0 {
tileW = img.Bounds().Dx()
tileH = img.Bounds().Dy()
} else if img.Bounds().Dx() != tileW || img.Bounds().Dy() != tileH {
return nil, fmt.Errorf("goavif: grid tile %d dims %v differ from first %dx%d",
i, img.Bounds(), tileW, tileH)
}
}
out := image.NewRGBA(image.Rect(0, 0, int(grid.OutputWidth), int(grid.OutputHeight)))
for i, t := range tiles {
row := i / int(grid.Columns)
col := i % int(grid.Columns)
dstX := col * tileW
dstY := row * tileH
pasteClipped(out, t, dstX, dstY, int(grid.OutputWidth), int(grid.OutputHeight))
}
return out, nil
}
// pasteClipped copies tile pixels into dst at (dstX, dstY), clipped
// to (outW, outH).
func pasteClipped(dst *image.RGBA, tile image.Image, dstX, dstY, outW, outH int) {
b := tile.Bounds()
for y := 0; y < b.Dy(); y++ {
if dstY+y >= outH {
break
}
for x := 0; x < b.Dx(); x++ {
if dstX+x >= outW {
break
}
c := tile.At(b.Min.X+x, b.Min.Y+y)
dst.Set(dstX+x, dstY+y, c)
}
}
}
// primaryTransformProps returns the ordered list of irot / imir
// transform properties associated with itemID, in the order they
// appear in the ipma entry.
func primaryTransformProps(ct *isobmff.Container, itemID uint32) []isobmff.Box {
iprp := findIprp(ct)
if iprp == nil {
return nil
}
var out []isobmff.Box
for _, m := range iprp.Ipma {
for _, e := range m.Entries {
if e.ItemID != itemID {
continue
}
for _, a := range e.Associations {
if a.PropertyIndex == 0 || int(a.PropertyIndex) > len(iprp.Ipco.Properties) {
continue
}
switch p := iprp.Ipco.Properties[a.PropertyIndex-1].(type) {
case *isobmff.Irot:
if p.Angle != 0 {
out = append(out, p)
}
case *isobmff.Imir:
out = append(out, p)
}
}
}
}
return out
}
// applyTransforms rebuilds img with irot / imir transforms applied in
// sequence. Each transform returns a freshly-allocated image of the
// rotated / mirrored pixels.
func applyTransforms(img image.Image, props []isobmff.Box) image.Image {
for _, p := range props {
switch t := p.(type) {
case *isobmff.Irot:
for i := uint8(0); i < t.Angle; i++ {
img = rotate90CCW(img)
}
case *isobmff.Imir:
if t.Axis == 0 {
// vertical axis = mirror across horizontal axis (flip
// top↔bottom). AVIF 1.1 errata redefined imir to the
// same convention the HEIF spec always used.
img = mirror(img, false)
} else {
img = mirror(img, true)
}
}
}
return img
}
// rotate90CCW returns a new RGBA image with img rotated 90° counter-
// clockwise.
func rotate90CCW(img image.Image) image.Image {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
out := image.NewRGBA(image.Rect(0, 0, h, w))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
// (x, y) → (y, w-1-x) for 90 CCW.
out.Set(y, w-1-x, img.At(b.Min.X+x, b.Min.Y+y))
}
}
return out
}
// mirror returns a new RGBA image with img flipped horizontally
// (flipH=true) or vertically (flipH=false).
func mirror(img image.Image, flipH bool) image.Image {
b := img.Bounds()
w, h := b.Dx(), b.Dy()
out := image.NewRGBA(image.Rect(0, 0, w, h))
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
sx, sy := x, y
if flipH {
sx = w - 1 - x
} else {
sy = h - 1 - y
}
out.Set(x, y, img.At(b.Min.X+sx, b.Min.Y+sy))
}
}
return out
}
// primarySpatialExtents returns the (width, height) from the primary
// item's ispe box. The third return value is false when ispe is
// absent or zero-valued.
func primarySpatialExtents(ct *isobmff.Container, itemID uint32) (uint32, uint32, bool) {
iprp := findIprp(ct)
if iprp == nil {
return 0, 0, false
}
for _, m := range iprp.Ipma {
for _, e := range m.Entries {
if e.ItemID != itemID {
continue
}
for _, a := range e.Associations {
if a.PropertyIndex == 0 || int(a.PropertyIndex) > len(iprp.Ipco.Properties) {
continue
}
if ispe, ok := iprp.Ipco.Properties[a.PropertyIndex-1].(*isobmff.Ispe); ok {
if ispe.Width > 0 && ispe.Height > 0 {
return ispe.Width, ispe.Height, true
}
}
}
}
}
return 0, 0, false
}
// cropToRect narrows img to rect. For standard library image types
// whose SubImage returns a shared-pixel view, we use that directly.
func cropToRect(img image.Image, rect image.Rectangle) image.Image {
type subImager interface {
SubImage(r image.Rectangle) image.Image
}
if s, ok := img.(subImager); ok {
return s.SubImage(rect)
}
return img
}
// frameToImage builds an [image.Image] from a decoded [decoder.Frame].
//
// 8-bit frames return an image.YCbCr that shares the decoded Y/U/V
// planes to avoid copies. Callers needing RGB should use
// [colorspace.ConvertPlanar420] on the YCbCr planes directly.
//
// 10/12-bit frames return an image.RGBA64 produced by
// [colorspace.ConvertPlanar420_16] — the Go stdlib has no HBD planar
// image type, so RGB conversion happens on the decode path.
// Monochrome HBD returns image.Gray16 built from the high bits of the
// luma plane.
func frameToImage(f *decoder.Frame) (image.Image, error) {
if f == nil {
return nil, fmt.Errorf("goavif: nil frame")
}
if f.BitDepth > 8 {
return frameToImage16(f)
}
if f.Monochrome {
gray := image.NewGray(image.Rect(0, 0, f.Width, f.Height))
if len(f.Y) != 0 {
copy(gray.Pix, f.Y)
}
return gray, nil
}
// Planar YUV 4:2:0 is the only chroma sampling in the still-image AVIF
// baseline we support today. 4:2:2 and 4:4:4 land with Phase 3.
sub := image.YCbCrSubsampleRatio420
switch {
case f.Subsampling.X == 1 && f.Subsampling.Y == 1:
sub = image.YCbCrSubsampleRatio420
case f.Subsampling.X == 1 && f.Subsampling.Y == 0:
sub = image.YCbCrSubsampleRatio422
case f.Subsampling.X == 0 && f.Subsampling.Y == 0:
sub = image.YCbCrSubsampleRatio444
default:
return nil, fmt.Errorf("goavif: unsupported chroma subsampling %d/%d",
f.Subsampling.X, f.Subsampling.Y)
}
img := image.NewYCbCr(image.Rect(0, 0, f.Width, f.Height), sub)
if len(f.Y) == len(img.Y) {
copy(img.Y, f.Y)
}
if len(f.U) == len(img.Cb) {
copy(img.Cb, f.U)
}
if len(f.V) == len(img.Cr) {
copy(img.Cr, f.V)
}
return img, nil
}
// frameToImage16 converts a 10/12-bit decoded frame to an image.Image
// in 16-bit-per-channel space. Monochrome frames land in Gray16;
// 4:2:0 frames convert through YUV→RGB and return RGBA64. Other
// subsamplings (4:2:2, 4:4:4) are not yet implemented for HBD.
func frameToImage16(f *decoder.Frame) (image.Image, error) {
if f.Monochrome {
img := image.NewGray16(image.Rect(0, 0, f.Width, f.Height))
shift := uint(16 - f.BitDepth)
for i, v := range f.Y16 {
s := uint16(v) << shift
img.Pix[i*2+0] = uint8(s >> 8)
img.Pix[i*2+1] = uint8(s & 0xFF)
}
return img, nil
}
img := image.NewRGBA64(image.Rect(0, 0, f.Width, f.Height))
mc := colorspace.MCBT709
rng := colorspace.Studio
if f.Seq != nil {
if f.Seq.Color.ColorRange {
rng = colorspace.Full
}
if cicp := colorspace.MatrixCoefficients(f.Seq.Color.MatrixCoefficients); cicp != colorspace.MCUnspecified {
mc = cicp
}
}
colorspace.ConvertPlanar16(img.Pix, f.Y16, f.U16, f.V16, f.Width, f.Height,
int(f.Subsampling.X), int(f.Subsampling.Y), mc, rng, f.BitDepth)
return img, nil
}
// extractSequenceHeader finds the av1C property associated with itemID and
// parses its first OBU_SEQUENCE_HEADER OBU (there must be exactly one per
// AVIF spec). The av1C ConfigOBUs blob is encoded without OBU size fields,
// so we parse directly with an implicit length.
func extractSequenceHeader(ct *isobmff.Container, itemID uint32) (*obu.SequenceHeader, error) {
iprp := findIprp(ct)
if iprp == nil {
return nil, fmt.Errorf("goavif: no iprp")
}
var av1c *isobmff.Av1C
for _, m := range iprp.Ipma {
for _, e := range m.Entries {
if e.ItemID != itemID {
continue
}
for _, a := range e.Associations {
if a.PropertyIndex == 0 || int(a.PropertyIndex) > len(iprp.Ipco.Properties) {
continue
}
if c, ok := iprp.Ipco.Properties[a.PropertyIndex-1].(*isobmff.Av1C); ok {
av1c = c
}
}
}
}
if av1c == nil {
return nil, fmt.Errorf("goavif: item %d has no av1C", itemID)
}
// av1C's ConfigOBUs blob carries OBUs that do have a size field per the
// AV1-in-ISOBMFF binding (§2.3), so Split works directly.
obus, err := obu.Split(av1c.ConfigOBUs)
if err != nil {
return nil, fmt.Errorf("goavif: av1C OBU split: %w", err)
}
for _, u := range obus {
if u.Header.Type == obu.TypeSequenceHeader {
sh, err := obu.ParseSequenceHeader(u.Payload)
if err != nil {
return nil, fmt.Errorf("goavif: av1C sequence header: %w", err)
}
return sh, nil
}
}
return nil, fmt.Errorf("goavif: av1C has no sequence header OBU")
}
// imageToYUV420 extracts BT.601 Y/Cb/Cr planes from an image at
// 4:2:0 subsampling. Convenience wrapper for [imageToYUV].
func imageToYUV420(m image.Image) (y, u, v []uint8) {
return imageToYUV(m, 1, 1)
}
// imageToYUV extracts BT.601 Y/Cb/Cr planes at the given chroma
// subsampling factors (subX / subY ∈ {0, 1}):
//
// - 4:2:0: subX=1, subY=1 → chroma is (w/2)×(h/2), 2×2 box-averaged
// - 4:2:2: subX=1, subY=0 → chroma is (w/2)×h, 2×1 horizontal average
// - 4:4:4: subX=0, subY=0 → chroma is w×h, no averaging
//
// Fast paths avoid m.At's per-pixel interface allocation for the
// common *image.RGBA / *image.NRGBA / *image.YCbCr types; other
// types fall through to the generic m.At path.
func imageToYUV(m image.Image, subX, subY int) (y, u, v []uint8) {
// *image.YCbCr fast path: if the source's native subsampling
// matches the requested output layout, copy planes directly
// and skip the RGB round-trip entirely.
if yc, ok := m.(*image.YCbCr); ok {
if srcSubX, srcSubY, ok := ycbcrSubFactors(yc.SubsampleRatio); ok && srcSubX == subX && srcSubY == subY {
return copyYCbCrPlanes(yc)
}
}
bounds := m.Bounds()
w, h := bounds.Dx(), bounds.Dy()
y = make([]uint8, w*h)
cw := w >> subX
ch := h >> subY
if cw < 1 {
cw = 1
}
if ch < 1 {
ch = 1
}
u = make([]uint8, cw*ch)
v = make([]uint8, cw*ch)
uf := make([]int, w*h)
vf := make([]int, w*h)
readRGB := rgbReader(m)
for r := 0; r < h; r++ {
for c := 0; c < w; c++ {
R, G, B := readRGB(bounds.Min.X+c, bounds.Min.Y+r)
yv := (66*R + 129*G + 25*B + 128) >> 8
uv := (-38*R - 74*G + 112*B + 128) >> 8
vv := (112*R - 94*G - 18*B + 128) >> 8
y[r*w+c] = clampByte(yv + 16)
uf[r*w+c] = uv + 128
vf[r*w+c] = vv + 128
}
}
// Chroma box-average: (1<<subX) × (1<<subY) samples per chroma cell.
sx := 1 << subX
sy := 1 << subY
for cr := 0; cr < ch; cr++ {
for cc := 0; cc < cw; cc++ {
su, sv := 0, 0
n := 0
for dy := 0; dy < sy && cr*sy+dy < h; dy++ {
for dx := 0; dx < sx && cc*sx+dx < w; dx++ {
idx := (cr*sy+dy)*w + (cc*sx + dx)
su += uf[idx]
sv += vf[idx]
n++
}
}
if n > 0 {
u[cr*cw+cc] = clampByte(su / n)
v[cr*cw+cc] = clampByte(sv / n)
}
}
}
return y, u, v
}
// ycbcrSubFactors maps a YCbCr SubsampleRatio to (subX, subY). Returns
// ok=false for ratios we don't handle (currently everything but
// 4:2:0 / 4:2:2 / 4:4:4).
func ycbcrSubFactors(r image.YCbCrSubsampleRatio) (subX, subY int, ok bool) {
switch r {
case image.YCbCrSubsampleRatio420:
return 1, 1, true
case image.YCbCrSubsampleRatio422:
return 1, 0, true
case image.YCbCrSubsampleRatio444:
return 0, 0, true
}
return 0, 0, false
}
// copyYCbCrPlanes returns tightly-packed copies of yc's planes. The
// result matches the encoder tile writer's row-major convention
// (stride == width for every plane).
func copyYCbCrPlanes(yc *image.YCbCr) (y, u, v []uint8) {
b := yc.Rect
w, h := b.Dx(), b.Dy()
y = make([]uint8, w*h)
for r := 0; r < h; r++ {
yiStart := yc.YOffset(b.Min.X, b.Min.Y+r)
copy(y[r*w:r*w+w], yc.Y[yiStart:yiStart+w])
}
var chromaSubY int
cw, ch := w, h
switch yc.SubsampleRatio {
case image.YCbCrSubsampleRatio420:
cw, ch = w>>1, h>>1
chromaSubY = 1
case image.YCbCrSubsampleRatio422:
cw = w >> 1
}
if cw < 1 {
cw = 1
}
if ch < 1 {
ch = 1
}
u = make([]uint8, cw*ch)
v = make([]uint8, cw*ch)
for r := 0; r < ch; r++ {
// Walk chroma rows at full-res Y intervals when subY=1.
cOff := yc.COffset(b.Min.X, b.Min.Y+(r<<uint(chromaSubY)))
copy(u[r*cw:r*cw+cw], yc.Cb[cOff:cOff+cw])
copy(v[r*cw:r*cw+cw], yc.Cr[cOff:cOff+cw])
}
return y, u, v
}
// rgbReader returns a closure that reads 8-bit R/G/B components at
// image coordinates (x, y). It specialises on the concrete image
// type to avoid the Color-interface allocation that m.At does every
// pixel.
func rgbReader(m image.Image) func(x, y int) (R, G, B int) {
switch src := m.(type) {
case *image.RGBA:
return func(x, y int) (int, int, int) {
i := (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
// RGBA stores premultiplied alpha; for a fully-opaque
// source (which is the common case here) this equals the
// non-premultiplied value. For translucent pixels the
// chroma is computed from premultiplied samples, which is
// consistent with how Color.RGBA() reports them.
return int(src.Pix[i]), int(src.Pix[i+1]), int(src.Pix[i+2])
}
case *image.NRGBA:
return func(x, y int) (int, int, int) {
i := (y-src.Rect.Min.Y)*src.Stride + (x-src.Rect.Min.X)*4
return int(src.Pix[i]), int(src.Pix[i+1]), int(src.Pix[i+2])
}
case *image.YCbCr:
return func(x, y int) (int, int, int) {
yi := src.YOffset(x, y)
ci := src.COffset(x, y)
Y := int(src.Y[yi])
Cb := int(src.Cb[ci])
Cr := int(src.Cr[ci])
// BT.601 YCbCr → RGB. Standard library uses the same
// formula in image.YCbCrToRGB.
r := (298*(Y-16) + 409*(Cr-128) + 128) >> 8
g := (298*(Y-16) - 100*(Cb-128) - 208*(Cr-128) + 128) >> 8
b := (298*(Y-16) + 516*(Cb-128) + 128) >> 8
return clampInt(r), clampInt(g), clampInt(b)
}
}
// Fallback: generic m.At. Allocates a Color per pixel for
// image types we don't specialise.
bounds := m.Bounds()
_ = bounds
return func(x, y int) (int, int, int) {
rr, gg, bb, _ := m.At(x, y).RGBA()
return int(rr >> 8), int(gg >> 8), int(bb >> 8)
}
}
func clampInt(v int) int {
if v < 0 {
return 0
}
if v > 255 {
return 255
}
return v
}
// padToMultiple returns an image whose dimensions are the smallest
// multiples of align ≥ the source dimensions. Border rows/columns
// repeat the last source pixel (edge-extend). The original image is
// returned unchanged when already aligned.
//
// The returned image has the same concrete type as the input for
// the types we specialize on (*image.RGBA, *image.NRGBA,
// *image.Gray, *image.NRGBA64, *image.RGBA64, *image.Gray16,
// *image.YCbCr); other types drop through to a generic *image.RGBA
// container built via m.At.
func padToMultiple(m image.Image, align int) image.Image {
bounds := m.Bounds()
w, h := bounds.Dx(), bounds.Dy()
pw := ((w + align - 1) / align) * align
ph := ((h + align - 1) / align) * align
if pw == w && ph == h {
return m
}
// Build a fresh image of the padded size. Start by copying the
// source into the top-left, then edge-extend.
switch src := m.(type) {
case *image.RGBA:
return padRGBALike(src.Pix, src.Stride, 4, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.RGBA{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
case *image.NRGBA:
return padRGBALike(src.Pix, src.Stride, 4, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.NRGBA{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
case *image.Gray:
return padRGBALike(src.Pix, src.Stride, 1, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.Gray{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
case *image.RGBA64:
return padRGBALike(src.Pix, src.Stride, 8, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.RGBA64{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
case *image.NRGBA64:
return padRGBALike(src.Pix, src.Stride, 8, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.NRGBA64{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
case *image.Gray16:
return padRGBALike(src.Pix, src.Stride, 2, bounds, pw, ph, func(pix []uint8, stride, pw, ph int) image.Image {
return &image.Gray16{Pix: pix, Stride: stride, Rect: image.Rect(0, 0, pw, ph)}
})
}
// Generic fallback: rebuild as RGBA via m.At.
dst := image.NewRGBA(image.Rect(0, 0, pw, ph))
for y := 0; y < ph; y++ {
sy := y
if sy >= h {
sy = h - 1
}
for x := 0; x < pw; x++ {
sx := x
if sx >= w {
sx = w - 1
}
dst.Set(x, y, m.At(bounds.Min.X+sx, bounds.Min.Y+sy))
}
}
return dst
}
// padRGBALike is the shared edge-extend routine for all Pix-based
// image types. bpp is the bytes-per-pixel for the concrete type.
func padRGBALike(srcPix []uint8, srcStride, bpp int, bounds image.Rectangle, pw, ph int,
build func(pix []uint8, stride, pw, ph int) image.Image) image.Image {
w, h := bounds.Dx(), bounds.Dy()
dstStride := pw * bpp
dst := make([]uint8, ph*dstStride)
// Copy source rows into the top of dst.
for y := 0; y < h; y++ {
srcRow := (bounds.Min.Y+y)*srcStride + bounds.Min.X*bpp
dstRow := y * dstStride
copy(dst[dstRow:dstRow+w*bpp], srcPix[srcRow:srcRow+w*bpp])
// Extend right edge.
for x := w; x < pw; x++ {
copy(dst[dstRow+x*bpp:dstRow+(x+1)*bpp], dst[dstRow+(w-1)*bpp:dstRow+w*bpp])
}
}
// Extend bottom edge by repeating last source row.
if h > 0 {
lastRow := dst[(h-1)*dstStride : h*dstStride]
for y := h; y < ph; y++ {
copy(dst[y*dstStride:(y+1)*dstStride], lastRow)
}
}
return build(dst, dstStride, pw, ph)
}
func clampByte(v int) uint8 {
if v < 0 {
return 0
}
if v > 255 {
return 255
}
return uint8(v)
}
// isGrayscale reports whether m should be encoded as a monochrome
// AV1 item. Currently recognizes image.Gray and image.Gray16.
func isGrayscale(m image.Image) bool {
switch m.(type) {
case *image.Gray, *image.Gray16:
return true
}
return false
}
// hbdBitDepth picks a bit depth for the encoded primary item.
//
// Explicit opts.BitDepth wins when in {8, 10, 12}. Otherwise the
// decision comes from the input image type: 16-bit-per-channel
// Go types (NRGBA64 / RGBA64 / Gray16) opt in to 10-bit encoding;
// 8-bit types default to 8-bit.
func hbdBitDepth(m image.Image, opts *Options) int {
if opts != nil && opts.BitDepth != 0 {
switch opts.BitDepth {
case 8:
return 8
case 10:
return 10
case 12:
return 12
}
}
switch m.(type) {
case *image.NRGBA64, *image.RGBA64, *image.Gray16:
return 10
}
return 8
}
// imageToLuma16 extracts a w*h HBD luma plane. Samples are in
// [0, (1<<bitDepth)-1], compressed from the input range into BT.601
// studio luma when applicable.
func imageToLuma16(m image.Image, bitDepth int) []uint16 {
bounds := m.Bounds()
w, h := bounds.Dx(), bounds.Dy()
out := make([]uint16, w*h)
// Studio luma range at N bits: [16 << (N-8), 235 << (N-8)].
offset := 16 << uint(bitDepth-8)
scale := 219 << uint(bitDepth-8)
maxV := (1 << uint(bitDepth)) - 1
switch src := m.(type) {
case *image.Gray:
for y := 0; y < h; y++ {
base := (bounds.Min.Y+y-src.Rect.Min.Y)*src.Stride + (bounds.Min.X - src.Rect.Min.X)
for x := 0; x < w; x++ {
v := int(src.Pix[base+x])
out[y*w+x] = clampU16(offset+(v*scale+128)>>8, maxV)
}
}
return out
case *image.Gray16:
for y := 0; y < h; y++ {
base := (bounds.Min.Y+y-src.Rect.Min.Y)*src.Stride + (bounds.Min.X-src.Rect.Min.X)*2
for x := 0; x < w; x++ {
// Downscale big-endian 16-bit Gray to bitDepth.
v16 := int(src.Pix[base+x*2])<<8 | int(src.Pix[base+x*2+1])
v := v16 >> uint(16-bitDepth)
// Compress full-range to studio range.
out[y*w+x] = clampU16(offset+(v*scale+((1<<uint(bitDepth-1))))>>uint(bitDepth), maxV)
}
}
return out
}
for y := 0; y < h; y++ {
for x := 0; x < w; x++ {
yc, _, _, _ := m.At(bounds.Min.X+x, bounds.Min.Y+y).RGBA()
v := int(yc) >> uint(16-bitDepth)
out[y*w+x] = clampU16(offset+(v*scale+((1<<uint(bitDepth-1))))>>uint(bitDepth), maxV)
}
}
return out
}
// imageToYUV420_16 extracts BT.601 Y/Cb/Cr planes at the given bit
// depth with 4:2:0 subsampling.
func imageToYUV420_16(m image.Image, bitDepth int) (y, u, v []uint16) {
return imageToYUV16(m, bitDepth, 1, 1)
}
// imageToYUV16 is the HBD counterpart of [imageToYUV]. Output
// samples occupy [0, (1<<bitDepth)-1].
func imageToYUV16(m image.Image, bitDepth, subX, subY int) (y, u, v []uint16) {
bounds := m.Bounds()
w, h := bounds.Dx(), bounds.Dy()
y = make([]uint16, w*h)
cw := w >> subX
ch := h >> subY
if cw < 1 {
cw = 1
}
if ch < 1 {
ch = 1
}
u = make([]uint16, cw*ch)
v = make([]uint16, cw*ch)
uf := make([]int, w*h)
vf := make([]int, w*h)
readRGB := rgbReader16(m, bitDepth)
maxV := (1 << uint(bitDepth)) - 1
shift := uint(bitDepth - 8)
for r := 0; r < h; r++ {
for c := 0; c < w; c++ {
R, G, B := readRGB(bounds.Min.X+c, bounds.Min.Y+r)
yv := (66*R + 129*G + 25*B + (128 << shift)) >> 8
uv := (-38*R - 74*G + 112*B + (128 << shift)) >> 8
vv := (112*R - 94*G - 18*B + (128 << shift)) >> 8
y[r*w+c] = clampU16(yv+(16<<shift), maxV)
uf[r*w+c] = uv + (128 << shift)
vf[r*w+c] = vv + (128 << shift)
}
}
sx := 1 << subX
sy := 1 << subY
for cr := 0; cr < ch; cr++ {
for cc := 0; cc < cw; cc++ {
su, sv := 0, 0
n := 0
for dy := 0; dy < sy && cr*sy+dy < h; dy++ {
for dx := 0; dx < sx && cc*sx+dx < w; dx++ {
idx := (cr*sy+dy)*w + (cc*sx + dx)
su += uf[idx]
sv += vf[idx]
n++
}
}
if n > 0 {
u[cr*cw+cc] = clampU16(su/n, maxV)
v[cr*cw+cc] = clampU16(sv/n, maxV)
}
}
}
return y, u, v
}