package camera import ( "bytes" "fmt" "image" "image/jpeg" "image/png" "os" "path/filepath" "strings" ) // ImageSource provides a single JPEG frame from an uploaded image file. // ReadFrame() returns the frame once, then subsequent calls return an error // to signal the pipeline that the source is exhausted. type ImageSource struct { jpegData []byte width int height int filePath string done bool } // NewImageSource reads an image file (JPG or PNG) and converts to JPEG bytes. func NewImageSource(filePath string) (*ImageSource, error) { f, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("failed to open image: %w", err) } defer f.Close() ext := strings.ToLower(filepath.Ext(filePath)) var img image.Image switch ext { case ".jpg", ".jpeg": img, err = jpeg.Decode(f) case ".png": img, err = png.Decode(f) default: return nil, fmt.Errorf("unsupported image format: %s", ext) } if err != nil { return nil, fmt.Errorf("failed to decode image: %w", err) } bounds := img.Bounds() var buf bytes.Buffer if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil { return nil, fmt.Errorf("failed to encode JPEG: %w", err) } return &ImageSource{ jpegData: buf.Bytes(), width: bounds.Dx(), height: bounds.Dy(), filePath: filePath, }, nil } func (s *ImageSource) ReadFrame() ([]byte, error) { if !s.done { s.done = true } // Always return the same frame so the MJPEG streamer can serve it // to clients that connect at any time. return s.jpegData, nil } func (s *ImageSource) Close() error { return os.Remove(s.filePath) } // Dimensions returns the image width and height. func (s *ImageSource) Dimensions() (int, int) { return s.width, s.height } // IsDone returns whether the single frame has been consumed. func (s *ImageSource) IsDone() bool { return s.done }