Image_new_item = class Menupullright "_New" "make new things" {
    Image_black_item = class Menuaction "_Image" "make a new image" {
        format_names = [
            "8-bit unsigned int - UCHAR",       // 0
            "8-bit signed int - CHAR",          // 1
            "16-bit unsigned int - USHORT",     // 2
            "16-bit signed int - SHORT",        // 3
            "32-bit unsigned int - UINT",       // 4
            "32-bit signed int - INT",          // 5
            "32-bit float - FLOAT",             // 6
            "64-bit complex - COMPLEX",         // 7
            "64-bit float - DOUBLE",            // 8
            "128-bit complex - DPCOMPLEX"       // 9
        ];

        action = class
            Image _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;
            nbands = Expression "Image bands" 1;
            format_option = Option "Image format" format_names 0;
            type_option = Option_enum "Image type"
                Image_type.type_names "B_W";
            pixel = Expression "Pixel value" 0;

            _result
                = image_new (to_real nwidth) (to_real nheight) (to_real nbands)
                    (to_real format_option) Image_coding.NOCODING
                    type_option.value_thing pixel.expr 0 0;
        }
    }

    Image_new_from_image_item = class
        Menuaction "_From Image" "make a new image based on image x" {
        action x = class
            Image _result {
                _vislevel = 3;

                pixel = Expression "Pixel value" 0;

                _result
                    = image_new x.width x.height x.bands
                        x.format x.coding x.type pixel.expr x.xoffset x.yoffset;
        }
    }

    Image_region_item = class
        Menupullright "_Region on Image" "make a new region on an image" {
        Region_item = class
            Menuaction "_Region" "make a region on an image" {
            action image = scope.Region_relative image 0.25 0.25 0.5 0.5;
        }

        Mark_item = class
            Menuaction "_Point" "make a point on an image" {
            action image = scope.Mark_relative image 0.5 0.5;
        }

        Arrow_item = class
            Menuaction "_Arrow" "make an arrow on an image" {
            action image = scope.Arrow_relative image 0.25 0.25 0.5 0.5;
        }

        HGuide_item = class
            Menuaction "_Horizontal Guide"
                "make a horizontal guide on an image" {
            action image = scope.HGuide image 0.5;
        }

        VGuide_item = class
            Menuaction "_Vertical Guide" "make a vertical guide on an image" {
            action image = scope.VGuide image 0.5;
        }

        sep1 = Menuseparator;

        Move_item = class
            Menuaction "From Region"
                "new region on image using existing region as a guide" {
            action a b
                = map_binary process a b
            {
                process a b
                    = x.Region target x.left x.top x.width x.height,
                            is_Region x
                    = x.Arrow target x.left x.top x.width x.height,
                            is_Arrow x
                    = error "bad arguments to region-from-region"
                {
                    // prefer image then region
                    compare a b
                        = false,
                            !is_Image a && is_Image b
                        = false,
                            is_Region a && !is_Region b
                        = true;

                    [target, x] = sortc compare [a, b];
                }
            }
        }
    }
}

Image_convert_to_image_item = class
    Menuaction "Con_vert to Image" "convert anything to an image" {
    action x = to_image x;
}

Image_number_format_item = class
    Menupullright "_Format" "convert numeric format" {

    U8_item = class
        Menuaction "_8 bit unsigned" "convert to unsigned 8 bit [0, 255]" {
        action x = map_unary cast_unsigned_char x;
    }

    U16_item = class
        Menuaction "1_6 bit unsigned"
            "convert to unsigned 16 bit [0, 65535]" {
        action x = map_unary cast_unsigned_short x;
    }

    U32_item = class
        Menuaction "_32 bit unsigned"
            "convert to unsigned 32 bit [0, 4294967295]" {
        action x = map_unary cast_unsigned_int x;
    }

    sep1 = Menuseparator;

    S8_item = class
        Menuaction "8 _bit signed" "convert to signed 8 bit [-128, 127]" {
        action x = map_unary cast_signed_char x;
    }

    S16_item = class
        Menuaction "16 b_it signed"
            "convert to signed 16 bit [-32768, 32767]" {
        action x = map_unary cast_signed_short x;
    }

    S32_item = class
        Menuaction "32 bi_t signed"
            "convert to signed 32 bit [-2147483648, 2147483647]" {
        action x = map_unary cast_signed_int x;
    }

    sep2 = Menuseparator;

    Float_item = class
        Menuaction "_Single precision float"
            "convert to IEEE 32 bit float" {
        action x = map_unary cast_float x;
    }

    Double_item = class
        Menuaction "_Double precision float"
            "convert to IEEE 64 bit float" {
        action x = map_unary cast_double x;
    }

    sep3 = Menuseparator;

    Scmplxitem = class
        Menuaction "Single _precision complex"
            "convert to 2 x IEEE 32 bit float" {
        action x = map_unary cast_complex x;
    }

    Dcmplx_item = class
        Menuaction "Double p_recision complex"
            "convert to 2 x IEEE 64 bit float" {
        action x = map_unary cast_double_complex x;
    }
}

