Contents Previous Next

Programming in Java Advanced Imaging


C H A P T E R14

Extending the API




THIS chapter describes how the JAI API may be extended.

14.1 Introduction

No image processing API can hope to capture the enormous variety of operations that can be performed on a digital image. Although the JAI API supports a large number of imaging operations, it was designed from the beginning to encourage programmers to write extensions rather than manipulating image data directly. JAI allows virtually any image processing algorithm to be added to the API and used as if it were a native part of the API.

The mechanism for adding functionality to the API can be presented at multiple levels of encapsulation and complexity. This allows programmers who wish to add simple things to the API to deal with simple concepts, while more complex extensions have complete control over their environment at the lowest levels of abstraction. The API also supports a variety of programming styles, including an immediate mode and a deferred mode of execution for different types of imaging applications.

14.2 Package Naming Convention

All extensions to JAI require the addition of new classes. All new classes are grouped into packages as a convenient means of organizing the new classes and separating the new classes from code libraries provided by others.

All new packages are given a product name. A product name is the accepted Java method of using your company's reversed Internet address to name new packages. This product naming convention helps to guarantee the uniqueness of package names. Supposing that your company's Internet address is WebStuff.COM and you wish to create a new package named Prewitt. A good choice of package name would be

     com.webstuff.Prewitt
Or, even

     com.webstuff.media.jai.Prewitt
To uniquely identify the package as part of JAI.

The above new prewitt class file must now be placed into a subdirectory that matches the product name, such as:

com/webstuff/media/jai for Solaris-based systems

or

com\webstuff\media\jai for Windows systems

The Java convention for class naming is to use initial caps for the name, as in the Prewitt example above. So called multi-word class names use initial caps for each word. For example AddOpImage.

Vendors are encouraged to use unique product names (by means of the Java programming language convention of reversed internet addresses) to maximize the likelihood of a clean installation.

14.3 Writing New Operators

To extend the JAI API by creating new operations, you will need to write a new OpImage subclass. This may be done by subclassing one or more existing utility classes to automate some of the details of the operator you wish to implement. For most operators, you need only supply a routine that is capable of producing an arbitrary rectangle of output, given contiguous source data.

Once created, new operators may be made available to users transparently and without user source code changes using the JAI registry mechanism. Existing applications may be tuned for new hardware platforms by strategic insertion of new implementations of existing operators.

To create a new operator, you need to create the following new classes:

14.3.1 Extending the OpImage Class

Every new operator being written must be a subclass of OpImage or one of its subclasses. The OpImage class currently has the following subclasses:

Table 14-1 OpImage Subclasses
Class Description
AreaOpImage
An abstract base class for image operators that require only a fixed rectangular source region around a source pixel in order to compute each destination pixel.
NullOpImage
Extends: PointOpImage
A trivial OpImage subclass that simply transmits its source unchanged. Potentially useful when an interface requires an OpImage but another sort of RenderedImage (such as a TiledImage) is to be used.
PointOpImage
An abstract base class for image operators that require only a single source pixel in order to compute each destination pixel.
ScaleOpImage
Extends: WarpOpImage
An abstract base class for scale-like operations that require rectilinear backwards mapping and padding by the resampling filter dimensions.
SourcelessOpImage
An abstract base class for image operators that have no image sources.
StatisticsOpImage
An abstract base class for image operators that compute statistics on a given region of an image, and with a given sampling rate.
UntiledOpImage
A general class for single-source operations in which the values of all pixels in the source image contribute to the value of each pixel in the destination image.
WarpOpImage
A general implementation of image warping, and a superclass for other geometric image operations.

All abstract methods defined in OpImage must be implemented by any new OpImage subclass. Specifically, there are two fundamental methods that must be implemented:

Method Description
getTile
Gets a tile for reading. This method is called by the object that has the new operator name as its source with a rectangle as its parameter. The operation is responsible for returning a rectangle filled in with the correct values.
computeRect
Computes a rectangle of output, given Raster sources. The method is called by getTile to do the actual computation. The extension must override this method.

