Initial QSfera import
This commit is contained in:
+532
@@ -0,0 +1,532 @@
|
||||
package netpbm
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/kovidgoyal/go-parallel"
|
||||
"github.com/kovidgoyal/imaging/nrgb"
|
||||
"github.com/kovidgoyal/imaging/types"
|
||||
)
|
||||
|
||||
var _ = fmt.Print
|
||||
|
||||
// skip_comments reads ahead past any comment lines (starting with #) and returns the first non-comment, non-empty line.
|
||||
func skip_comments(br *bufio.Reader) (string, error) {
|
||||
for {
|
||||
line, err := br.ReadString('\n')
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
return line, nil
|
||||
}
|
||||
}
|
||||
|
||||
type data_type int
|
||||
|
||||
const (
|
||||
rgb data_type = iota
|
||||
blackwhite
|
||||
grayscale
|
||||
)
|
||||
|
||||
type header struct {
|
||||
format string
|
||||
width, height, num_channels uint
|
||||
maxval uint32
|
||||
has_alpha bool
|
||||
data_type data_type
|
||||
}
|
||||
|
||||
func (h header) bytes_per_channel() uint {
|
||||
if h.maxval > 255 {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (h header) num_bytes_per_pixel() uint {
|
||||
return h.num_channels * h.bytes_per_channel()
|
||||
}
|
||||
|
||||
func read_ppm_header(br *bufio.Reader, magic string) (ans header, err error) {
|
||||
ans.format = magic
|
||||
required_num_fields := 3
|
||||
switch magic {
|
||||
case "P1", "P4":
|
||||
ans.data_type = blackwhite
|
||||
ans.num_channels = 1
|
||||
ans.maxval = 1
|
||||
required_num_fields = 2
|
||||
case "P2", "P5":
|
||||
ans.data_type = grayscale
|
||||
ans.num_channels = 1
|
||||
default:
|
||||
ans.data_type = rgb
|
||||
ans.num_channels = 3
|
||||
}
|
||||
var fields []uint
|
||||
for len(fields) < required_num_fields {
|
||||
var line string
|
||||
if line, err = skip_comments(br); err != nil {
|
||||
return
|
||||
}
|
||||
for x := range strings.FieldsSeq(line) {
|
||||
var val uint64
|
||||
if val, err = strconv.ParseUint(x, 10, 0); err != nil {
|
||||
return
|
||||
}
|
||||
fields = append(fields, uint(val))
|
||||
}
|
||||
}
|
||||
ans.width = fields[0]
|
||||
ans.height = fields[1]
|
||||
if required_num_fields > 2 {
|
||||
if fields[2] > 65535 {
|
||||
return ans, fmt.Errorf("header specifies a maximum value %d larger than 65535", ans.maxval)
|
||||
}
|
||||
ans.maxval = uint32(fields[2])
|
||||
}
|
||||
if ans.maxval > 65535 {
|
||||
return ans, fmt.Errorf("header specifies a maximum value %d larger than 65535", ans.maxval)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func read_pam_header(br *bufio.Reader) (ans header, err error) {
|
||||
ans.format = "P7"
|
||||
ans.data_type = rgb
|
||||
ans.num_channels = 3
|
||||
for {
|
||||
line, err := skip_comments(br)
|
||||
if err != nil {
|
||||
return ans, err
|
||||
}
|
||||
if line == "ENDHDR" {
|
||||
break
|
||||
}
|
||||
prefix, payload, found := strings.Cut(line, " ")
|
||||
if !found {
|
||||
return ans, fmt.Errorf("invalid line in header: %#v", line)
|
||||
}
|
||||
switch prefix {
|
||||
case "WIDTH":
|
||||
w, err := strconv.ParseUint(payload, 10, 0)
|
||||
if err != nil {
|
||||
return ans, fmt.Errorf("invalid width %#v in header: %w", payload, err)
|
||||
}
|
||||
ans.width = uint(w)
|
||||
case "HEIGHT":
|
||||
w, err := strconv.ParseUint(payload, 10, 0)
|
||||
if err != nil {
|
||||
return ans, fmt.Errorf("invalid height %#v in header: %w", payload, err)
|
||||
}
|
||||
ans.height = uint(w)
|
||||
case "MAXVAL":
|
||||
w, err := strconv.ParseUint(payload, 10, 0)
|
||||
if err != nil {
|
||||
return ans, fmt.Errorf("invalid maxval %#v in header: %w", payload, err)
|
||||
}
|
||||
ans.maxval = uint32(w)
|
||||
case "DEPTH":
|
||||
w, err := strconv.ParseUint(payload, 10, 0)
|
||||
if err != nil {
|
||||
return ans, fmt.Errorf("invalid depth %#v in header: %w", payload, err)
|
||||
}
|
||||
if w == 0 || w > 4 {
|
||||
return ans, fmt.Errorf("invalid depth %d in header", w)
|
||||
}
|
||||
ans.num_channels = uint(w)
|
||||
case "TUPLTYPE":
|
||||
switch payload {
|
||||
case "BLACKANDWHITE":
|
||||
ans.data_type = blackwhite
|
||||
case "BLACKANDWHITE_ALPHA":
|
||||
ans.has_alpha = true
|
||||
ans.data_type = blackwhite
|
||||
case "GRAYSCALE":
|
||||
ans.data_type = grayscale
|
||||
case "GRAYSCALE_ALPHA":
|
||||
ans.has_alpha = true
|
||||
ans.data_type = grayscale
|
||||
case "RGB":
|
||||
case "RGB_ALPHA":
|
||||
ans.has_alpha = true
|
||||
default:
|
||||
return ans, fmt.Errorf("invalid TUPLTYPE in header: %#v", payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
if ans.width == 0 || ans.height == 0 || ans.maxval == 0 {
|
||||
return ans, fmt.Errorf("header does not specify width, height and maximum value")
|
||||
}
|
||||
ok := true
|
||||
switch ans.data_type {
|
||||
case rgb:
|
||||
ok = (!ans.has_alpha && ans.num_channels == 3) || (ans.has_alpha && ans.num_channels == 4)
|
||||
case blackwhite, grayscale:
|
||||
ok = (!ans.has_alpha && ans.num_channels == 1) || (ans.has_alpha && ans.num_channels == 2)
|
||||
}
|
||||
if !ok {
|
||||
return ans, fmt.Errorf("header specified depth: %d does not match TUPLTYPE", ans.num_channels)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func read_header(br *bufio.Reader) (ans header, err error) {
|
||||
b := []byte{0, 0}
|
||||
if _, err = io.ReadFull(br, b); err != nil {
|
||||
return ans, err
|
||||
}
|
||||
magic := string(b)
|
||||
switch magic {
|
||||
case "P1", "P2", "P3", "P4", "P5", "P6":
|
||||
return read_ppm_header(br, magic)
|
||||
case "P7":
|
||||
return read_pam_header(br)
|
||||
default:
|
||||
err = fmt.Errorf("unsupported netPBM format: %#v", magic)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func ascii_range_over_values(br *bufio.Reader, h header, callback func(uint32, []uint8) []uint8) (ans []uint8, err error) {
|
||||
anssz := h.width * h.height * h.num_bytes_per_pixel()
|
||||
ans = make([]uint8, 0, anssz)
|
||||
for uint(len(ans)) < anssz {
|
||||
token, err := br.ReadString(' ')
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
for field := range strings.FieldsSeq(token) {
|
||||
if val, perr := strconv.ParseUint(field, 10, 16); perr == nil {
|
||||
ans = callback(uint32(val), ans)
|
||||
}
|
||||
}
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func decode_rgb_ascii(br *bufio.Reader, h header) (ans []byte, err error) {
|
||||
mult := uint32(255)
|
||||
if h.maxval > 255 {
|
||||
mult = 65535
|
||||
}
|
||||
anssz := h.width * h.height * h.num_bytes_per_pixel()
|
||||
if mult == 255 {
|
||||
ans, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 {
|
||||
ch := (uint32(val) * mult) / h.maxval
|
||||
return append(ans, uint8(ch))
|
||||
})
|
||||
} else {
|
||||
ans, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 {
|
||||
ch := (uint32(val) * mult) / h.maxval
|
||||
ans = append(ans, uint8(ch))
|
||||
if len(ans)%6 == 0 { // alpha is always 255
|
||||
ans = append(ans, 255, 255)
|
||||
}
|
||||
return ans
|
||||
})
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if uint(len(ans)) < anssz {
|
||||
return nil, errors.New("insufficient color data present in PPM file")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeConfigAndFormat(r io.Reader) (cfg image.Config, fmt types.Format, err error) {
|
||||
br := bufio.NewReader(r)
|
||||
h, err := read_header(br)
|
||||
if err != nil {
|
||||
return cfg, types.UNKNOWN, err
|
||||
}
|
||||
cfg.Width = int(h.width)
|
||||
cfg.Height = int(h.height)
|
||||
cfg.ColorModel = nrgb.Model
|
||||
switch h.data_type {
|
||||
case blackwhite, grayscale:
|
||||
if h.has_alpha {
|
||||
if h.maxval > 255 {
|
||||
cfg.ColorModel = color.NRGBA64Model
|
||||
} else {
|
||||
cfg.ColorModel = color.NRGBAModel
|
||||
}
|
||||
} else {
|
||||
if h.maxval > 255 {
|
||||
cfg.ColorModel = color.Gray16Model
|
||||
} else {
|
||||
cfg.ColorModel = color.GrayModel
|
||||
}
|
||||
}
|
||||
default:
|
||||
if h.has_alpha {
|
||||
if h.maxval > 255 {
|
||||
cfg.ColorModel = color.NRGBA64Model
|
||||
} else {
|
||||
cfg.ColorModel = color.NRGBAModel
|
||||
}
|
||||
} else {
|
||||
if h.maxval > 255 {
|
||||
cfg.ColorModel = color.NRGBA64Model
|
||||
} else {
|
||||
cfg.ColorModel = nrgb.Model
|
||||
}
|
||||
}
|
||||
}
|
||||
switch h.format {
|
||||
case "P7":
|
||||
fmt = types.PAM
|
||||
case "P1", "P4":
|
||||
fmt = types.PBM
|
||||
case "P2", "P5":
|
||||
fmt = types.PGM
|
||||
case "P3", "P6":
|
||||
fmt = types.PPM
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func DecodeConfig(r io.Reader) (cfg image.Config, err error) {
|
||||
cfg, _, err = DecodeConfigAndFormat(r)
|
||||
return
|
||||
}
|
||||
|
||||
func decode_black_white_ascii(br *bufio.Reader, h header) (img image.Image, err error) {
|
||||
r := image.Rect(0, 0, int(h.width), int(h.height))
|
||||
g := &image.Gray{Stride: r.Dx(), Rect: r}
|
||||
g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 {
|
||||
var c uint8 = 255 * uint8(1-(val&1))
|
||||
return append(ans, c)
|
||||
})
|
||||
return g, err
|
||||
}
|
||||
|
||||
func decode_grayscale_ascii(br *bufio.Reader, h header) (img image.Image, err error) {
|
||||
r := image.Rect(0, 0, int(h.width), int(h.height))
|
||||
if h.maxval > 255 {
|
||||
g := &image.Gray16{Stride: 2 * r.Dx(), Rect: r}
|
||||
g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 {
|
||||
c := uint16(val * 65535 / h.maxval)
|
||||
return append(ans, uint8(c>>8), uint8(c))
|
||||
})
|
||||
return g, err
|
||||
} else {
|
||||
g := &image.Gray{Stride: r.Dx(), Rect: r}
|
||||
g.Pix, err = ascii_range_over_values(br, h, func(val uint32, ans []uint8) []uint8 {
|
||||
c := uint8(val * 255 / h.maxval)
|
||||
return append(ans, c)
|
||||
})
|
||||
return g, err
|
||||
}
|
||||
}
|
||||
|
||||
// Consume whitespace after header (per spec, it's a single whitespace, but can be more)
|
||||
func skip_whitespace_before_pixel_data(br *bufio.Reader, num_of_bytes_needed uint) ([]uint8, error) {
|
||||
for {
|
||||
b, err := br.Peek(1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if b[0] == '\n' || b[0] == '\r' || b[0] == '\t' || b[0] == ' ' {
|
||||
br.ReadByte()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
ans := make([]byte, num_of_bytes_needed)
|
||||
_, err := io.ReadFull(br, ans)
|
||||
return ans, err
|
||||
}
|
||||
|
||||
func rescale(v uint32, num, den uint32) uint32 {
|
||||
return (v * num) / den
|
||||
}
|
||||
|
||||
func rescale_binary_data(b []uint8, num, den uint32) error {
|
||||
return parallel.Run_in_parallel_over_range(0, func(start, end int) {
|
||||
for i := start; i < end; i++ {
|
||||
b[i] = uint8(rescale(uint32(b[i]), num, den))
|
||||
}
|
||||
}, 0, len(b))
|
||||
}
|
||||
|
||||
func rescale_binary_data16(b []uint8, num, den uint32) error {
|
||||
if len(b)&1 != 0 {
|
||||
return fmt.Errorf("pixel data is not a multiple of two but uses 16 bits per channel")
|
||||
}
|
||||
return parallel.Run_in_parallel_over_range(0, func(start, end int) {
|
||||
start *= 2
|
||||
end *= 2
|
||||
for i := start; i < end; i += 2 {
|
||||
v := uint32((uint16(b[i]) << 8) | uint16(b[i+1]))
|
||||
v = rescale(v, num, den)
|
||||
b[i] = uint8(v >> 8)
|
||||
b[i+1] = uint8(v)
|
||||
}
|
||||
}, 0, len(b)/2)
|
||||
}
|
||||
|
||||
func decode_binary_data(br *bufio.Reader, h header) (ans image.Image, err error) {
|
||||
var binary_data []uint8
|
||||
if binary_data, err = skip_whitespace_before_pixel_data(br, h.width*h.height*h.num_bytes_per_pixel()); err != nil {
|
||||
return
|
||||
}
|
||||
if n := h.num_bytes_per_pixel() * h.width * h.height; uint(len(binary_data)) < n {
|
||||
return nil, fmt.Errorf(
|
||||
"insufficient pixel data for image area and num_channels (%d): %f < %d",
|
||||
h.num_channels, float64(len(binary_data))/float64(h.width*h.height), n/(h.width*h.height))
|
||||
}
|
||||
switch {
|
||||
case h.maxval < 255:
|
||||
if err = rescale_binary_data(binary_data, 255, h.maxval); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case 255 < h.maxval && h.maxval < 65535:
|
||||
if err = rescale_binary_data16(binary_data, 65535, h.maxval); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
r := image.Rect(0, 0, int(h.width), int(h.height))
|
||||
switch h.num_channels {
|
||||
case 1:
|
||||
// bw or gray without alpha
|
||||
if h.maxval > 255 {
|
||||
return &image.Gray16{Rect: r, Stride: r.Dx() * 2, Pix: binary_data}, nil
|
||||
}
|
||||
return &image.Gray{Rect: r, Stride: r.Dx(), Pix: binary_data}, nil
|
||||
case 2:
|
||||
// bw or gray with alpha
|
||||
if h.maxval > 255 {
|
||||
g := image.NewNRGBA64(r)
|
||||
b := g.Pix
|
||||
if err = parallel.Run_in_parallel_over_range(0, func(start, end int) {
|
||||
for i := start; i < end; i++ {
|
||||
src := binary_data[i*4 : i*4+4]
|
||||
dest := b[i*8 : i*8+8]
|
||||
gray1, gray2 := src[0], src[1]
|
||||
dest[0], dest[1], dest[2], dest[3], dest[4], dest[5] = gray1, gray2, gray1, gray2, gray1, gray2
|
||||
dest[6], dest[7] = src[2], src[3]
|
||||
}
|
||||
}, 0, int(h.width*h.height)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
g := image.NewNRGBA(r)
|
||||
b := g.Pix
|
||||
if err = parallel.Run_in_parallel_over_range(0, func(start, end int) {
|
||||
for i := start; i < end; i++ {
|
||||
src := binary_data[i*2 : i*2+2]
|
||||
dest := b[i*4 : i*4+4]
|
||||
dest[0], dest[1], dest[2], dest[3] = src[0], src[0], src[0], src[1]
|
||||
}
|
||||
}, 0, int(h.width*h.height)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
case 3:
|
||||
// RGB without alpha
|
||||
if h.maxval > 255 {
|
||||
g := image.NewNRGBA64(r)
|
||||
b := g.Pix
|
||||
if err = parallel.Run_in_parallel_over_range(0, func(start, end int) {
|
||||
for i := start; i < end; i++ {
|
||||
src := binary_data[i*6 : i*6+6]
|
||||
dest := b[i*8 : i*8+8]
|
||||
copy(dest[:6], src)
|
||||
dest[6], dest[7] = 255, 255
|
||||
}
|
||||
}, 0, int(h.width*h.height)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return g, nil
|
||||
}
|
||||
return nrgb.NewNRGBWithContiguousRGBPixels(binary_data, 0, 0, r.Dx(), r.Dy())
|
||||
case 4:
|
||||
// RGB with alpha
|
||||
if h.maxval <= 255 {
|
||||
return &image.NRGBA{Rect: r, Stride: r.Dx() * int(h.num_bytes_per_pixel()), Pix: binary_data}, nil
|
||||
}
|
||||
return &image.NRGBA64{Rect: r, Stride: r.Dx() * int(h.num_bytes_per_pixel()), Pix: binary_data}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported number of channels: %d", h.num_channels)
|
||||
}
|
||||
}
|
||||
|
||||
// Decode decodes a PPM image from r and returns it as an image.Image.
|
||||
// Supports both P3 (ASCII) and P6 (binary) variants.
|
||||
func Decode(r io.Reader) (img image.Image, err error) {
|
||||
br := bufio.NewReader(r)
|
||||
h, err := read_header(br)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var binary_data []uint8
|
||||
switch h.format {
|
||||
case "P1":
|
||||
return decode_black_white_ascii(br, h)
|
||||
case "P2":
|
||||
return decode_grayscale_ascii(br, h)
|
||||
case "P3":
|
||||
vals, err := decode_rgb_ascii(br, h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if h.maxval <= 255 {
|
||||
return nrgb.NewNRGBWithContiguousRGBPixels(vals, 0, 0, int(h.width), int(h.height))
|
||||
}
|
||||
return &image.NRGBA64{Pix: vals, Stride: int(h.width) * 8, Rect: image.Rect(0, 0, int(h.width), int(h.height))}, nil
|
||||
case "P4":
|
||||
bytes_per_row := (h.width + 7) / 8
|
||||
if binary_data, err = skip_whitespace_before_pixel_data(br, h.height*bytes_per_row); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ans := image.NewGray(image.Rect(0, 0, int(h.width), int(h.height)))
|
||||
i := 0
|
||||
for range h.height {
|
||||
for x := range h.width {
|
||||
byteIdx := x / 8
|
||||
bitIdx := 7 - uint(x%8)
|
||||
bit := (binary_data[byteIdx] >> bitIdx) & 1
|
||||
ans.Pix[i] = (1 - bit) * 255
|
||||
i++
|
||||
}
|
||||
binary_data = binary_data[bytes_per_row:]
|
||||
}
|
||||
if len(binary_data) > 0 {
|
||||
return nil, fmt.Errorf("insufficient color data in netPBM file, need %d more bytes", len(binary_data))
|
||||
}
|
||||
return ans, nil
|
||||
case "P5", "P6", "P7":
|
||||
return decode_binary_data(br, h)
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid format for PPM: %#v", h.format)
|
||||
}
|
||||
}
|
||||
|
||||
// Register this decoder with Go's image package
|
||||
func init() {
|
||||
image.RegisterFormat("pbm", "P1", Decode, DecodeConfig)
|
||||
image.RegisterFormat("pgm", "P2", Decode, DecodeConfig)
|
||||
image.RegisterFormat("ppm", "P3", Decode, DecodeConfig)
|
||||
image.RegisterFormat("pbm", "P4", Decode, DecodeConfig)
|
||||
image.RegisterFormat("pgm", "P5", Decode, DecodeConfig)
|
||||
image.RegisterFormat("ppm", "P6", Decode, DecodeConfig)
|
||||
image.RegisterFormat("pam", "P7", Decode, DecodeConfig)
|
||||
}
|
||||
Reference in New Issue
Block a user