Image_header_item = class
    Menupullright "_Header" "do stuff to the image header" {

    Image_get_item = class
        Menupullright "_Get" "get header fields" {

        // the header fields we can get
        fields = class {
            type = 0;
            width = 1;
            height = 2;
            format = 3;
            bands = 4;
            xres = 5;
            yres = 6;
            xoffset = 7;
            yoffset = 8;
            coding = 9;

            field_names = Enum [
                $width => width,
                $height => height,
                $bands => bands,
                $format => format,
                $type => type,
                $xres => xres,
                $yres => yres,
                $xoffset => xoffset,
                $yoffset => yoffset,
                $coding => coding
            ];

            field_option name = Option_enum (_ "Field") field_names name;

            field_funcs = Table [
                [type, get_type],
                [width, get_width],
                [height, get_height],
                [format, get_format],
                [bands, get_bands],
                [xres, get_xres],
                [yres, get_yres],
                [xoffset, get_xoffset],
                [yoffset, get_yoffset],
                [coding, get_coding]
            ];
        }

        get_field field_name x = class
            _result {
            _vislevel = 3;

            field = fields.field_option field_name;

            _result
                = map_unary (Real @
                    fields.field_funcs.lookup 0 1 field.value_thing) x;
        }

        Width_item = class
            Menuaction "_Width" "get width" {
            action x = get_field "width" x;
        }

        Height_item = class
            Menuaction "_Height" "get height" {
            action x = get_field "height" x;
        }

        Bands_item = class
            Menuaction "_Bands" "get bands" {
            action x = get_field "bands" x;
        }

        Format_item = class
            Menuaction "_Format" "get format" {
            action x = get_field "format" x;
        }

        Type_item = class
            Menuaction "_Type" "get type" {
            action x = get_field "type" x;
        }

        Xres_item = class
            Menuaction "_Xres" "get X resolution" {
            action x = get_field "xres" x;
        }

        Yres_item = class
            Menuaction "_Yres" "get Y resolution" {
            action x = get_field "yres" x;
        }

        Xoffset_item = class
            Menuaction "X_offset" "get X offset" {
            action x = get_field "xoffset" x;
        }

        Yoffset_item = class
            Menuaction "Yo_ffset" "get Y offset" {
            action x = get_field "yoffset" x;
        }

        Coding_item = class
            Menuaction "_Coding" "get coding" {
            action x = get_field "coding" x;
        }

        sep1 = Menuseparator;

        Custom_item = class
            Menuaction "C_ustom" "get any header field" {
            action x = class
                _result {
                _vislevel = 3;

                field = String "Field" "Xsize";
                parse = Option "Parse" [
                    "No parsing",
                    "Parse string as integer",
                    "Parse string as real",
                    "Parse string as hh:mm:ss"
                ] 0;

                _result
                    = map_unary (wrap @ process @ get_header field.value) x
                {
                    parse_str parse str = parse (split is_space str)?0;

                    parse_field name cast parse x
                        = cast x, is_number x
                        = parse_str parse x, is_string x
                        = error ("not " ++ name);

                    get_int = parse_field "int"
                        cast_signed_int parse_int;
                    get_float = parse_field "float"
                        cast_float parse_float;
                    get_time = parse_field "hh:mm:ss"
                        cast_signed_int parse_time;

                    wrap x
                        = Real x, is_real x
                        = Vector x, is_real_list x
                        = Image x, is_image x
                        = Bool x, is_bool x
                        = Matrix x, is_matrix x
                        = String "String" x, is_string x
                        = List x, is_list x
                        = x;

                    process = [
                        id,
                        get_int,
                        get_float,
                        get_time
                    ]?parse;
                }
            }
        }
    }

    sep1 = Menuseparator;

    Image_set_meta_item = class
        Menuaction "_Set" "set image metadata" {
        action x = class
            _result {
            _vislevel = 3;

            fname = String "Field" "field-name";
            val = Expression "Value" 42;

            _result
                = map_unary process x
            {
                process image
                    = set_header fname.value val.expr image;
            }
        }
    }

    Image_edit_header_item = class
        Menuaction "_Edit" "change advisory header fields of image" {
        type_names = Image_type.type_names;
        all_names = sort (map (extract 0) type_names.value);

        get_prop has get def x
            = get x, has x
            = def;

        action x = class
            _result {
            _vislevel = 3;

            nxres = Expression "Xres" (get_prop has_xres get_xres 1 x);
            nyres = Expression "Yres" (get_prop has_yres get_yres 1 x);
            nxoff = Expression "Xoffset" (get_prop has_xoffset get_xoffset 0 x);
            nyoff = Expression "Yoffset" (get_prop has_yoffset get_yoffset 0 x);

            type_option
                = Option_enum "Image type" Image_type.type_names
                    (Image_type.type_names.get_name type)
            {
                type
                    = x.type, is_Image x
                    = Image_type.MULTIBAND;
            }

            _result
                = map_unary process x
            {
                process image
                    = Image (im_copy_set image.value type_option.value_thing
                        (to_real nxres) (to_real nyres)
                        (to_real nxoff) (to_real nyoff));
            }
        }
    }
}

Image_cache_item = class
    Menuaction "C_ache" "cache calculated image pixels" {
    action x = class
        _result {
        _vislevel = 3;

        tile_width = Number "Tile width" 128;
        tile_height = Number "Tile height" 128;
        max_tiles = Number "Maximum number of tiles to cache" (-1);

        _result
            = map_unary process x
        {
            process image
                = cache (to_real tile_width) (to_real tile_height)
                    (to_real max_tiles) image;
        }
    }
}

#separator

Image_levels_item = class
    Menupullright "_Levels" "change image levels" {
    Scale_item = class
        Menuaction "_Scale to 0 - 255" "linear transform to fit 0 - 255 range" {
        action x = map_unary scale x;
    }

    Linear_item = class
        Menuaction "_Linear" "linear transform of image levels" {
        action x = class
            _result {
            _vislevel = 3;

            scale = Scale "Scale" 0.001 3 1;
            offset = Scale "Offset" (-128) 128 0;

            _result
                = map_unary adj x
            {
                adj x
                    // only force back to input type if this is a thing
                    // with a type ... so we work for Colour / Matrix etc.
                    = clip2fmt x.format x', has_member "format" x
                    = x'
                {
                    x' = x * scale + offset;
                }
            }
        }
    }

    Gamma_item = class
        Menuaction "_Power" "power transform of image levels (gamma)" {
        action x = class
            _result {
            _vislevel = 3;

            gamma = Scale "Gamma" 0.001 4 1;
            image_maximum_hint = "You may need to change image_maximum if " ++
                "this is not an 8 bit image";
            im_mx
                = Expression "Image maximum" mx
            {
                mx
                        = Image_format.maxval x.format, has_format x
                        = 255;
            }

            _result
                = map_unary gam x
            {
                gam x
                    = clip2fmt (get_format x) x', has_format x
                    = x'
                {
                    x' = (im_mx.expr / im_mx.expr ** gamma) * x ** gamma;
                }
            }
        }
    }

    Tone_item = class
        Menuaction "_Tone Curve" "adjust tone curve" {
        action x = class
            _result {
            _vislevel = 3;

            b = Scale "Black point"  0 100 0;
            w = Scale "White point"  0 100 100;

            sp = Scale "Shadow point" 0.1 0.3 0.2;
            mp = Scale "Mid-tone point" 0.4 0.6 0.5;
            hp = Scale "Highlight point" 0.7 0.9 0.8;

            sa = Scale "Shadow adjust" (-15) 15 0;
            ma = Scale "Mid-tone adjust" (-30) 30 0;
            ha = Scale "Highlight adjust" (-15) 15 0;

            curve = tone_build x.format b w sp mp hp sa ma ha;

            _result = map_unary (hist_map curve) x;
        }
    }
}