First, you have to decide which of the OpImage subclasses to extend. If you are writing a new operator to manipulate two or more images, you would most likely extend the MultiSourceOpImage class. To write a new statistics operation, you would most likely extend the StatisticsOpImage class. Each subclass has a specific purpose, as described in Table 14-1.

The following code sample extends MultiSourceOpImage by creating a new OpImage class named AddOpImage. This new operator adds two images together. It uses the methods defined in its superclass, MultiSourceOpImage, to determine the image size and pixel layout.

Listing 14-1 Example of Extending the OpImage Class


     // AddOpImage.java
     package javax.jai.operator;
     import java.awt.*
     import java.util.*
     import javax.media.jai.*
     //
     // An OpImage class to add two source images, pixelwise.
     //
     public class AddOpImage extends MultiSourceOpImage {
          // A dummy constructor used by the class loader.
          public AddOpImage() {}
          //
          // Constructs an AddOpImage. The image dimensions are taken
          // from the source images. The tile layout, SampleModel, and
          // ColorModel may optionally be specified by an ImageLayout
          // object.
          //
          // @param source0 the first RenderedImage source.
          // @param source1 the second RenderedImage source.
          // @param layout an ImageLayout optionally containing
          //               the tile grid layout, SampleModel, and
          //               ColorModel, or null.
          public AddOpImage(RenderedImage source0,
                           RenderedImage source1,
                           ImageLayout layout) {
              super(vectorize(source0, source1), layout, true);
     }
          // Adds the pixel values of a rectangle from the
          // two source images.
          protected void computeRect(Raster[] sources,
                                     WritableRaster dest,
                                     Rectangle destRect) {
              Raster source0 = sources[0];
              Raster source1 = sources[1];
              // Code omitted: add the two Rasters pixel by pixel
              ...
          }
     }

14.3.2 Extending the OperationDescriptor Interface

All high-level operation names in JAI (such as Rotate, Convolve, and AddConst) are mapped to instances of RenderedImageFactory that are capable of instantiating OpImage chains to perform the named operation. This mapping is done within the OperationRegistry class, as follows:

The OperationDescriptor interface provides a comprehensive description of a specific image operation. All of the information regarding the operation, such as the operation name, version, input, and property, should be listed. Any conditions placed on the operation, such as its input format and legal parameter range, should also be included, and the methods to enforce these conditions should be implemented. A set of PropertyGenerators may be specified to be used as a basis for the operation's property management.

Each family of the image operation in JAI must have a descriptor that implements this interface. The following basic resource data must be provided:

Additional information about the operation must be provided when appropriate. It is also good idea to provide a detailed description of the operation's functionality in the class comment. When all of the above data is provided, the operation can be added to an OperationRegistry.

Listing 14-2 shows an example of an operation descriptor for the Clamp operation. Note that the descriptor also contains descriptions of the two required operation parameters, but no hints as these aren't required for the operation.

