From f02828a17643d68e4a4f967502af7b5e70b620a9 Mon Sep 17 00:00:00 2001 From: Scott Kidder Date: Thu, 28 Nov 2024 19:20:54 +0000 Subject: [PATCH] docs: Add function comments, no functional changes. --- avcodec.go | 22 +++++++++ giflib.go | 35 ++++++++++++-- opencv.go | 132 ++++++++++++++++++++++++++++----------------------- ops.go | 66 +++++++++++++++++--------- thumbhash.go | 10 ++++ webp.go | 25 ++++++++++ 6 files changed, 204 insertions(+), 86 deletions(-) diff --git a/avcodec.go b/avcodec.go index 7070059f..3c959e7a 100644 --- a/avcodec.go +++ b/avcodec.go @@ -16,6 +16,7 @@ const atomHeaderSize = 8 // Enable by building/running with "-ldflags=-X=github.com/discord/lilliput.hevcEnabled=true" var hevcEnabled string +// avCodecDecoder handles decoding of various video/image formats using FFmpeg's avcodec. type avCodecDecoder struct { decoder C.avcodec_decoder mat C.opencv_mat @@ -26,6 +27,8 @@ type avCodecDecoder struct { hasSubtitles bool } +// newAVCodecDecoder creates a new decoder instance from the provided buffer. +// Returns an error if the buffer is too small or contains invalid data. func newAVCodecDecoder(buf []byte) (*avCodecDecoder, error) { mat := createMatFromBytes(buf) if mat == nil { @@ -48,18 +51,24 @@ func newAVCodecDecoder(buf []byte) (*avCodecDecoder, error) { }, nil } +// createMatFromBytes creates an OpenCV matrix from a byte buffer. +// The matrix is created as a single-channel 8-bit unsigned type. func createMatFromBytes(buf []byte) C.opencv_mat { return C.opencv_mat_create_from_data(C.int(len(buf)), 1, C.CV_8U, unsafe.Pointer(&buf[0]), C.size_t(len(buf))) } +// hasSubtitles checks if the decoder has detected any subtitle streams. func hasSubtitles(d C.avcodec_decoder) bool { return bool(C.avcodec_decoder_has_subtitles(d)) } +// isStreamable determines if the media content can be streamed. func isStreamable(mat C.opencv_mat) bool { return bool(C.avcodec_decoder_is_streamable(mat)) } +// Description returns the format description of the media. +// Special handling is included to differentiate between MOV and MP4 formats. func (d *avCodecDecoder) Description() string { fmt := C.GoString(C.avcodec_decoder_get_description(d.decoder)) @@ -71,22 +80,27 @@ func (d *avCodecDecoder) Description() string { return fmt } +// HasSubtitles returns whether the media contains subtitle streams. func (d *avCodecDecoder) HasSubtitles() bool { return d.hasSubtitles } +// IsStreamable returns whether the media content can be streamed. func (d *avCodecDecoder) IsStreamable() bool { return d.isStreamable } +// BackgroundColor returns the default background color (white). func (d *avCodecDecoder) BackgroundColor() uint32 { return 0xFFFFFFFF } +// LoopCount returns the number of times the media should loop (0 for no looping). func (d *avCodecDecoder) LoopCount() int { return 0 } +// ICC returns the ICC color profile data if present, or an empty slice if not. func (d *avCodecDecoder) ICC() []byte { iccDst := make([]byte, 8192) iccLength := C.avcodec_decoder_get_icc(d.decoder, unsafe.Pointer(&iccDst[0]), C.size_t(cap(iccDst))) @@ -96,10 +110,13 @@ func (d *avCodecDecoder) ICC() []byte { return iccDst[:iccLength] } +// Duration returns the total duration of the media content. func (d *avCodecDecoder) Duration() time.Duration { return time.Duration(float64(C.avcodec_decoder_get_duration(d.decoder)) * float64(time.Second)) } +// Header returns the image metadata including dimensions, pixel format, and orientation. +// Frame count is always 1 since it requires the entire buffer to be decoded. func (d *avCodecDecoder) Header() (*ImageHeader, error) { return &ImageHeader{ width: int(C.avcodec_decoder_get_width(d.decoder)), @@ -111,6 +128,8 @@ func (d *avCodecDecoder) Header() (*ImageHeader, error) { }, nil } +// DecodeTo decodes the next frame into the provided Framebuffer. +// Returns io.EOF when no more frames are available. func (d *avCodecDecoder) DecodeTo(f *Framebuffer) error { if d.hasDecoded { return io.EOF @@ -136,16 +155,19 @@ func (d *avCodecDecoder) DecodeTo(f *Framebuffer) error { return nil } +// SkipFrame attempts to skip the next frame, but is not supported by this decoder. func (d *avCodecDecoder) SkipFrame() error { return ErrSkipNotSupported } +// Close releases all resources associated with the decoder. func (d *avCodecDecoder) Close() { C.avcodec_decoder_release(d.decoder) C.opencv_mat_release(d.mat) d.buf = nil } +// init initializes the avcodec library when the package is loaded. func init() { C.avcodec_init() } diff --git a/giflib.go b/giflib.go index 38cdd3e4..6c7da028 100644 --- a/giflib.go +++ b/giflib.go @@ -11,6 +11,7 @@ import ( "unsafe" ) +// gifDecoder implements image decoding for GIF format type gifDecoder struct { decoder C.giflib_decoder mat C.opencv_mat @@ -25,6 +26,7 @@ type gifDecoder struct { bgAlpha uint8 } +// gifEncoder implements image encoding for GIF format type gifEncoder struct { encoder C.giflib_encoder decoder C.giflib_decoder @@ -41,13 +43,15 @@ var ( ErrGifEncoderNeedsDecoder = errors.New("GIF encoder needs decoder used to create image") ) -// SetGIFMaxFrameDimension sets the largest GIF width/height that can be -// decoded +// SetGIFMaxFrameDimension sets the largest GIF width/height that can be decoded. +// This helps prevent loading extremely large GIF images that could exhaust memory. func SetGIFMaxFrameDimension(dim uint64) { // TODO we should investigate if this can be removed/become a mat check in decoder atomic.StoreUint64(&gifMaxFrameDimension, dim) } +// newGifDecoder creates a new GIF decoder from the provided byte buffer. +// Returns an error if the buffer is too small or contains invalid GIF data. func newGifDecoder(buf []byte) (*gifDecoder, error) { mat := C.opencv_mat_create_from_data(C.int(len(buf)), 1, C.CV_8U, unsafe.Pointer(&buf[0]), C.size_t(len(buf))) @@ -68,6 +72,7 @@ func newGifDecoder(buf []byte) (*gifDecoder, error) { }, nil } +// Header returns the image header information including dimensions and pixel format. func (d *gifDecoder) Header() (*ImageHeader, error) { return &ImageHeader{ width: int(C.giflib_decoder_get_width(d.decoder)), @@ -79,6 +84,7 @@ func (d *gifDecoder) Header() (*ImageHeader, error) { }, nil } +// FrameHeader returns the current frame's header information. func (d *gifDecoder) FrameHeader() (*ImageHeader, error) { return &ImageHeader{ width: int(C.giflib_decoder_get_frame_width(d.decoder)), @@ -90,40 +96,46 @@ func (d *gifDecoder) FrameHeader() (*ImageHeader, error) { }, nil } +// Close releases resources associated with the decoder. func (d *gifDecoder) Close() { C.giflib_decoder_release(d.decoder) C.opencv_mat_release(d.mat) d.buf = nil } +// Description returns the image format description ("GIF"). func (d *gifDecoder) Description() string { return "GIF" } +// IsStreamable returns whether the format supports streaming decoding. func (d *gifDecoder) IsStreamable() bool { return true } +// HasSubtitles returns whether the format supports subtitles. func (d *gifDecoder) HasSubtitles() bool { return false } +// ICC returns the ICC color profile data, if any. func (d *gifDecoder) ICC() []byte { return []byte{} } +// Duration returns the total duration of the GIF animation. func (d *gifDecoder) Duration() time.Duration { return time.Duration(0) } +// BackgroundColor returns the GIF background color as a 32-bit RGBA value. func (d *gifDecoder) BackgroundColor() uint32 { d.readAnimationInfo() return uint32(d.bgRed)<<16 | uint32(d.bgGreen)<<8 | uint32(d.bgBlue) | uint32(d.bgAlpha)<<24 } -// readAnimationInfo reads the GIF info from the decoder and caches it -// this involves reading extension blocks and is relatively expensive -// so we only do it once when we need it +// readAnimationInfo reads and caches GIF animation metadata like loop count and frame count. +// This is done lazily since reading extension blocks is relatively expensive. func (d *gifDecoder) readAnimationInfo() { if !d.animationInfoRead { info := C.giflib_decoder_get_animation_info(d.decoder) @@ -137,16 +149,21 @@ func (d *gifDecoder) readAnimationInfo() { } } +// LoopCount returns the number of times the GIF animation should loop. +// A value of 0 means loop forever. func (d *gifDecoder) LoopCount() int { d.readAnimationInfo() return d.loopCount } +// FrameCount returns the total number of frames in the GIF. func (d *gifDecoder) FrameCount() int { d.readAnimationInfo() return d.frameCount } +// DecodeTo decodes the next GIF frame into the provided Framebuffer. +// Returns io.EOF when all frames have been decoded. func (d *gifDecoder) DecodeTo(f *Framebuffer) error { h, err := d.Header() if err != nil { @@ -188,6 +205,8 @@ func (d *gifDecoder) DecodeTo(f *Framebuffer) error { return nil } +// SkipFrame skips decoding of the next frame. +// Returns io.EOF when all frames have been skipped. func (d *gifDecoder) SkipFrame() error { nextFrameResult := int(C.giflib_decoder_skip_frame(d.decoder)) @@ -201,6 +220,8 @@ func (d *gifDecoder) SkipFrame() error { return nil } +// newGifEncoder creates a new GIF encoder that will write to the provided buffer. +// Requires the original decoder that was used to decode the source GIF. func newGifEncoder(decodedBy Decoder, buf []byte) (*gifEncoder, error) { // we must have a decoder since we can't build our own palettes // so if we don't get a gif decoder, bail out @@ -227,6 +248,8 @@ func newGifEncoder(decodedBy Decoder, buf []byte) (*gifEncoder, error) { }, nil } +// Encode encodes a frame into the GIF. If f is nil, flushes any remaining data +// and returns the complete encoded GIF. Returns io.EOF after flushing. func (e *gifEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { if e.hasFlushed { return nil, io.EOF @@ -259,10 +282,12 @@ func (e *gifEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { return nil, nil } +// Close releases resources associated with the encoder. func (e *gifEncoder) Close() { C.giflib_encoder_release(e.encoder) } +// init initializes the GIF decoder with default settings func init() { SetGIFMaxFrameDimension(defaultMaxFrameDimension) } diff --git a/opencv.go b/opencv.go index db8c9f41..a14e45d2 100644 --- a/opencv.go +++ b/opencv.go @@ -17,7 +17,9 @@ import ( type DisposeMethod int const ( + // NoDispose indicates the previous frame should remain as-is NoDispose DisposeMethod = iota + // DisposeToBackgroundColor indicates the previous frame area should be cleared to background color DisposeToBackgroundColor ) @@ -25,7 +27,9 @@ const ( type BlendMethod int const ( + // UseAlphaBlending indicates alpha blending should be used when compositing frames UseAlphaBlending BlendMethod = iota + // NoBlend indicates frames should be copied directly without blending NoBlend ) @@ -33,12 +37,13 @@ const ( type ImageOrientation int const ( - JpegQuality = int(C.CV_IMWRITE_JPEG_QUALITY) - PngCompression = int(C.CV_IMWRITE_PNG_COMPRESSION) - WebpQuality = int(C.CV_IMWRITE_WEBP_QUALITY) - - JpegProgressive = int(C.CV_IMWRITE_JPEG_PROGRESSIVE) + // Standard image encoding constants + JpegQuality = int(C.CV_IMWRITE_JPEG_QUALITY) // Quality parameter for JPEG encoding (0-100) + PngCompression = int(C.CV_IMWRITE_PNG_COMPRESSION) // Compression level for PNG encoding (0-9) + WebpQuality = int(C.CV_IMWRITE_WEBP_QUALITY) // Quality parameter for WebP encoding (0-100) + JpegProgressive = int(C.CV_IMWRITE_JPEG_PROGRESSIVE) // Enable progressive JPEG encoding + // Image orientation constants OrientationTopLeft = ImageOrientation(C.CV_IMAGE_ORIENTATION_TL) OrientationTopRight = ImageOrientation(C.CV_IMAGE_ORIENTATION_TR) OrientationBottomRight = ImageOrientation(C.CV_IMAGE_ORIENTATION_BR) @@ -48,32 +53,35 @@ const ( OrientationRightBottom = ImageOrientation(C.CV_IMAGE_ORIENTATION_RB) OrientationLeftBottom = ImageOrientation(C.CV_IMAGE_ORIENTATION_LB) + // PNG chunk field lengths pngChunkSizeFieldLen = 4 pngChunkTypeFieldLen = 4 pngChunkAllFieldsLen = 12 - jpegEOISegmentType byte = 0xD9 - jpegSOSSegmentType byte = 0xDA + // JPEG segment type markers + jpegEOISegmentType byte = 0xD9 // End of Image marker + jpegSOSSegmentType byte = 0xDA // Start of Scan marker ) +// PNG chunk type identifiers var ( - pngActlChunkType = []byte{byte('a'), byte('c'), byte('T'), byte('L')} - pngFctlChunkType = []byte{byte('f'), byte('c'), byte('T'), byte('L')} - pngFdatChunkType = []byte{byte('f'), byte('d'), byte('A'), byte('T')} - pngIendChunkType = []byte{byte('I'), byte('E'), byte('N'), byte('D')} + pngActlChunkType = []byte{byte('a'), byte('c'), byte('T'), byte('L')} // Animation Control Chunk + pngFctlChunkType = []byte{byte('f'), byte('c'), byte('T'), byte('L')} // Frame Control Chunk + pngFdatChunkType = []byte{byte('f'), byte('d'), byte('A'), byte('T')} // Frame Data Chunk + pngIendChunkType = []byte{byte('I'), byte('E'), byte('N'), byte('D')} // Image End Chunk - // Helpful: https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure + // Map of JPEG segment types that don't have a size field jpegUnsizedSegmentTypes = map[byte]bool{ - 0xD0: true, // RST segments - 0xD1: true, - 0xD2: true, - 0xD3: true, - 0xD4: true, - 0xD5: true, - 0xD6: true, - 0xD7: true, // end RST segments - 0xD8: true, // SOI - jpegEOISegmentType: true, + 0xD0: true, // RST0 marker + 0xD1: true, // RST1 marker + 0xD2: true, // RST2 marker + 0xD3: true, // RST3 marker + 0xD4: true, // RST4 marker + 0xD5: true, // RST5 marker + 0xD6: true, // RST6 marker + 0xD7: true, // RST7 marker + 0xD8: true, // SOI marker + jpegEOISegmentType: true, // EOI marker } ) @@ -82,40 +90,42 @@ type PixelType int // ImageHeader contains basic decoded image metadata. type ImageHeader struct { - width int - height int - pixelType PixelType - orientation ImageOrientation - numFrames int - contentLength int + width int // Width of the image in pixels + height int // Height of the image in pixels + pixelType PixelType // Type of pixels in the image + orientation ImageOrientation // Orientation from image metadata + numFrames int // Number of frames (1 for static images) + contentLength int // Length of actual image content } // Framebuffer contains an array of raw, decoded pixel data. type Framebuffer struct { - buf []byte - mat C.opencv_mat - width int - height int - pixelType PixelType - duration time.Duration - xOffset int - yOffset int - dispose DisposeMethod - blend BlendMethod -} - + buf []byte // Raw pixel data + mat C.opencv_mat // OpenCV matrix containing the pixel data + width int // Width of the frame in pixels + height int // Height of the frame in pixels + pixelType PixelType // Type of pixels in the frame + duration time.Duration // Duration to display this frame + xOffset int // X offset for drawing this frame + yOffset int // Y offset for drawing this frame + dispose DisposeMethod // How to dispose previous frame + blend BlendMethod // How to blend with previous frame +} + +// openCVDecoder implements the Decoder interface for images supported by OpenCV. type openCVDecoder struct { - decoder C.opencv_decoder - mat C.opencv_mat - buf []byte - hasReadHeader bool - hasDecoded bool + decoder C.opencv_decoder // Native OpenCV decoder + mat C.opencv_mat // OpenCV matrix containing the image data + buf []byte // Original encoded image data + hasReadHeader bool // Whether header has been read + hasDecoded bool // Whether image has been decoded } +// openCVEncoder implements the Encoder interface for images supported by OpenCV. type openCVEncoder struct { - encoder C.opencv_encoder - dst C.opencv_mat - dstBuf []byte + encoder C.opencv_encoder // Native OpenCV encoder + dst C.opencv_mat // Destination OpenCV matrix + dstBuf []byte // Destination buffer for encoded data } // Depth returns the number of bits in the PixelType. @@ -143,29 +153,30 @@ func (h *ImageHeader) PixelType() PixelType { return h.pixelType } -// ImageOrientation returns the metadata-based image orientation. +// Orientation returns the metadata-based image orientation. func (h *ImageHeader) Orientation() ImageOrientation { return h.orientation } +// IsAnimated returns true if the image contains multiple frames. func (h *ImageHeader) IsAnimated() bool { return h.numFrames > 1 } +// HasAlpha returns true if the image has an alpha channel. func (h *ImageHeader) HasAlpha() bool { return h.pixelType.Channels() == 4 } -// Some images have extra padding bytes at the end that aren't needed. -// In the worst case, this might be unwanted data that the user intended -// to crop (e.g. "acropalypse" bug). -// This function returns the length of the necessary image data. Data -// past this point can be safely truncated `data[:h.ContentLength()]` +// ContentLength returns the length of the necessary image data. +// Data past this point can be safely truncated using data[:h.ContentLength()]. +// This helps handle padding bytes and potential unwanted trailing data. +// This could be applicable to images with unwanted data at the end (e.g. "acropalypse" bug). func (h *ImageHeader) ContentLength() int { return h.contentLength } -// NewFramebuffer creates the backing store for a pixel frame buffer. +// NewFramebuffer creates a backing store for a pixel frame buffer with the specified dimensions. func NewFramebuffer(width, height int) *Framebuffer { return &Framebuffer{ buf: make([]byte, width*height*4), @@ -181,8 +192,7 @@ func (f *Framebuffer) Close() { } } -// Clear resets all of the pixel data in Framebuffer for the active frame -// It also resets the mat if it exists. +// Clear resets all pixel data in Framebuffer for the active frame and resets the mat if it exists. func (f *Framebuffer) Clear() { C.memset(unsafe.Pointer(&f.buf[0]), 0, C.size_t(len(f.buf))) if f.mat != nil { @@ -190,6 +200,7 @@ func (f *Framebuffer) Clear() { } } +// Create3Channel initializes the framebuffer for 3-channel (RGB) image data. func (f *Framebuffer) Create3Channel(width, height int) error { if err := f.resizeMat(width, height, C.CV_8UC3); err != nil { return err @@ -198,6 +209,7 @@ func (f *Framebuffer) Create3Channel(width, height int) error { return nil } +// Create4Channel initializes the framebuffer for 4-channel (RGBA) image data. func (f *Framebuffer) Create4Channel(width, height int) error { if err := f.resizeMat(width, height, C.CV_8UC4); err != nil { return err @@ -206,6 +218,8 @@ func (f *Framebuffer) Create4Channel(width, height int) error { return nil } +// resizeMat resizes the OpenCV matrix to the specified dimensions and pixel type. +// Returns ErrBufTooSmall if the matrix cannot be created at the specified size. func (f *Framebuffer) resizeMat(width, height int, pixelType PixelType) error { if f.mat != nil { C.opencv_mat_release(f.mat) @@ -225,8 +239,8 @@ func (f *Framebuffer) resizeMat(width, height int, pixelType PixelType) error { return nil } -// OrientationTransform rotates and/or mirrors the Framebuffer. Passing the -// orientation given by the ImageHeader will normalize the orientation of the Framebuffer. +// OrientationTransform rotates and/or mirrors the Framebuffer according to the given orientation. +// Passing the orientation from ImageHeader will normalize the orientation. func (f *Framebuffer) OrientationTransform(orientation ImageOrientation) { if f.mat == nil { return diff --git a/ops.go b/ops.go index c03a0304..c6ce6d3b 100644 --- a/ops.go +++ b/ops.go @@ -61,7 +61,8 @@ type ImageOps struct { } // NewImageOps creates a new ImageOps object that will operate -// on images up to maxSize on each axis. +// on images up to maxSize on each axis. It initializes two framebuffers +// for double-buffering operations. func NewImageOps(maxSize int) *ImageOps { frames := make([]*Framebuffer, 2) frames[0] = NewFramebuffer(maxSize, maxSize) @@ -72,21 +73,24 @@ func NewImageOps(maxSize int) *ImageOps { } } +// active returns the currently active framebuffer used for operations func (o *ImageOps) active() *Framebuffer { return o.frames[o.frameIndex] } +// secondary returns the secondary framebuffer used for double-buffering operations func (o *ImageOps) secondary() *Framebuffer { return o.frames[1-o.frameIndex] } +// swap toggles between the active and secondary framebuffers func (o *ImageOps) swap() { o.frameIndex = 1 - o.frameIndex } -// Clear resets all pixel data in ImageOps. This need not be called -// between calls to Transform. You may choose to call this to remove -// image data from memory. +// Clear frees the pixel data held in all framebuffers. While not required between +// Transform operations, you can call this to reduce memory usage when the ImageOps +// object will be idle for a while. func (o *ImageOps) Clear() { o.frames[0].Clear() o.frames[1].Clear() @@ -105,8 +109,9 @@ func (o *ImageOps) Close() { } } -// setupAnimatedFrameBuffers sets up the animated frame buffer. -// It returns an error if the frame could not be created. +// setupAnimatedFrameBuffers initializes the composite buffer needed for animated image processing. +// It creates a buffer with the appropriate number of channels based on whether the image has alpha. +// Returns an error if buffer creation fails. func (o *ImageOps) setupAnimatedFrameBuffers(d Decoder, inputCanvasWidth, inputCanvasHeight int, hasAlpha bool) error { // Create a buffer to hold the composite of the current frame and the previous frame if o.animatedCompositeBuffer == nil { @@ -127,15 +132,16 @@ func (o *ImageOps) setupAnimatedFrameBuffers(d Decoder, inputCanvasWidth, inputC return nil } -// decode decodes the active frame from the decoder specified by d. +// decode reads the current frame from the decoder into the active framebuffer. +// Returns an error if decoding fails. func (o *ImageOps) decode(d Decoder) error { active := o.active() return d.DecodeTo(active) } -// fit fits the active frame to the specified output canvas size. -// It returns true if the frame was resized and false if it was not. -// It returns an error if the frame could not be resized. +// fit resizes the active frame to fit within the specified dimensions while maintaining aspect ratio. +// For animated images, it handles frame compositing and disposal. +// Returns (true, nil) if resizing was performed successfully, (false, error) if an error occurred. func (o *ImageOps) fit(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCanvasWidth, outputCanvasHeight int, isAnimated, hasAlpha bool) (bool, error) { newWidth, newHeight := calculateExpectedSize(inputCanvasWidth, inputCanvasHeight, outputCanvasWidth, outputCanvasHeight) @@ -171,7 +177,9 @@ func (o *ImageOps) fit(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCan return true, nil } -// resize resizes the active frame to the specified output canvas size. +// resize scales the active frame to exactly match the specified dimensions. +// For animated images, it handles frame compositing and disposal. +// Returns (true, nil) if resizing was performed successfully, (false, error) if an error occurred. func (o *ImageOps) resize(d Decoder, inputCanvasWidth, inputCanvasHeight, outputCanvasWidth, outputCanvasHeight, frameCount int, isAnimated, hasAlpha bool) (bool, error) { // If the image is animated, we need to resize the frame to the input canvas size // and then copy the previous frame's data to the working buffer. @@ -204,6 +212,9 @@ func (o *ImageOps) resize(d Decoder, inputCanvasWidth, inputCanvasHeight, output return true, nil } +// calculateExpectedSize determines the final dimensions for an image based on +// original and requested sizes, handling special cases for square resizing +// and oversized requests. func calculateExpectedSize(origWidth, origHeight, reqWidth, reqHeight int) (int, int) { if reqWidth == reqHeight && reqWidth > min(origWidth, origHeight) { // Square resize request larger than smaller original dimension @@ -218,6 +229,7 @@ func calculateExpectedSize(origWidth, origHeight, reqWidth, reqHeight int) (int, } } +// min returns the smaller of two integers func min(a, b int) int { if a < b { return a @@ -225,24 +237,28 @@ func min(a, b int) int { return b } -// normalizeOrientation flips and rotates the active frame to undo EXIF orientation. +// normalizeOrientation applies EXIF orientation corrections to the active frame +// by performing the necessary flips and rotations. func (o *ImageOps) normalizeOrientation(orientation ImageOrientation) { active := o.active() active.OrientationTransform(orientation) } -// encode encodes the active frame using the encoder specified by e. +// encode writes the active frame to an encoded format using the provided encoder +// and encoding options. Returns the encoded bytes or an error. func (o *ImageOps) encode(e Encoder, opt map[int]int) ([]byte, error) { active := o.active() return e.Encode(active, opt) } -// encodeEmpty encodes an empty frame using the encoder specified by e. +// encodeEmpty signals the encoder to finalize the encoding process without +// additional frame data. Used for handling animation termination. func (o *ImageOps) encodeEmpty(e Encoder, opt map[int]int) ([]byte, error) { return e.Encode(nil, opt) } -// skipToEnd skips to the end of the animation specified by d. +// skipToEnd advances the decoder to the final frame of an animation. +// Returns io.EOF when the end is reached or an error if seeking fails. func (o *ImageOps) skipToEnd(d Decoder) error { var err error for { @@ -353,9 +369,9 @@ func (o *ImageOps) Transform(d Decoder, opt *ImageOptions, dst []byte) ([]byte, } } -// transformCurrentFrame transforms the current frame using the decoder specified by d. -// It returns true if the frame was resized and false if it was not. -// It returns an error if the frame could not be resized. +// transformCurrentFrame applies the requested resize operation to the current frame. +// Handles both static and animated images, managing frame compositing when needed. +// Returns (true, nil) if transformation was performed, (false, error) if an error occurred. func (o *ImageOps) transformCurrentFrame(d Decoder, opt *ImageOptions, inputHeader *ImageHeader, frameCount int) (bool, error) { if opt.ResizeMethod == ImageOpsNoResize && !inputHeader.IsAnimated() { return false, nil @@ -376,8 +392,8 @@ func (o *ImageOps) transformCurrentFrame(d Decoder, opt *ImageOptions, inputHead } } -// initializeTransform initializes the transform process. -// It returns the image header, encoder, and error. +// initializeTransform prepares for image transformation by reading the input header +// and creating an appropriate encoder. Returns the header, encoder, and any error. func (o *ImageOps) initializeTransform(d Decoder, opt *ImageOptions, dst []byte) (*ImageHeader, Encoder, error) { inputHeader, err := d.Header() if err != nil { @@ -392,6 +408,10 @@ func (o *ImageOps) initializeTransform(d Decoder, opt *ImageOptions, dst []byte) return inputHeader, enc, nil } +// applyDisposeMethod handles frame disposal according to the active frame's +// dispose method in animated images. For frames marked with DisposeToBackgroundColor, +// it clears the affected region to transparent. For NoDispose, the previous frame's +// content is preserved. func (o *ImageOps) applyDisposeMethod(d Decoder) error { active := o.active() switch active.dispose { @@ -404,6 +424,8 @@ func (o *ImageOps) applyDisposeMethod(d Decoder) error { return nil } +// applyBlendMethod composites the active frame onto the animation buffer using +// the specified blending mode (alpha blending or direct copy). func (o *ImageOps) applyBlendMethod(d Decoder) error { active := o.active() rect := image.Rect( @@ -422,8 +444,8 @@ func (o *ImageOps) applyBlendMethod(d Decoder) error { return nil } -// copyFrameProperties copies the properties from the active frame to the secondary frame -// and then swaps the frames. +// copyFramePropertiesAndSwap transfers animation metadata (duration, disposal method, +// and blend mode) from the active frame to the secondary frame, then swaps buffers. func (o *ImageOps) copyFramePropertiesAndSwap() { o.secondary().duration = o.active().duration o.secondary().dispose = o.active().dispose diff --git a/thumbhash.go b/thumbhash.go index 410cd451..2ab2fadd 100644 --- a/thumbhash.go +++ b/thumbhash.go @@ -8,11 +8,16 @@ import ( "unsafe" ) +// thumbhashEncoder handles the encoding of images into ThumbHash format. +// ThumbHash is a very compact representation of a placeholder for an image. type thumbhashEncoder struct { encoder C.thumbhash_encoder buf []byte } +// newThumbhashEncoder creates a new ThumbHash encoder instance. +// It takes a decoder and a buffer as input, initializing the C-based encoder. +// Returns an error if the provided buffer is too small. func newThumbhashEncoder(decodedBy Decoder, buf []byte) (*thumbhashEncoder, error) { buf = buf[:1] enc := C.thumbhash_encoder_create(unsafe.Pointer(&buf[0]), C.size_t(cap(buf))) @@ -25,6 +30,9 @@ func newThumbhashEncoder(decodedBy Decoder, buf []byte) (*thumbhashEncoder, erro }, nil } +// Encode converts the given Framebuffer into a ThumbHash byte representation. +// The opt parameter allows passing encoding options as key-value pairs. +// Returns the encoded bytes or an error if the input is invalid. func (e *thumbhashEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { if f == nil { return nil, io.EOF @@ -38,6 +46,8 @@ func (e *thumbhashEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, erro return e.buf[:length], nil } +// Close releases the resources associated with the ThumbHash encoder. +// This should be called when the encoder is no longer needed. func (e *thumbhashEncoder) Close() { C.thumbhash_encoder_release(e.encoder) } diff --git a/webp.go b/webp.go index ee84a799..30f45785 100644 --- a/webp.go +++ b/webp.go @@ -9,12 +9,14 @@ import ( "unsafe" ) +// webpDecoder implements the Decoder interface for WebP images. type webpDecoder struct { decoder C.webp_decoder mat C.opencv_mat buf []byte } +// webpEncoder implements the Encoder interface for WebP images. type webpEncoder struct { encoder C.webp_encoder dstBuf []byte @@ -24,6 +26,8 @@ type webpEncoder struct { hasFlushed bool } +// newWebpDecoder creates a new WebP decoder from the provided byte buffer. +// Returns an error if the buffer is too small or contains invalid WebP data. func newWebpDecoder(buf []byte) (*webpDecoder, error) { mat := C.opencv_mat_create_from_data(C.int(len(buf)), 1, C.CV_8U, unsafe.Pointer(&buf[0]), C.size_t(len(buf))) @@ -43,6 +47,7 @@ func newWebpDecoder(buf []byte) (*webpDecoder, error) { }, nil } +// Header returns the image metadata including dimensions, pixel type, and frame count. func (d *webpDecoder) Header() (*ImageHeader, error) { return &ImageHeader{ width: int(C.webp_decoder_get_width(d.decoder)), @@ -54,24 +59,30 @@ func (d *webpDecoder) Header() (*ImageHeader, error) { }, nil } +// Close releases all resources associated with the decoder. func (d *webpDecoder) Close() { C.webp_decoder_release(d.decoder) C.opencv_mat_release(d.mat) d.buf = nil } +// Description returns the image format description ("WEBP"). func (d *webpDecoder) Description() string { return "WEBP" } +// Duration returns the total duration of the WebP animation. +// Returns 0 for static images. func (d *webpDecoder) Duration() time.Duration { return time.Duration(0) } +// HasSubtitles returns whether the image contains subtitle data (always false for WebP). func (d *webpDecoder) HasSubtitles() bool { return false } +// IsStreamable returns whether the image format supports streaming (always false for WebP). func (d *webpDecoder) IsStreamable() bool { return false } @@ -87,20 +98,26 @@ func (d *webpDecoder) advanceFrameIndex() { C.webp_decoder_advance_frame(d.decoder) } +// ICC returns the ICC color profile data embedded in the WebP image. func (d *webpDecoder) ICC() []byte { iccDst := make([]byte, 8192) iccLength := C.webp_decoder_get_icc(d.decoder, unsafe.Pointer(&iccDst[0]), C.size_t(cap(iccDst))) return iccDst[:iccLength] } +// BackgroundColor returns the background color of the WebP image. func (d *webpDecoder) BackgroundColor() uint32 { return uint32(C.webp_decoder_get_bg_color(d.decoder)) } +// LoopCount returns the number of times the animation should loop. func (d *webpDecoder) LoopCount() int { return int(C.webp_decoder_get_loop_count(d.decoder)) } +// DecodeTo decodes the current frame into the provided Framebuffer. +// Returns io.EOF when all frames have been decoded. +// Returns ErrDecodingFailed if the frame cannot be decoded. func (d *webpDecoder) DecodeTo(f *Framebuffer) error { if f == nil { return io.EOF @@ -141,10 +158,13 @@ func (d *webpDecoder) DecodeTo(f *Framebuffer) error { return nil } +// SkipFrame is not supported for WebP images and always returns ErrSkipNotSupported. func (d *webpDecoder) SkipFrame() error { return ErrSkipNotSupported } +// newWebpEncoder creates a new WebP encoder using the provided decoder for metadata +// and destination buffer for the encoded output. func newWebpEncoder(decodedBy Decoder, dstBuf []byte) (*webpEncoder, error) { dstBuf = dstBuf[:1] icc := decodedBy.ICC() @@ -168,6 +188,10 @@ func newWebpEncoder(decodedBy Decoder, dstBuf []byte) (*webpEncoder, error) { }, nil } +// Encode encodes a frame into the WebP format. +// If f is nil, finalizes the WebP animation and returns the encoded data. +// Returns io.EOF after the animation has been finalized. +// The opt parameter allows specifying encoding options as key-value pairs. func (e *webpEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { if e.hasFlushed { return nil, io.EOF @@ -206,6 +230,7 @@ func (e *webpEncoder) Encode(f *Framebuffer, opt map[int]int) ([]byte, error) { return nil, nil } +// Close releases all resources associated with the encoder. func (e *webpEncoder) Close() { C.webp_encoder_release(e.encoder) }