Image_transform_item = class
    Menupullright "_Transform" "transform images" {
    Rotate_item = class
        Menupullright "Ro_tate" "rotate image" {
        Fixed_item = class
            Menupullright "_Fixed" "clockwise rotation by fixed angles" {
            rotate_widget default x = class
                _result {
                _vislevel = 3;

                angle = Option "Rotate by" [
                    "Don't rotate",
                    "90 degrees clockwise",
                    "180 degrees",
                    "90 degrees anticlockwise"
                ] default;

                _result
                    = map_unary process x
                {
                    process = [
                        // we can't use id here since we want to "declass"
                        // the members of x ... consider if x is a crop class,
                        // for example, we don't want to inherit from crop, we
                        // want to make a new image class
                        rot180 @ rot180,
                        rot90,
                        rot180,
                        rot270
                    ] ? angle;
                }
            }

            Rot90_item = class
                Menuaction "_90 Degrees" "clockwise rotation by 90 degrees" {
                action x = rotate_widget 1 x;
            }

            Rot180_item = class
                Menuaction "_180 Degrees" "clockwise rotation by 180 degrees" {
                action x = rotate_widget 2 x;
            }

            Rot270_item = class
                Menuaction "_270 Degrees" "clockwise rotation by 270 degrees" {
                action x = rotate_widget 3 x;
            }
        }

        Free_item = class
            Menuaction "_Free" "clockwise rotation by any angle" {
            action x = class
                _result {
                _vislevel = 3;

                angle = Scale "Angle" (-180) 180 0;
                interp = Interpolate_picker Interpolate_type.BILINEAR;

                _result
                    = map_unary process x
                {
                    process image
                        = rotate interp angle image;
                }
            }
        }

        Straighten_item = class
            Menuaction "_Straighten"
                ("smallest rotation that makes an arrow either horizontal " ++
                "or vertical") {
            action x = class
                _result {
                _vislevel = 3;

                interp = Interpolate_picker Interpolate_type.BILINEAR;

                _result
                    = map_unary straighten x
                {
                    straighten arrow
                        = rotate interp angle'' arrow.image
                    {
                        x = arrow.width;
                        y = arrow.height;

                        angle = im (polar (x, y));

                        angle'
                            = angle - 360, angle > 315
                            = angle - 180, angle > 135
                            = angle;

                        angle''
                            = -angle', angle' >= (-45) && angle' < 45
                            = 90 - angle';
                    }
                }
            }
        }
    }

    Flip_item = class
        Menupullright "_Flip" "mirror left/right or up/down" {
        Left_right_item = class
            Menuaction "_Left Right" "mirror object left/right" {
            action x = map_unary fliplr x;
        }

        Top_bottom_item = class
            Menuaction "_Top Bottom" "mirror object top/bottom" {
            action x = map_unary fliptb x;
        }
    }

    Resize_item = class
        Menupullright "_Resize" "change image size" {
        Scale_item = class
            Menuaction "_Scale" "scale image size by a factor" {
            action x = class
                _result {
                _vislevel = 3;

                xfactor = Expression "Horizontal scale factor" 1;
                yfactor = Expression "Vertical scale factor" 1;
                kernel = Kernel_picker Kernel_type.LINEAR;

                _result
                    = map_unary process x
                {
                    process image
                        = resize kernel xfactor yfactor image;
                }
            }
        }

        Size_item = class
            Menuaction "_Size To" "resize to a fixed size" {
            action x = class
                _result {
                _vislevel = 3;

                which = Option "Resize axis" [
                    "Shortest",
                    "Longest",
                    "Horizontal",
                    "Vertical"
                ] 0;
                size = Expression "Resize to (pixels)" 128;
                aspect = Toggle "Break aspect ratio" false;
                kernel = Kernel_picker Kernel_type.LINEAR;

                _result
                    = map_unary process x
                {
                    process image
                        = resize kernel h v image, aspect
                        = resize kernel fac fac image
                    {
                        xfac = to_real size / image.width;
                        yfac = to_real size / image.height;
                        max_factor
                            = [xfac, 1], xfac > yfac
                            = [1, yfac];
                        min_factor
                            = [xfac, 1], xfac < yfac
                            = [1, yfac];
                        [h, v] = [
                            max_factor,
                            min_factor,
                            [xfac, 1],
                            [1, yfac]]?which;

                        fac
                            = h, v == 1
                            = v;
                    }
                }
            }
        }

        Size_within_item = class
            Menuaction "Size _Within" "size to fit within a rectangle" {
            action x = class
                _result {
                _vislevel = 3;

                // the rects we size to fit within
                _rects = [
                    [2048, 1536], [1920, 1200], [1600, 1200], [1400, 1050],
                    [1280, 1024], [1024, 768], [800, 600], [640, 480]
                ];

                within = Option "Fit within (pixels)" (
                    [print w ++ " x " ++ print h :: [w, h] <- _rects] ++
                    ["Custom"]
                ) 4;
                custom_width = Expression "Custom width" 1000;
                custom_height = Expression "Custom height" 1000;
                size = Option "Page size" [
                    "Full page", "Half page", "Quarter page"
                ] 0;
                kernel = Kernel_picker Kernel_type.LINEAR;

                _result
                    = map_unary process x
                {
                    xdiv = [1, 2, 2]?size;
                    ydiv = [1, 1, 2]?size;
                    allrect = _rects ++ [
                        [custom_width.expr, custom_height.expr]
                    ];
                    [width, height] = allrect?within;

                    process x
                        = resize kernel fac fac x, fac < 1
                        = x
                    {
                        xfac = (width / xdiv) / x.width;
                        yfac = (height / ydiv) / x.height;
                        fac = min_pair xfac yfac;
                    }
                }
            }
        }

        Resize_canvas_item = class
            Menuaction "_Canvas" "change size of surrounding image" {
            action x = class
                _result {
                _vislevel = 3;

                // try to guess a sensible size for the new image
                _guess_size
                    = x.rect, is_Image x
                    = Rect 0 0 100 100;

                nwidth = Expression "New width (pixels)" _guess_size.width;
                nheight = Expression "New height (pixels)" _guess_size.height;
                bgcolour = Expression "Background colour" 0;

                position = Option "Position" [
                    "North-west",
                    "North",
                    "North-east",
                    "West",
                    "Centre",
                    "East",
                    "South-west",
                    "South",
                    "South-east",
                    "Specify in pixels"
                ] 4;
                left = Expression "Pixels from left" 0;
                top = Expression "Pixels from top" 0;

                _result
                    = map_unary process x
                {
                    process image
                        = insert_noexpand xp yp image background
                    {
                        width = image.width;
                        height = image.height;
                        coding = image.coding;
                        bands
                            = 3, coding == Image_coding.LABPACK
                            = image.bands;
                        format
                            = Image_format.FLOAT, coding == Image_coding.LABPACK
                            = image.format;
                        type = image.type;

                        // placement vectors ... left, centre, right
                        xposv = [0, to_real nwidth / 2 - width / 2,
                            to_real nwidth - width];
                        yposv = [0, to_real nheight / 2 - height / 2,
                            to_real nheight - height];
                        xp
                            = left, position == 9
                            = xposv?((int) (position % 3));
                        yp
                            = top, position == 9
                            = yposv?((int) (position / 3));

                        background = image_new nwidth nheight
                            bands format coding type bgcolour.expr 0 0;
                    }
                }
            }
        }
    }

    Image_map_item = class
        Menuaction "_Map" "map an image through a 2D transform image" {
        action a b = class
            _result {
            _vislevel = 3;

            interp = Interpolate_picker Interpolate_type.BILINEAR;

            _result
                = map_binary trans a b
            {
                trans a b
                    = mapim interp.value in index
                {
                    // get the index image first
                    [index, in] = sortc (const is_twocomponent) [a, b];

                    // is a two-component image, ie. one band complex, or
                    // two-band non-complex
                    is_twocomponent x
                        = is_nonc x || is_c x;
                    is_nonc x
                        = has_bands x && get_bands x == 2 &&
                            has_format x && !is_complex_format (get_format x);
                    is_c x
                        = has_bands x && get_bands x == 1 &&
                            has_format x && is_complex_format (get_format x);
                    is_complex_format f
                        = f == Image_format.COMPLEX ||
                            f == Image_format.DPCOMPLEX;
                }
            }
        }
    }

    Image_perspective_item = Perspective_item;

    Image_rubber_item = class
        Menupullright "Ru_bber Sheet"
            "automatically warp images to superposition" {
        rubber_interp = Option "Interpolation" ["Nearest", "Bilinear"] 1;
        rubber_order = Option "Order" ["0", "1", "2", "3"] 1;
        rubber_wrap = Toggle "Wrap image edges" false;

        // a transform ... a matrix, plus the size of the image the
        // matrix was made for
        Transform matrix image_width image_height = class
            matrix {
            // scale a transform ... if it worked for a m by n image, make
            // it work for a (m * xfac) by (y * yfac) image
            rescale xfac yfac
                = Transform (Matrix (map2 (map2 multiply) matrix.value facs))
                    (image_width * xfac) (image_height * yfac)
            {
                facs = [
                    [xfac, yfac],
                    [1, 1],
                    [1, 1],
                    [1 / xfac, 1 / yfac],
                    [1 / xfac, 1 / yfac],
                    [1 / xfac, 1 / yfac]
                ];
            }
        }

        // yuk!!!! fix is_instanceof to not need absolute names
        is_Transform = is_instanceof
            "Image_transform_item.Image_rubber_item.Transform";

        Find_item = class
            Menuaction "_Find"
                ("find a transform which will map sample image onto " ++
                "reference") {
            action reference sample = class
                _trn {
                _vislevel = 3;

                // controls
                order = rubber_order;
                interp = rubber_interp;
                wrap = rubber_wrap;
                max_err = Expression "Maximum error" 0.3;
                max_iter = Expression "Maximum iterations" 10;

                // transform
                [sample', trn, err] = transform_search
                    max_err max_iter order interp wrap
                    sample reference;
                transformed_image = Image sample';
                _trn = Transform trn reference.width reference.height;
                final_error = err;
            }
        }

        Apply_item = class
            Menuaction "_Apply" "apply a transform to an image" {
            action a b = class
                _result {
                _vislevel = 3;

                // controls
                interp = rubber_interp;
                wrap = rubber_wrap;

                _result
                    = map_binary trans a b
                {
                    trans a b
                        = transform interp wrap t' i
                    {
                        // get the transform arg first
                        [i, t] = sortc (const is_Transform) [a, b];
                        t' = t.rescale (i.width / t.image_width)
                            (i.height / t.image_height);
                    }
                }
            }
        }
    }

    sep1 = Menuseparator;

    Match_item = class
        Menuaction "_Linear Match"
            "rotate and scale one image to match another" {
        action x y = class
            _result {
            _vislevel = 3;

            // try to find an image ... for a group, get the first item
            find_image x
                = x, is_Image x
                = find_image x?0, is_list x
                = find_image x.value, is_class x && has_value x
                = error "unable to find image";

            _a = find_image x;
            _b = find_image y;

            ap1 = Mark_relative _a 0.5 0.25;
            bp1 = Mark_relative _b 0.5 0.25;
            ap2 = Mark_relative _a 0.5 0.75;
            bp2 = Mark_relative _b 0.5 0.75;

            refine = Toggle "Refine selected tie-points" false;
            lock = Toggle "No resize" false;

            _result
                = map_binary process x y
            {
                process a b
                    = Image b'''
                {
                    _prefs = Workspaces.Preferences;
                    window = _prefs.MOSAIC_WINDOW_SIZE;
                    object = _prefs.MOSAIC_OBJECT_SIZE;

                    a' = a.value;
                    b' = b.value;

                    b'' = clip2fmt a.format b';

                    // return p2 ... if lock is set, return a p2 a standard
                    // distance along the vector joining p1 and p2
                    norm p1 p2
                        = Rect left' top' 0 0, lock
                        = p2
                    {
                        v = (p2.left - p1.left, p2.top - p1.top);
                        // 100000 to give precision since we pass points as
                        // ints to match
                        n = 100000 * sign v;
                        left' = p1.left + re n;
                        top' = p1.top + im n;
                    }

                    ap2'' = norm ap1 ap2;
                    bp2'' = norm bp1 bp2;

                    b'''
                        = im_match_linear_search a' b''
                            ap1.left ap1.top bp1.left bp1.top
                            ap2''.left ap2''.top bp2''.left bp2''.top
                            object window,
                                // we can't search if lock is on
                                refine && !lock
                        = im_match_linear a' b''
                            ap1.left ap1.top bp1.left bp1.top
                            ap2''.left ap2''.top bp2''.left bp2''.top;
                }
            }
        }
    }

    Image_perspective_match_item = Perspective_match_item;
}

