/**
   * Paint the image onto a Graphics object. The painting is performed tile-by-tile, and includes a
   * grey region covering the unused portion of image tiles as well as the general background. At
   * this point the image must be byte data.
   */
  public synchronized void paintComponent(Graphics g) {

    Graphics2D g2D = null;
    if (g instanceof Graphics2D) {
      g2D = (Graphics2D) g;
    } else {
      return;
    }

    // if source is null, it's just a component
    if (source == null) {
      g2D.setColor(getBackground());
      g2D.fillRect(0, 0, componentWidth, componentHeight);
      return;
    }

    int transX = -originX;
    int transY = -originY;

    // Get the clipping rectangle and translate it into image coordinates.
    Rectangle clipBounds = g.getClipBounds();

    if (clipBounds == null) {
      clipBounds = new Rectangle(0, 0, componentWidth, componentHeight);
    }

    // clear the background (clip it) [minimal optimization here]
    if (transX > 0
        || transY > 0
        || transX < (componentWidth - source.getWidth())
        || transY < (componentHeight - source.getHeight())) {
      g2D.setColor(getBackground());
      g2D.fillRect(0, 0, componentWidth, componentHeight);
    }

    clipBounds.translate(-transX, -transY);

    // Determine the extent of the clipping region in tile coordinates.
    int txmin, txmax, tymin, tymax;
    int ti, tj;

    txmin = XtoTileX(clipBounds.x);
    txmin = Math.max(txmin, minTileX);
    txmin = Math.min(txmin, maxTileX);

    txmax = XtoTileX(clipBounds.x + clipBounds.width - 1);
    txmax = Math.max(txmax, minTileX);
    txmax = Math.min(txmax, maxTileX);

    tymin = YtoTileY(clipBounds.y);
    tymin = Math.max(tymin, minTileY);
    tymin = Math.min(tymin, maxTileY);

    tymax = YtoTileY(clipBounds.y + clipBounds.height - 1);
    tymax = Math.max(tymax, minTileY);
    tymax = Math.min(tymax, maxTileY);
    Insets insets = getInsets();

    // Loop over tiles within the clipping region
    for (tj = tymin; tj <= tymax; tj++) {
      for (ti = txmin; ti <= txmax; ti++) {
        int tx = TileXtoX(ti);
        int ty = TileYtoY(tj);

        Raster tile = source.getTile(ti, tj);
        if (tile != null) {
          DataBuffer dataBuffer = tile.getDataBuffer();

          WritableRaster wr = tile.createWritableRaster(sampleModel, dataBuffer, null);

          BufferedImage bi =
              new BufferedImage(colorModel, wr, colorModel.isAlphaPremultiplied(), null);

          // correctly handles band offsets
          if (brightnessEnabled == true) {
            SampleModel sm =
                sampleModel.createCompatibleSampleModel(tile.getWidth(), tile.getHeight());

            WritableRaster raster = RasterFactory.createWritableRaster(sm, null);

            BufferedImage bimg =
                new BufferedImage(colorModel, raster, colorModel.isAlphaPremultiplied(), null);

            // don't move this code
            ByteLookupTable lutTable = new ByteLookupTable(0, lutData);
            LookupOp lookup = new LookupOp(lutTable, null);
            lookup.filter(bi, bimg);

            g2D.drawImage(bimg, biop, tx + transX + insets.left, ty + transY + insets.top);
          } else {
            AffineTransform transform;

            transform =
                AffineTransform.getTranslateInstance(
                    tx + transX + insets.left, ty + transY + insets.top);

            g2D.drawRenderedImage(bi, transform);
          }
        }
      }
    }
  }
  /**
   * {@collect.stats} Rescales the pixel data in the source Raster. If the destination Raster is
   * null, a new Raster will be created. The source and destination must have the same number of
   * bands. Otherwise, an IllegalArgumentException is thrown. Note that the number of scaling
   * factors/offsets in this object must meet the restrictions stated in the class comments above.
   * Otherwise, an IllegalArgumentException is thrown.
   *
   * @param src the <code>Raster</code> to be filtered
   * @param dst the destination for the filtering operation or <code>null</code>
   * @return the filtered <code>WritableRaster</code>.
   * @throws IllegalArgumentException if <code>src</code> and <code>dst</code> do not have the same
   *     number of bands, or if the number of scaling factors and offsets in this <code>RescaleOp
   *     </code> do not meet the requirements stated in the class comments.
   */
  public final WritableRaster filter(Raster src, WritableRaster dst) {
    int numBands = src.getNumBands();
    int width = src.getWidth();
    int height = src.getHeight();
    int[] srcPix = null;
    int step = 0;
    int tidx = 0;

    // Create a new destination Raster, if needed
    if (dst == null) {
      dst = createCompatibleDestRaster(src);
    } else if (height != dst.getHeight() || width != dst.getWidth()) {
      throw new IllegalArgumentException("Width or height of Rasters do not " + "match");
    } else if (numBands != dst.getNumBands()) {
      // Make sure that the number of bands are equal
      throw new IllegalArgumentException(
          "Number of bands in src "
              + numBands
              + " does not equal number of bands in dest "
              + dst.getNumBands());
    }
    // Make sure that the arrays match
    // Make sure that the low/high/constant arrays match
    if (length != 1 && length != src.getNumBands()) {
      throw new IllegalArgumentException(
          "Number of scaling constants "
              + "does not equal the number of"
              + " of bands in the src raster");
    }

    //
    // Try for a native raster rescale first
    //
    if (ImagingLib.filter(this, src, dst) != null) {
      return dst;
    }

    //
    // Native raster rescale failed.
    // Try to see if a lookup operation can be used
    //
    if (canUseLookup(src, dst)) {
      int srcNgray = (1 << srcNbits);
      int dstNgray = (1 << dstNbits);

      if (dstNgray == 256) {
        ByteLookupTable lut =
            createByteLut(
                scaleFactors, offsets,
                numBands, srcNgray);
        LookupOp op = new LookupOp(lut, hints);
        op.filter(src, dst);
      } else {
        ShortLookupTable lut =
            createShortLut(
                scaleFactors, offsets,
                numBands, srcNgray);
        LookupOp op = new LookupOp(lut, hints);
        op.filter(src, dst);
      }
    } else {
      //
      // Fall back to the slow code
      //
      if (length > 1) {
        step = 1;
      }

      int sminX = src.getMinX();
      int sY = src.getMinY();
      int dminX = dst.getMinX();
      int dY = dst.getMinY();
      int sX;
      int dX;

      //
      //  Determine bits per band to determine maxval for clamps.
      //  The min is assumed to be zero.
      //  REMIND: This must change if we ever support signed data types.
      //
      int nbits;
      int dstMax[] = new int[numBands];
      int dstMask[] = new int[numBands];
      SampleModel dstSM = dst.getSampleModel();
      for (int z = 0; z < numBands; z++) {
        nbits = dstSM.getSampleSize(z);
        dstMax[z] = (1 << nbits) - 1;
        dstMask[z] = ~(dstMax[z]);
      }

      int val;
      for (int y = 0; y < height; y++, sY++, dY++) {
        dX = dminX;
        sX = sminX;
        for (int x = 0; x < width; x++, sX++, dX++) {
          // Get data for all bands at this x,y position
          srcPix = src.getPixel(sX, sY, srcPix);
          tidx = 0;
          for (int z = 0; z < numBands; z++, tidx += step) {
            val = (int) (srcPix[z] * scaleFactors[tidx] + offsets[tidx]);
            // Clamp
            if ((val & dstMask[z]) != 0) {
              if (val < 0) {
                val = 0;
              } else {
                val = dstMax[z];
              }
            }
            srcPix[z] = val;
          }

          // Put it back for all bands
          dst.setPixel(dX, dY, srcPix);
        }
      }
    }
    return dst;
  }