Listing 14-2 Operation Descriptor for Clamp Operation


     public class ClampDescriptor extends OperationDescriptorImpl {
     /**
     * The resource strings that provide the general documentation
     * and specify the parameter list for this operation.
     */
     private static final String[][] resources = {
         {"GlobalName",  "Clamp"},
         {"LocalName",   "Clamp"},
         {"Vendor",      "com.sun.javax.media.jai"},
         {"Description", "Clamps the pixel values of a rendered image"},
         {"DocURL",      "http://java.sun.com/products/java-media/jai/
                          forDevelopers/jaiapi/
                          javax.media.jai.operator.ClampDescriptor.html"},
         {"Version",     "Beta")},
         {"arg0Desc",    "The lower boundary for each band."},
         {"arg1Desc",    "The upper boundary for each band."}
     };

As described in Section 3.3, "Processing Graphs," JAI has two image modes: Rendered and Renderable. An operation supporting the Rendered mode takes RenderedImages as its sources, can only be used in a Rendered op chain, and produces a RenderedImage. An operation supporting the Renderable mode takes RenderableImages as its sources, can only be used in a Renderable op chain, and produces a RenderableImage. Therefore, the class types of the sources and the destination of an operation are different between the two modes, but the parameters must be the same for both modes.

All operations must support the rendered mode and implement those methods that supply the information for this mode. Those operations that support the renderable mode must specify this feature using the isRenderableSupported method and implement those methods that supply the additional information for the Renderable mode.

Table 14-2 lists the Rendered mode methods. Table 14-3 lists the Renderable mode methods. Table 14-4 lists the methods relative to operation parameters.

Table 14-2 Rendered Mode Methods
Method Description
isRenderedSupported
Returns true if the operation supports the Rendered image mode. This must be true for all operations.
isImmediate
Returns true if the operation should be rendered immediately during the call to JAI.create; that is, the operation is placed in immediate mode.
getSourceClasses
Returns an array of Classes that describe the types of sources required by this operation in the Rendered image mode.
getDestClass
Returns a Class that describes the type of destination this operation produces in the Rendered image mode.
validateArguments
Returns true if this operation is capable of handling the input rendered source(s) and/or parameter(s) specified in the ParameterBlock.

Table 14-3 Renderable Mode Methods
Method Description
isRenderableSupported
Returns true if the operation supports the Renderable image mode.
getRenderableSourceClasses
Returns an array of Classes that describe the types of sources required by this operation in the Renderable image mode.
getRenderableDestClass
Returns a Class that describes the type of destination this operation produces in the Renderable image mode.
validateRenderableArguments
Returns true if this operation is capable of handling the input Renderable source(s) and/or parameter(s) specified in the ParameterBlock.

Table 14-4 Parameter Methods
Method Description
getNumParameters
Returns the number of parameters (not including the sources) required by this operation.
getParamClasses
Returns an array of Classes that describe the types of parameters required by this operation.
getParamNames
Returns an array of Strings that are the localized parameter names of this operation.
getParamDefaults
Returns an array of Objects that define the default values of the parameters for this operation.
getParamDefaultValue
Returns the default value of a specified parameter.
getParamMinValue
Returns the minimum legal value of a specified numeric parameter for this operation.
getParamMaxValue
Returns the maximum legal value of a specified numeric parameter for this operation.


API: javax.media.jai.OperationRegistry

registers an OperationDescriptor with the registry. Each operation must have an OperationDescriptor before registerRIF() may be called to add RIFs to the operation.

Parameter:

odesc

An OperationDescriptor containing information about the operation.

operationName

The operation name as a String.

A OperationDescriptor cannot be registered under an operation name under which another OperationDescriptor was registered previously. If such an attempt is made, an Error will be thrown.

registers an OperationDescriptor by its class name.

Parameter:

odescClassName

The fully-qualified class name of the OperationDescriptor.

operationName

The operation name as a String.

unregisters an OperationDescriptor from the registry.

registers a RIF with a particular product and operation.

Parameter:

operationName

The operation name as a String.

productName

The product name, as a String.

RIF

The RenderedImageFactory to be registered.

registers a RIF with a particular product and operation, constructing an instance using its class name.

Parameter:

operationName

The operation name as a String.

productName

The product name, as a String.

RIFClassName

The fully-qualified class name of a RenderedImageFactory.

14.4 Iterators

Iterators are provided to help the programmer who writes extensions to the JAI API and does not want to use any of the existing API methods for traversing pixels. Iterators define the manner in which the source image pixels are traversed for processing. Iterators may be used both in the implementation of computeRect methods or getTile methods of OpImage subclasses, and for ad-hoc pixel-by-pixel image manipulation.

Iterators provide a mechanism for avoiding the need to cobble sources, as well as to abstract away the details of source pixel formatting. An iterator is instantiated to iterate over a specified rectangular area of a source RenderedImage or Raster. The iterator returns pixel values in int, float, or double format, automatically promoting integral values smaller than 32 bits to int when reading, and performing the corresponding packing when writing.

JAI offers three different types of iterator, which should cover nearly all of a programmer's needs. However, extenders may wish to build others for more specialized needs.

The most basic iterator is RectIter, which provides the ability to move one line or pixel at a time to the right or downwards, and to step forward in the list of bands. RookIter offers slightly more functionality than RectIter, allowing leftward and upward movement and backwards motion through the set of bands. Both RectIter and RookIter allow jumping to an arbitrary line or pixel, and reading and writing of a random band of the current pixel. The RookIter also allows jumping back to the first line or pixel, and to the last line or pixel.

RandomIter allows an unrelated set of samples to be read by specifying their x and y coordinates and band offset. The RandomIter will generally be slower than either the RectIter or RookIter, but remains useful for its ability to hide pixel formats and tile boundaries.

Figure 14-1 shows the Iterator package hierarchy. The classes are described in the following paragraphs.

14.4.1 RectIter

The RectIter interface represents an iterator for traversing a read-only image in top-to-bottom, left-to-right order (Figure 14-2). The RectIter traversal will generally be the fastest style of iterator, since it does not need to perform bounds checks against the top or left edges of tiles. The WritableRectIter interface traverses a read/write image in the same manner as the RectIter.

The iterator is initialized with a particular rectangle as its bounds. The initialization takes place in a factory method (the RectIterFactory class) and is not a part of the iterator interface itself. Once initialized, the iterator may be reset to its initial state by means of the startLines(), startPixels(), and startBands() methods. Its position may be advanced using the nextLine(), jumpLines(), nextPixel(), jumpPixels(), and nextBand() methods.



Figure 14-1 Iterator Hierarchy



Figure 14-2 RectIter Traversal Pattern

The WritableRookIter interface adds the ability to alter the source pixel values using the various setSample() and setPixel() methods.

An instance of RectIter may be obtained by means of the RectIterFactory.create() method, which returns an opaque object implementing this interface.


API: javax.media.jai.iterator.RectIterFactory

constructs and returns an instance of RectIter suitable for iterating over the given bounding rectangle within the given RenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A read-only RenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of RectIter suitable for iterating over the given bounding rectangle within the given Raster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A read-only Raster source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRectIter suitable for iterating over the given bounding rectangle within the given WritableRenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A WritableRenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRectIter suitable for iterating over the given bounding rectangle within the given WritableRaster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A WritableRaster source.

bounds

The bounding Rectangle for the iterator, or null.


API: javax.media.jai.iterator.RectIter
sets the iterator to the first line of its bounding rectangle. The pixel and band offsets are unchanged.

sets the iterator to the leftmost pixel of its bounding rectangle. The line and band offsets are unchanged.

sets the iterator to the first band of the image. The pixel column and line are unchanged.

sets the iterator to the next line of the image. The pixel and band offsets are unchanged. If the iterator passes the bottom line of the rectangles, calls to get() methods are not valid.

jumps downward num lines from the current position. The num parameter may be negative. The pixel and band offsets are unchanged.

sets the iterator to the next pixel in the image (that is, move rightward). The line and band offsets are unchanged.

jumps rightward num pixels from the current position. The num parameter may be negative. The line and band offsets are unchanged.

sets the iterator to the next band in the image. The pixel column and line are unchanged.

14.4.2 RookIter

The RookIter interface represents an iterator for traversing a read-only image using arbitrary up-down and left-right moves (Figure 14-3 shows two of the possibilities for traversing the pixels). The RookIter traversal will generally be somewhat slower than a corresponding instance of RectIter, since it must perform bounds checks against the top and left edges of tiles in addition to their bottom and right edges. The WritableRookIter interface traverses a read/write image in the same manner as the RookIter.

An instance of RookIter may be obtained by means of the RookIterFactory.create() or RookIterFactory.createWritable() methods, which return an opaque object implementing this interface. The iterator is initialized with a particular rectangle as its bounds. This initialization takes place in a factory method (the RookIterFactory class) and is not a part of the iterator interface itself.

Once initialized, the iterator may be reset to its initial state by means of the startLines(), startPixels(), and startBands() methods. As with RectIter, its position may be advanced using the nextLine(), jumpLines(), nextPixel(), jumpPixels(), and nextBand() methods.



Figure 14-3 RookIter Traversal Patterns


API: avax.media.jai.iterator.RookIterFactory

constructs and returns an instance of RookIter suitable for iterating over the given bounding rectangle within the given RenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A read-only RenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of RookIter suitable for iterating over the given bounding rectangle within the given Raster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A read-only Raster source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRookIter suitable for iterating over the given bounding rectangle within the given WritableRenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A WritableRenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRookIter suitable for iterating over the given bounding rectangle within the given WritableRaster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A WritableRaster source.

bounds

The bounding Rectangle for the iterator, or null.

14.4.3 RandomIter

The RandomIter interface represents an iterator that allows random access to any sample within its bounding rectangle. The flexibility afforded by this class will generally exact a corresponding price in speed and setup overhead.

The iterator is initialized with a particular rectangle as its bounds. This initialization takes place in a factory method (the RandomIterFactory class) and is not a part of the iterator interface itself. An instance of RandomIter may be obtained by means of the RandomIterFactory.create() method, which returns an opaque object implementing this interface.

The getSample(), getSampleFloat(), and getSampleDouble() methods are provided to allow read-only access to the source data. The getPixel() methods allow retrieval of all bands simultaneously.


API: javax.media.jai.iterator.RandomIterFactory

constructs and returns an instance of RandomIter suitable for iterating over the given bounding rectangle within the given RenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A read-only RenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of RandomIter suitable for iterating over the given bounding rectangle within the given Raster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A read-only Raster source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRandomIter suitable for iterating over the given bounding rectangle within the given WritableRenderedImage source. If the bounds parameter is null, the entire image will be used.

Parameters:

im

A WritableRenderedImage source.

bounds

The bounding Rectangle for the iterator, or null.

constructs and returns an instance of WritableRandomIter suitable for iterating over the given bounding rectangle within the given WritableRaster source. If the bounds parameter is null, the entire Raster will be used.

Parameters:

ras

A read-only Raster source.

bounds

The bounding Rectangle for the iterator, or null.

14.4.4 Example RectIter

Listing 14-3 shows an example of the construction of a new RectIter.

Listing 14-3 Example RectIter (Sheet 1 of 4)


     import java.awt.Rectangle;
     import java.awt.image.ColorModel;
     import java.awt.image.DataBuffer;
     import java.awt.image.PixelInterleavedSampleModel;
     import java.awt.image.SampleModel;
     import java.util.Random;
     import javax.media.jai.*;
     import javax.media.jai.iterator.*;
     class RectIterTest {
         int width = 10;
         int height = 10;
         int tileWidth = 4;
         int tileHeight = 4;
         public static void main(String[] args) {
             new RectIterTest();
         }
         public RectIterTest() {
             Random rand = new Random(1L);
             Rectangle rect = new Rectangle();
             int[] bandOffsets = { 2, 1, 0 };
             SampleModel sampleModel =
                 new PixelInterleavedSampleModel(DataBuffer.TYPE_BYTE,
                                                 tileWidth, tileHeight,
                                                 3, 3*tileWidth,
                                                 bandOffsets);
             ColorModel colorModel = null;
             TiledImage im = new TiledImage(0, 0, width, height, 0, 0,
                                            sampleModel,
                                            colorModel);
             int[][][] check = new int[width][height][3];
             int x, y, b;
             for (int i = 0; i < 10; i++) {
                 rect.x = rand.nextInt(width);
                 rect.width = rand.nextInt(width - rect.x) + 1;
                 rect.y = rand.nextInt(height);
                 rect.height = rand.nextInt(height - rect.y) + 1;
     System.out.println("Filling rect " + rect + " with " + i);
     WritableRectIter witer = RectIterFactory.createWritable(im,
                                                             rect);
                 b = 0;
                 witer.startBands();
                 while (!witer.finishedBands()) {
                     y = rect.y;
                     witer.startLines();
                     while (!witer.finishedLines()) {
                         x = rect.x;
                         witer.startPixels();
                         while (!witer.finishedPixels()) {
                             witer.setSample(i);
                             check[x][y][b] = i;
                             ++x;
                             witer.nextPixel();
                         }
                         ++y;
                         witer.nextLine();
                     }
                     ++b;
                     witer.nextBand();
                 }
             }
             rect.x = 0;
             rect.y = 0;
             rect.width = width;
             rect.height = height;
             RectIter iter = RectIterFactory.createWritable(im, rect);
             b = 0;
             iter.startBands();
             while (!iter.finishedBands()) {
                 System.out.println();
                 y = 0;
                 iter.startLines();
                 while (!iter.finishedLines()) {
                     x = 0;
                     iter.startPixels();
                     while (!iter.finishedPixels()) {
                         int val = iter.getSample();
                         System.out.print(val);
                         if (val != check[x][y][b]) {
                         System.out.print("(" + check[x][y][b] + ")  ");
                         } else {
                             System.out.print("     ");
                         }
                         ++x;
                         iter.nextPixel();
                     }
                     ++y;
                     iter.nextLine();
                     System.out.println();
                 }
                 ++b;
                 iter.nextBand();
             }
         }
     }

14.5 Writing New Image Decoders and Encoders

The sample directory contains an example of how to create a new image codec. The example is of a PNM codec, but can be used as a basis for creating any codec. The PNM codec consists of three files:

File Name Description
SamplePNMCodec.java
Defines a subclass of ImageCodec for handling the PNM family of image files.
SamplePNMImageDecoder.java
Defines an ImageDecoder for the PNM family of image files. Necessary for reading PNM files.
SamplePNMImageEncoder.java
Defines an ImageEncoder for the PNM family of image files. Necessary for writing PNM files.

14.5.1 Image Codecs


Note: The codec classes are provided for the developer as a convenience for file IO. These classes are not part of the official Java Advanced Imaging API and are subject to change as a result of the near future File IO extension API. Until the File IO extension API is defined, these classes and functions will be supported for JAI use.
The ImageCodec class allows the creation of image decoders and encoders. Instances of ImageCodec may be registered by name. The registerCodec method associates an ImageCodec with the given name. Any codec previously associated with the name is discarded. Once a codec has been registered, the name associated with it may be used as the name parameter in the createImageEncoder and createImageDecoder methods.

The ImageCodec class maintains a registry of FormatRecognizer objects that examine an InputStream and determine whether it adheres to the format handled by a particular ImageCodec. A FormatRecognizer is added to the registry with the registerFormatRecognizer method. The unregisterFormatRecognizer method removes a previously registered FormatRecognizer from the registry.

The getCodec method returns the ImageCodec associated with a given name. If no codec is registered with the given name, null is returned.


API: com.sun.media.jai.codec.ImageCodec

returns an ImageEncoder object suitable for encoding to the supplied OutputStream, using the supplied ImageEncodeParam object.

Parameter:

name

The name associated with the codec.

dst

An OutputStream to write to.

param

An instance of ImageEncodeParam suitable for use with the named codec, or null.

returns an ImageEncoder object suitable for encoding to the supplied OutputStream object. A null ImageEncodeParam is used.

returns an ImageDecoder object suitable for decoding from the supplied InputStream, using the supplied ImageDecodeParam object.

Parameter:

name

The name associated with the codec.

src

An InputStream to read from.

param

An instance of ImageEncodeParam suitable for use with the named codec, or null.

returns an ImageDecoder object suitable for decoding from the supplied InputStream. A null ImageDecodeParam is used.

associates an ImageCodec with the given name. Case is not significant. Any codec previously associated with the name is discarded.

Parameter:

name

The name associated with the codec.

codec

The ImageCodec object to be associated with the given name.

removes the association between a given name and an ImageCodec object. Case is not significant.

returns the ImageCodec associated with the given name. If no codec is registered with the given name, null is returned. Case is not significant.

Parameter:

name

The name associated with the codec.



Contents Previous Next

Programming in Java Advanced Imaging


Copyright © 1999, Sun Microsystems, Inc. All rights reserved.

Casa de Bender