Image_band_item = class
    Menupullright "_Band" "manipulate image bands" {
    // like extract_bands, but return [] for zero band image
    // makes compose a bit simpler
    exb b n x
        = [], to_real n == 0
        = extract_bands b n x;

    Extract_item = class Menuaction "_Extract" "extract bands from image" {
        action x = class
            _result {
            _vislevel = 3;

            first = Expression "Extract from band" 0;
            number = Expression "Extract this many bands" 1;

            _result = map_unary (exb first number) x;
        }
    }

    Insert_item = class Menuaction "_Insert" "insert bands into image" {
        action x y = class
            _result {
            _vislevel = 3;

            first = Expression "Insert at position" 0;

            _result
                = map_binary process x y
            {
                process im1 im2
                    = exb 0 f im1 ++ im2 ++ exb f (b - f) im1
                {
                    f = to_real first;
                    b = im1.bands;
                }
            }
        }
    }

    Delete_item = class Menuaction "_Delete" "delete bands from image" {
        action x = class
            _result {
            _vislevel = 3;

            first = Expression "Delete from band" 0;
            number = Expression "Delete this many bands" 1;

            _result
                = map_unary process x
            {
                process im
                    = exb 0 f im ++ exb (f + n) (b - (f + n)) im
                {
                    f = to_real first;
                    n = to_real number;
                    b = im.bands;
                }
            }
        }
    }

    Bandwise_item = Image_join_item.Bandwise_item;

    sep1a = Menuseparator;

    Bandand_item = class
        Menuaction "Bitwise Band AND" "bitwise AND of image bands" {
        action x = bandand x;
    }

    Bandor_item = class
        Menuaction "Bitwise Band OR" "bitwise OR of image bands" {
        action x = bandor x;
    }

    sep2 = Menuseparator;

    To_dimension_item = class
        Menuaction "To D_imension" "convert bands to width or height" {
        action x = class
            _result {
            _vislevel = 3;

            orientation = Option "Orientation" [
                "Horizontal",
                "Vertical"
            ] 0;

            _result
                = map_unary process x
            {
                process im
                    = foldl1 [join_lr, join_tb]?orientation (bandsplit im);
            }
        }
    }

    To_bands_item = class
        Menuaction "To B_ands" "turn width or height to bands" {
        action x = class
            _result {
            _vislevel = 3;

            orientation = Option "Orientation" [
                "Horizontal",
                "Vertical"
            ] 0;

            _result
                = map_unary process x
            {
                process im
                    = bandjoin (map extract_column [0 .. im.width - 1]),
                        orientation == 0
                    = bandjoin (map extract_row [0 .. im.height - 1])
                {
                    extract_column n
                        = extract_area n 0 1 im.height im;
                    extract_row n
                        = extract_area 0 n im.width 1 im;
                }
            }
        }
    }
}

