diff --git a/composite/render_test.go b/composite/render_test.go index f352fab..14753d8 100644 --- a/composite/render_test.go +++ b/composite/render_test.go @@ -24,7 +24,7 @@ var tree = load() func BenchmarkRender(b *testing.B) { ctx := context.Background() - dest := image.NewRGBA(tree.CanvasRect) + dest := image.NewNRGBA(tree.CanvasRect) if err := tree.Renderer.Render(ctx, dest); err != nil { b.Fatal(err) } diff --git a/encode.go b/encode.go new file mode 100644 index 0000000..8b14df7 --- /dev/null +++ b/encode.go @@ -0,0 +1,162 @@ +package psd + +import ( + "bytes" + "encoding/binary" + "fmt" + "io" + + "github.com/depp/packbits" +) + +func Encode(psd *PSD, w io.Writer) error { + if err := psd.Config.encode(w); err != nil { + return err + } + if err := psd.encodeLayers(w); err != nil { + return err + } + + // image data + return psd.Config.CompressionMethod.encode(psd.Data, &psd.Config, w) +} + +func (psd *PSD) encodeLayers(w io.Writer) error { + // empty data block for layers + if err := writeDataBlock([]byte{}, w); err != nil { + return err + } + if len(psd.Layer) > 0 { + return fmt.Errorf("encoding layers not yet supported") + } + return nil +} + +func (c *Config) encode(w io.Writer) error { + // header + h := &headerWrite{ + Signature: headerSignatureBytes, + Version: uint16(c.Version), + Channels: uint16(c.Channels), + Height: uint32(c.Rect.Dy()), + Width: uint32(c.Rect.Dx()), + Depth: uint16(c.Depth), + ColorMode: uint16(c.ColorMode), + } + + if err := binaryWrite(w, h); err != nil { + return err + } + + // color mode data + if err := writeDataBlock(c.ColorModeData, w); err != nil { + return err + } + + // image resources + buf := &bytes.Buffer{} + for id, imgResource := range c.Res { + if err := imgResource.encode(id, buf); err != nil { + return err + } + } + if err := writeDataBlock(buf.Bytes(), w); err != nil { + return err + } + return nil +} + +func (i *ImageResource) encode(id int, w io.Writer) error { + if _, err := w.Write(imageResourceSignatureBytes[:]); err != nil { + return err + } + if err := binaryWrite(w, uint16(id)); err != nil { + return err + } + b, err := stringToPascalBytes(i.Name, true) + if err != nil { + return err + } + if _, err := w.Write(b); err != nil { + return err + } + return writeDataBlockEvenLength(i.Data, w) +} + +func (c CompressionMethod) encode(imgDataRaw []byte, cfg *Config, w io.Writer) error { + // compression method + if err := binaryWrite(w, uint16(c)); err != nil { + return err + } + // data + switch c { + case CompressionMethodRaw: + if _, err := w.Write(imgDataRaw); err != nil { + return err + } + case CompressionMethodRLE: + rowSize := cfg.Rect.Dx() * cfg.Depth / 8 + buf := bytes.NewBuffer([]byte{}) + read := 0 + for i := 0; i < cfg.Rect.Dy()*cfg.Channels; i++ { + n, err := buf.Write(packbits.Pack(imgDataRaw[read : read+rowSize])) + if err != nil { + return err + } + read += rowSize + if err := binaryWrite(w, uint16(n)); err != nil { + return err + } + } + + if _, err := w.Write(buf.Bytes()); err != nil { + return err + } + default: + return fmt.Errorf("%d image data econding not supported", c) + } + return nil +} + +func binaryWrite(w io.Writer, data any) error { + return binary.Write(w, binary.BigEndian, data) +} +func writeDataBlock(data []byte, w io.Writer) error { + if err := binaryWrite(w, uint32(len(data))); err != nil { + return err + } + if _, err := w.Write(data); err != nil { + return err + } + return nil +} +func writeDataBlockEvenLength(data []byte, w io.Writer) error { + if err := writeDataBlock(data, w); err != nil { + return err + } + if len(data)%2 == 1 { + if _, err := w.Write([]byte{0}); err != nil { + return err + } + } + return nil +} + +type headerWrite struct { + Signature [4]byte + Version uint16 + Reserved [6]byte + Channels uint16 + Height uint32 + Width uint32 + Depth uint16 + ColorMode uint16 +} + +var headerSignatureBytes [4]byte +var imageResourceSignatureBytes [4]byte + +func init() { + copy(headerSignatureBytes[:], []byte(headerSignature)) + copy(imageResourceSignatureBytes[:], []byte(sectionSignature)) +} diff --git a/encode_test.go b/encode_test.go new file mode 100644 index 0000000..ac3542c --- /dev/null +++ b/encode_test.go @@ -0,0 +1,176 @@ +package psd_test + +import ( + "bytes" + "fmt" + "image" + "image/png" + "log" + "os" + "testing" + + "github.com/oov/psd" + "github.com/stretchr/testify/assert" +) + +func init() { + psd.Debug = log.New(os.Stdout, "psd: ", log.Lshortfile) +} + +func getOrig(t *testing.T) *psd.PSD { + fr, err := os.Open("testdata/cmyk-spot.psd") + if err != nil { + t.Fatal(err) + } + doc, _, err := psd.Decode(fr, nil) + if err != nil { + t.Fatal(err) + } + return doc +} + +func TestEncodeDecode(t *testing.T) { + doc := buildNew(t, psd.CompressionMethodRaw) + + // re-read and compare image data again + docReread := writeRead(t, doc) + assert.Equal(t, doc.Config.Channels, docReread.Config.Channels) +} + +func TestDecodeChannelImages(t *testing.T) { + doc := getOrig(t) + imgs, err := doc.GetChannelImages() + if err != nil { + t.Fatal(err) + } + writeImage(t, imgs, "orig") // write imges to compare +} + +func TestEncodeChannelImages(t *testing.T) { + // add images to new doc + doc := buildNew(t, psd.CompressionMethodRLE) + imgs := make([]*image.Gray, doc.Config.Channels) + + var err error + for i, fnm := range []string{ + "testdata/cmyk-spot-channel-5.png", + "testdata/cmyk-spot-channel-6.png", + "testdata/cmyk-spot-channel-7.png", + } { + imgs[i+4], err = readGrayImage(fnm) + if err != nil { + t.Fatal(err) + } + } + if err := doc.AddImageChannelData(imgs); err != nil { + t.Fatal(err) + } + + // compare image data + docOrig := getOrig(t) + assert.Len(t, doc.Data, doc.Config.Channels*doc.Config.Rect.Dx()*doc.Config.Rect.Dy()) + assert.EqualValues(t, docOrig.Data, doc.Data) + for i := 4; i < doc.Config.Channels; i++ { + assert.EqualValues(t, docOrig.Channel[i].Data, doc.Channel[i].Data) + } + + // write + fw, err := os.Create("output/cmyk-spot.psd") + if err != nil { + t.Fatal(err) + } + defer fw.Close() + if err := psd.Encode(doc, fw); err != nil { + t.Fatal(err) + } + + // read again and write those images to compare + docNew := writeRead(t, doc) + imgsEnc, err := docNew.GetChannelImages() + if err != nil { + t.Fatal(err) + } + writeImage(t, imgsEnc, "encoded") +} + +func buildNew(t *testing.T, cmp psd.CompressionMethod) *psd.PSD { + doc := &psd.PSD{ + Config: psd.Config{ + Version: 1, + Rect: image.Rect(0, 0, 640, 637), + Channels: 7, // CMYK plus three spot channels + Depth: 8, + ColorMode: psd.ColorModeCMYK, + CompressionMethod: cmp, + }, + } + imgResources := make(map[int]psd.ImageResource) + irAlphaNames, err := testAlphaNames.Encode() + if err != nil { + t.Fatal(err) + } + irDisplayInfo, err := testDisplayInfo.Encode() + if err != nil { + t.Fatal(err) + } + irResInfo, err := testResolutionInfo.Encode() + if err != nil { + t.Fatal(err) + } + imgResources[irAlphaNames.ID] = *irAlphaNames + imgResources[irDisplayInfo.ID] = *irDisplayInfo + imgResources[irResInfo.ID] = *irResInfo + doc.Config.Res = imgResources + + // dummy image data + imgs := make([]*image.Gray, doc.Config.Channels) + for i := range imgs { + imgs[i] = image.NewGray(doc.Config.Rect) + } + if err := doc.AddImageChannelData(imgs); err != nil { + t.Fatal(err) + } + return doc +} + +func writeRead(t *testing.T, doc *psd.PSD) *psd.PSD { + buf := bytes.NewBuffer([]byte{}) + if err := psd.Encode(doc, buf); err != nil { + t.Fatal(err) + } + + log.Printf("The file is %d bytes long", len(buf.Bytes())) + + out, _, err := psd.Decode(buf, nil) + if err != nil { + t.Fatal(err) + } + return out +} + +func writeImage(t *testing.T, imgs []*image.Gray, prefix string) { + for c, img := range imgs { + w, err := os.Create(fmt.Sprintf("output/%s-c%d.png", prefix, c)) + if err != nil { + t.Fatal(err) + } + defer w.Close() + if err := png.Encode(w, img); err != nil { + t.Fatal(err) + } + } +} + +func readGrayImage(fnm string) (*image.Gray, error) { + r, err := os.Open(fnm) + if err != nil { + return nil, err + } + defer r.Close() + + img, _, err := image.Decode(r) + if err != nil { + return nil, err + } + return psd.ImgToGray(img), nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..9f1d53e --- /dev/null +++ b/go.mod @@ -0,0 +1,22 @@ +module github.com/oov/psd + +go 1.20 + +require ( + github.com/gopherjs/gopherjs v1.17.2 + golang.org/x/text v0.18.0 +) + +require ( + github.com/oov/downscale v0.0.0-20170819221759-1bbcb5d498e2 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.9.0 + golang.org/x/image v0.20.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/depp/packbits v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a588102 --- /dev/null +++ b/go.sum @@ -0,0 +1,21 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/depp/packbits v1.1.0 h1:8X9+Jejw2+MhDTiy/EEg4N59Og96+zpbOLdwoooXSQo= +github.com/depp/packbits v1.1.0/go.mod h1:wDV3NXiMB4a+KztSJ93UMH9cBKj5cEGooAbgRXTpQ78= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/oov/downscale v0.0.0-20170819221759-1bbcb5d498e2 h1:DOkvJh3Bh7i1p5QitV4aJEyxBIivy1GhPk7S19bWn9c= +github.com/oov/downscale v0.0.0-20170819221759-1bbcb5d498e2/go.mod h1:YE+WWC4V4TL0IBHoNZtnZeIZ6e/Jr0oHKajiPwx+B7E= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw= +golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/imageres.go b/imageres.go index af5f22a..4cf56ea 100644 --- a/imageres.go +++ b/imageres.go @@ -1,12 +1,16 @@ package psd import ( + "bytes" + "encoding/binary" "errors" + "fmt" "io" ) // ImageResource represents the image resource that is used in psd file. type ImageResource struct { + ID int Name string Data []byte } @@ -24,6 +28,9 @@ func readImageResource(r io.Reader) (resMap map[int]ImageResource, read int, err } read += l imageResourceLen := int(readUint32(b, 0)) + if Debug != nil { + Debug.Println("image resources bytes=", imageResourceLen) + } if imageResourceLen == 0 { return map[int]ImageResource{}, read, nil } @@ -58,6 +65,7 @@ func readImageResource(r io.Reader) (resMap map[int]ImageResource, read int, err // Image resource IDs contains a list of resource IDs used by Photoshop. // http://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_38034 id = int(readUint16(b, 4)) + res.ID = id // Name: Pascal string, padded to make the size even // (a null name consists of two bytes of 0) @@ -92,6 +100,7 @@ func readImageResource(r io.Reader) (resMap map[int]ImageResource, read int, err } if Debug != nil { Debug.Println("end - image resources section") + reportReaderPosition(" file offset: %d", r) } return resMap, read, nil } @@ -115,3 +124,133 @@ func hasAlphaID0(Res map[int]ImageResource) bool { } return false } + +type AlphaNames struct { + Names []string // pascal format strings +} + +const idAlphaNames = 1006 + +func (c *Config) ParseAlphaNames() (*AlphaNames, error) { + res, ok := c.Res[idAlphaNames] + if !ok { + return nil, fmt.Errorf("no alpha names data defined") + } + r := bytes.NewReader(res.Data) + names := make([]string, 0) + read := 0 + for read < len(res.Data) { + s, n, err := readPascalString(r) + if err != nil { + return nil, err + } + names = append(names, s) + read += n + } + return &AlphaNames{names}, nil +} +func (an *AlphaNames) Encode() (*ImageResource, error) { + data := []byte{} + for _, n := range an.Names { + b, err := stringToPascalBytes(n, false) + if err != nil { + return nil, err + } + data = append(data, b...) + } + return &ImageResource{ + ID: idAlphaNames, + Data: data, + }, nil +} + +type DisplayInfo struct { + Channels []DisplayInfoChannel +} +type DisplayInfoChannel struct { + ColorSpace uint16 // TODO: define enum for this + Color [4]uint16 + Opacity uint16 + Mode DisplayInfoChannelMode +} + +type DisplayInfoChannelMode uint8 + +const ( + DisplayChannelModeAlpha DisplayInfoChannelMode = iota + DisplayChannelModeAlphaInverted + DisplayChannelModeSpot +) + +const idDisplayInfo = 1077 + +func (c *Config) ParseDisplayInfo() (*DisplayInfo, error) { + res, ok := c.Res[idDisplayInfo] + if !ok { + return nil, fmt.Errorf("no display info data defined") + } + read := 4 // start reading after version + channels := make([]DisplayInfoChannel, 0) + for read < len(res.Data) { + r := bytes.NewReader(res.Data[read : read+13]) + c := &DisplayInfoChannel{} + if err := binary.Read(r, binary.BigEndian, c); err != nil { + return nil, err + } + read += 13 + channels = append(channels, *c) + } + return &DisplayInfo{channels}, nil +} +func (di *DisplayInfo) Encode() (*ImageResource, error) { + data := bytes.NewBuffer([]byte{}) + // version + if err := binaryWrite(data, uint32(1)); err != nil { + return nil, err + } + // channels + for _, c := range di.Channels { + if err := binaryWrite(data, c); err != nil { + return nil, err + } + } + return &ImageResource{ + ID: idDisplayInfo, + Data: data.Bytes(), + }, nil +} + +const idResolutionInfo = 1005 + +type ResolutionInfo struct { + HorizontalRes uint32 + HorizontalUnit uint16 + WidthUnit uint16 + VerticalRes uint32 + VerticalUnit uint16 + HeightUnit uint16 +} + +func (c *Config) ParseResolutionInfo() (*ResolutionInfo, error) { + res, ok := c.Res[idResolutionInfo] + if !ok { + return nil, fmt.Errorf("no resolution info data defined") + } + + r := bytes.NewReader(res.Data) + out := &ResolutionInfo{} + if err := binary.Read(r, binary.BigEndian, out); err != nil { + return nil, err + } + return out, nil +} +func (ri *ResolutionInfo) Encode() (*ImageResource, error) { + data := bytes.NewBuffer([]byte{}) + if err := binaryWrite(data, ri); err != nil { + return nil, err + } + return &ImageResource{ + ID: idResolutionInfo, + Data: data.Bytes(), + }, nil +} diff --git a/imageres_test.go b/imageres_test.go new file mode 100644 index 0000000..dfd2fb8 --- /dev/null +++ b/imageres_test.go @@ -0,0 +1,88 @@ +package psd_test + +import ( + "testing" + + "github.com/oov/psd" + "github.com/stretchr/testify/assert" +) + +var testAlphaNames = &psd.AlphaNames{[]string{"blue", "fluorescent pink", "yellow"}} +var testDisplayInfo = &psd.DisplayInfo{ + Channels: []psd.DisplayInfoChannel{ + { + Color: [4]uint16{0, 30840, 49087, 0}, + Mode: psd.DisplayChannelModeSpot, + }, + { + Color: [4]uint16{65535, 18504, 45232, 0}, + Mode: psd.DisplayChannelModeSpot, + }, + { + Color: [4]uint16{65535, 59624, 0, 0}, + Mode: psd.DisplayChannelModeSpot, + }, + }, +} +var testResolutionInfo = &psd.ResolutionInfo{ + HorizontalRes: 19660800, + HorizontalUnit: 1, + WidthUnit: 1, + VerticalRes: 19660800, + VerticalUnit: 1, + HeightUnit: 1, +} + +func TestParseAlpha(t *testing.T) { + doc := getOrig(t) + alphaNames, err := doc.Config.ParseAlphaNames() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, testAlphaNames, alphaNames) +} + +func TestParseDisplayInfo(t *testing.T) { + doc := getOrig(t) + displayInfo, err := doc.Config.ParseDisplayInfo() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, testDisplayInfo, displayInfo) +} + +func TestParseResolutionInfo(t *testing.T) { + doc := getOrig(t) + resInfo, err := doc.Config.ParseResolutionInfo() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, testResolutionInfo, resInfo) +} + +func TestEncodeAlphaNames(t *testing.T) { + doc := getOrig(t) + ir, err := testAlphaNames.Encode() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, doc.Config.Res[ir.ID].Data, ir.Data) +} + +func TestEncodeDisplayInfo(t *testing.T) { + doc := getOrig(t) + ir, err := testDisplayInfo.Encode() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, doc.Config.Res[ir.ID].Data, ir.Data) +} + +func TestEncodeResolutionInfoInfo(t *testing.T) { + doc := getOrig(t) + ir, err := testResolutionInfo.Encode() + if err != nil { + t.Fatal(err) + } + assert.EqualValues(t, doc.Config.Res[ir.ID].Data, ir.Data) +} diff --git a/layer.go b/layer.go index 7771a92..a9153ae 100644 --- a/layer.go +++ b/layer.go @@ -122,7 +122,7 @@ func readLayerAndMaskInfo(r io.Reader, cfg *Config, o *DecodeOptions) (psd *PSD, layerAndMaskInfoLen := int(readUint(b, 0, intSize)) if Debug != nil { Debug.Println(" layerAndMaskInfoLen:", layerAndMaskInfoLen) - reportReaderPosition(" file offset: 0x%08x", r) + reportReaderPosition(" file offset: %d", r) } if layerAndMaskInfoLen == 0 { return psd, read, nil diff --git a/psd.go b/psd.go index 4e66663..d11647b 100644 --- a/psd.go +++ b/psd.go @@ -2,9 +2,11 @@ package psd import ( "errors" + "fmt" "image" "image/color" "io" + "log" ) // logger is subset of log.Logger. @@ -16,7 +18,8 @@ type logger interface { // Debug is useful for debugging. // // You can use by performing the following steps. -// psd.Debug = log.New(os.Stdout, "psd: ", log.Lshortfile) +// +// psd.Debug = log.New(os.Stdout, "psd: ", log.Lshortfile) var Debug logger const ( @@ -68,13 +71,14 @@ const ( // Config represents Photoshop image file configuration. type Config struct { - Version int - Rect image.Rectangle - Channels int - Depth int // 1 or 8 or 16 or 32 - ColorMode ColorMode - ColorModeData []byte - Res map[int]ImageResource + Version int + Rect image.Rectangle + Channels int + Depth int // 1 or 8 or 16 or 32 + ColorMode ColorMode + ColorModeData []byte + Res map[int]ImageResource + CompressionMethod CompressionMethod } // PSB returns whether image is large document format. @@ -195,6 +199,7 @@ func DecodeConfig(r io.Reader) (cfg Config, read int, err error) { Debug.Printf(" channels: %d depth: %d colorMode %d", cfg.Channels, cfg.Depth, cfg.ColorMode) Debug.Printf(" colorModeDataLen: %d", len(cfg.ColorModeData)) Debug.Println("end - header") + reportReaderPosition(" file offset: %d", r) } if cfg.Res, l, err = readImageResource(r); err != nil { @@ -266,6 +271,7 @@ func Decode(r io.Reader, o *DecodeOptions) (psd *PSD, read int, err error) { } read += l cmpMethod := CompressionMethod(readUint16(b, 0)) + psd.Config.CompressionMethod = cmpMethod l, err = cmpMethod.Decode( psd.Data, r, @@ -276,6 +282,7 @@ func Decode(r io.Reader, o *DecodeOptions) (psd *PSD, read int, err error) { psd.Config.PSB(), ) if err != nil { + log.Printf("decode failed with n=%d. len(data)=%d. read=%d", l, len(psd.Data), read) return nil, read, err } read += l @@ -311,3 +318,57 @@ func init() { image.RegisterFormat("psd", headerSignature+"\x00\x01", decode, decodeConfig) image.RegisterFormat("psb", headerSignature+"\x00\x02", decode, decodeConfig) } + +func (psd *PSD) GetChannelImages() ([]*image.Gray, error) { + if psd.Config.Depth != 8 { + return nil, fmt.Errorf("depth!=8 not supported") + } + imgs := make([]*image.Gray, psd.Config.Channels) + for c := range imgs { + imgs[c] = ImgToGray(psd.Channel[c].Picker) + } + return imgs, nil +} + +func (psd *PSD) AddImageChannelData(imgs []*image.Gray) error { + // convert image.Image -> psd.Data (uncompressed merged image data) + // raw data has dimensions: (channels, width, height) + if len(imgs) != psd.Config.Channels { + return fmt.Errorf("got n=%d images but expecting n=m=%d channels", len(imgs), psd.Config.Channels) + } + if psd.Config.Depth != 8 { + return fmt.Errorf("depth!=8 not supported") + } + plane := (psd.Config.Rect.Dx()*psd.Config.Depth + 7) >> 3 * psd.Config.Rect.Dy() + psd.Data = make([]byte, plane*psd.Config.Channels) + chs := make([][]byte, psd.Config.Channels) + psd.Channel = make(map[int]Channel) + for i := 0; i < psd.Config.Channels; i++ { + var pix []uint8 + if imgs[i] == nil { + // set empty images to all white + pix = make([]uint8, psd.Config.Rect.Dx()*psd.Config.Rect.Dy()) + for p := range pix { + pix[p] = 255 + } + } else { + if imgs[i].Rect.Dx() != psd.Config.Rect.Dx() || imgs[i].Rect.Dy() != psd.Config.Rect.Dy() { + return fmt.Errorf("expected config dim=%s but got image dim=%s", psd.Config.Rect, imgs[i].Rect) + } + pix = imgs[i].Pix + } + chs[i] = pix + pk := findGrayPicker(psd.Config.Depth) + pk.setSource(psd.Config.Rect, chs[i]) + psd.Channel[i] = Channel{ + Data: chs[i], + Picker: pk, + } + for j, d := range chs[i] { + psd.Data[plane*i+j] = d + } + + } + + return nil +} diff --git a/testdata/cmyk-spot-channel-5.png b/testdata/cmyk-spot-channel-5.png new file mode 100644 index 0000000..79fa287 Binary files /dev/null and b/testdata/cmyk-spot-channel-5.png differ diff --git a/testdata/cmyk-spot-channel-6.png b/testdata/cmyk-spot-channel-6.png new file mode 100644 index 0000000..50c1cd3 Binary files /dev/null and b/testdata/cmyk-spot-channel-6.png differ diff --git a/testdata/cmyk-spot-channel-7.png b/testdata/cmyk-spot-channel-7.png new file mode 100644 index 0000000..e48850c Binary files /dev/null and b/testdata/cmyk-spot-channel-7.png differ diff --git a/testdata/cmyk-spot.psd b/testdata/cmyk-spot.psd new file mode 100644 index 0000000..c41934b Binary files /dev/null and b/testdata/cmyk-spot.psd differ diff --git a/util.go b/util.go index cddf128..7ef405d 100644 --- a/util.go +++ b/util.go @@ -1,6 +1,9 @@ package psd import ( + "bytes" + "image" + "image/draw" "io" "io/ioutil" "math" @@ -129,6 +132,7 @@ func discard(r io.Reader, skip int) (read int, err error) { } func readPascalString(r io.Reader) (str string, read int, err error) { + // no padding b := make([]byte, 1) if _, err := io.ReadFull(r, b); err != nil { return "", 0, err @@ -156,3 +160,34 @@ func reportReaderPosition(format string, r io.Reader) error { Debug.Printf(format, pos) return nil } + +func stringToPascalBytes(str string, padEven bool) ([]byte, error) { + n := len(str) + if n == 0 && padEven { + // bytes are always even length + // (so a null name consists of two bytes of 0) + return []byte{0, 0}, nil + } + + buf := &bytes.Buffer{} + if _, err := buf.Write([]byte{byte(n)}); err != nil { + return nil, err + } + n, err := buf.WriteString(str) + if err != nil { + return nil, err + } + if padEven { + remainder := n % 2 + if remainder != 0 { + buf.Write([]byte{0}) + } + } + return buf.Bytes(), nil +} + +func ImgToGray(img image.Image) *image.Gray { + out := image.NewGray(img.Bounds()) + draw.Draw(out, out.Rect, img, image.Point{}, draw.Src) + return out +}