diff --git a/internal/format/avif/avif.go b/internal/format/avif/avif.go index aa740dd..9ad045a 100644 --- a/internal/format/avif/avif.go +++ b/internal/format/avif/avif.go @@ -48,6 +48,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) const ( @@ -125,16 +126,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -345,11 +340,11 @@ func measuredSize(r format.Request, label string, q int) (memo, error) { // namedSize handles a recipe that asked for a picture of its own size. func namedSize(r format.Request, label string, q int) (memo, error) { - w, err := dimension(r.Properties, "width", sizeLadder[0].width) + w, err := imagedim.Value("avif", imagedim.SettingWidth, r.Properties, maxDimension, sizeLadder[0].width) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", sizeLadder[0].height) + h, err := imagedim.Value("avif", imagedim.SettingHeight, r.Properties, maxDimension, sizeLadder[0].height) if err != nil { return memo{}, err } @@ -389,21 +384,6 @@ func checkJointLimits(w, h int) error { return nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("avif: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("avif: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func quality(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/bmp/bmp.go b/internal/format/bmp/bmp.go index 153c3ee..ad46f96 100644 --- a/internal/format/bmp/bmp.go +++ b/internal/format/bmp/bmp.go @@ -18,10 +18,10 @@ import ( "image" "image/color" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -85,16 +85,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -195,11 +189,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("bmp", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("bmp", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -284,21 +278,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("bmp: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("bmp: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/gif/gif.go b/internal/format/gif/gif.go index 1fb4375..187427a 100644 --- a/internal/format/gif/gif.go +++ b/internal/format/gif/gif.go @@ -39,6 +39,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -123,16 +124,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "frames", Kind: format.PropertyInt, Min: minFrames, Max: maxFrames, @@ -422,11 +417,11 @@ func chooseSize(r format.Request, label string) (memo, error) { } if wSet || hSet { - w, err := dimension(r.Properties, "width", 640) + w, err := imagedim.Value("gif", imagedim.SettingWidth, r.Properties, maxDimension, 640) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", 480) + h, err := imagedim.Value("gif", imagedim.SettingHeight, r.Properties, maxDimension, 480) if err != nil { return memo{}, err } @@ -475,21 +470,6 @@ func chooseSize(r format.Request, label string) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("gif: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("gif: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func encodedBodySize(m memo) (int64, error) { holder := &tailHolder{w: io.Discard, keep: trailerSize} if err := encode(holder, m); err != nil { diff --git a/internal/format/ico/ico.go b/internal/format/ico/ico.go index 813ffd8..4af1f75 100644 --- a/internal/format/ico/ico.go +++ b/internal/format/ico/ico.go @@ -19,10 +19,10 @@ import ( "image/color" stdpng "image/png" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -77,16 +77,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the icon is. Left out, the largest standard icon size that fits is used.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the icon is. Left out, the largest standard icon size that fits is used.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the icon is. Left out, the largest standard icon size that fits is used."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the icon is. Left out, the largest standard icon size that fits is used."}), { Name: "embed", Kind: format.PropertyChoice, Choices: []string{embedBMP, embedPNG}, @@ -221,11 +215,11 @@ func chooseSize(r format.Request, label, embed string) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", maxDimension) + w, err := imagedim.Value("ico", imagedim.SettingWidth, r.Properties, maxDimension, maxDimension) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", maxDimension) + h, err := imagedim.Value("ico", imagedim.SettingHeight, r.Properties, maxDimension, maxDimension) if err != nil { return 0, 0, err } @@ -245,21 +239,6 @@ func chooseSize(r format.Request, label, embed string) (int, int, error) { return smallest, smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("ico: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("ico: %s must be between %d and %d pixels, got %d - an icon stores each side in a single byte", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/imagedim/imagedim.go b/internal/format/imagedim/imagedim.go new file mode 100644 index 0000000..82d22b7 --- /dev/null +++ b/internal/format/imagedim/imagedim.go @@ -0,0 +1,122 @@ +// Package imagedim is the width and height that every picture format shares. +// +// It lives here rather than inside one of them because ten formats ask the +// same question and used to answer it ten times. Measured on 2026-09-08: the +// two declarations were written out in full in all ten packages, and the +// function reading them - the same twelve lines, differing only in the format +// named in its message - stood in all ten as well. That is the shape thirteen +// packages had before one filler loop replaced their four, and the shape two +// containers had before the archive package replaced their two. +// +// What is shared is what cannot legitimately differ: the name, that it is a +// whole number, that the smallest picture is one pixel on a side, and that the +// number counts pixels. What each format supplies is what genuinely differs +// between them, which was measured before this package existed rather than +// guessed: +// +// - The largest side. Five formats reach 20000, AVIF and JXL 16384, WEBP +// 16383 and ICO 256, and each of those is the ceiling of the thing itself +// rather than a number somebody picked. +// - The sentence. An icon is not a picture and an SVG only SAYS how wide it +// is, so four different sentences are correct rather than four copies of +// one. A shared sentence would have made three of them wrong. +// - The default, which only SVG declares, because it is the only one that +// cannot work a size out from the bytes asked for. +// +// The shape is enforced by construction rather than by a check: a format +// cannot declare a width of a different kind or in a different unit, because +// it does not write those fields. What stops an ELEVENTH format declaring +// width its own way without using this package at all is +// TestOneSettingNameMeansOneKindOfSetting. +package imagedim + +import ( + "fmt" + "strconv" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" +) + +// Setting names. Public names, so they are spelled once. +const ( + SettingWidth = "width" + SettingHeight = "height" +) + +const ( + // Smallest is the shortest side any picture can have. Zero is not a + // picture, and every format that has ever declared these agreed on one. + Smallest = 1 + // Unit is what the number counts, for the refusal and for a window's field. + Unit = "pixels" +) + +// Side is one dimension of a picture as one format takes it. +type Side struct { + // Largest is the longest side this format can encode. It is the ceiling of + // the format or of its encoder, not a limit chosen for comfort. + Largest int64 + // Default is what the format uses when nothing says otherwise, written the + // way a person writes it. Empty means the format works it out from the + // bytes it was asked for, which is what nine of the ten do. + Default string + // Detail is the one sentence a person reads, beside the field in a window + // and under the name in "tfg formats". It is per format on purpose - see + // the note above this package. + Detail string +} + +// Width is the declaration of the width setting for a format taking this side. +func Width(s Side) format.Property { return s.property(SettingWidth) } + +// Height is the declaration of the height setting for a format taking this side. +func Height(s Side) format.Property { return s.property(SettingHeight) } + +func (s Side) property(name string) format.Property { + return format.Property{ + Name: name, Kind: format.PropertyInt, + Min: Smallest, Max: s.Largest, Unit: Unit, + Default: s.Default, + Detail: s.Detail, + } +} + +// Value reads one side out of what a recipe stated, falling back to what the +// format works out for itself when nothing was said. +// +// The range is checked here as well as in the registry, and that is deliberate +// rather than left over. The registry refuses first on both surfaces - measured +// on 2026-09-08, where a width of 999999 comes back as exit 4 with the sentence +// built from the declaration, and nothing in internal/guard reaches a generator +// with a value outside its range. But this function is callable directly, a +// guard is such a caller, and a generator that trusts its input is one registry +// change away from encoding a picture nobody ordered. Same reasoning as the +// branches textenc.Parse keeps, and it costs one copy now rather than ten. +// +// The refusal is the structured one rather than a sentence, and that is the +// ten being unified on the BEST of them rather than on the most common. Nine +// built a string with fmt.Errorf and named the format inside it by hand. SVG +// returned format.PropertyValueError - the same type the registry itself +// raises, so what reports it can ask for the parts separately instead of +// parsing a sentence. The wording is SVG's, unchanged, because that is the one +// of the ten a reader could actually reach. +func Value(formatID, key string, props map[string]string, largest, fallback int) (int, error) { + raw, ok := props[key] + if !ok || raw == "" { + return fallback, nil + } + n, err := strconv.Atoi(raw) + if err != nil { + return 0, &format.PropertyValueError{ + Format: formatID, Key: key, Value: raw, + Reason: "it has to be a whole number of " + Unit, + } + } + if n < Smallest || n > largest { + return 0, &format.PropertyValueError{ + Format: formatID, Key: key, Value: raw, + Reason: fmt.Sprintf("it has to be between %d and %d", Smallest, largest), + } + } + return n, nil +} diff --git a/internal/format/jpg/jpg.go b/internal/format/jpg/jpg.go index 7c1a094..0738c28 100644 --- a/internal/format/jpg/jpg.go +++ b/internal/format/jpg/jpg.go @@ -23,6 +23,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -107,16 +108,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -313,11 +308,11 @@ func chooseSize(r format.Request, label string, q int) (memo, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", defaultWidth) + w, err := imagedim.Value("jpg", imagedim.SettingWidth, r.Properties, maxDimension, defaultWidth) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", defaultHeight) + h, err := imagedim.Value("jpg", imagedim.SettingHeight, r.Properties, maxDimension, defaultHeight) if err != nil { return memo{}, err } @@ -352,21 +347,6 @@ func chooseSize(r format.Request, label string, q int) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("jpg: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("jpg: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func qualityOf(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/jxl/jxl.go b/internal/format/jxl/jxl.go index 8b5ed0b..095d7ad 100644 --- a/internal/format/jxl/jxl.go +++ b/internal/format/jxl/jxl.go @@ -62,6 +62,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) const ( @@ -168,16 +169,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow-jxl", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), { Name: "quality", Kind: format.PropertyInt, Min: minQuality, Max: maxQuality, Default: strconv.Itoa(defaultQuality), @@ -387,11 +382,11 @@ func measuredSize(r format.Request, label string, q int) (memo, error) { // namedSize handles a recipe that asked for a picture of its own size. func namedSize(r format.Request, label string, q int) (memo, error) { - w, err := dimension(r.Properties, "width", sizeLadder[0].width) + w, err := imagedim.Value("jxl", imagedim.SettingWidth, r.Properties, maxDimension, sizeLadder[0].width) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", sizeLadder[0].height) + h, err := imagedim.Value("jxl", imagedim.SettingHeight, r.Properties, maxDimension, sizeLadder[0].height) if err != nil { return memo{}, err } @@ -431,21 +426,6 @@ func checkJointLimits(w, h int) error { return nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("jxl: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("jxl: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func quality(props map[string]string) (int, error) { raw, ok := props["quality"] if !ok || raw == "" { diff --git a/internal/format/png/png.go b/internal/format/png/png.go index 55ab2e6..c9003ba 100644 --- a/internal/format/png/png.go +++ b/internal/format/png/png.go @@ -14,10 +14,10 @@ import ( "image/color" stdpng "image/png" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -53,7 +53,6 @@ const ( defaultWidth = 640 defaultHeight = 480 - minDimension = 1 maxDimension = 20000 // The picture is built in memory as one buffer and encoded twice, once @@ -91,16 +90,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, a size is chosen that fits the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, a size is chosen that fits the bytes you asked for."}), }, JointLimits: []format.JointLimit{{ Of: "width", By: "height", Max: maxPixels, @@ -344,11 +337,11 @@ func chooseSize(r format.Request, label string) (memo, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", defaultWidth) + w, err := imagedim.Value("png", imagedim.SettingWidth, r.Properties, maxDimension, defaultWidth) if err != nil { return memo{}, err } - h, err := dimension(r.Properties, "height", defaultHeight) + h, err := imagedim.Value("png", imagedim.SettingHeight, r.Properties, maxDimension, defaultHeight) if err != nil { return memo{}, err } @@ -414,21 +407,6 @@ func chooseSize(r format.Request, label string) (memo, error) { return smallest, nil } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("png: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("png: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - // picture builds the image. Deterministic from the seed, and compressible, so // that the encoded result is small next to any realistic requested size and // the padding chunk carries the difference. diff --git a/internal/format/svgfile/svg.go b/internal/format/svgfile/svg.go index b277d44..41644bd 100644 --- a/internal/format/svgfile/svg.go +++ b/internal/format/svgfile/svg.go @@ -17,6 +17,7 @@ import ( "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" ) // Measured on 2026-08-01, a comment holds arbitrary bytes to 1 MiB both after @@ -79,8 +80,12 @@ const ( // Width and Height name the two settings. Exported so that a guard presses // the key this format actually declares rather than a string spelled twice, // which is the same reason jsonfile exports the name of its layout setting. - Width = "width" - Height = "height" + // + // Taken from the package the picture formats share rather than spelled + // again here, so that a drawing and a photograph cannot end up naming the + // same setting differently. + Width = imagedim.SettingWidth + Height = imagedim.SettingHeight declaration = `` + "\n" rootClose = "\n" @@ -216,20 +221,14 @@ func init() { // later. Declaring none of them makes a recipe asking for one fail // loudly rather than quietly. Properties: []format.Property{ - { - Name: Width, Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", + imagedim.Width(imagedim.Side{Largest: maxDimension, Default: strconv.Itoa(defaultWidth), Detail: "How wide the drawing says it is. Nothing is drawn into pixels here, " + - "so a large number costs a few bytes in the file and a great deal of memory in whatever opens it.", - }, - { - Name: Height, Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", + "so a large number costs a few bytes in the file and a great deal of memory in whatever opens it."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, Default: strconv.Itoa(defaultHeight), Detail: "How tall the drawing says it is. The label sits along the bottom edge, " + - "so a drawing shorter than that strip carries no visible label.", - }, + "so a drawing shorter than that strip carries no visible label."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -245,39 +244,12 @@ type memo struct { height int } -// dimension reads one of the two size settings. -// -// The registry has already refused anything outside the declared range by the -// time a run reaches here, so the bounds below are a backstop for a caller -// that reaches the generator directly - the same belt the page count of PDF -// wears, and for the same reason. -func dimension(props map[string]string, name string, fallback int) (int, error) { - raw, ok := props[name] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, &format.PropertyValueError{ - Format: "svg", Key: name, Value: raw, - Reason: "it has to be a whole number of pixels", - } - } - if n < minDimension || n > maxDimension { - return 0, &format.PropertyValueError{ - Format: "svg", Key: name, Value: raw, - Reason: fmt.Sprintf("it has to be between %d and %d", minDimension, maxDimension), - } - } - return n, nil -} - func (generator) Plan(r format.Request) (format.Plan, error) { - w, err := dimension(r.Properties, Width, defaultWidth) + w, err := imagedim.Value("svg", Width, r.Properties, maxDimension, defaultWidth) if err != nil { return format.Plan{}, err } - h, err := dimension(r.Properties, Height, defaultHeight) + h, err := imagedim.Value("svg", Height, r.Properties, maxDimension, defaultHeight) if err != nil { return format.Plan{}, err } diff --git a/internal/format/tiff/tiff.go b/internal/format/tiff/tiff.go index 220733b..52877cb 100644 --- a/internal/format/tiff/tiff.go +++ b/internal/format/tiff/tiff.go @@ -30,10 +30,10 @@ import ( "image" "image/color" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -130,16 +130,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -236,11 +230,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("tiff", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("tiff", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -323,21 +317,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("tiff: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("tiff: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/format/webp/webp.go b/internal/format/webp/webp.go index 8ca881c..95d4fc5 100644 --- a/internal/format/webp/webp.go +++ b/internal/format/webp/webp.go @@ -37,10 +37,10 @@ import ( "encoding/binary" "fmt" "io" - "strconv" "github.com/donislawdev/TestingFilesGenerator/internal/core" "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" "github.com/donislawdev/TestingFilesGenerator/internal/format/imagelabel" ) @@ -102,16 +102,10 @@ func init() { Label: format.LabelVisible, Oracle: "pillow", Properties: []format.Property{ - { - Name: "width", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, - { - Name: "height", Kind: format.PropertyInt, - Min: minDimension, Max: maxDimension, Unit: "pixels", - Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for.", - }, + imagedim.Width(imagedim.Side{Largest: maxDimension, + Detail: "How wide the picture is. Left out, the picture is sized to fill the bytes you asked for."}), + imagedim.Height(imagedim.Side{Largest: maxDimension, + Detail: "How tall the picture is. Left out, the picture is sized to fill the bytes you asked for."}), }, GeneratorVersion: generatorVersion, Generator: generator{}, @@ -230,11 +224,11 @@ func chooseSize(r format.Request) (int, int, error) { _, hSet := r.Properties["height"] if wSet || hSet { - w, err := dimension(r.Properties, "width", 0) + w, err := imagedim.Value("webp", imagedim.SettingWidth, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } - h, err := dimension(r.Properties, "height", 0) + h, err := imagedim.Value("webp", imagedim.SettingHeight, r.Properties, maxDimension, 0) if err != nil { return 0, 0, err } @@ -316,21 +310,6 @@ func isqrt(n uint64) uint64 { return x } -func dimension(props map[string]string, key string, fallback int) (int, error) { - raw, ok := props[key] - if !ok || raw == "" { - return fallback, nil - } - n, err := strconv.Atoi(raw) - if err != nil { - return 0, fmt.Errorf("webp: %s must be a whole number of pixels, got %q", key, raw) - } - if n < minDimension || n > maxDimension { - return 0, fmt.Errorf("webp: %s must be between %d and %d pixels, got %d", key, minDimension, maxDimension, n) - } - return n, nil -} - func (generator) Write(ctx context.Context, w io.Writer, p format.Plan) error { m, ok := p.Memo.(memo) if !ok { diff --git a/internal/guard/layers_test.go b/internal/guard/layers_test.go index 1740cf4..58cbd83 100644 --- a/internal/guard/layers_test.go +++ b/internal/guard/layers_test.go @@ -36,6 +36,7 @@ var layer = map[string]int{ "internal/format": 1, "internal/format/all": 1, "internal/format/imagelabel": 1, + "internal/format/imagedim": 1, "internal/format/textenc": 1, "internal/format/archive": 1, "internal/format/txt": 1, @@ -115,6 +116,7 @@ var sameLayerAllowed = map[string][]string{ "internal/format/wav", }, "internal/format/imagelabel": {"internal/format"}, + "internal/format/imagedim": {"internal/format"}, "internal/format/textenc": {"internal/format"}, "internal/format/archive": {"internal/format"}, "internal/format/txt": {"internal/format", "internal/format/imagelabel", "internal/format/textenc"}, @@ -124,23 +126,23 @@ var sameLayerAllowed = map[string][]string{ "internal/format/jsonfile": {"internal/format"}, "internal/format/xmlfile": {"internal/format", "internal/format/textenc"}, "internal/format/htmlfile": {"internal/format"}, - "internal/format/svgfile": {"internal/format"}, - "internal/format/bmp": {"internal/format", "internal/format/imagelabel"}, + "internal/format/svgfile": {"internal/format", "internal/format/imagedim"}, + "internal/format/bmp": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/opc": {"internal/format"}, "internal/format/docx": {"internal/format", "internal/format/opc"}, "internal/format/xlsx": {"internal/format", "internal/format/opc"}, "internal/format/pptx": {"internal/format", "internal/format/opc"}, - "internal/format/gif": {"internal/format", "internal/format/imagelabel"}, - "internal/format/ico": {"internal/format", "internal/format/imagelabel"}, - "internal/format/jpg": {"internal/format", "internal/format/imagelabel"}, - "internal/format/png": {"internal/format", "internal/format/imagelabel"}, + "internal/format/gif": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/ico": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/jpg": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/png": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/pdf": {"internal/format", "internal/format/imagelabel"}, "internal/format/zip": {"internal/format", "internal/format/imagelabel", "internal/format/archive"}, "internal/format/targz": {"internal/format", "internal/format/archive"}, - "internal/format/tiff": {"internal/format", "internal/format/imagelabel"}, - "internal/format/webp": {"internal/format", "internal/format/imagelabel"}, - "internal/format/avif": {"internal/format", "internal/format/imagelabel"}, - "internal/format/jxl": {"internal/format", "internal/format/imagelabel"}, + "internal/format/tiff": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/webp": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/avif": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, + "internal/format/jxl": {"internal/format", "internal/format/imagelabel", "internal/format/imagedim"}, "internal/format/wav": {"internal/format", "internal/format/imagelabel"}, "internal/preset": {"internal/recipe"}, diff --git a/internal/guard/picturesides_test.go b/internal/guard/picturesides_test.go new file mode 100644 index 0000000..3ea625f --- /dev/null +++ b/internal/guard/picturesides_test.go @@ -0,0 +1,89 @@ +package guard + +import ( + "errors" + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + "github.com/donislawdev/TestingFilesGenerator/internal/format/imagedim" +) + +// The backstop under the ten picture formats has to refuse what it says it +// refuses. +// +// Ten formats used to carry their own copy of this and every copy checked the +// range. Measured on 2026-09-08, none of those checks could be reached: the +// registry refuses a width outside the declared bounds first, in engine.go and +// in recipe/target.go, which are the only two production callers, and a run +// asking png for a width of 999999 comes back as exit 4 with the sentence built +// from the declaration. Nothing in this package reached a generator with a +// value outside its range either. +// +// That is the shape this project calls a defence nothing can redden, and it had +// two honest endings. It was kept rather than deleted, for the reason +// textenc.Parse gives about its own branches - the function is callable +// directly and a generator that trusts its input is one registry change away +// from encoding a picture nobody ordered - and keeping it puts the burden here: +// a backstop with no test is a comment. This is the test, so the ten copies +// became one copy that something presses. +// +// What it does NOT claim is that a person can reach these. A person cannot, and +// the guards on the declaration are what prove the sentence they do get. +func TestThePictureSideBackstopRefusesWhatTheDeclarationWould(t *testing.T) { + const largest = 256 + + for _, c := range []struct { + name string + key string + value string + }{ + {"above the largest side", imagedim.SettingWidth, "257"}, + {"below the smallest side", imagedim.SettingHeight, "0"}, + {"a negative side", imagedim.SettingWidth, "-1"}, + {"not a number at all", imagedim.SettingHeight, "wide"}, + } { + t.Run(c.name, func(t *testing.T) { + got, err := imagedim.Value("ico", c.key, map[string]string{c.key: c.value}, largest, 32) + if err == nil { + t.Fatalf("%s=%q was accepted and came back as %d", c.key, c.value, got) + } + + // The structured refusal rather than a sentence, because whatever + // reports it asks for the parts by name. Nine of the ten built a + // string instead and the tenth did this. + var refused *format.PropertyValueError + if !errors.As(err, &refused) { + t.Fatalf("refused %s=%q with %T, which nothing can take apart: %v", c.key, c.value, err, err) + } + if refused.Format != "ico" { + t.Errorf("the refusal names %q rather than the format that asked", refused.Format) + } + if refused.Key != c.key { + t.Errorf("the refusal names the setting %q rather than %q", refused.Key, c.key) + } + if refused.Value != c.value { + t.Errorf("the refusal quotes %q rather than what was written, %q", refused.Value, c.value) + } + }) + } +} + +// A side inside the range is not refused, and that is the control. +// +// Without it the test above passes for a function that refuses everything, +// which would be a backstop that has become a wall - and a wall here means no +// picture format can be given a size at all. +func TestASideInsideTheRangeIsTakenAsWritten(t *testing.T) { + const largest = 256 + + for _, value := range []string{"1", "32", "256"} { + got, err := imagedim.Value("ico", imagedim.SettingWidth, + map[string]string{imagedim.SettingWidth: value}, largest, 99) + if err != nil { + t.Fatalf("width=%q is inside the range and was refused: %v", value, err) + } + if want := atoi(value); got != want { + t.Errorf("width=%q came back as %d", value, got) + } + } +} diff --git a/internal/guard/settingmeaning_test.go b/internal/guard/settingmeaning_test.go new file mode 100644 index 0000000..92e44ce --- /dev/null +++ b/internal/guard/settingmeaning_test.go @@ -0,0 +1,156 @@ +package guard + +import ( + "testing" + + "github.com/donislawdev/TestingFilesGenerator/internal/format" + _ "github.com/donislawdev/TestingFilesGenerator/internal/format/all" +) + +// declaredSetting is one format's declaration of one setting. +type declaredSetting struct { + format string + property format.Property +} + +// deliberateHomonyms are the names two formats spell alike and mean +// differently on purpose, with the reason each one is allowed. +// +// There is one, and it is here rather than absent because the guard that would +// have banned it was written on 2026-09-01 and deleted the same day for going +// red on it. "entry" is ordinary English and no package owns it - the note at +// the foot of archiveaxes_test.go carries that measurement in full. What this +// file refuses is an ACCIDENT wearing the same shape, so the deliberate case +// has to be sayable. +// +// A name written here is checked from both sides. It has to still be shared, +// and the formats sharing it have to still disagree, because an excuse that +// outlived its case is read by the next person as a rule. +var deliberateHomonyms = map[string]string{ + "entry_format": "log means the shape of a log line - apache-combined, nginx, syslog - " + + "where a container means the format of the files it holds", +} + +// One name has to describe one kind of setting. +// +// Twenty four formats declare seventy two settings between them under thirty +// nine names, and thirteen of those names are written by more than one format. +// Nothing compared them until 2026-09-08. What held them together was that +// somebody kept them alike by hand, which is the mechanism archiveaxes_test.go +// replaced for containers - and the one that let thirteen packages carry four +// different versions of a single filler loop. +// +// The drift this closes is quiet on both surfaces. "tfg formats" prints each +// declaration from the format that made it, and a window builds each field +// from that format's own Detail, so a width offered as free text by one format +// and as a bounded number by another draws two different fields with nothing +// said. A recipe moved between two formats then behaves differently for a +// reason nobody can see. +// +// What it asks is narrow, and the narrowness is measured rather than cautious. +// Three fields diverge legitimately today and are deliberately NOT asked about: +// +// - Max. width runs to 20000 for five formats, 16384 for AVIF and JXL, 16383 +// for WEBP and 256 for ICO, and each is the ceiling of the thing itself. +// - Default. quality is 60 for AVIF and 90 for JPG and JXL, and both carry +// weight: the AVIF ladder ceilings were measured at its default and the +// JPG minimum is the floor of an ordinary run at its own. +// - Min. columns starts at 2 for CSV and at 1 for XLSX. +// +// Kind and Unit are what is left, and they are where a typo lands: a unit of +// "px" beside nine of "pixels", or an eleventh picture format declaring width +// as free text. Neither moves a byte, and both would sit unnoticed until +// somebody read ten declarations side by side. +// +// Asked of every registered format and of every name any two of them share, so +// the fifteenth picture format and the twentieth text format are covered on the +// day they arrive rather than the day somebody remembers this file. +func TestOneSettingNameMeansOneKindOfSetting(t *testing.T) { + shared := settingsMoreThanOneFormatDeclares() + if len(shared) == 0 { + t.Fatal("no setting name is written by two formats, so this proved nothing") + } + + compared := 0 + for name, declared := range shared { + if _, deliberate := deliberateHomonyms[name]; deliberate { + continue + } + compared++ + first := declared[0] + for _, other := range declared[1:] { + if other.property.Kind != first.property.Kind { + t.Errorf("%q is a %s in %s and a %s in %s - one name has to describe one kind of setting, "+ + "or it is a homonym and belongs in deliberateHomonyms with the reason", + name, first.property.Kind, first.format, other.property.Kind, other.format) + } + if other.property.Unit != first.property.Unit { + t.Errorf("%q counts %q in %s and %q in %s - the same setting cannot count two things", + name, first.property.Unit, first.format, other.property.Unit, other.format) + } + } + } + + if compared == 0 { + t.Fatal("every shared name was excused as a homonym, so this proved nothing") + } +} + +// An excuse has to still describe the tree it excuses. +// +// The list above is the one place this guard can be told to look away, so it is +// the one place a stale entry turns into silence. Two ways it can go stale and +// they fail differently: the name stops being shared at all, which is an excuse +// with nothing behind it, or the formats sharing it come into agreement, which +// is an excuse still pointing at a case that has been fixed. Either way the +// next person reads it as a rule saying these names may drift. +// +// The same shape as the exception registry in notelemetry_test.go, for the same +// reason: consent that outlived its subject is consent nobody gave. +func TestEveryDeliberateHomonymStillNamesTwoMeanings(t *testing.T) { + shared := settingsMoreThanOneFormatDeclares() + + for name, why := range deliberateHomonyms { + declared, isShared := shared[name] + if !isShared { + t.Errorf("%q is excused as a homonym (%s) and no two formats declare it - "+ + "take the excuse off, an excuse with nothing behind it reads as a rule", name, why) + continue + } + if oneMeaning(declared) { + t.Errorf("%q is excused as a homonym (%s) and every format now declares it the same way - "+ + "take the excuse off, the case it names has been closed", name, why) + } + } +} + +// settingsMoreThanOneFormatDeclares is every setting name at least two +// registered formats write, with each declaration and the format that made it. +func settingsMoreThanOneFormatDeclares() map[string][]declaredSetting { + everywhere := map[string][]declaredSetting{} + for _, d := range format.All() { + for _, p := range d.Properties { + everywhere[p.Name] = append(everywhere[p.Name], declaredSetting{format: d.ID, property: p}) + } + } + + shared := make(map[string][]declaredSetting, len(everywhere)) + for name, declared := range everywhere { + if len(declared) > 1 { + shared[name] = declared + } + } + return shared +} + +// oneMeaning says whether every format declaring this name describes the same +// kind of setting by it. +func oneMeaning(declared []declaredSetting) bool { + first := declared[0].property + for _, other := range declared[1:] { + if other.property.Kind != first.Kind || other.property.Unit != first.Unit { + return false + } + } + return true +}