Image_alpha_item = class
    Menupullright "_Alpha" "manipulate image alpha" {

    Add_item = class Menuaction "_Add" "add alpha" {
        action x = class
            _result {
            _vislevel = 3;

            opacity = Expression "Opacity (255 == solid)" 255;

            _result = x ++ to_real opacity;
        }
    }

    Flatten_item = class Menuaction "_Flatten" "flatten alpha out of image" {
        action x = class
            _result {
            _vislevel = 3;

            bg = Expression "Background" 0;

            _result = map_unary (flattenimage bg) x;
        }
    }

    Extract_item = class Menuaction "_Extract" "extract alpha" {
        action x
            = map_unary exb x
        {
            exb x = extract_bands (x.bands - 1) 1 x;
        }
    }

    Drop_item = class Menuaction "_Drop" "drop alpha" {
        action x
            = map_unary exb x
        {
            exb x = extract_bands 0 (x.bands - 1) x;
        }
    }

    sep1 = Menuseparator;

    Premultiply_item = class Menuaction "_Premultiply" "premultiply alpha" {
        action x = premultiply x;
    }

    Unpremultiply_item = class
        Menuaction "_Unpremultiply" "unpremultiply alpha" {
        action x = unpremultiply x;
    }

    sep2 = Menuseparator;

    Composite2_item = class
        Menuaction "_Composite two" "composite a pair of images" {
        action x y = class
            _result {
            _vislevel = 3;

            blend = Option_enum (_ "Blend mode") modes "over"
            {
                modes = Blend_type.types;
            }
            compositing_space = Option_enum (_ "Compositing space") spaces "sRGB"
            {
                spaces = Image_type.image_colour_spaces;
            }
            premultiplied = Toggle (_ "Premultiplied") false;

            _result
                = Image output
            {
                output = vips_composite [
                    $compositing_space => compositing_space.value_thing,
                    $premultiplied => premultiplied.value
                ] [y.value, x.value] blend.value;
            }
        }
    }

    Composite3_item = class
        Menuaction "_Composite three" "composite three images" {
        action x y z = class
            _result {
            _vislevel = 3;

            blend1 = Option_enum (_ "Blend mode") modes "over"
            {
                modes = Blend_type.types;
            }
            blend2 = Option_enum (_ "Blend mode") modes "over"
            {
                modes = Blend_type.types;
            }
            compositing_space = Option_enum (_ "Compositing space") spaces "sRGB"
            {
                spaces = Image_type.image_colour_spaces;
            }
            premultiplied = Toggle (_ "Premultiplied") false;

            _result
                = Image output
            {
                output = vips_composite [
                    $compositing_space => compositing_space.value_thing,
                    $premultiplied => premultiplied.value
                ] [z.value, y.value, x.value] [blend1.value, blend2.value];
            }
        }
    }
}

Image_crop_item = class
    Menuaction "_Crop" "extract a rectangular area from an image" {
    action x
        = crop x [l, t, w, h]
    {
        fields = [
            [has_left, get_left, 0],
            [has_top, get_top, 0],
            [has_width, get_width, 100],
            [has_height, get_height, 100]
        ];

        [l, t, w, h]
            = map get_default fields
        {
            get_default line
                = get x, has x
                = default
            {
                [has, get, default] = line;
            }
        }
    }

    crop x geo = class
        _result {
        _vislevel = 3;

        l = Expression "Crop left" ((int) (geo?0 + geo?2 / 4));
        t = Expression "Crop top" ((int) (geo?1 + geo?3 / 4));
        w = Expression "Crop width" (max_pair 1 ((int) (geo?2 / 2)));
        h = Expression "Crop height" (max_pair 1 ((int) (geo?3 / 2)));

        _result
            = map_nary (list_5ary extract) [x, l.expr, t.expr, w.expr, h.expr]
        {
            extract im l t w h
                = extract_area left' top' width' height' im
            {
                width' = min_pair (to_real w) im.width;
                height' = min_pair (to_real h) im.height;
                left' = range 0 (to_real l) (im.width - width');
                top' = range 0 (to_real t) (im.height - height');
            }
        }
    }
}

Trim_item = class Menuaction "_Trim" "crop away edges" {
    action x = class
        _result {
        _vislevel = 3;

        thresh = Scale "threshold" 0 100 10;
        background = Expression "Background" default_background
        {
            default_background
                = map mean (bandsplit (extract_area 0 0 1 1 x));
        }

        _result
            = Region x l t w h
        {
            [l, t, w, h] = vips_find_trim [
                $threshold => thresh.value,
                $background => background.expr
            ] x.value;
        }
    }
}

