Help us shape the future of Go! Take the 2026 Go Developer Survey

psd

package module
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 17, 2026 License: BSD-3-Clause Imports: 7 Imported by: 0

README

psd

Adobe Photoshop (.psd) and large-document (.psb) files, for image.Decode. Pure Go, CGO-free, including GOOS=js, with no dependency outside the standard library.

import _ "github.com/go-images/psd"

m, _, err := image.Decode(f)   // a .psd now decodes like a .png

⛔ The composite, and deliberately nothing else

A Photoshop file holds two different pictures: the layers an editor works with, and the flattened composite Photoshop writes at the end of the file so that anything which is not Photoshop has something to show. This package reads the second one.

That is not a shortcut, it is the job. Reading layers means understanding blend modes, clipping, masks, adjustment layers, smart objects and layer effects — re-implementing a compositor — and the result would still differ from what Photoshop shows. The composite is what Photoshop shows.

What a caller loses is the ability to turn a layer off. What a caller gains is the picture, at full resolution, from about 600 lines of Go.

colour mode Bitmap, Grayscale, Indexed, RGB, CMYK, Duotone, Lab
depth 1, 8 and 16 bits a channel
compression raw, and RLE (PackBits)
container PSD and PSB, which differ only in their length fields

Alpha is honoured wherever the mode has room for it: a second channel of a grayscale file, a fourth of an RGB one, a fifth of a CMYK one.

⛔ The four decisions that make or break a PSD reader

1. Everything is planar. All of the red, then all of the green, then all of the blue — scanline counts included. An interleaved read produces the same recognisable failure every time: a picture in vertical bands of one colour each.

2. CMYK is stored inverted. 255 means no ink. A reader that takes the bytes as written turns a white page black. There is no flag in the file saying so; it is simply how the format is, which is why the test for it asserts that a page with no ink comes back white.

3. PackBits, read as signed. 0…127 copies the next n+1 bytes; -1…-127 repeats the next byte 1−n times; -128 is a no-operation, and Photoshop writes it. A summary that says "128…255 means 1 to 129 repetitions" is off by one for every run in the file — which looks like a slight shear rather than like a broken decoder, so it survives a casual look.

4. Alpha is straight, not premultiplied. The result is NRGBA, because a PSD's alpha is unassociated. Handing straight bytes to a premultiplied image makes every semi-transparent pixel too bright, and the difference is invisible on an opaque file — which is how it survives a casual look at an icon.

⛔ The limits are the security surface

psd.MaxPixels = 64 << 20   // the default

A PSD declares its size in a 26-byte header, and the RLE scanline table lets a tiny file claim an enormous picture: 30 000 × 30 000 is inside the format's own limit and is 900 million pixels — 3.6 GB as RGBA. The declaration is a number in a file, and a reader that believes it allocates what it is told to.

  • the pixel count is multiplied as int64 before being compared, because 300000*300000 overflows an int on a 32-bit build and a guard that overflows waves the bomb through. This package builds for 386 and arm;
  • a declared scanline longer than PackBits could ever produce for that row is refused, which is what stops a per-row allocation from being a lie;
  • the length-prefixed sections are bounded by the same ceiling, because a length in the colour-mode section costs exactly what one in the header does;
  • the layer section — most of a working file — is skipped with io.CopyN to io.Discard rather than buffered, so reading the last three megabytes of a file does not mean holding the first hundred.

32-bit (floating-point) channels, a ZIP-compressed composite and Multichannel mode are refused by name. A refusal that names its reason is something somebody can close; "unsupported format" is not.

ErrFormat is returned only for "this is not a Photoshop file", so image.Decode goes on to try the other codecs. Everything else — a version 3 file, a 32-bit file, a truncated one — comes back as a named error, because a PSB reported as "unknown format" sends the reader to look at the wrong thing entirely.

Measured against an independent decoder

⛔ A decoder checked only against its own fixtures is checked against its author's reading of the specification, which is the thing most likely to be wrong. macOS ships sips, whose PSD support comes from ImageIO and has nothing to do with this code, so the corpus test converts each file with sips and compares pixel for pixel.

On 2026-10-17, over the Photoshop files on one machine:

read    example.psd    1920x1080  *image.NRGBA
        judged against sips: 0 of 2073600 pixels differ (0.0000%)
read    icon.psd        256x256   *image.NRGBA
        judged against sips: 81 of 65536 pixels differ, worst channel delta 1
REFUSED servers-….psd   image: unknown format
  • exact on 2 073 600 pixels of an RGB file;
  • 81 pixels of an antialiased icon edge differ by one count, which is rounding in the premultiply;
  • the refused file is a JPEG with a .psd name — the same shape as the 27 PNGs named .pdf in go-pdfkit's corpus. Refusing it is correct.

⛔ And the first version of that comparison was wrong in a way that blamed this package. It compared At().RGBA() on both sides — and RGBA() premultiplies by definition, while sips has already written premultiplied values into its PNG, so the reference was premultiplied twice. It reported 207 pixels differing with a worst delta of 59, every one of them on an antialiased edge. Measured rather than argued: of the 2 661 channels that differ when the two are compared as straight values, zero differ once this side is premultiplied once and sips's bytes are read as they stand. The file's RGB is straight, this package is right, and the disagreement was the measuring instrument. The judge and the subject have to be in the same alpha space.

