libvips/libvips/conversion/embed.c

866 lines
21 KiB
C

/* VipsEmbed
*
* Author: J. Cupitt
* Written on: 21/2/95
* Modified on:
* 6/4/04
* - added extend pixels from edge mode
* - sets Xoffset / Yoffset to x / y
* 15/4/04
* - added replicate and mirror modes
* 4/3/05
* - added solid white mode
* 4/1/07
* - degenerate to im_copy() for 0/0/w/h
* 1/8/07
* - more general ... x and y can be negative
* 24/3/09
* - added IM_CODING_RAD support
* 5/11/09
* - gtkdoc
* 27/1/10
* - use im_region_paint()
* - cleanups
* 15/10/11
* - rewrite as a class
* 10/10/12
* - add @background
* 19/9/17
* - break into embed and gravity
*/
/*
This file is part of VIPS.
VIPS is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
02110-1301 USA
*/
/*
These files are distributed with VIPS - http://www.vips.ecs.soton.ac.uk
*/
/*
#define VIPS_DEBUG
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif /*HAVE_CONFIG_H*/
#include <glib/gi18n-lib.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <math.h>
#include <vips/vips.h>
#include <vips/internal.h>
#include <vips/debug.h>
#include "pconversion.h"
typedef struct _VipsEmbedBase {
VipsConversion parent_instance;
/* The input image.
*/
VipsImage *in;
VipsExtend extend;
VipsArrayDouble *background;
int width;
int height;
/* Pixel we paint calculated from background.
*/
VipsPel *ink;
/* Geometry calculations.
*/
VipsRect rout; /* Whole output area */
VipsRect rsub; /* Rect occupied by image */
/* The 8 border pieces. The 4 borders strictly up/down/left/right of
* the main image, and the 4 corner pieces.
*/
VipsRect border[8];
/* Passed to us by subclasses.
*/
int x;
int y;
} VipsEmbedBase;
typedef VipsConversionClass VipsEmbedBaseClass;
G_DEFINE_ABSTRACT_TYPE( VipsEmbedBase, vips_embed_base, VIPS_TYPE_CONVERSION );
/* r is the bit we are trying to paint, guaranteed to be entirely within
* border area i. Set out to be the edge of the image we need to paint the
* pixels in r.
*/
static void
vips_embed_base_find_edge( VipsEmbedBase *base,
VipsRect *r, int i, VipsRect *out )
{
/* Expand the border by 1 pixel, intersect with the image area, and we
* get the edge. Usually too much though: eg. we could make the entire
* right edge.
*/
*out = base->border[i];
vips_rect_marginadjust( out, 1 );
vips_rect_intersectrect( out, &base->rsub, out );
/* Usually too much though: eg. we could make the entire
* right edge. If we're strictly up/down/left/right of the image, we
* can trim.
*/
if( i == 0 ||
i == 2 ) {
VipsRect extend;
/* Above or below.
*/
extend = *r;
extend.top = 0;
extend.height = base->height;
vips_rect_intersectrect( out, &extend, out );
}
if( i == 1 ||
i == 3 ) {
VipsRect extend;
/* Left or right.
*/
extend = *r;
extend.left = 0;
extend.width = base->width;
vips_rect_intersectrect( out, &extend, out );
}
}
/* Copy a single pixel sideways into a line of pixels.
*/
static void
vips_embed_base_copy_pixel( VipsEmbedBase *base,
VipsPel *q, VipsPel *p, int n )
{
const int bs = VIPS_IMAGE_SIZEOF_PEL( base->in );
int x, b;
for( x = 0; x < n; x++ )
for( b = 0; b < bs; b++ )
*q++ = p[b];
}
/* Paint r of region or. It's a border area, lying entirely within
* base->border[i]. p points to the top-left source pixel to fill with.
* plsk is the line stride.
*/
static void
vips_embed_base_paint_edge( VipsEmbedBase *base,
VipsRegion *or, int i, VipsRect *r, VipsPel *p, int plsk )
{
const int bs = VIPS_IMAGE_SIZEOF_PEL( base->in );
VipsRect todo;
VipsPel *q;
int y;
VIPS_GATE_START( "vips_embed_base_paint_edge: work" );
/* Pixels left to paint.
*/
todo = *r;
/* Corner pieces ... copy the single pixel to paint the top line of
* todo, then use the line copier below to paint the rest of it.
*/
if( i > 3 ) {
q = VIPS_REGION_ADDR( or, todo.left, todo.top );
vips_embed_base_copy_pixel( base, q, p, todo.width );
p = q;
todo.top += 1;
todo.height -= 1;
}
if( i == 1 || i == 3 ) {
/* Vertical line of pixels to copy.
*/
for( y = 0; y < todo.height; y++ ) {
q = VIPS_REGION_ADDR( or, todo.left, todo.top + y );
vips_embed_base_copy_pixel( base, q, p, todo.width );
p += plsk;
}
}
else {
/* Horizontal line of pixels to copy.
*/
for( y = 0; y < todo.height; y++ ) {
q = VIPS_REGION_ADDR( or, todo.left, todo.top + y );
memcpy( q, p, bs * todo.width );
}
}
VIPS_GATE_STOP( "vips_embed_base_paint_edge: work" );
}
static int
vips_embed_base_gen( VipsRegion *or,
void *seq, void *a, void *b, gboolean *stop )
{
VipsRegion *ir = (VipsRegion *) seq;
VipsEmbedBase *base = (VipsEmbedBase *) b;
VipsRect *r = &or->valid;
VipsRect ovl;
int i;
VipsPel *p;
int plsk;
/* Entirely within the input image? Generate the subimage and copy
* pointers.
*/
if( vips_rect_includesrect( &base->rsub, r ) ) {
VipsRect need;
need = *r;
need.left -= base->x;
need.top -= base->y;
if( vips_region_prepare( ir, &need ) ||
vips_region_region( or, ir, r, need.left, need.top ) )
return( -1 );
return( 0 );
}
/* Does any of the input image appear in the area we have been asked
* to make? Paste it in.
*/
vips_rect_intersectrect( r, &base->rsub, &ovl );
if( !vips_rect_isempty( &ovl ) ) {
/* Paint the bits coming from the input image.
*/
ovl.left -= base->x;
ovl.top -= base->y;
if( vips_region_prepare_to( ir, or, &ovl,
ovl.left + base->x, ovl.top + base->y ) )
return( -1 );
ovl.left += base->x;
ovl.top += base->y;
}
switch( base->extend ) {
case VIPS_EXTEND_BLACK:
case VIPS_EXTEND_WHITE:
VIPS_GATE_START( "vips_embed_base_gen: work1" );
/* Paint the borders a solid value.
*/
for( i = 0; i < 8; i++ )
vips_region_paint( or, &base->border[i],
base->extend == 0 ? 0 : 255 );
VIPS_GATE_STOP( "vips_embed_base_gen: work1" );
break;
case VIPS_EXTEND_BACKGROUND:
VIPS_GATE_START( "vips_embed_base_gen: work2" );
/* Paint the borders a solid value.
*/
for( i = 0; i < 8; i++ )
vips_region_paint_pel( or, &base->border[i], base->ink );
VIPS_GATE_STOP( "vips_embed_base_gen: work2" );
break;
case VIPS_EXTEND_COPY:
/* Extend the borders.
*/
for( i = 0; i < 8; i++ ) {
VipsRect todo;
VipsRect edge;
vips_rect_intersectrect( r, &base->border[i], &todo );
if( !vips_rect_isempty( &todo ) ) {
vips_embed_base_find_edge( base,
&todo, i, &edge );
/* Did we paint any of the input image? If we
* did, we can fetch the edge pixels from
* that.
*/
if( !vips_rect_isempty( &ovl ) ) {
p = VIPS_REGION_ADDR( or,
edge.left, edge.top );
plsk = VIPS_REGION_LSKIP( or );
}
else {
/* No pixels painted ... fetch
* directly from the input image.
*/
edge.left -= base->x;
edge.top -= base->y;
if( vips_region_prepare( ir, &edge ) )
return( -1 );
p = VIPS_REGION_ADDR( ir,
edge.left, edge.top );
plsk = VIPS_REGION_LSKIP( ir );
}
vips_embed_base_paint_edge( base,
or, i, &todo, p, plsk );
}
}
break;
default:
g_assert_not_reached();
}
return( 0 );
}
static int
vips_embed_base_build( VipsObject *object )
{
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS( object );
VipsConversion *conversion = VIPS_CONVERSION( object );
VipsEmbedBase *base = (VipsEmbedBase *) object;
VipsImage **t = (VipsImage **) vips_object_local_array( object, 7 );
VipsRect want;
if( VIPS_OBJECT_CLASS( vips_embed_base_parent_class )->build( object ) )
return( -1 );
/* nip2 can generate this quite often ... just copy.
*/
if( base->x == 0 &&
base->y == 0 &&
base->width == base->in->Xsize &&
base->height == base->in->Ysize )
return( vips_image_write( base->in, conversion->out ) );
if( !vips_object_argument_isset( object, "extend" ) &&
vips_object_argument_isset( object, "background" ) )
base->extend = VIPS_EXTEND_BACKGROUND;
if( base->extend == VIPS_EXTEND_BACKGROUND )
if( !(base->ink = vips__vector_to_ink(
class->nickname, base->in,
VIPS_AREA( base->background )->data, NULL,
VIPS_AREA( base->background )->n )) )
return( -1 );
switch( base->extend ) {
case VIPS_EXTEND_REPEAT:
{
/* Clock arithmetic: we want negative x/y to wrap around
* nicely.
*/
const int nx = base->x < 0 ?
-base->x % base->in->Xsize :
base->in->Xsize - base->x % base->in->Xsize;
const int ny = base->y < 0 ?
-base->y % base->in->Ysize :
base->in->Ysize - base->y % base->in->Ysize;
if( vips_replicate( base->in, &t[0],
base->width / base->in->Xsize + 2,
base->height / base->in->Ysize + 2, NULL ) ||
vips_extract_area( t[0], &t[1],
nx, ny, base->width, base->height, NULL ) ||
vips_image_write( t[1], conversion->out ) )
return( -1 );
}
break;
case VIPS_EXTEND_MIRROR:
{
/* As repeat, but the tiles are twice the size because of
* mirroring.
*/
const int w2 = base->in->Xsize * 2;
const int h2 = base->in->Ysize * 2;
const int nx = base->x < 0 ? -base->x % w2 : w2 - base->x % w2;
const int ny = base->y < 0 ? -base->y % h2 : h2 - base->y % h2;
if(
/* Make a 2x2 mirror tile.
*/
vips_flip( base->in, &t[0],
VIPS_DIRECTION_HORIZONTAL, NULL ) ||
vips_join( base->in, t[0], &t[1],
VIPS_DIRECTION_HORIZONTAL, NULL ) ||
vips_flip( t[1], &t[2],
VIPS_DIRECTION_VERTICAL, NULL ) ||
vips_join( t[1], t[2], &t[3],
VIPS_DIRECTION_VERTICAL, NULL ) ||
/* Repeat, then cut out the centre.
*/
vips_replicate( t[3], &t[4],
base->width / t[3]->Xsize + 2,
base->height / t[3]->Ysize + 2, NULL ) ||
vips_extract_area( t[4], &t[5],
nx, ny, base->width, base->height, NULL ) ||
/* Overwrite the centre with the in, much faster
* for centre pixels.
*/
vips_insert( t[5], base->in, &t[6],
base->x, base->y, NULL ) ||
vips_image_write( t[6], conversion->out ) )
return( -1 );
}
break;
case VIPS_EXTEND_BLACK:
case VIPS_EXTEND_WHITE:
case VIPS_EXTEND_BACKGROUND:
case VIPS_EXTEND_COPY:
/* embed is used in many places. We don't really care about
* geometry, so use ANY to avoid disturbing all pipelines.
*/
if( vips_image_pipelinev( conversion->out,
VIPS_DEMAND_STYLE_ANY, base->in, NULL ) )
return( -1 );
conversion->out->Xsize = base->width;
conversion->out->Ysize = base->height;
/* Whole output area.
*/
base->rout.left = 0;
base->rout.top = 0;
base->rout.width = conversion->out->Xsize;
base->rout.height = conversion->out->Ysize;
/* Rect occupied by image (can be clipped to nothing).
*/
want.left = base->x;
want.top = base->y;
want.width = base->in->Xsize;
want.height = base->in->Ysize;
vips_rect_intersectrect( &want, &base->rout, &base->rsub );
/* FIXME ... actually, it can't. base_find_edge() will fail
* if rsub is empty. Make this more general at some point
* and remove this test.
*/
if( vips_rect_isempty( &base->rsub ) ) {
vips_error( class->nickname,
"%s", _( "bad dimensions" ) );
return( -1 );
}
/* Edge rects of new pixels ... top, right, bottom, left. Order
* important. Can be empty.
*/
base->border[0].left = base->rsub.left;
base->border[0].top = 0;
base->border[0].width = base->rsub.width;
base->border[0].height = base->rsub.top;
base->border[1].left = VIPS_RECT_RIGHT( &base->rsub );
base->border[1].top = base->rsub.top;
base->border[1].width = conversion->out->Xsize -
VIPS_RECT_RIGHT( &base->rsub );
base->border[1].height = base->rsub.height;
base->border[2].left = base->rsub.left;
base->border[2].top = VIPS_RECT_BOTTOM( &base->rsub );
base->border[2].width = base->rsub.width;
base->border[2].height = conversion->out->Ysize -
VIPS_RECT_BOTTOM( &base->rsub );
base->border[3].left = 0;
base->border[3].top = base->rsub.top;
base->border[3].width = base->rsub.left;
base->border[3].height = base->rsub.height;
/* Corner rects. Top-left, top-right, bottom-right,
* bottom-left. Order important.
*/
base->border[4].left = 0;
base->border[4].top = 0;
base->border[4].width = base->rsub.left;
base->border[4].height = base->rsub.top;
base->border[5].left = VIPS_RECT_RIGHT( &base->rsub );
base->border[5].top = 0;
base->border[5].width = conversion->out->Xsize -
VIPS_RECT_RIGHT( &base->rsub );
base->border[5].height = base->rsub.top;
base->border[6].left = VIPS_RECT_RIGHT( &base->rsub );
base->border[6].top = VIPS_RECT_BOTTOM( &base->rsub );
base->border[6].width = conversion->out->Xsize -
VIPS_RECT_RIGHT( &base->rsub );
base->border[6].height = conversion->out->Ysize -
VIPS_RECT_BOTTOM( &base->rsub );
base->border[7].left = 0;
base->border[7].top = VIPS_RECT_BOTTOM( &base->rsub );
base->border[7].width = base->rsub.left;
base->border[7].height = conversion->out->Ysize -
VIPS_RECT_BOTTOM( &base->rsub );
if( vips_image_generate( conversion->out,
vips_start_one, vips_embed_base_gen, vips_stop_one,
base->in, base ) )
return( -1 );
break;
default:
g_assert_not_reached();
}
return( 0 );
}
static void
vips_embed_base_class_init( VipsEmbedBaseClass *class )
{
GObjectClass *gobject_class = G_OBJECT_CLASS( class );
VipsObjectClass *vobject_class = VIPS_OBJECT_CLASS( class );
VIPS_DEBUG_MSG( "vips_embed_base_class_init\n" );
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
vobject_class->nickname = "embed_base";
vobject_class->description = _( "embed an image in a larger image" );
vobject_class->build = vips_embed_base_build;
/* Not seq with mirror.
*/
VIPS_ARG_IMAGE( class, "in", 1,
_( "Input" ),
_( "Input image" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsEmbedBase, in ) );
VIPS_ARG_INT( class, "width", 5,
_( "Width" ),
_( "Image width in pixels" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsEmbedBase, width ),
1, 1000000000, 1 );
VIPS_ARG_INT( class, "height", 6,
_( "Height" ),
_( "Image height in pixels" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsEmbedBase, height ),
1, 1000000000, 1 );
VIPS_ARG_ENUM( class, "extend", 7,
_( "Extend" ),
_( "How to generate the extra pixels" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsEmbedBase, extend ),
VIPS_TYPE_EXTEND, VIPS_EXTEND_BLACK );
VIPS_ARG_BOXED( class, "background", 12,
_( "Background" ),
_( "Color for background pixels" ),
VIPS_ARGUMENT_OPTIONAL_INPUT,
G_STRUCT_OFFSET( VipsEmbedBase, background ),
VIPS_TYPE_ARRAY_DOUBLE );
}
static void
vips_embed_base_init( VipsEmbedBase *base )
{
base->extend = VIPS_EXTEND_BLACK;
base->background = vips_array_double_newv( 1, 0.0 );
}
/* Embed with specified x, y
*/
typedef struct _VipsEmbed {
VipsEmbedBase parent_instance;
int x;
int y;
} VipsEmbed;
typedef VipsConversionClass VipsEmbedClass;
G_DEFINE_TYPE( VipsEmbed, vips_embed, vips_embed_base_get_type() );
static int
vips_embed_build( VipsObject *object )
{
VipsEmbedBase *base = (VipsEmbedBase *) object;
VipsEmbed *embed = (VipsEmbed *) object;
/* Just pass the specified x, y down.
*/
base->x = embed->x;
base->y = embed->y;
if( VIPS_OBJECT_CLASS( vips_embed_parent_class )->build( object ) )
return( -1 );
return( 0 );
}
static void
vips_embed_class_init( VipsEmbedClass *class )
{
GObjectClass *gobject_class = G_OBJECT_CLASS( class );
VipsObjectClass *vobject_class = VIPS_OBJECT_CLASS( class );
VIPS_DEBUG_MSG( "vips_embed_class_init\n" );
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
vobject_class->nickname = "embed";
vobject_class->description = _( "embed an image in a larger image" );
vobject_class->build = vips_embed_build;
VIPS_ARG_INT( class, "x", 3,
_( "x" ),
_( "Left edge of input in output" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsEmbed, x ),
-1000000000, 1000000000, 0 );
VIPS_ARG_INT( class, "y", 4,
_( "y" ),
_( "Top edge of input in output" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsEmbed, y ),
-1000000000, 1000000000, 0 );
}
static void
vips_embed_init( VipsEmbed *embed )
{
}
/**
* vips_embed: (method)
* @in: input image
* @out: (out): output image
* @x: place @in at this x position in @out
* @y: place @in at this y position in @out
* @width: @out should be this many pixels across
* @height: @out should be this many pixels down
* @...: %NULL-terminated list of optional named arguments
*
* Optional arguments:
*
* * @extend: #VipsExtend to generate the edge pixels (default: black)
* * @background: #VipsArrayDouble colour for edge pixels
*
* The opposite of vips_extract_area(): embed @in within an image of size
* @width by @height at position @x, @y.
*
* @extend
* controls what appears in the new pels, see #VipsExtend.
*
* See also: vips_extract_area(), vips_insert().
*
* Returns: 0 on success, -1 on error.
*/
int
vips_embed( VipsImage *in, VipsImage **out,
int x, int y, int width, int height, ... )
{
va_list ap;
int result;
va_start( ap, height );
result = vips_call_split( "embed", ap, in, out, x, y, width, height );
va_end( ap );
return( result );
}
/* Embed with a general direction.
*/
typedef struct _VipsGravity {
VipsEmbedBase parent_instance;
VipsCompassDirection direction;
} VipsGravity;
typedef VipsConversionClass VipsGravityClass;
G_DEFINE_TYPE( VipsGravity, vips_gravity, vips_embed_base_get_type() );
static int
vips_gravity_build( VipsObject *object )
{
VipsEmbedBase *base = (VipsEmbedBase *) object;
VipsGravity *gravity = (VipsGravity *) object;
if( vips_object_argument_isset( object, "in" ) &&
vips_object_argument_isset( object, "width" ) &&
vips_object_argument_isset( object, "height" ) &&
vips_object_argument_isset( object, "direction" ) ) {
switch( gravity->direction ) {
case VIPS_COMPASS_DIRECTION_CENTRE:
base->x = (base->width - base->in->Xsize) / 2;
base->y = (base->height - base->in->Ysize) / 2;
break;
case VIPS_COMPASS_DIRECTION_NORTH:
base->x = (base->width - base->in->Xsize) / 2;
base->y = 0;
break;
case VIPS_COMPASS_DIRECTION_EAST:
base->x = base->width - base->in->Xsize;
base->y = (base->height - base->in->Ysize) / 2;
break;
case VIPS_COMPASS_DIRECTION_SOUTH:
base->x = (base->width - base->in->Xsize) / 2;
base->y = base->height - base->in->Ysize;
break;
case VIPS_COMPASS_DIRECTION_WEST:
base->x = 0;
base->y = (base->height - base->in->Ysize) / 2;
break;
case VIPS_COMPASS_DIRECTION_NORTH_EAST:
base->x = base->width - base->in->Xsize;
base->y = 0;
break;
case VIPS_COMPASS_DIRECTION_SOUTH_EAST:
base->x = base->width - base->in->Xsize;
base->y = base->height - base->in->Ysize;
break;
case VIPS_COMPASS_DIRECTION_SOUTH_WEST:
base->x = 0;
base->y = base->height - base->in->Ysize;
break;
case VIPS_COMPASS_DIRECTION_NORTH_WEST:
base->x = 0;
base->y = 0;
break;
default:
g_assert_not_reached();
}
}
if( VIPS_OBJECT_CLASS( vips_gravity_parent_class )->build( object ) )
return( -1 );
return( 0 );
}
static void
vips_gravity_class_init( VipsGravityClass *class )
{
GObjectClass *gobject_class = G_OBJECT_CLASS( class );
VipsObjectClass *vobject_class = VIPS_OBJECT_CLASS( class );
VIPS_DEBUG_MSG( "vips_gravity_class_init\n" );
gobject_class->set_property = vips_object_set_property;
gobject_class->get_property = vips_object_get_property;
vobject_class->nickname = "gravity";
vobject_class->description = _( "place an image within a larger "
"image with a certain gravity" );
vobject_class->build = vips_gravity_build;
VIPS_ARG_ENUM( class, "direction", 3,
_( "Direction" ),
_( "Direction to place image within width/height" ),
VIPS_ARGUMENT_REQUIRED_INPUT,
G_STRUCT_OFFSET( VipsGravity, direction ),
VIPS_TYPE_COMPASS_DIRECTION, VIPS_COMPASS_DIRECTION_CENTRE );
}
static void
vips_gravity_init( VipsGravity *gravity )
{
gravity->direction = VIPS_COMPASS_DIRECTION_CENTRE;
}
/**
* vips_gravity:
* @in: input image
* @out: output image
* @direction: place @in at this direction in @out
* @width: @out should be this many pixels across
* @height: @out should be this many pixels down
* @...: %NULL-terminated list of optional named arguments
*
* Optional arguments:
*
* * @extend: #VipsExtend to generate the edge pixels (default: black)
* * @background: #VipsArrayDouble colour for edge pixels
*
* The opposite of vips_extract_area(): place @in within an image of size
* @width by @height at a certain gravity.
*
* @extend
* controls what appears in the new pels, see #VipsExtend.
*
* See also: vips_extract_area(), vips_insert().
*
* Returns: 0 on success, -1 on error.
*/
int
vips_gravity( VipsImage *in, VipsImage **out,
VipsCompassDirection direction, int width, int height, ... )
{
va_list ap;
int result;
va_start( ap, height );
result = vips_call_split( "gravity", ap, in, out,
direction, width, height );
va_end( ap );
return( result );
}