Image_insert_item = class
    Menuaction "_Insert" "insert a small image into a large image" {
    action a b
        = insert_position, is_Group a || is_Group b
        = insert_area
    {
        insert_area = class
            _result {
            _check_args = [
                [a, "a", check_Image],
                [b, "b", check_Image]
            ];
            _vislevel = 3;

            // sort to get smallest first
            _pred x y = x.width * x.height < y.width * y.height;
            [_a', _b'] = sortc _pred [a, b];

            place
                = Area _b' left top width height
            {
                // be careful in case b is smaller than a
                left = max_pair 0 ((_b'.width - _a'.width) / 2);
                top = max_pair 0 ((_b'.height - _a'.height) / 2);
                width = min_pair _a'.width _b'.width;
                height = min_pair _a'.height _b'.height;
            }

            _result
                = insert_noexpand place.left place.top
                    (clip2fmt _b'.format a'') _b'
            {
                a'' = extract_area 0 0 place.width place.height _a';
            }
        }

        insert_position = class
            _result {
            _vislevel = 3;

            position = Option "Position" [
                "North-west",
                "North",
                "North-east",
                "West",
                "Centre",
                "East",
                "South-west",
                "South",
                "South-east",
                "Specify in pixels"
            ] 4;
            left = Expression "Pixels from left" 0;
            top = Expression "Pixels from top" 0;

            _result
                = map_binary insert a b
            {
                insert a b
                    = insert_noexpand left top (clip2fmt b.format a) b,
                        position == 9
                    = insert_noexpand xp yp (clip2fmt b.format a) b
                {
                    xr = b.width - a.width;
                    yr = b.height - a.height;
                    xp = [0, xr / 2, xr]?((int) (position % 3));
                    yp = [0, yr / 2, yr]?((int) (position / 3));
                }
            }
        }
    }
}

Image_select_item = Select_item;

Image_draw_item = class
    Menupullright "_Draw" "draw lines, circles, rectangles, floods" {
    Line_item = class Menuaction "_Line" "draw line on image" {
        action x = class
            _result {
            _vislevel = 3;

            x1 = Expression "Start x" 0;
            y1 = Expression "Start y" 0;
            x2 = Expression "End x" 100;
            y2 = Expression "End y" 100;

            i = Expression "Ink" [0];

            _result
                = map_unary line x
            {
                line im
                    = draw_line x1 y1 x2 y2 i.expr im;
            }
        }
    }

    Rect_item = class Menuaction "_Rectangle" "draw rectangle on image" {
        action x = class
            _result {
            _vislevel = 3;

            rx = Expression "Left" 50;
            ry = Expression "Top" 50;
            rw = Expression "Width" 100;
            rh = Expression "Height" 100;

            f = Toggle "Fill" true;

            t = Scale "Line thickness" 1 50 3;

            i = Expression "Ink" [0];

            _result
                = map_unary rect x
            {
                rect im
                    = draw_rect_width rx ry rw rh f t i.expr im;
            }
        }
    }

    Circle_item = class Menuaction "_Circle" "draw circle on image" {
        action x = class
            _result {
            _vislevel = 3;

            cx = Expression "Centre x" 100;
            cy = Expression "Centre y" 100;
            r = Expression "Radius" 50;

            f = Toggle "Fill" true;

            i = Expression "Ink" [0];

            _result
                = map_unary circle x
            {
                circle im
                    = draw_circle cx cy r f i.expr im;
            }
        }
    }

    Flood_item = class Menuaction "_Flood" "flood bounded area of image" {
        action x = class
            _result {
            _vislevel = 3;

            sx = Expression "Start x" 0;
            sy = Expression "Start y" 0;

            e = Option "Flood while" [
                "Not equal to ink",
                "Equal to start point"
            ] 0;

            // pick a default ink that won't flood, if we can
            i
                = Expression "Ink" default_ink
            {
                default_ink
                    = [0], ! has_image x
                    = pixel;
                pixel = map mean (bandsplit (extract_area sx sy 1 1 im));
                im = get_image x;
            }

            _result
                = map_unary flood x
            {
                flood im
                    = draw_flood sx sy i.expr im, e == 0
                    = draw_flood_blob sx sy i.expr im;
            }
        }
    }

    Draw_scalebar_item = class Menuaction "_Scale" "draw scale bar" {
        action x = class
            _result {
            _vislevel = 3;

            px = Expression "Left" 50;
            py = Expression "Top" 50;
            wid = Expression "Width" 100;
            thick = Scale "Line thickness" 1 50 3;
            text = String "Dimension text" "50μm";
            font = Fontname "Use font" Workspaces.Preferences.PAINTBOX_FONT;
            pos = Option "Position Text" ["Above", "Below"] 1;
            vp = Option "Dimension by" [
                "Inner Vertical Edge",
                "Centre of Vertical",
                "Outer Vertical Edge"
            ] 1;
            dpi = Expression "DPI" 100;
            ink = Colour "Lab" [50,0,0];

            _result
                = map_unary process x
            {
                process im
                    = blend (Image scale) ink' im
                {
                    // make an ink compatible with the image
                    ink' = colour_transform_to (get_type im) ink;

                    x = to_real px;
                    y = to_real py;
                    w = to_real wid;
                    d = to_real dpi;

                    t = floor thick;

                    bg = image_new (get_width im) (get_height im) (get_bands im)
                        (get_format im) (get_coding im) (get_type im) 0 0 0;
                    draw_block x y w t im =
                        draw_rect_width x y w t true 1 [255] im;
                    label = im_text text.value font.value w 1 d;
                    lw = get_width label;
                    lh = get_height label;
                    ly = [y - lh - t, y + 2 * t]?pos;
                    vx = [
                        [x - t, x + w],
                        [x - t / 2, x + w - t / 2],
                        [x, x + w - t]
                    ]?vp;

                    scale = (draw_block x y w t @
                        draw_block vx?0 (y - 2 * t) t (t * 5) @
                        draw_block vx?1 (y - 2 * t) t (t * 5) @
                        insert_noexpand (x + w / 2 - lw / 2) ly label)
                        bg;
                }
            }
        }
    }
}

Image_join_item = class
    Menupullright "_Join" "join two or more images together" {
    Bandwise_item = class
        Menuaction "_Bandwise Join" "join two images bandwise" {
        action a b = join a b;
    }

    sep1 = Menuseparator;

    join_lr shim bg align a b
        = im2
    {
        w = a.width + b.width + shim;
        h = max_pair a.height b.height;

        back = image_new w h a.bands a.format a.coding a.type bg 0 0;

        ya = [0, max_pair 0 ((b.height - a.height)/2),
            max_pair 0 (b.height - a.height)];
        yb = [0, max_pair 0 ((a.height - b.height)/2),
            max_pair 0 (a.height - b.height)];

        im1 = insert_noexpand 0 ya?align a back;
        im2 = insert_noexpand (a.width + shim) yb?align b im1;
    }

    join_tb shim bg align a b
        = im2
    {
        w = max_pair a.width b.width;
        h = a.height + b.height + shim;

        back = image_new w h a.bands a.format a.coding a.type bg 0 0;

        xa = [0, max_pair 0 ((b.width - a.width)/2),
            max_pair 0 (b.width - a.width)];
        xb = [0, max_pair 0 ((a.width - b.width)/2),
            max_pair 0 (a.width - b.width)];

        im1 = insert_noexpand xa?align 0 a back;
        im2 = insert_noexpand xb?align (a.height + shim) b im1;
    }

    halign_names = ["Top", "Centre", "Bottom"];
    valign_names = ["Left", "Centre", "Right"];

    Left_right_item = class
        Menuaction "_Left to Right" "join two images left-right" {
        action a b = class
            _result {
            _vislevel = 3;

            shim = Scale "Spacing" 0 100 0;
            bg_colour = Expression "Background colour" 0;
            align = Option "Alignment" halign_names 1;

            _result = map_binary
                (join_lr shim.value bg_colour.expr align.value) a b;
        }
    }

    Top_bottom_item = class
        Menuaction "_Top to Bottom" "join two images top-bottom" {
        action a b = class
            _result {
            _vislevel = 3;

            shim = Scale "Spacing" 0 100 0;
            bg_colour = Expression "Background colour" 0;
            align = Option "Alignment" valign_names 1;

            _result = map_binary
                (join_tb shim.value bg_colour.expr align.value) a b;
        }
    }

    sep2 = Menuseparator;

    Array_item = class
        Menuaction "_Array"
            "join a list of lists of images into a single image" {
        action x = class
            _result {
            _vislevel = 3;

            hshim = Scale "Horizontal spacing" (-100) (100) 0;
            vshim = Scale "Vertical spacing" (-100) (100) 0;
            bg_colour = Expression "Background colour" 0;
            halign = Option "Horizontal alignment" valign_names 1;
            valign = Option "Vertical alignment" halign_names 1;

            // we can't use map_unary since chop-into-tiles returns a group of
            // groups and we want to be able to reassemble that
            // TODO: chop-into-tiles should return an array class which
            // displays as group but does not have the looping behaviour?
            _result
                = (image_set_origin 0 0 @
                    foldl1 (join_tb vshim.value bg_colour.expr halign.value) @
                    map (foldl1 (join_lr hshim.value
                        bg_colour.expr valign.value))) (to_list (to_list x));
        }
    }

    ArrayFL_item = class
        Menuaction "_Array from List"
            "join a list of images into a single image" {
        action x = class
            _result {
            _vislevel = 3;

            ncol = Number "Max. Number of Columns" 1;
            hshim = Scale "Horizontal spacing" (-100) (100) 0;
            vshim = Scale "Vertical spacing" (-100) (100) 0;
            bg_colour = Expression "Background colour" 0;
            halign = Option "Horizontal alignment" valign_names 1;
            valign = Option "Vertical alignment" halign_names 1;
            snake = Toggle "Reverse the order of every other row" false;

            _l
                = split_lines ncol.value x.value, is_Group x
                = split_lines ncol.value x;

            _l'
                = map2 reverse_if_odd [0..] _l, snake
                = _l
            {
                reverse_if_odd n x
                    = reverse x, n % 2 == 1
                    = x;
            }

            _result
                = (image_set_origin 0 0 @
                    foldl1 (join_tb vshim.value bg_colour.expr halign.value) @
                    map (foldl1 (join_lr hshim.value
                        bg_colour.expr valign.value))) (to_list (to_list _l'));
        }
    }
}

Image_tile_item = class
    Menupullright "Til_e" "tile an image across and down" {
    tile_widget default_type x = class
        _result {
        _vislevel = 3;

        across = Expression "Tiles across" 2;
        down = Expression "Tiles down" 2;
        repeat = Option "Tile type"
            ["Replicate", "Four-way mirror"] default_type;

        _result
            = map_unary process x
        {
            process image
                = tile across down image, repeat == 0
                = tile across down image''
            {
                image' = insert image.width 0 (fliplr image) image;
                image'' = insert 0 image.height (fliptb image') image';
            }
        }
    }

    Replicate_item = class
        Menuaction "_Replicate" "replicate image across and down" {
        action x = tile_widget 0 x;
    }

    Fourway_item = class
        Menuaction "_Four-way Mirror" "four-way mirror across and down" {
        action x = tile_widget 1 x;
    }

    Chop_item = class
        Menuaction "_Chop Into Tiles" "slice an image into tiles" {
        action x = class
            _result {
            _vislevel = 3;

            tile_width = Expression "Tile width" 100;
            tile_height = Expression "Tile height" 100;
            hoverlap = Expression "Horizontal overlap" 0;
            voverlap = Expression "Vertical overlap" 0;

            _result
                = map_unary (Group @ map Group @ process) x
            {
                process x
                    = imagearray_chop tile_width tile_height
                        hoverlap voverlap x;
            }
        }
    }
}

#separator

Pattern_images_item = class
    Menupullright "_Patterns" "make a variety of useful patterns" {
    Grey_item = class
        Menuaction "Grey _Ramp" "make a smooth grey ramp" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;
            orientation = Option "Orientation" [
                "Horizontal",
                "Vertical"
            ] 0;
            foption = Option "Format" ["8 bit", "float"] 0;

            _result
                = Image im
            {
                gen
                    = im_grey, foption == 0
                    = im_fgrey;
                w = to_real nwidth;
                h = to_real nheight;
                im
                    = gen w h, orientation == 0
                    = rot90 (gen h w);
            }
        }
    }

    Xy_item = class
        Menuaction "_XY Image"
            "make a two band image whose pixel values are their coordinates" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;

            _result = Image (make_xy nwidth nheight);
        }
    }

    Noise_item = class
        Menupullright "_Noise" "various noise generators" {
        Gaussian_item = class
            Menuaction "_Gaussian" "make an image of gaussian noise" {
            action = class
                _result {
                _vislevel = 3;

                nwidth = Expression "Image width (pixels)" 64;
                nheight = Expression "Image height (pixels)" 64;
                mean = Scale "Mean" 0 255 128;
                deviation = Scale "Deviation" 0 128 50;

                _result = Image (gaussnoise nwidth nheight
                    mean.value deviation.value);
            }
        }

        Fractal_item = class
            Menuaction "_Fractal" "make a fractal noise image" {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                dimension = Scale "Dimension" 2.001 2.999 2.001;

                _result = Image (im_fractsurf (to_real nsize) dimension.value);
            }
        }

        Perlin_item = class
            Menuaction "_Perlin" "Perlin noise image" {
            action = class
                _result {
                _vislevel = 3;

                nwidth = Expression "Image width (pixels)" 64;
                nheight = Expression "Image height (pixels)" 64;
                cell_size = Expression "Cell size (pixels)" 8;
                eight = Toggle "Eight bit output" true;

                _result
                    = 128 * im + 128, eight
                    = im
                {
                    im = perlin cell_size nwidth nheight;
                }
            }
        }

        Worley_item = class
            Menuaction "_Worley" "Worley noise image" {
            action = class
                _result {
                _vislevel = 3;

                nwidth = Expression "Image width (pixels)" 512;
                nheight = Expression "Image height (pixels)" 512;
                cell_size = Expression "Cell size (pixels)" 256;

                _result
                    = worley cell_size nwidth nheight;
            }
        }

    }

    Checkerboard_item = class
        Menuaction "_Checkerboard" "make a checkerboard image" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;
            hpsize = Expression "Horizontal patch size" 8;
            vpsize = Expression "Vertical patch size" 8;
            hpoffset = Expression "Horizontal patch offset" 0;
            vpoffset = Expression "Vertical patch offset" 0;

            _result
                = Image (xstripes ^ ystripes)
            {
                pixels = make_xy nwidth nheight;
                xpixels = pixels?0 + to_real hpoffset;
                ypixels = pixels?1 + to_real vpoffset;

                make_stripe pix swidth = pix % (swidth * 2) >= swidth;

                xstripes = make_stripe xpixels (to_real hpsize);
                ystripes = make_stripe ypixels (to_real vpsize);
            }
        }
    }

    Grid_item = class
        Menuaction "Gri_d" "make a grid" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;
            hspace = Expression "Horizontal line spacing" 8;
            vspace = Expression "Vertical line spacing" 8;
            thick = Expression "Line thickness" 1;
            hoff = Expression "Horizontal grid offset" 4;
            voff = Expression "Vertical grid offset" 4;

            _result
                = Image (xstripes | ystripes)
            {
                pixels = make_xy nwidth nheight;
                xpixels = pixels?0 + to_real hoff;
                ypixels = pixels?1 + to_real voff;

                make_stripe pix swidth = pix % swidth < to_real thick;

                xstripes = make_stripe xpixels (to_real hspace);
                ystripes = make_stripe ypixels (to_real vspace);
            }
        }
    }

    Text_item = class
        Menuaction "_Text" "make a bitmap of some text" {
        action = class
            _result {
            _vislevel = 3;

            text = String "Text to paint" "<i>Hello</i> world!";
            font = Fontname "Use font" Workspaces.Preferences.PAINTBOX_FONT;
            fontfile = Pathname "Font file" "";
            textw = Expression "Text width" 0;
            texth = Expression "Text height" 0;
            align = Option "Alignment" [
                "Left",
                "Centre",
                "Right"
            ] 0;
            dpi = Expression "DPI" 300;
            fit = Toggle "Fit text to box" false;
            spacing = Expression "Line spacing" 0;

            _result
                = Image out
            {
                base_options = [
                    $font => font.value,
                    $align => align.value
                ];

                set_option name default value
                    = [name => value], value != default
                    = [];

                options = base_options ++ concat [
                    set_option $width 0 (to_real textw),
                    set_option $height 0 (to_real texth),
                    set_option $fontfile "" fontfile.value,
                    if !fit then set_option $dpi 72 (to_real dpi) else [],
                    set_option $spacing 0 (to_real spacing)
                ];

                out = vips_text options text.value;
            }
        }
    }

    New_CIELAB_slice_item = class
        Menuaction "CIELAB _Slice" "make a slice through CIELAB space" {
        action = class
            _result {
            _vislevel = 3;

            nsize = Expression "Image size (pixels)" 64;
            L = Scale "L value" 0 100 50;

            _result = Image (lab_slice (to_real nsize) L.value);
        }
    }

    sense_option = Option "Sense" [
        "Pass",
        "Reject"
    ] 0;

    build fn size
        = (Image @ image_set_type Image_type.FOURIER @ rotquad @ fn)
            (im_create_fmask size size);

    New_ideal_item = class
        Menupullright "_Ideal Fourier Mask"
            "make various ideal Fourier filter masks" {
        High_low_item = class
            Menuaction "_High or Low Pass"
                ("make a mask image for a highpass/lowpass " ++
                    "ideal Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f sense.value fc.value 0 0 0 0;
                }
            }
        }

        Ring_item = class
            Menuaction "_Ring Pass or Ring Reject"
                ("make a mask image for an ring pass/reject " ++
                "ideal Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;
                rw = Scale "Ring width" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 6) fc.value rw.value 0 0 0;
                }
            }
        }

        Band_item = class
            Menuaction "_Band Pass or Band Reject"
                ("make a mask image for a band pass/reject " ++
                "ideal Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fcx = Scale "Horizontal frequency cutoff" 0.01 0.99 0.5;
                fcy = Scale "Vertical frequency cutoff" 0.01 0.99 0.5;
                r = Scale "Radius" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 12) fcx.value fcy.value
                            r.value 0 0;
                }
            }
        }
    }

    New_gaussian_item = class
        Menupullright "_Gaussian Fourier Mask"
            "make various Gaussian Fourier filter masks" {
        High_low_item = class
            Menuaction "_High or Low Pass"
                ("make a mask image for a highpass/lowpass " ++
                    "Gaussian Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 4) fc.value ac.value 0 0 0;
                }
            }
        }

        Ring_item = class
            Menuaction "_Ring Pass or Ring Reject"
                ("make a mask image for an ring pass/reject " ++
                "Gaussian Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;
                rw = Scale "Ring width" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 10) fc.value rw.value
                            ac.value 0 0;
                }
            }
        }

        Band_item = class
            Menuaction "_Band Pass or Band Reject"
                ("make a mask image for a band pass/reject " ++
                "Gaussian Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fcx = Scale "Horizontal frequency cutoff" 0.01 0.99 0.5;
                fcy = Scale "Vertical frequency cutoff" 0.01 0.99 0.5;
                r = Scale "Radius" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 16) fcx.value fcy.value
                            r.value ac.value 0;
                }
            }
        }
    }

    New_butterworth_item = class
        Menupullright "_Butterworth Fourier Mask"
            "make various Butterworth Fourier filter masks" {
        High_low_item = class
            Menuaction "_High or Low Pass"
                ("make a mask image for a highpass/lowpass " ++
                    "Butterworth Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;
                order = Scale "Order" 1 10 2;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 2) order.value fc.value
                            ac.value 0 0;
                }
            }
        }

        Ring_item = class
            Menuaction "_Ring Pass or Ring Reject"
                ("make a mask image for an ring pass/reject " ++
                "Butterworth Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fc = Scale "Frequency cutoff" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;
                rw = Scale "Ring width" 0.01 0.99 0.5;
                order = Scale "Order" 1 10 2;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 8) order.value fc.value
                        rw.value ac.value 0;
                }
            }
        }

        Band_item = class
            Menuaction "_Band Pass or Band Reject"
                ("make a mask image for a band pass/reject " ++
                "Butterworth Fourier filter") {
            action = class
                _result {
                _vislevel = 3;

                nsize = Expression "Image size (pixels)" 64;
                sense = sense_option;
                fcx = Scale "Horizontal frequency cutoff" 0.01 0.99 0.5;
                fcy = Scale "Vertical frequency cutoff" 0.01 0.99 0.5;
                r = Scale "Radius" 0.01 0.99 0.5;
                ac = Scale "Amplitude cutoff" 0.01 0.99 0.5;
                order = Scale "Order" 1 10 2;

                _result
                    = build param (to_real nsize)
                {
                    param f = f (sense.value + 14) order.value fcx.value
                        fcy.value r.value ac.value;
                }
            }
        }
    }
}