⛔ There is no .psd in testdata, deliberately: the files measured are one machine owner's own and third parties' artwork, and a fixture a test ships has to be one the repository may carry. The corpus is read from wherever the person running the test keeps theirs, and the test skips loudly without it:

PSD_CORPUS=~/Pictures go test -run TestARealCorpus -v ./...

⛔ Two conversions no real file exercised

Bitmap mode (1 bit, where Photoshop writes 0 for white — it stores ink, not light) and Lab mode (which Photoshop writes against a D50 white point, where sRGB's own D65 would tilt the grey axis towards blue) are implemented from the specification and tested against fixtures written here. No file in the measured corpus is in either mode, so they are marked as read from the specification rather than presented as measured.

How it is checked

  • 100% of statements, no uncovered block, race detector, eleven build targets including 386, arm and js/wasm.
  • The fixture builder in the test writes PSDs, including a PackBits packer — so unpack is checked against an independent implementation of the format rather than against itself, and the two round-trip over data that exercises both branches.
  • ⛔ The coverage profile found the first real hole: the repeat-run branch was never exercised, because the builder only emits a repeat for a run of three or more and every earlier fixture had runs of two. A PSD of a photograph is mostly literals; a PSD of a screenshot or a logo is mostly repeats — the untested half was the half that files of the commonest kind are made of.
  • ⛔ The mutation sweep found the second, which is a cross-architecture hazard: the clamp in the sRGB transfer function survived deletion, because every Lab fixture was inside the gamut. Lab holds colours sRGB cannot, so a legal triple gives a negative linear value — and Go says a float-to-integer conversion the target cannot represent is implementation-dependent: 0 on one architecture and 251 on another. It is now clamped at both ends before the conversion, with an out-of-gamut fixture that pins it.

Licence

BSD-3-Clause. Written against the Adobe Photoshop File Formats Specification; no Adobe code is used or needed.

Documentation

Overview

Package psd decodes the composite image of an Adobe Photoshop file (.psd) or a large-document one (.psb).

It registers itself with image.Decode, so importing it for its side effect is enough:

import _ "github.com/go-images/psd"

m, _, err := image.Decode(f)   // a .psd now decodes like a .png

⛔ The COMPOSITE, and deliberately nothing else

A Photoshop file holds two different pictures: the layers an editor works with, and the flattened composite Photoshop writes at the END of the file so that anything which is not Photoshop has something to show. This package reads the second one.

That is not a shortcut, it is the job. Reading layers means understanding blend modes, clipping, masks, adjustment layers, smart objects and layer effects — re-implementing a compositor — and the result would still differ from what Photoshop shows. The composite IS what Photoshop shows.

What a caller loses is the ability to turn a layer off. What a caller gains is the picture, at full resolution, from a package with no dependencies outside the standard library.

What it reads

colour mode    Bitmap, Grayscale, Indexed, RGB, CMYK, Duotone, Lab
depth          1, 8 and 16 bits a channel
compression    raw, and RLE (PackBits)
container      PSD and PSB, which differ in their length fields

Alpha is honoured where the mode has room for it: a second channel of a grayscale file, a fourth of an RGB one, a fifth of a CMYK one.

⛔ CMYK is stored INVERTED by Photoshop — 255 means no ink — and is un-inverted here. That inversion is the single most common way a CMYK reader comes out looking like a photographic negative.

What it refuses, by name

32-bit (floating-point) channels, a ZIP-compressed composite, and the Multichannel mode, each with an error that says which. A refusal that names its reason is something somebody can close; "unsupported format" is not.

⛔ Two conversions no file in the measured corpus exercises

Bitmap mode (1 bit, where Photoshop writes 0 for WHITE) and Lab mode (which Photoshop writes against a D50 white point) are implemented from the specification and tested against fixtures written here — which is to say against this package's own reading of the specification, and not against a file Photoshop produced. They are marked as such rather than presented as measured. Everything else was checked against real files, pixel for pixel, using an independent decoder as the judge.

Nothing here is CGO, and it builds for GOOS=js.

Index

Constants

This section is empty.

Variables

View Source
var ErrFormat = image.ErrFormat

ErrFormat is returned when the stream is not a Photoshop file at all. It matches image.ErrFormat so that image.Decode's registry treats it the same way as every other codec's "not mine".

View Source
var MaxPixels = 64 << 20

MaxPixels is the largest composite this package will allocate, in pixels.

⛔ A VARIABLE and not a constant, and that is not a style choice: a ceiling measured in hundreds of megabytes cannot be reached by a test that has to run in a second, so a constant here would be a guard nobody could exercise. A test lowers it; nothing else should.

⛔ It exists because a PSD declares its size in the header and the RLE scanline table lets a tiny file claim an enormous picture: 30 000 × 30 000 is inside the specification's own limit and is 900 million pixels, which as RGBA is 3.6 GB. The declaration is a number in a file, and a reader that believes it allocates what it is told to.

Functions

func Decode

func Decode(r io.Reader) (image.Image, error)

Decode reads the composite image of a Photoshop file.

func DecodeConfig

func DecodeConfig(r io.Reader) (image.Config, error)

DecodeConfig reads the dimensions and colour model without the pixels.

It is what image.Decode's documentation asks a caller to use first on untrusted input, and it reads only the header and the colour table: the picture's size is known 26 bytes in.

Types

This section is empty.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL