I'm trying to export a PNG quicklook of an ALOS AVNIR-2 product using the BEAM java APIs. The following picture shows the RGB preview of the prduct, as it appears in the GUI of beam.
As you can see, the image is not upright, due to its geocoding. I've developed a very simple java program to export the quicklook of the product.
public static void main(String[] args) {
String[] rgbBandNames = new String[3];
rgbBandNames[0] = "radiance_3";
rgbBandNames[1] = "radiance_2";
rgbBandNames[2] = "radiance_1";
try {
//Product inputProduct = ProductIO.readProduct(args[0]);
Product inputProduct = ProductIO.readProduct("C:\\nfsdata\\VOL-ALAV2A152763430-O1B2R_U");
Band[] produtBands = inputProduct.getBands();
Band[] rgbBands = new Band[3];
int n = 0;
for (Band band : produtBands) {
if (band.getName().equals(rgbBandNames[0])) {
rgbBands[0] = band;
} else if (band.getName().equals(rgbBandNames[1])) {
rgbBands[1] = band;
} else if (band.getName().equals(rgbBandNames[2])) {
rgbBands[2] = band;
}
n = n + 1;
}
ImageInfo outImageInfo = ProductUtils.createImageInfo(rgbBands, true, ProgressMonitor.NULL);
BufferedImage outImage = ProductUtils.createRgbImage(rgbBands, outImageInfo, ProgressMonitor.NULL);
ImageIO.write(outImage, "PNG", new File(inputProduct.getName() + ".png"));
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
The program works, but every PNG image i get from it is an upright PNG image, like the following.
Now, I know that it is not possible to have geocoding information inside a PNG image. I need only to reproduce the same "rotation" of the image.
Any idea?
I managed to solve my problem. In other words, I managed to extract the quicklook from an ALOS AV2 O1B2R_U product, rotated according to the geocoding information of the product (see the image below).
The reason for this is that the ALOS AV2 O1B2R_U products have the geocoding rotation already applied to the raster. As a consequence, in order to successfulyl export a quicklook, the rotation must be retrieved from the native raster and applied to the output image.
For future reference, I'd like to recap and share my solution with the community. This is my main class:
import com.bc.ceres.core.ProgressMonitor;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.geom.AffineTransform;
import java.awt.image.AffineTransformOp;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import javax.imageio.ImageIO;
import org.esa.beam.framework.dataio.ProductIO;
import org.esa.beam.framework.datamodel.Band;
import org.esa.beam.framework.datamodel.ImageInfo;
import org.esa.beam.framework.datamodel.MapGeoCoding;
import org.esa.beam.framework.datamodel.Product;
import org.esa.beam.util.ProductUtils;
public static void main(String[] args) throws IOException {
String inputProductPath = "path\to\input\product";
String outputProductPath = "path\to\output\image";
// Read the source product.
Product inputProduct = ProductIO.readProduct(inputProductPath);
// Extract the RGB bands.
String[] bandNames = new String[3];
Band[] bandData = new Band[3];
bandNames[0] = "radiance_3";
bandNames[1] = "radiance_2";
bandNames[2] = "radiance_1";
for (Band band : inputProduct.getBands()) {
for (int i = 0; i < bandNames.length; i++) {
if (band.getName().equalsIgnoreCase(bandNames[ i ])) {
bandData[ i ] = band;
}
}
}
// Generate quicklook image.
ImageInfo outImageInfo = ProductUtils.createImageInfo(bandData, true, ProgressMonitor.NULL);
BufferedImage outImage = ProductUtils.createRgbImage(bandData, outImageInfo, ProgressMonitor.NULL);
outImage = resize(outImage, WIDTH, 1200);
// Extract the orientation.
double orientation;
if (inputProduct.getGeoCoding() != null) {
orientation = -((MapGeoCoding) inputProduct.getGeoCoding()).getMapInfo().getOrientation();
} else {
orientation = 0.0;
}
outImage = rotate(outImage, orientation);
// Write image.
ImageIO.write(outImage, "PNG", new File(outputProductPath));
}
Once the rotation angle of the quicklook has been extracted from the source product (see the above code), it must be applied to the output image (BufferedImage). In the above code, two simple image manipulation functions are employed: resize(...) and rotate(...), see below for their definition.
/**
* Resizes the image {#code tgtImage} by setting one of its dimensions
* (width or height, specified via {#code tgtDimension}) to {#code tgtSize}
* and dynamically calculating the other one in order to preserve the aspect
* ratio.
*
* #param tgtImage The image to be resized.
* #param tgtDimension The selected dimension: {#code ImageUtil.WIDTH} or
* {#code ImageUtil.WIDTH}.
* #param tgtSize The new value for the selected dimension.
*
* #return The resized image.
*/
public static BufferedImage resize(BufferedImage tgtImage, short tgtDimension, int tgtSize) {
int newWidth = 0, newHeight = 0;
if (HEIGHT == tgtDimension) {
newHeight = tgtSize;
newWidth = (tgtImage.getWidth() * tgtSize) / tgtImage.getHeight();
} else {
newHeight = (tgtImage.getHeight() * tgtSize) / tgtImage.getWidth();
newWidth = tgtSize;
}
Image tmp = tgtImage.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
BufferedImage outImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGemoticon;
Graphics2D g2d = outImage.createGraphics();
g2d.drawImage(tmp, 0, 0, null);
g2d.dispose();
return outImage;
}
/**
* Rotates the image {#code tgtImage} by {#code tgtAngle} degrees clockwise.
*
* #param tgtImage The image to be rotated.
* #param tgtAngle The rotation angle (expressed in degrees).
*
* #return The resized image.
*/
public static BufferedImage rotate(BufferedImage tgtImage, double tgtAngle) {
int w = tgtImage.getWidth();
int h = tgtImage.getHeight();
AffineTransform t = new AffineTransform();
t.setToRotation(Math.toRadians(tgtAngle), w / 2d, h / 2d);
Point[] points = {
new Point(0, 0),
new Point(w, 0),
new Point(w, h),
new Point(0, h)
};
// Transform to destination rectangle.
t.transform(points, 0, points, 0, 4);
// Get destination rectangle bounding box
Point min = new Point(points[0]);
Point max = new Point(points[0]);
for (int i = 1, n = points.length; i < n; i++) {
Point p = points[ i ];
double pX = p.getX(), pY = p.getY();
// Update min/max x
if (pX < min.getX()) {
min.setLocation(pX, min.getY());
}
if (pX > max.getX()) {
max.setLocation(pX, max.getY());
}
// Update min/max y
if (pY < min.getY()) {
min.setLocation(min.getX(), pY);
}
if (pY > max.getY()) {
max.setLocation(max.getX(), pY);
}
}
// Determine new width, height
w = (int) (max.getX() - min.getX());
h = (int) (max.getY() - min.getY());
// Determine required translation
double tx = min.getX();
double ty = min.getY();
// Append required translation
AffineTransform translation = new AffineTransform();
translation.translate(-tx, -ty);
t.preConcatenate(translation);
AffineTransformOp op = new AffineTransformOp(t, null);
BufferedImage outImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGemoticon;
op.filter(tgtImage, outImage);
return outImage;
}
Related
I wrote a code which splits a gif-image into frames.
Then, they are scaled-down into smaller ones, the resolution is 128 * 128 pixels. Then they combine a full frame with a specific resolution.
The getFrames (String path) method gets the GIF file along the path from the jar and creates image frames.
The getResizedimages (…) method cuts the frame into smaller 128 by 128 images.
The getPixels (Bufferedlmage image) method gets an array of pixels from an image.
The problem is that during the execution of this code (via the test method), a very very large amount of memory is used even at the stage of execution of the getFrames (…) method or the getFramesRaw (…) method.
import com.sun.imageio.plugins.gif.GIFImageReader;
import com.sun.imageio.plugins.gif.GIFImageReaderSpi;
import org.apache.commons.io.FileUtils;
import org.bukkit.map.MapPalette;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
public class Gif {
static void test(String path, int height, int width) {
List<BufferedImage> images = getFrames(path);
for (int x = 0; x < width; x ++) for (int y = 0; y < height; y ++)
getResizedFrames(images, width, height, x, y).forEach(image -> getPixels(image));
}
#SuppressWarnings("deprecation")
public static byte[] getPixels(BufferedImage image) {
int pixelCount = image.getWidth() * image.getHeight();
int[] pixels = new int[pixelCount];
image.getRGB(0, 0, image.getWidth(), image.getHeight(), pixels, 0, image.getWidth());
byte[] colors = new byte[pixelCount];
for (int i = 0; i < pixelCount; i++)
colors[i] = MapPalette.matchColor(new Color(pixels[i], true));
return colors;
}
public static BufferedImage getScaledImage(BufferedImage image) {
BufferedImage newImg = new BufferedImage(128, 128, 3);
Graphics2D g2 = newImg.createGraphics();
g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2.drawImage(image, 0, 0, 128, 128, null);
g2.dispose();
return newImg;
}
public static BufferedImage cropImage(BufferedImage completeImg, int width, int height, int xCoord, int yCoord) {
int coordWidth = completeImg.getWidth() / width;
int coordHeight = completeImg.getHeight() / height;
BufferedImage croppedImg = new BufferedImage(completeImg.getWidth(), completeImg.getHeight(), 1);
Graphics2D g2 = (Graphics2D)croppedImg.getGraphics();
g2.drawImage(completeImg, 0, 0, completeImg.getWidth(), completeImg.getHeight(), null);
g2.dispose();
BufferedImage image = croppedImg.getSubimage(coordWidth * xCoord, coordHeight * yCoord, coordWidth, coordHeight);
return getScaledImage(image);
}
public static java.util.List<BufferedImage> getFramesRaw(String path) {
java.util.List<BufferedImage> frames = new ArrayList<>();
try {
File file = File.createTempFile("data", ".gif");
InputStream stream = Main.class.getResourceAsStream(path);
FileUtils.copyInputStreamToFile(stream, file);
stream.close();
ImageInputStream inputStream = ImageIO.createImageInputStream(file);
ImageReader ir = new GIFImageReader(new GIFImageReaderSpi());
ir.setInput(inputStream);
int number = ir.getNumImages(true);
for (int i = 0; i < number; i++) frames.add(ir.read(i));
inputStream.close();
System.gc();
} catch (Exception e) {
e.printStackTrace();
}
return frames;
}
public static java.util.List<BufferedImage> getFrames(String path) {
java.util.List<BufferedImage> copies = new ArrayList<>();
java.util.List<BufferedImage> frames = getFramesRaw(path);
copies.add(frames.remove(0));
int width = copies.get(0).getWidth(), height = copies.get(0).getHeight();
for (BufferedImage frame : frames) {
BufferedImage img = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics g = img.getGraphics();
g.drawImage(copies.get(copies.size()-1),0,0,null);
g.drawImage(frame,0,0,null);
copies.add(img);
}
return copies;
}
public static java.util.List<BufferedImage> getResizedFrames(java.util.List<BufferedImage> frames, int width, int height, int x, int y) {
List<BufferedImage> copies = new ArrayList<>();
for (BufferedImage image : frames)
copies.add(cropImage(image, width, height, width - 1 - x, height - 1 - y));
return copies;
}
}
I need your help.
The reason your program uses a lot of memory, is because your code operates at all the frames at each step (in many small loops). All the decoded image data for all the frames will be held in memory at the same time, resulting in unnecessary memory usage. While this may feel logical, it is for sure not efficient.
Instead, create one loop that does all the needed operations for a single frame, before moving on to the next.
Something like this:
public static void test(String path) throws IOException {
List<BufferedImage> thumbnails = new ArrayList<>();
readFrames(path, image -> thumbnails.add(getScaledImage(image)));
}
private static void readFrames(String path, Consumer<BufferedImage> forEachFrame) throws IOException {
try (ImageInputStream inputStream = ImageIO.createImageInputStream(GifSplitter.class.getResourceAsStream(path))) {
Iterator<ImageReader> readers = ImageIO.getImageReaders(inputStream); // ImageIO detects format, no need to hardcode GIF plugin
if (!readers.hasNext()) {
return;
}
ImageReader ir = readers.next();
ir.setInput(inputStream);
// For GIF format (which does not contain frame count in header),
// it's more efficient to just read each frame until IOOBE occurs,
// instead of invoking getNumImages(true)
int i = 0;
while (i >= 0) {
try {
BufferedImage image = ir.read(i++);
// crop, scale, etc, for a SINGLE image
forEachFrame.accept(image);
}
catch (IndexOutOfBoundsException endOfGif) {
// No more frames
break;
}
}
ir.dispose();
}
}
I have an org.eclipse.swt.graphics.Image, loaded from a PNG, and want to scale it in high quality (antialiasing, interpolation). But I do not want to lose transparency and get just a white background. (I need this Image to put it on an org.eclipse.swt.widgets.Label .)
Does anybody know how to do that?
Thank you!
Based on Mark's answer I found a better solution without the "hacky bit": first copy the alphaData from the origin then use GC to scale the image.
public static Image scaleImage(final Device device, final Image orig, final int scaledWidth, final int scaledHeight) {
final Rectangle origBounds = orig.getBounds();
if (origBounds.width == scaledWidth && origBounds.height == scaledHeight) {
return orig;
}
final ImageData origData = orig.getImageData();
final ImageData destData = new ImageData(scaledWidth, scaledHeight, origData.depth, origData.palette);
if (origData.alphaData != null) {
destData.alphaData = new byte[destData.width * destData.height];
for (int destRow = 0; destRow < destData.height; destRow++) {
for (int destCol = 0; destCol < destData.width; destCol++) {
final int origRow = destRow * origData.height / destData.height;
final int origCol = destCol * origData.width / destData.width;
final int o = origRow * origData.width + origCol;
final int d = destRow * destData.width + destCol;
destData.alphaData[d] = origData.alphaData[o];
}
}
}
final Image dest = new Image(device, destData);
final GC gc = new GC(dest);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.drawImage(orig, 0, 0, origBounds.width, origBounds.height, 0, 0, scaledWidth, scaledHeight);
gc.dispose();
return dest;
}
This way we don't have to make assumptions about the underlying ImageData.
Using a method described by Sean Bright here: https://stackoverflow.com/a/15685473/6245535, we can extract the alpha information from the image and use it to fill the ImageData.alphaData array which is responsible for the transparency:
public static Image resizeImage(Display display, Image image, int width, int height) {
Image scaled = new Image(display, width, height);
GC gc = new GC(scaled);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.drawImage(image, 0, 0, image.getBounds().width, image.getBounds().height, 0, 0, width, height);
gc.dispose();
ImageData canvasData = scaled.getImageData();
canvasData.alphaData = new byte[width * height];
// This is the hacky bit that is making assumptions about
// the underlying ImageData. In my case it is 32 bit data
// so every 4th byte in the data array is the alpha for that
// pixel...
for (int idx = 0; idx < (width * height); idx++) {
int coord = (idx * 4) + 3;
canvasData.alphaData[idx] = canvasData.data[coord];
}
// Now that we've set the alphaData, we can create our
// final image
Image finalImage = new Image(display, canvasData);
scaled.dispose();
return finalImage;
}
Note that this method assumes that you are working with 32 bit depth of color; it won't work otherwise.
i'm trying to extract text with coordinates from a pdf file using PDFBox.
I mixed some methods/info found on internet (stackoverflow too), but the problem i have the coordinates doesnt'seems to be right. When i try to use coordinates for drawing a rectangle on top of tex, for example, the rect is painted elsewhere.
This is my code (please don't judge the style, was written very fast just to test)
TextLine.java
import java.util.List;
import org.apache.pdfbox.text.TextPosition;
/**
*
* #author samue
*/
public class TextLine {
public List<TextPosition> textPositions = null;
public String text = "";
}
myStripper.java
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
/*
* To change this license header, choose License Headers in Project Properties.
* To change this template file, choose Tools | Templates
* and open the template in the editor.
*/
/**
*
* #author samue
*/
public class myStripper extends PDFTextStripper {
public myStripper() throws IOException
{
}
#Override
protected void startPage(PDPage page) throws IOException
{
startOfLine = true;
super.startPage(page);
}
#Override
protected void writeLineSeparator() throws IOException
{
startOfLine = true;
super.writeLineSeparator();
}
#Override
public String getText(PDDocument doc) throws IOException
{
lines = new ArrayList<TextLine>();
return super.getText(doc);
}
#Override
protected void writeWordSeparator() throws IOException
{
TextLine tmpline = null;
tmpline = lines.get(lines.size() - 1);
tmpline.text += getWordSeparator();
super.writeWordSeparator();
}
#Override
protected void writeString(String text, List<TextPosition> textPositions) throws IOException
{
TextLine tmpline = null;
if (startOfLine) {
tmpline = new TextLine();
tmpline.text = text;
tmpline.textPositions = textPositions;
lines.add(tmpline);
} else {
tmpline = lines.get(lines.size() - 1);
tmpline.text += text;
tmpline.textPositions.addAll(textPositions);
}
if (startOfLine)
{
startOfLine = false;
}
super.writeString(text, textPositions);
}
boolean startOfLine = true;
public ArrayList<TextLine> lines = null;
}
click event on AWT button
private void jButton1MouseClicked(java.awt.event.MouseEvent evt) {
// TODO add your handling code here:
try {
File file = new File("C:\\Users\\samue\\Desktop\\mwb_I_201711.pdf");
PDDocument doc = PDDocument.load(file);
myStripper stripper = new myStripper();
stripper.setStartPage(1); // fix it to first page just to test it
stripper.setEndPage(1);
stripper.getText(doc);
TextLine line = stripper.lines.get(1); // the line i want to paint on
float minx = -1;
float maxx = -1;
for (TextPosition pos: line.textPositions)
{
if (pos == null)
continue;
if (minx == -1 || pos.getTextMatrix().getTranslateX() < minx) {
minx = pos.getTextMatrix().getTranslateX();
}
if (maxx == -1 || pos.getTextMatrix().getTranslateX() > maxx) {
maxx = pos.getTextMatrix().getTranslateX();
}
}
TextPosition firstPosition = line.textPositions.get(0);
TextPosition lastPosition = line.textPositions.get(line.textPositions.size() - 1);
float x = minx;
float y = firstPosition.getTextMatrix().getTranslateY();
float w = (maxx - minx) + lastPosition.getWidth();
float h = lastPosition.getHeightDir();
PDPageContentStream contentStream = new PDPageContentStream(doc, doc.getPage(0), PDPageContentStream.AppendMode.APPEND, false);
contentStream.setNonStrokingColor(Color.RED);
contentStream.addRect(x, y, w, h);
contentStream.fill();
contentStream.close();
File fileout = new File("C:\\Users\\samue\\Desktop\\pdfbox.pdf");
doc.save(fileout);
doc.close();
} catch (Exception ex) {
}
}
any suggestion? what am i doing wrong?
This is just another case of the excessive PdfTextStripper coordinate normalization. Just like you I had thought that by using TextPosition.getTextMatrix() (instead of getX() and getY) one would get the actual coordinates, but no, even these matrix values have to be corrected (at least in PDFBox 2.0.x, I haven't checked 1.8.x) because the matrix is multiplied by a translation making the lower left corner of the crop box the origin.
Thus, in your case (in which the lower left of the crop box is not the origin), you have to correct the values, e.g. by replacing
float x = minx;
float y = firstPosition.getTextMatrix().getTranslateY();
by
PDRectangle cropBox = doc.getPage(0).getCropBox();
float x = minx + cropBox.getLowerLeftX();
float y = firstPosition.getTextMatrix().getTranslateY() + cropBox.getLowerLeftY();
Instead of
you now get
Obviously, though, you will also have to correct the height somewhat. This is due to the way the PdfTextStripper determines the text height:
// 1/2 the bbox is used as the height todo: why?
float glyphHeight = bbox.getHeight() / 2;
(from showGlyph(...) in LegacyPDFStreamEngine, the parent class of PdfTextStripper)
While the font bounding box indeed usually is too large, half of it often is not enough.
The following code worked for me:
// Definition of font baseline, ascent, descent: https://en.wikipedia.org/wiki/Ascender_(typography)
//
// The origin of the text coordinate system is the top-left corner where Y increases downward.
// TextPosition.getX(), getY() return the baseline.
TextPosition firstLetter = textPositions.get(0);
TextPosition lastLetter = textPositions.get(textPositions.size() - 1);
// Looking at LegacyPDFStreamEngine.showGlyph(), ascender and descender heights are calculated like
// CapHeight: https://stackoverflow.com/a/42021225/14731
float ascent = firstLetter.getFont().getFontDescriptor().getAscent() / 1000 * lastLetter.getFontSize();
Point topLeft = new Point(firstLetter.getX(), firstLetter.getY() - ascent);
float descent = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
// Descent is negative, so we need to negate it to move downward.
Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
lastLetter.getY() - descent);
float descender = lastLetter.getFont().getFontDescriptor().getDescent() / 1000 * lastLetter.getFontSize();
// Descender height is negative, so we need to negate it to move downward
Point bottomRight = new Point(lastLetter.getX() + lastLetter.getWidth(),
lastLetter.getY() - descender);
In other words, we are creating a bounding box from the font's ascender down to its descender.
If you want to render these coordinates with the origin in the bottom-left corner, see https://stackoverflow.com/a/28114320/14731 for more details. You'll need to apply a transform like this:
contents.transform(new Matrix(1, 0, 0, -1, 0, page.getHeight()));
In my Java Spring MVC Web Application, I have options to upload images of any size. Currently I have an API that reduces the size of any uploaded image that is above a specified width. I use the following code for that:
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.image.BufferedImage;
import java.awt.image.renderable.ParameterBlock;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import javax.imageio.ImageIO;
import javax.media.jai.JAI;
import javax.media.jai.PlanarImage;
import javax.media.jai.RenderedOp;
public class ImageRescale
{
private static RenderingHints hints;
static
{
hints = new RenderingHints(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
hints.put(RenderingHints.KEY_COLOR_RENDERING,RenderingHints.VALUE_COLOR_RENDER_QUALITY);
hints.put(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
/*
* this block is to silence the warning that we're not using JAI
* native acceleration but are using the pure java implementation.
*/
Properties p = new Properties(System.getProperties());
p.put("com.sun.media.jai.disableMediaLib", "true");
System.setProperties(p);
}
public static byte[] getScaledInstance(byte[] image, int maxWidth)
{
InputStream in = new ByteArrayInputStream(image);
BufferedImage img = null;
try
{
img = ImageIO.read(in);
double scale = (double) maxWidth / img.getWidth();
if ( scale > 1.0d )
{
return image;
}
else if (scale > 0.5d && scale < 1.0d)
{
return getByteArray(getScaledDownByGraphics(img, scale));
}
else if (scale <= 0.5d)
{
return getByteArray(getScaledDownByJAI(img, scale));
}
}
catch (IOException e)
{
e.printStackTrace();
}
return image;
}
public static byte[] getByteArray(BufferedImage img)
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] imageInByte = null;
try
{
ImageIO.write( img, "jpg", baos );
baos.flush();
imageInByte = baos.toByteArray();
baos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
return imageInByte;
}
/**
* See http://www.digitalsanctuary.com/tech-blog/java/how-to-resize-uploaded-images-using-java-better-way.html
* This instance seems to produce quality images ONLY when you are
* scaling down to something less than 50% of the original size.
* #param img
* #param scale
* #return the scaled image
*/
private static BufferedImage getScaledDownByJAI(BufferedImage img, double scale)
{
if(scale > 1.0d)
{
throw new RuntimeException("Can't scale according to " + scale + " : This method only scales down.");
}
PlanarImage originalImage = PlanarImage.wrapRenderedImage(img);
// now resize the image
ParameterBlock paramBlock = new ParameterBlock();
paramBlock.addSource(originalImage); // The source image
paramBlock.add(scale); // The xScale
paramBlock.add(scale); // The yScale
paramBlock.add(0.0); // The x translation
paramBlock.add(0.0); // The y translation
RenderedOp resizedImage = JAI.create("SubsampleAverage", paramBlock, hints);
return resizedImage.getAsBufferedImage();
}
/**
* This method produces high quality images when target scale is greater
* than 50% of the original.
* #param img
* #param scale
* #return the scaled image
*/
private static BufferedImage getScaledDownByGraphics(BufferedImage img, double scale)
{
final float scaleFactor = 0.8f;
BufferedImage ret = (BufferedImage)img;
int w = img.getWidth();
int h = img.getHeight();
int targetWidth = (int)(img.getWidth() * scale);
int targetHeight = (int)(img.getHeight() * scale);
int loopCount = 0;
int maxLoopCount = 20;
BufferedImage tmp;
do {
if (w > targetWidth) {
w *= scaleFactor;
if (w < targetWidth) {
w = targetWidth;
}
}
if (h > targetHeight) {
h *= scaleFactor;
if (h < targetHeight) {
h = targetHeight;
}
}
tmp = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = tmp.createGraphics();
g2.addRenderingHints(hints);
g2.drawImage(ret, 0, 0, w, h, null);
g2.dispose();
ret = tmp;
if(++loopCount > maxLoopCount) {
throw new RuntimeException("Hit maximum loop count " + maxLoopCount);
}
} while (w != targetWidth || h != targetHeight);
return ret;
}
}
But what I want now is an API that compresses all uploaded images and bring down its resolution to around 72 dpi which I believe is the web standard for images. Is there any way I can achieve this.
Could you check if this would help you?
/**
*
* #param bi image
* #param mimeType type
* #param quality quality
* #return compressed image
* #throws IOException on error
*/
private static BufferedImage compressImage(BufferedImage bi, String mimeType
, float quality) throws IOException {
// converting buffered image to byte array
// do not compress GIFs
if (mimeType.equalsIgnoreCase("image/gif")) {
return bi;
}
ImageWriter writer = ImageIO.getImageWritersByMIMEType(mimeType).next();
ImageWriteParam iwp = writer.getDefaultWriteParam();
if (!mimeType.equalsIgnoreCase("image/png") && !mimeType.equalsIgnoreCase("image/bmp")) {
iwp.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
iwp.setCompressionQuality(quality);
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedOutputStream bos = new java.io.BufferedOutputStream(baos);
ImageOutputStream ios = ImageIO.createImageOutputStream(bos);
writer.setOutput(ios);
IIOImage optimizedImage = new IIOImage(bi, null, null);
writer.write(null, optimizedImage, iwp);
writer.dispose();
baos.flush();
ByteArrayInputStream in = new ByteArrayInputStream(baos.toByteArray());
return ImageIO.read(in);
}
I have the following code which does (the first part of) what I want drawing a chessboard with some pieces on it.
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scale = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
transform.setToTranslation(xPos, yPos);
transform.scale(scale, scale);
realGraphics.drawImage(pieceImage, transform, this);
that is, it gets a chess piece's image and the image's height, it translates the drawing of that image to the square the piece is on and scales the image to the size of the square.
Llet's say I want to rotate the black pieces 180 degrees. Somewhere I expect to have something like:
transform.rotate(Math.toRadians(180) /* ?, ? */);
But I can't figure out what to put in as X and Y. If I put nothing, the image is nicely rotated around the 0,0 point of its chessboard square, putting the piece upside down in the square to the northeast of where it is supposed to be. I've guessed at various other combinations of x,y, with no luck yet.
I am already using translation to put the piece in the right square, the rotation transform wants another x,y around which to rotate things, but I don't know how to tell the transform to rotate the piece around one x,y and write the image to a different x,y. Can someone help me with the rotation parameters, or point me to something that explains how these things work? I've found examples of things that don't explain how they work, and so far I haven't figured out how to alter them to my situation...
Major edit: addition of working code. Sorry, I don't know how to post images, please substitute your own.
When I run the following I get a 2x2 chess board with a rook at the top left and a knight at the bottom right.
If I go into SmallChessboardComponent and take the comment delims off the first rotation transform statement, I get the rook in its original place upside down and the knight does not appear. If I instead take the comment delims off the second transform statement, neither piece appears at all.
I am looking for a way to turn the pieces upside down on the square on which they would appear anyway. I want to draw each piece onto the board; I don't want code that flips the board.
main program:
package main;
import java.awt.BorderLayout;
import javax.swing.JFrame;
import directredraw.SmallChessboardComponent;
public class SmallChessboardMain
{
private static void dbg (String message) { System.out.println(message); }
public static void main(String[] args)
{
//Create the top-level container and add contents to it.
final JFrame frame = new JFrame("Small Chessboard");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// create the chessboard itself and set it in the component
SmallChessboard chessboard = new SmallChessboard();
// create the GUI component that will contain the chessboard
SmallChessboardComponent chessboardComponent = new SmallChessboardComponent();
chessboardComponent.setBoard (chessboard);
frame.getContentPane().add(chessboardComponent, BorderLayout.CENTER);
// pack and display all this
frame.pack();
frame.setVisible(true);
}
}
chessboard class:
package main;
public class SmallChessboard
{
Piece [][] squares = new Piece[2][2];
public SmallChessboard()
{
squares[0][0] = new Piece(Piece.WHITECOLOR, Piece.ROOK);
squares[1][1] = new Piece(Piece.WHITECOLOR, Piece.KNIGHT);
}
/**
* get the piece at the given rank and file; null if
* no piece exists there.
*/
public Piece getPiece(int rank, int file)
{
if (0 > rank || rank > 2 || 0 > file || file > 2) { return null; }
else { return squares[rank][file]; }
}
}
chessboard component class:
package directredraw;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.geom.AffineTransform;
import javax.swing.JPanel;
import main.Piece;
import main.PieceImages;
import main.SmallChessboard;
public class SmallChessboardComponent extends JPanel
{
private static final long serialVersionUID = 1L;
Color whiteSquareColor = Color.yellow;
Color blackSquareColor = Color.blue;
private static void dbg (String msg) { System.out.println(msg); }
private SmallChessboard chessboard = null;
// currently playing with rotating images; this affine transform
// should help
AffineTransform rotationTransform = null;
private final int DEFAULT_PREFERRED_SIDE = 400;
int wholeSide = DEFAULT_PREFERRED_SIDE;
int side = DEFAULT_PREFERRED_SIDE / 8;
public void setBoard (SmallChessboard givenBoard)
{ chessboard = givenBoard;
}
/**
* set either or both colors for this chessboard; if either of
* the arguments are null, they do not change the existing color
* setting.
*/
public void setColors (Color darkSquare, Color lightSquare)
{
if (darkSquare != null) { blackSquareColor = darkSquare; }
if (lightSquare != null) { whiteSquareColor = lightSquare; }
}
/**
* return the preferred size for this component.s
*/
public Dimension getPreferredSize()
{ return new Dimension(wholeSide, wholeSide);
}
/*
* return the image object for the given piece
*/
private Image getImage(Piece piece)
{ return PieceImages.getPieceImage(this, piece);
}
public void paintComponent (Graphics graphics)
{
Graphics2D realGraphics = (Graphics2D) graphics;
// the image container might have been stretched.
// calculate the largest square held by the current container,
// and then 1/2 of that size for an individual square.
int wholeWidth = this.getWidth();
int wholeHeight = this.getHeight();
wholeSide = (wholeWidth / 2) * 2;
if (wholeHeight < wholeWidth) { wholeSide = (wholeHeight / 2) * 2; }
side = wholeSide / 2;
Rectangle clip = realGraphics.getClipBounds();
boolean firstColumnWhite = false;
// for each file on the board:
// set whether top square is white
// set background color according to white/black square
//
for (int fileIndex=0; fileIndex<8; fileIndex++)
{ boolean currentColorWhite = firstColumnWhite;
firstColumnWhite = !firstColumnWhite;
// draw the board and all the pieces
int rankIndex = 2;
for (rankIndex=2; rankIndex>=0; rankIndex--)
{
currentColorWhite = !currentColorWhite;
// x and y position of the top left corner of the square we're drawing,
// and rect becomes the dimensions and position of the square itself.
int xPos = fileIndex * side;
int yPos = rankIndex * side;
Rectangle rect = new Rectangle(xPos, yPos, side, side);
// if this square intersects the clipping rectangle we're drawing,
// then we'll draw the square and the piece on the square.
if (rect.intersects(clip))
{
// this puts down the correct color of square
if (currentColorWhite) { realGraphics.setColor(whiteSquareColor); }
else { realGraphics.setColor(blackSquareColor); }
realGraphics.fillRect(xPos, yPos, side, side);
// if there is a piece on this square and it isn't selected at the
// moment, then draw it.
Piece currentPiece = chessboard.getPiece(rankIndex, fileIndex);
if (currentPiece != null)
{
Image pieceImage = getImage(currentPiece);
int pieceHeight = pieceImage.getHeight(null);
double scalePiece = (double)side/(double)pieceHeight;
AffineTransform transform = new AffineTransform();
// transform.setToRotation(Math.toRadians(180));
transform.setToRotation(Math.toRadians(180), side/2, side/2);
transform.scale(scalePiece, scalePiece);
transform.translate(xPos/scalePiece, yPos/scalePiece);
// if (currentPiece.isBlack())
// {
// transform.translate(xPos + (side+2), yPos + (side+2));
// transform.rotate(Math.toRadians(180) /*, ,*/ );
// }
// else
// {
// transform.translate(xPos, yPos);
// }
realGraphics.drawImage(pieceImage, transform, this);
}
}
}
}
}
}
Piece.java
package main;
public class Piece
{
// piece types; the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int PAWN = 0;
public static final int KNIGHT = 1;
public static final int BISHOP = 2;
public static final int ROOK = 3;
public static final int QUEEN = 4;
public static final int KING = 5;
// one of these is the color of the current piece
public static final int NOCOLOR = -1;
// the sum of the piece type and the
// color gives a number unique to both type and color,
// which is used for things like image indices.
public static final int BLACKCOLOR = 0;
public static final int WHITECOLOR = 6;
int color = NOCOLOR;
int imageIndex;
public Piece(int color, int pieceType)
{
// dbg -- all pieces are white rooks for now...
this.color = color;
imageIndex = color + pieceType;
}
/**
* return the integer associated with this piece's color;
*/
int getPieceColor()
{ return color;
}
/**
* return true if the piece is black
*/
public boolean isBlack()
{
return (color == BLACKCOLOR);
}
/**
* set the color associated with this piece; constants
* found in this class.
*/
public void setPieceColor(int givenColor)
{ color = givenColor;
}
/**
* return the integer designated for the image used for this piece.
*/
int getImageIndex()
{ return imageIndex;
}
}
and PieceImages.java
package main;
import java.awt.Component;
import java.awt.Image;
import java.awt.MediaTracker;
import java.awt.Toolkit;
import java.net.URL;
public class PieceImages
{ static Image images[] = null;
private static void dbg (String msg) { System.out.println(msg); }
public static Image getPieceImage (Component target, Piece piece)
{
if (images == null)
try
{
MediaTracker tracker = new MediaTracker(target);
images = new Image[12];
images[Piece.BLACKCOLOR + Piece.PAWN] = getImage(tracker, "bPawn.gif");
images[Piece.BLACKCOLOR + Piece.KNIGHT] = getImage(tracker, "bKnight.gif");
images[Piece.BLACKCOLOR + Piece.BISHOP] = getImage(tracker, "bBishop.gif");
images[Piece.BLACKCOLOR + Piece.ROOK] = getImage(tracker, "bRook.gif");
images[Piece.BLACKCOLOR + Piece.QUEEN] = getImage(tracker, "bQueen.gif");
images[Piece.BLACKCOLOR + Piece.KING] = getImage(tracker, "bKing.gif");
images[Piece.WHITECOLOR + Piece.PAWN] = getImage(tracker, "wPawn.gif");
images[Piece.WHITECOLOR + Piece.KNIGHT] = getImage(tracker, "wKnight.gif");
images[Piece.WHITECOLOR + Piece.BISHOP] = getImage(tracker, "wBishop.gif");
images[Piece.WHITECOLOR + Piece.ROOK] = getImage(tracker, "wRook.gif");
images[Piece.WHITECOLOR + Piece.QUEEN] = getImage(tracker, "wQueen.gif");
images[Piece.WHITECOLOR + Piece.KING] = getImage(tracker, "wKing.gif");
if (!tracker.waitForAll(10000))
{ System.out.println("ERROR: not all piece main.images loaded");
}
dbg("piece images loaded");
}
catch (Exception xcp)
{ System.out.println("Error loading images");
xcp.printStackTrace();
}
return images[piece.getImageIndex()];
}
private static Image getImage(MediaTracker tracker, String file)
{
URL url = PieceImages.class.getResource("images/" + file);
Image image = Toolkit.getDefaultToolkit().getImage(url);
tracker.addImage(image, 1);
return image;
}
}
Okay, this is a little slight of hand. The example code will only work for 90 degree increments (it was only designed this way), to do smaller increments you to use some trig to calculate the image width and height (there's a answer somewhere for that to ;))
public class ImagePane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public ImagePane(BufferedImage image) {
masterImage = image;
applyRotation(0);
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected int getVirtualAngle(int angle) {
float fRotations = (float) angle / 360f;
int rotations = (int) (fRotations - (fRotations / 1000));
int virtual = angle - (rotations * 360);
if (virtual < 0) {
virtual = 360 + virtual;
}
return virtual;
}
public void applyRotation(int angle) {
// This will only work for angles of 90 degrees...
// Normalize the angle to make sure it's only between 0-360 degrees
int virtualAngle = getVirtualAngle(angle);
Dimension size = new Dimension(masterImage.getWidth(), masterImage.getHeight());
int masterWidth = masterImage.getWidth();
int masterHeight = masterImage.getHeight();
double x = 0; //masterWidth / 2.0;
double y = 0; //masterHeight / 2.0;
switch (virtualAngle) {
case 0:
break;
case 180:
break;
case 90:
case 270:
size = new Dimension(masterImage.getHeight(), masterImage.getWidth());
x = (masterHeight - masterWidth) / 2.0;
y = (masterWidth - masterHeight) / 2.0;
break;
}
renderedImage = new BufferedImage(size.width, size.height, masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
AffineTransform at = AffineTransform.getTranslateInstance(x, y);
at.rotate(Math.toRadians(virtualAngle), masterWidth / 2.0, masterHeight / 2.0);
g2d.drawImage(masterImage, at, null);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
Now, you could simply "flip" the image vertically, if that works better for you
public class FlipPane extends JPanel {
private BufferedImage masterImage;
private BufferedImage renderedImage;
public FlipPane(BufferedImage image) {
masterImage = image;
flipMaster();
}
#Override
public Dimension getPreferredSize() {
return new Dimension(renderedImage.getWidth(), renderedImage.getHeight());
}
#Override
public Dimension getMinimumSize() {
return getPreferredSize();
}
protected void flipMaster() {
renderedImage = new BufferedImage(masterImage.getWidth(), masterImage.getHeight(), masterImage.getTransparency());
Graphics2D g2d = renderedImage.createGraphics();
g2d.setTransform(AffineTransform.getScaleInstance(1, -1));
g2d.drawImage(masterImage, 0, -masterImage.getHeight(), this);
g2d.dispose();
}
#Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2d = (Graphics2D) g;
int width = getWidth() - 1;
int height = getHeight() - 1;
int x = (width - renderedImage.getWidth()) / 2;
int y = (height - renderedImage.getHeight()) / 2;
g2d.drawImage(renderedImage, x, y, this);
}
}
This basically results in:
Original | 180 degree rotation | Vertical inversion...
Now, if you change the flipMaster method to read:
g2d.setTransform(AffineTransform.getScaleInstance(-1, -1));
g2d.drawImage(masterImage, -masterImage.getWidth(), -masterImage.getHeight(), this);
You'll get the same effect as the 180 rotation ;)
Try performing the rotation before translating it into the correct position. Simply reorder the transformations so that first you scale, then you rotate (around the center point of the image), and then you translate:
transform.scale(scale, scale);
transform.rotate(Math.PI, pieceWidth / 2, pieceHeight /2);
transform.translation(xPos, yPos);
By the way, the black pieces on a chess board usually aren't rotated. :)
Update
In what way does it not work? The solution I provided also also differs from your code in that scaling is performed before translating. You can try the rotating, translating, and then scaling.
I strongly suggest that you modify your code so that you can perform the translation last. If you do this, everything will become a lot less complicated. Once you have done so, you only have to scale once to automatically take care of the rotation.
transform.scale(scale, scale); // or transform.scale(scale, -scale); to rotate
transform.translate(xPos, yPos);