Test_images_item = class
    Menupullright "Test I_mages" "make a variety of test images" {
    Eye_item = class
        Menuaction "_Spatial Response"
            "image for testing the eye's spatial response" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            nheight = Expression "Image height (pixels)" 64;
            factor = Scale "Factor" 0.001 1 0.2;

            _result = Image (im_eye (to_real nwidth) (to_real nheight)
                factor.value);
        }
    }

    Zone_plate = class
        Menuaction "_Zone Plate" "make a zone plate" {
        action = class
            _result {
            _vislevel = 3;

            nsize = Expression "Image size (pixels)" 64;

            _result = Image (im_zone (to_real nsize));
        }
    }

    Frequency_test_chart_item = class
        Menuaction "_Frequency Testchart"
            "make a black/white frequency test pattern" {
        action = class
            _result {
            _vislevel = 3;

            nwidth = Expression "Image width (pixels)" 64;
            sheight = Expression "Strip height (pixels)" 10;
            waves = Expression "Wavelengths" [64, 32, 16, 8, 4, 2];

            _result
                = imagearray_assemble 0 0 (transpose [strips])
            {
                freq_slice wave = Image (sin (grey / wave) > 0);
                strips = map freq_slice waves.expr;
                grey = im_fgrey (to_real nwidth) (to_real sheight) *
                    360 * (to_real nwidth);
            }
        }
    }

    CRT_test_chart_item = class
        Menuaction "CRT _Phosphor Chart"
            "make an image for measuring phosphor colours" {
        action = class
            _result {
            _vislevel = 3;

            brightness = Scale "Brightness" 0 255 200;
            psize = Expression "Patch size (pixels)" 32;

            _result
                = Image (imagearray_assemble 0 0 [[green, red], [blue, white]])
            {

                black = image_new (to_real psize) (to_real psize) 1
                    Image_format.FLOAT Image_coding.NOCODING
                    Image_type.B_W 0 0 0;
                notblack = black + brightness;

                green = black ++ notblack ++ black;
                red = notblack ++ black ++ black;
                blue = black ++ black ++ notblack;
                white = notblack ++ notblack ++ notblack;
            }
        }
    }

    Greyscale_chart_item = class
        Menuaction "_Greyscale" "make a greyscale" {
        action = class
            _result {
            _vislevel = 3;

            pwidth = Expression "Patch width" 8;
            pheight = Expression "Patch height" 8;
            npatches = Expression "Number of patches" 16;

            _result
                = Image (image_set_type Image_type.B_W
                    (clip2fmt Image_format.UCHAR wedge))
            {
                wedge
                    = 255 / (to_real npatches - 1) *
                        (int) (strip?0 / to_real pwidth)
                {
                    strip = make_xy (to_real pwidth * to_real npatches) pheight;
                }
            }
        }
    }

    CMYK_test_chart_item = class
        Menuaction "_CMYK Wedges" "make a set of CMYK wedges" {
        action = class
            _result {
            _vislevel = 3;

            pwidth = Expression "Patch width" 8;
            pheight = Expression "Patch height" 8;
            npatches = Expression "Number of patches" 16;

            _result
                = Image (image_set_type Image_type.CMYK
                    (clip2fmt Image_format.UCHAR strips))
            {
                wedge
                    = 255 / (to_real npatches - 1) *
                        (int) (strip?0 / to_real pwidth)
                {
                    strip = make_xy (to_real pwidth * to_real npatches) pheight;
                }

                black = wedge * 0;

                C = wedge ++ black ++ black ++ black;
                M = black ++ wedge ++ black ++ black;
                Y = black ++ black ++ wedge ++ black;
                K = black ++ black ++ black ++ wedge;

                strips = imagearray_assemble 0 0 [[C],[M],[Y],[K]];
            }
        }
    }

    Colour_atlas_item = class
        Menuaction "_Colour Atlas"
            "make a grid of patches grouped around a colour" {
        action = class
            _result {
            _vislevel = 3;

            start = Colour_picker "Lab" [50,0,0];
            nstep = Expression "Number of steps" 9;
            ssize = Expression "Step size" 10;
            psize = Expression "Patch size" 32;
            sepsize = Expression "Separator size" 4;

            _result
                = colour_transform_to (get_type start) blocks'''
            {
                size = (to_real nstep * 2 + 1) * to_real psize -
                    to_real sepsize;
                xy = make_xy size size;

                xy_grid = (xy % to_real psize) <
                    (to_real psize - to_real sepsize);
                grid = xy_grid?0 & xy_grid?1;

                blocks = (int) (to_real ssize * ((int) (xy / to_real psize)));
                lab_start = colour_transform_to Image_type.LAB start;
                blocks' = blocks - to_real nstep * to_real ssize +
                    Vector (tl lab_start.value);
                blocks'' = Vector [hd lab_start.value] ++ Image blocks';
                blocks'''
                    = image_set_type Image_type.LAB blocks'', Image grid
                    = 0;
            }
        }
    }
}

