I'm trying to test a class which purpose is taking a specification of what to do with a BufferedImage array and proccess it.
I'm concerned about how to aproach this task, it makes no sense to me just duplicate the function code on the test in order to generate a "expected bufferedImage array" and check if the generated BufferedImages are equals to the returned by the class methods.
I've read some slightly similar questions here, but the answers was "charge expected images from file and check with returned ones by class methods" but that sounds real hard to maintain if the proccess changes, new methods are added to the renderer class, it's hard or imposible pregenerate a resulting image or simply the class is refactorized and functions are splitted.
My actual question might be: Is there a little more elegant, "better to maintain" way to do that. I haven't so much experience with unit testing so i'm not sure about how to do this.
EDIT: sorry for the long code, i simplified and translated it. Excuse me if i did a bad translation. english it's not my main language.
public class Renderer extends SwingWorker<BufferedImage[], Integer>
{
private Device device;
private Main main;
private Controller controller;
public static final int FPS = 25;
public Renderer(Device device, Main main, Controller controller)
{
this.device = device;
this.main = main;
this.controller = controller;
}
#Override
protected BufferedImage[] doInBackground() throws Exception
{
// rendering image array
BufferedImage[] output = renderize(main.getActualEscene());
LOG.log.log(Level.INFO, "Rendering");
// getting how many times should repeat
String stringRepeat =
main.getActualEscene().getProperties().get(TextProperties.REPEAT.toString());
int repeat = (stringRepeat == null) ? 1 : Integer.parseInt(stringRepeat);
// getting text speed
String stringFps =
main.getActualEscene().getProperties().get(TextProperties.TEXT_SPEED.toString());
int fps = (stringFps == null) ? FPS : Integer.parseInt(stringFps);
if (!this.isCancelled()) // if this task is not cancelled
{
// we create a pre-viewer
controller.setPreviewer(
new Previewer(controller, repeat, fps, main.getActualEscene()));
SwingUtilities.invokeLater(new Runnable()
{// sincronizing this code with AWT thread
#Override
public void run()
{ // if it's not cancelled
if (controller.getPreviewer() != null
&& !controller.getPreviewer().isCancelled())
{
controller.getPreviewer().execute(); //execute the task
}
}
});
}
return output;
}
/**
* Renders a scene and transforms it in a image array, then save it to the scene
*
* #param scene -> scene
* #return an array with the scene frames
*/
public BufferedImage[] renderize(Scene scene)
{
BufferedImage[] output = null; // end result
BufferedImage[] base = new BufferedImage[1]; // base image
BufferedImage[] animationImages = null; // animation layer
BufferedImage[] textLayer = null; // text layer
BufferedImage[] overLayer = null; // overlayer
// omitted long process to retrieve image properties
// once the text properties are retrieved if it's not null it will be rendered
if (text != null)
{
String backgroundColorString =
scene.getProperties().get(TextProperties.BACKGROUND_COLOR.toString());
int backGroundcolorInt = Integer.parseInt(backgroundColorString);
if (animationImages != null) // if there is an animation layer we create a same size text layer
{
textLayer = new BufferedImage[animationImages.length];
} else
{
textLayer = new BufferedImage[1];
}
for (int i = 0; i < textLayer.length; i++)
{
textLayer[i] = new BufferedImage(device.getWidth(), device.getHeight(),
BufferedImage.TYPE_INT_ARGB);
}
String font = scene.getProperties().get(TextProperties.FONT.toString());
int lettersHeigth = device.getHeight() / 3;
int colorNumber =
Integer.parseInt(scene.getProperties().get(TextProperties.TEXT_COLOR.toString()));
textLayer = addTextToAImage(text, font, lettersHeigth, new Color(colorNumber),
new Color(backGroundcolorInt), textLayer);
}
// has it overlayer?
if (scene.getProperties().containsKey(AnimationProperties.OVER.toString())) // if it has over
{
String over = scene.getProperties().get(AnimationProperties.OVER.toString());
overLayer =
this.readingImagesFromAnimation(OverLibrary.getInstance().getOverByName(over));
}
// mixing layers
output = base; // adding base base
if (animationImages != null) // if there is animation layer we add it
{
output = mixingTwoImages(output, animationImages);
}
if (textLayer != null) // if there is text layer
{
output = mixingTwoImages(output, textLayer);
}
if (overLayer != null) //if there is overlayer
{
output = mixingTwoImages(output, overLayer);
}
// delimiting image operative zone.
output = delimite(output, device);
main.getActualEscene().setPreview(new Preview(output));
LOG.log.log(Level.INFO, "Rendering scene finished: " + scene.getNombre());
return output;
}
/**
* Apply the device mask to an image. Delimiting the scene operative zone.
*
* #param input -> BufferedImage array which need to be delimited
* #param device -> device which it mask need to be applied
* #return An BufferedImage array with a delimited image for the device.
*/
private BufferedImage[] delimite(BufferedImage[] input, Device device)
{
BufferedImage[] output = new BufferedImage[input.length];
for (int i = 0; i < output.length; i++) // copy all original frames
{
output[i] = new BufferedImage(device.getWidth(), device.getHeight(),
BufferedImage.TYPE_INT_ARGB);
Graphics2D graphics = output[i].createGraphics();
graphics.drawImage(input[i], 0, 0, null);
}
for (int i = 0; i < output.length; i++) // for each frame
{
for (int j = 0; j < output[i].getHeight(); j++) // for each row
{
for (int k = 0; k < output[i].getWidth(); k++) // for each column
{
if (!device.estaEnZonaOperativa(j, k)) // if the coordinate is not in the operative zone
{
int pixel = 0x00000000; // we create a transparent pixel
output[i].setRGB(j, k, pixel); // set the original pixel with this new one
}
}
}
}
return output;
}
/**
* method that reads an animation images and returns it into an array
*
* #param animation -> animaciĆ³n de la que obtendremos sus image
* #return array de BufferedImage con los frames de la animaciĆ³n ordenados por orden de lectura
* ascendente.
*/
private BufferedImage[] readingImagesFromAnimation(Animation animation)
{
BufferedImage[] output = new BufferedImage[animation.getData().length];
for (int i = 0; i < animation.getData().length; i++) // for each frame
{
try
{
File ruta = animation.getData()[i]; // getting its path
output[i] = ImageIO.read(ruta); // reading from the path and save it into output
} catch (IOException e)
{
LOG.log.log(Level.SEVERE, "error reading animation file: " + animation.getData()[i],
e);
}
}
return output;
}
/**
* create a text layer an add it to the image parameter center
*
* #param text -> text to include
* #param font -> font of the text
* #param size -> size
* #param color -> text color
* #param image -> image you want to add text
* #return a text layer that need to be mixed with original one.
*/
private BufferedImage[] addTextToAImage(String text, String font, int size, Color color,
Color backgroundColor, BufferedImage[] image)
{
// we set the size of the output as the same of the original image
BufferedImage[] output = new BufferedImage[image.length];
for (int i = 0; i < image.length; i++) // for each frame
{
output[i] = new BufferedImage(image[i].getWidth(), image[i].getHeight(),
BufferedImage.TYPE_INT_ARGB); // we create a single new image
Graphics2D graphics = output[i].createGraphics(); // get its graphics
// calculate for metrics
Font font = new Font(font, Font.PLAIN, size); // create a font
graphics.setFont(font); // setting it into the graphics
FontMetrics fontMetrics = graphics.getFontMetrics(font); // generating metric object
// calculate the text coordinates
int x = (image[i].getWidth() - fontMetrics.stringWidth(text)) / 2;
int y = ((image[i].getHeight() - fontMetrics.getAscent() - fontMetrics.getDescent()) / 2)
+ fontMetrics.getAscent();
int width = fontMetrics.stringWidth(text); // usefull metric to set a brackground
int height = fontMetrics.getAscent() + fontMetrics.getDescent();
// setting a background
graphics.setColor(backgroundColor);
graphics.fill(new Rectangle(x, y - fontMetrics.getAscent(), width, height));
// drawing text
graphics.setColor(color); // setting a color for text
graphics.drawString(text, x, y);
}
return output;
}
/**
* method to join two bufferedImage arrays and mix them. The shortest array will be played on loop
*
* WARNIN: the images needs to has the same resolution
*
* #param image -> image mixed as background
* #param image2 -> image mixed as foreground
* #return a mixed image array.
*/
private BufferedImage[] mixingTwoImages(BufferedImage[] image, BufferedImage[] image2)
{
// checking no empty images.
if (image.length == 0 || image2.length == 0)
{
RuntimeException exception =
new RuntimeException("empty images");
LOG.log.log(Level.SEVERE, exception.getMessage(), exception);
throw exception;
}
int width = Math.max(image[0].getWidth(), image2[0].getWidth());
int height = Math.max(image[0].getHeight(), image2[0].getHeight());
// creating a sequence of the longest size of them
BufferedImage[] output = new BufferedImage[Math.max(image.length, image2.length)];
for (int i = 0; i < output.length; i++) // for each frame
{
// we create a frame
output[i] = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics g = output[i].getGraphics();
// mixing the frame
if (image.length > image2.length) // if the first array is longer
{
g.drawImage(image[i], 0, 0, null);
g.drawImage(image2[i % image2.length], 0, 0, null);
} else if (image.length < image2.length) // if the second array is longer
{
g.drawImage(image[i % image.length], 0, 0, null);
g.drawImage(image2[i], 0, 0, null);
} else // bot of the same size
{
g.drawImage(image[i], 0, 0, null);
g.drawImage(image2[i], 0, 0, null);
}
}
return output;
}
Related
I haven't experience with processing images in java. My goal is to combine several images. To be more detail, I have a template image and some other images. I want to put those images into template image at specific places.
For e.g:
template image:
specific image:
So, I want to put the dog image onto cats' image places and store the created image.
Please, tell me what is the easiest way to do that?
As Fabian pointed out, identifying patterns mightn't give the expected results, so my suggestion would be an alternative
If you control the templates and provide them to the user as options, you could implement them yourself and populate the images in placeholder nodes. The merged image would come from taking an overall snapshot
I've included a quick example, but note that it's not fully implemented (layout etc) so consider it more as a proof of concept. It's still possible to build on the below to display different images at the same time, text decoration, stars etc to be a closer representation of the example image you provided
This may not be the easiest method, but it could be an enjoyable learning experience. This could also be a viable option since you don't have image processing experience in Java
public class ImageTemplateNode extends Region{
private SimpleObjectProperty<Image> displayedImageProperty;
private ObservableList<Node> children = FXCollections.observableArrayList();
private Random random = new Random();
private int rows, columns;
private final int maximumRotation = 15;
public ImageTemplateNode(int rows, int cols, Image imageToDisplay){
this.rows = rows;
this.columns = cols;
this.displayedImageProperty = new SimpleObjectProperty<>(imageToDisplay);
createDisplayNodes();
setPadding(new Insets(10));
Bindings.bindContentBidirectional(getChildren(), children);
}
public ImageTemplateNode(int rows, int cols, Image imageToDisplay, Image backgroundImage){
this(rows, cols, imageToDisplay);
setBackgroundImage(backgroundImage);
}
private void createDisplayNodes(){
for(int count = 0; count < (rows * columns); count++){
StackPane container = new StackPane();
container.setRotate(getRandomRotationValue());
container.setBackground(
new Background(new BackgroundFill(getRandomColour(), new CornerRadii(5), new Insets(5))));
container.maxWidthProperty().bind(displayedImageProperty.get().widthProperty().add(25));
container.maxHeightProperty().bind(displayedImageProperty.get().heightProperty().add(25));
ImageView displayNode = new ImageView();
displayNode.imageProperty().bind(displayedImageProperty);
displayNode.fitWidthProperty().bind(container.widthProperty().subtract(25));
displayNode.fitHeightProperty().bind(container.heightProperty().subtract(25));
container.getChildren().setAll(displayNode);
children.add(container);
}
}
private int getRandomRotationValue(){
int randomValue = random.nextInt(maximumRotation);
//Rotate clockwise if even, anti-clockwise if odd
return randomValue % 2 == 0 ? randomValue : 360 - randomValue;
}
private Color getRandomColour(){
int red = random.nextInt(256);
int green = random.nextInt(256);
int blue = random.nextInt(256);
return Color.rgb(red, green, blue);
}
#Override
protected void layoutChildren() {
//Calculate the dimensions for the children so that they do not breach the padding and allow for rotation
double cellWidth = (widthProperty().doubleValue()
- getPadding().getLeft() - getPadding().getRight() - maximumRotation) / columns;
double cellHeight = (heightProperty().doubleValue()
- getPadding().getTop() - getPadding().getBottom() - maximumRotation) / rows;
for (int i = 0; i < (rows); i++) {
for (int j = 0; j < (columns); j++) {
if (children.size() <= ((i * (columns)) + j)) {
break;
}
Node childNode = children.get((i * (columns)) + j);
layoutInArea(childNode,
(j * cellWidth) + getPadding().getLeft(),
(i * cellHeight) + getPadding().getTop(), cellWidth, cellHeight,
0.0d, HPos.CENTER, VPos.CENTER);
}
}
}
public void setBackgroundImage(Image backgroundImage){
setBackground(new Background(
new BackgroundImage(backgroundImage,
BackgroundRepeat.REPEAT, BackgroundRepeat.REPEAT, BackgroundPosition.CENTER,
BackgroundSize.DEFAULT)));
}
public void changeDisplayImage(Image newImageToDisplay){
displayedImageProperty.set(newImageToDisplay);
}
public void captureAndSaveDisplay(){
FileChooser fileChooser = new FileChooser();
//Set extension filter
fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("png files (*.png)", "*.png"));
//Prompt user to select a file
File file = fileChooser.showSaveDialog(null);
if(file != null){
try {
//Pad the capture area
WritableImage writableImage = new WritableImage((int)getWidth() + 20,
(int)getHeight() + 20);
snapshot(null, writableImage);
RenderedImage renderedImage = SwingFXUtils.fromFXImage(writableImage, null);
//Write the snapshot to the chosen file
ImageIO.write(renderedImage, "png", file);
} catch (IOException ex) { ex.printStackTrace(); }
}
}
}
Screen shots:
Saved snap shots:
I have inherited a code snippet which draws audio waveform of a given file. But this waveform is a simple image built using JAVA vector graphics without any labeling, Axes information etc. I would like to port it to the jfreechart to increase it's informative value. My problem is that the code is cryptic to say the least.
public class Plotter {
AudioInputStream audioInputStream;
Vector<Line2D.Double> lines = new Vector<Line2D.Double>();
String errStr;
Capture capture = new Capture();
double duration, seconds;
//File file;
String fileName = "out.png";
SamplingGraph samplingGraph;
String waveformFilename;
Color imageBackgroundColor = new Color(20,20,20);
public Plotter(URL url, String waveformFilename) throws Exception {
if (url != null) {
try {
errStr = null;
this.fileName = waveformFilename;
audioInputStream = AudioSystem.getAudioInputStream(url);
long milliseconds = (long)((audioInputStream.getFrameLength() * 1000) / audioInputStream.getFormat().getFrameRate());
duration = milliseconds / 1000.0;
samplingGraph = new SamplingGraph();
samplingGraph.createWaveForm(null);
} catch (Exception ex) {
reportStatus(ex.toString());
throw ex;
}
} else {
reportStatus("Audio file required.");
}
}
/**
* Render a WaveForm.
*/
class SamplingGraph implements Runnable {
private Thread thread;
private Font font10 = new Font("serif", Font.PLAIN, 10);
private Font font12 = new Font("serif", Font.PLAIN, 12);
Color jfcBlue = new Color(000, 000, 255);
Color pink = new Color(255, 175, 175);
public SamplingGraph() {
}
public void createWaveForm(byte[] audioBytes) {
lines.removeAllElements(); // clear the old vector
AudioFormat format = audioInputStream.getFormat();
if (audioBytes == null) {
try {
audioBytes = new byte[
(int) (audioInputStream.getFrameLength()
* format.getFrameSize())];
audioInputStream.read(audioBytes);
} catch (Exception ex) {
reportStatus(ex.getMessage());
return;
}
}
int w = 500;
int h = 200;
int[] audioData = null;
if (format.getSampleSizeInBits() == 16) {
int nlengthInSamples = audioBytes.length / 2;
audioData = new int[nlengthInSamples];
if (format.isBigEndian()) {
for (int i = 0; i < nlengthInSamples; i++) {
/* First byte is MSB (high order) */
int MSB = (int) audioBytes[2*i];
/* Second byte is LSB (low order) */
int LSB = (int) audioBytes[2*i+1];
audioData[i] = MSB << 8 | (255 & LSB);
}
} else {
for (int i = 0; i < nlengthInSamples; i++) {
/* First byte is LSB (low order) */
int LSB = (int) audioBytes[2*i];
/* Second byte is MSB (high order) */
int MSB = (int) audioBytes[2*i+1];
audioData[i] = MSB << 8 | (255 & LSB);
}
}
} else if (format.getSampleSizeInBits() == 8) {
int nlengthInSamples = audioBytes.length;
audioData = new int[nlengthInSamples];
if (format.getEncoding().toString().startsWith("PCM_SIGN")) {
for (int i = 0; i < audioBytes.length; i++) {
audioData[i] = audioBytes[i];
}
} else {
for (int i = 0; i < audioBytes.length; i++) {
audioData[i] = audioBytes[i] - 128;
}
}
}
int frames_per_pixel = audioBytes.length / format.getFrameSize()/w;
byte my_byte = 0;
double y_last = 0;
int numChannels = format.getChannels();
for (double x = 0; x < w && audioData != null; x++) {
int idx = (int) (frames_per_pixel * numChannels * x);
if (format.getSampleSizeInBits() == 8) {
my_byte = (byte) audioData[idx];
} else {
my_byte = (byte) (128 * audioData[idx] / 32768 );
}
double y_new = (double) (h * (128 - my_byte) / 256);
lines.add(new Line2D.Double(x, y_last, x, y_new));
y_last = y_new;
}
saveToFile();
}
public void saveToFile() {
int w = 500;
int h = 200;
int INFOPAD = 15;
BufferedImage bufferedImage = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = bufferedImage.createGraphics();
createSampleOnGraphicsContext(w, h, INFOPAD, g2);
g2.dispose();
// Write generated image to a file
try {
// Save as PNG
File file = new File(fileName);
System.out.println(file.getAbsolutePath());
ImageIO.write(bufferedImage, "png", file);
JOptionPane.showMessageDialog(null,
new JLabel(new ImageIcon(fileName)));
} catch (IOException e) {
}
}
private void createSampleOnGraphicsContext(int w, int h, int INFOPAD, Graphics2D g2) {
g2.setBackground(imageBackgroundColor);
g2.clearRect(0, 0, w, h);
g2.setColor(Color.white);
g2.fillRect(0, h-INFOPAD, w, INFOPAD);
if (errStr != null) {
g2.setColor(jfcBlue);
g2.setFont(new Font("serif", Font.BOLD, 18));
g2.drawString("ERROR", 5, 20);
AttributedString as = new AttributedString(errStr);
as.addAttribute(TextAttribute.FONT, font12, 0, errStr.length());
AttributedCharacterIterator aci = as.getIterator();
FontRenderContext frc = g2.getFontRenderContext();
LineBreakMeasurer lbm = new LineBreakMeasurer(aci, frc);
float x = 5, y = 25;
lbm.setPosition(0);
while (lbm.getPosition() < errStr.length()) {
TextLayout tl = lbm.nextLayout(w-x-5);
if (!tl.isLeftToRight()) {
x = w - tl.getAdvance();
}
tl.draw(g2, x, y += tl.getAscent());
y += tl.getDescent() + tl.getLeading();
}
} else if (capture.thread != null) {
g2.setColor(Color.black);
g2.setFont(font12);
//g2.drawString("Length: " + String.valueOf(seconds), 3, h-4);
} else {
g2.setColor(Color.black);
g2.setFont(font12);
//g2.drawString("File: " + fileName + " Length: " + String.valueOf(duration) + " Position: " + String.valueOf(seconds), 3, h-4);
if (audioInputStream != null) {
// .. render sampling graph ..
g2.setColor(jfcBlue);
for (int i = 1; i < lines.size(); i++) {
g2.draw((Line2D) lines.get(i));
}
// .. draw current position ..
if (seconds != 0) {
double loc = seconds/duration*w;
g2.setColor(pink);
g2.setStroke(new BasicStroke(3));
g2.draw(new Line2D.Double(loc, 0, loc, h-INFOPAD-2));
}
}
}
}
public void start() {
thread = new Thread(this);
thread.setName("SamplingGraph");
thread.start();
seconds = 0;
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
thread = null;
}
public void run() {
seconds = 0;
while (thread != null) {
if ( (capture.line != null) && (capture.line.isActive()) ) {
long milliseconds = (long)(capture.line.getMicrosecondPosition() / 1000);
seconds = milliseconds / 1000.0;
}
try { thread.sleep(100); } catch (Exception e) { break; }
while ((capture.line != null && !capture.line.isActive()))
{
try { thread.sleep(10); } catch (Exception e) { break; }
}
}
seconds = 0;
}
} // End class SamplingGraph
/**
* Reads data from the input channel and writes to the output stream
*/
class Capture implements Runnable {
TargetDataLine line;
Thread thread;
public void start() {
errStr = null;
thread = new Thread(this);
thread.setName("Capture");
thread.start();
}
public void stop() {
thread = null;
}
private void shutDown(String message) {
if ((errStr = message) != null && thread != null) {
thread = null;
samplingGraph.stop();
System.err.println(errStr);
}
}
public void run() {
duration = 0;
audioInputStream = null;
// define the required attributes for our line,
// and make sure a compatible line is supported.
AudioFormat format = audioInputStream.getFormat();
DataLine.Info info = new DataLine.Info(TargetDataLine.class,
format);
if (!AudioSystem.isLineSupported(info)) {
shutDown("Line matching " + info + " not supported.");
return;
}
// get and open the target data line for capture.
try {
line = (TargetDataLine) AudioSystem.getLine(info);
line.open(format, line.getBufferSize());
} catch (LineUnavailableException ex) {
shutDown("Unable to open the line: " + ex);
return;
} catch (SecurityException ex) {
shutDown(ex.toString());
//JavaSound.showInfoDialog();
return;
} catch (Exception ex) {
shutDown(ex.toString());
return;
}
// play back the captured audio data
ByteArrayOutputStream out = new ByteArrayOutputStream();
int frameSizeInBytes = format.getFrameSize();
int bufferLengthInFrames = line.getBufferSize() / 8;
int bufferLengthInBytes = bufferLengthInFrames * frameSizeInBytes;
byte[] data = new byte[bufferLengthInBytes];
int numBytesRead;
line.start();
while (thread != null) {
if((numBytesRead = line.read(data, 0, bufferLengthInBytes)) == -1) {
break;
}
out.write(data, 0, numBytesRead);
}
// we reached the end of the stream. stop and close the line.
line.stop();
line.close();
line = null;
// stop and close the output stream
try {
out.flush();
out.close();
} catch (IOException ex) {
ex.printStackTrace();
}
// load bytes into the audio input stream for playback
byte audioBytes[] = out.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(audioBytes);
audioInputStream = new AudioInputStream(bais, format, audioBytes.length / frameSizeInBytes);
long milliseconds = (long)((audioInputStream.getFrameLength() * 1000) / format.getFrameRate());
duration = milliseconds / 1000.0;
try {
audioInputStream.reset();
} catch (Exception ex) {
ex.printStackTrace();
return;
}
samplingGraph.createWaveForm(audioBytes);
}
} // End class Capture
}
I have gone through it several times and know that the below part is where the audio values are calculated but my problem is that I have no idea how can I retrieve the time information at that point, i.e that value belongs to what time interval.
int frames_per_pixel = audioBytes.length / format.getFrameSize()/w;
byte my_byte = 0;
double y_last = 0;
int numChannels = format.getChannels();
for (double x = 0; x < w && audioData != null; x++) {
int idx = (int) (frames_per_pixel * numChannels * x);
if (format.getSampleSizeInBits() == 8) {
my_byte = (byte) audioData[idx];
} else {
my_byte = (byte) (128 * audioData[idx] / 32768 );
}
double y_new = (double) (h * (128 - my_byte) / 256);
lines.add(new Line2D.Double(x, y_last, x, y_new));
y_last = y_new;
}
I would like to plot it using XYSeriesPLot of jfreechart but having trouble calculating required values of x(time ) and y (this is amplitude but is it y_new in this code)?
I understand it is a very easy thing but I am new to this whole audio stuff, I understand the theory behind audio files but this seems to be a simple problem with a tough solution
enter link description here
The key thing to realize is that, in the provided code, the plot is expected to be at a much lower resolution than the actual audio data. For example, consider the following waveform:
The plotting code then represents the data as the blue boxes in the graph:
When the boxes are 1-pixel wide, this correspond to the lines with endpoints (x,y_last) and (x,y_new). As you can see, when the waveform is sufficiently smooth the range of amplitudes from y_last to y_new is a fair approximation to the samples within the box.
Now this representation can be convenient when trying to render the waveform in a pixel-by-pixel fashion (raster display). However, for XYPlot graphs (as can be found in jfreechart) you only need to specify a sequence of (x,y) points and the XYPlot takes care of drawing segments between those point. This corresponds to the green line in the following graph:
In theory, you could just provide every single sample as-is to the XYPlot. However, unless you have few samples, this tends to be quite heavy to plot. So, typically one would downsample the data first. If the waveform is sufficiently smooth the downsampling process reduces to a decimation (i.e. taking 1 every N samples). The decimation factor N then controls the tradeoff between rendering performance and waveform approximation accuracy. Note that if the decimation factor frames_per_pixel used in the provided code to generate a good raster display (i.e. one where the waveform feature that you'll like to see are not hidden by the blocky pixel look, and that does not show aliasing artifacts), the same factor should still be sufficient for the XYPlot (in fact you may be able to downsample a bit more).
As far as mapping the samples to a time/amplitude axes, I would not use the x and y parameters as they are defined in the plotting code provided: they are just pixel indices applicable to a raster-type display (as is the blue box representation above).
Rather I'd map the sample index (idx in the provided code) directly to the time axis by dividing by the sampling rate (which you can get from format.getFrameRate()).
Similarly, I'd map the full-scale sample values to [-1,+1] range by dividing the audioData[idx] samples by either 128 for 8-bits-per-sample data, and by 32768 for 16-bits-per-sample data.
The w and h parameters' main purpose would remain to configure the plotting area size, but would no longer be directly required to compute the XYPlot input (the XYPlot itself takes care of mapping time/amplitude values to pixel coordinates). The w parameter on the other hand also served the additional purpose of determining the number of points to draw. Now you may want to control the number of points based on how much decimation the waveform can sustain without showing too much distortion, or you could keep it as-is to display the waveform at the maximum available plot resolution (with some performance cost).
Note however that you may have to convert frames_per_pixel to a floating point value if you are expecting to display waveforms with fewer than w samples.
I'm using RSyntaxTextArea for a minimized IDE I'm working on, Everything seems to be really working smoothly except for the line numbering, which I couldn't really make it show:
RSyntaxTextArea textArea = new RSyntaxTextArea(20, 60);
textArea.setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_C);
textArea.setCodeFoldingEnabled(true);
textArea.setAntiAliasingEnabled(true);
RTextScrollPane sp = new RTextScrollPane(textArea);
sp.setLineNumbersEnabled(true);
sp.setFoldIndicatorEnabled(true);
if ( sp.getLineNumbersEnabled() )
{
System.out.println("Enabled"); // it prints the line but it's not showing
}
contentPane.add(/*textEditorScrollPane*/ textArea, BorderLayout.CENTER);
I can't figure out why it's not showing the line numbers..
It's not showing the scrollbars either, right? Assuming that contentPane is where you want your components, you need to add the RTextScrollPane instance to the contentPane, not the RSyntaxTextArea instance. The Gutter, which displays line numbers, is a part of the RTextScrollPane - an extended JScrollPane.
If you don't add a scroll pane to your GUI, it will not be shown, nor will you be able to scroll around. :P
So try the following:
contentPane.add(sp, BorderLayout.CENTER);
Alternatively, you can use the following class:
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.*;
import java.awt.*;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
/**
* This class will display line numbers for a related text component. The text
* component must use the same line height for each line. TextLineNumber
* supports wrapped lines and will highlight the line number of the current
* line in the text component.
* <p>
* This class was designed to be used as a component added to the row header
* of a JScrollPane.
*/
public class TextLineNumber extends JPanel
implements CaretListener, DocumentListener, PropertyChangeListener
{
public final static float LEFT = 0.0f;
public final static float CENTER = 0.5f;
public final static float RIGHT = 1.0f;
private final static Border OUTER = new MatteBorder(0, 0, 0, 2, Color.GRAY);
private final static int HEIGHT = Integer.MAX_VALUE - 1000000;
// Text component this TextTextLineNumber component is in sync with
private JTextComponent component;
// Properties that can be changed
private boolean updateFont;
private int borderGap;
private Color currentLineForeground;
private float digitAlignment;
private int minimumDisplayDigits;
// Keep history information to reduce the number of times the component
// needs to be repainted
private int lastDigits;
private int lastHeight;
private int lastLine;
private HashMap<String, FontMetrics> fonts;
/**
* Create a line number component for a text component. This minimum
* display width will be based on 3 digits.
*
* #param component the related text component
*/
public TextLineNumber(JTextComponent component)
{
this(component, 3);
}
/**
* Create a line number component for a text component.
*
* #param component the related text component
* #param minimumDisplayDigits the number of digits used to calculate
* the minimum width of the component
*/
public TextLineNumber(JTextComponent component, int minimumDisplayDigits)
{
this.component = component;
setFont(component.getFont());
setBorderGap(5);
setCurrentLineForeground(Color.RED);
setDigitAlignment(RIGHT);
setMinimumDisplayDigits(minimumDisplayDigits);
component.getDocument().addDocumentListener(this);
component.addCaretListener(this);
component.addPropertyChangeListener("font", this);
}
/**
* Gets the update font property
*
* #return the update font property
*/
public boolean getUpdateFont()
{
return updateFont;
}
/**
* Set the update font property. Indicates whether this Font should be
* updated automatically when the Font of the related text component
* is changed.
*
* #param updateFont when true update the Font and repaint the line
* numbers, otherwise just repaint the line numbers.
*/
public void setUpdateFont(boolean updateFont)
{
this.updateFont = updateFont;
}
/**
* Gets the border gap
*
* #return the border gap in pixels
*/
public int getBorderGap()
{
return borderGap;
}
/**
* The border gap is used in calculating the left and right insets of the
* border. Default value is 5.
*
* #param borderGap the gap in pixels
*/
public void setBorderGap(int borderGap)
{
this.borderGap = borderGap;
Border inner = new EmptyBorder(0, borderGap, 0, borderGap);
setBorder(new CompoundBorder(OUTER, inner));
lastDigits = 0;
setPreferredWidth();
}
/**
* Gets the current line rendering Color
*
* #return the Color used to render the current line number
*/
public Color getCurrentLineForeground()
{
return currentLineForeground == null ? getForeground() : currentLineForeground;
}
/**
* The Color used to render the current line digits. Default is Coolor.RED.
*
* #param currentLineForeground the Color used to render the current line
*/
public void setCurrentLineForeground(Color currentLineForeground)
{
this.currentLineForeground = currentLineForeground;
}
/**
* Gets the digit alignment
*
* #return the alignment of the painted digits
*/
public float getDigitAlignment()
{
return digitAlignment;
}
/**
* Specify the horizontal alignment of the digits within the component.
* Common values would be:
* <ul>
* <li>TextLineNumber.LEFT
* <li>TextLineNumber.CENTER
* <li>TextLineNumber.RIGHT (default)
* </ul>
*
* #param currentLineForeground the Color used to render the current line
*/
public void setDigitAlignment(float digitAlignment)
{
this.digitAlignment =
digitAlignment > 1.0f ? 1.0f : digitAlignment < 0.0f ? -1.0f : digitAlignment;
}
/**
* Gets the minimum display digits
*
* #return the minimum display digits
*/
public int getMinimumDisplayDigits()
{
return minimumDisplayDigits;
}
/**
* Specify the mimimum number of digits used to calculate the preferred
* width of the component. Default is 3.
*
* #param minimumDisplayDigits the number digits used in the preferred
* width calculation
*/
public void setMinimumDisplayDigits(int minimumDisplayDigits)
{
this.minimumDisplayDigits = minimumDisplayDigits;
setPreferredWidth();
}
/**
* Calculate the width needed to display the maximum line number
*/
private void setPreferredWidth()
{
Element root = component.getDocument().getDefaultRootElement();
int lines = root.getElementCount();
int digits = Math.max(String.valueOf(lines).length(), minimumDisplayDigits);
// Update sizes when number of digits in the line number changes
if (lastDigits != digits)
{
lastDigits = digits;
FontMetrics fontMetrics = getFontMetrics(getFont());
int width = fontMetrics.charWidth('0') * digits;
Insets insets = getInsets();
int preferredWidth = insets.left + insets.right + width;
Dimension d = getPreferredSize();
d.setSize(preferredWidth, HEIGHT);
setPreferredSize(d);
setSize(d);
}
}
/**
* Draw the line numbers
*/
#Override
public void paintComponent(Graphics g)
{
super.paintComponent(g);
// Determine the width of the space available to draw the line number
FontMetrics fontMetrics = component.getFontMetrics(component.getFont());
Insets insets = getInsets();
int availableWidth = getSize().width - insets.left - insets.right;
// Determine the rows to draw within the clipped bounds.
Rectangle clip = g.getClipBounds();
int rowStartOffset = component.viewToModel(new Point(0, clip.y));
int endOffset = component.viewToModel(new Point(0, clip.y + clip.height));
while (rowStartOffset <= endOffset)
{
try
{
if (isCurrentLine(rowStartOffset))
{
g.setColor(getCurrentLineForeground());
} else
{
g.setColor(getForeground());
}
// Get the line number as a string and then determine the
// "X" and "Y" offsets for drawing the string.
String lineNumber = getTextLineNumber(rowStartOffset);
int stringWidth = fontMetrics.stringWidth(lineNumber);
int x = getOffsetX(availableWidth, stringWidth) + insets.left;
int y = getOffsetY(rowStartOffset, fontMetrics);
g.drawString(lineNumber, x, y);
// Move to the next row
rowStartOffset = Utilities.getRowEnd(component, rowStartOffset) + 1;
} catch (Exception e)
{
break;
}
}
}
/*
* We need to know if the caret is currently positioned on the line we
* are about to paint so the line number can be highlighted.
*/
private boolean isCurrentLine(int rowStartOffset)
{
int caretPosition = component.getCaretPosition();
Element root = component.getDocument().getDefaultRootElement();
if (root.getElementIndex(rowStartOffset) == root.getElementIndex(caretPosition))
{
return true;
} else
{
return false;
}
}
/*
* Get the line number to be drawn. The empty string will be returned
* when a line of text has wrapped.
*/
protected String getTextLineNumber(int rowStartOffset)
{
Element root = component.getDocument().getDefaultRootElement();
int index = root.getElementIndex(rowStartOffset);
Element line = root.getElement(index);
if (line.getStartOffset() == rowStartOffset)
{
return String.valueOf(index + 1);
} else
{
return "";
}
}
/*
* Determine the X offset to properly align the line number when drawn
*/
private int getOffsetX(int availableWidth, int stringWidth)
{
return (int) ((availableWidth - stringWidth) * digitAlignment);
}
/*
* Determine the Y offset for the current row
*/
private int getOffsetY(int rowStartOffset, FontMetrics fontMetrics)
throws BadLocationException
{
// Get the bounding rectangle of the row
Rectangle r = component.modelToView(rowStartOffset);
int lineHeight = fontMetrics.getHeight();
int y = r.y + r.height;
int descent = 0;
// The text needs to be positioned above the bottom of the bounding
// rectangle based on the descent of the font(s) contained on the row.
if (r.height == lineHeight) // default font is being used
{
descent = fontMetrics.getDescent();
} else // We need to check all the attributes for font changes
{
if (fonts == null)
{
fonts = new HashMap<>();
}
Element root = component.getDocument().getDefaultRootElement();
int index = root.getElementIndex(rowStartOffset);
Element line = root.getElement(index);
for (int i = 0; i < line.getElementCount(); i++)
{
Element child = line.getElement(i);
AttributeSet as = child.getAttributes();
String fontFamily = (String) as.getAttribute(StyleConstants.FontFamily);
Integer fontSize = (Integer) as.getAttribute(StyleConstants.FontSize);
String key = fontFamily + fontSize;
FontMetrics fm = fonts.get(key);
if (fm == null)
{
Font font = new Font(fontFamily, Font.PLAIN, fontSize);
fm = component.getFontMetrics(font);
fonts.put(key, fm);
}
descent = Math.max(descent, fm.getDescent());
}
}
return y - descent;
}
//
// Implement CaretListener interface
//
#Override
public void caretUpdate(CaretEvent e)
{
// Get the line the caret is positioned on
int caretPosition = component.getCaretPosition();
Element root = component.getDocument().getDefaultRootElement();
int currentLine = root.getElementIndex(caretPosition);
// Need to repaint so the correct line number can be highlighted
if (lastLine != currentLine)
{
repaint();
lastLine = currentLine;
}
}
//
// Implement DocumentListener interface
//
#Override
public void changedUpdate(DocumentEvent e)
{
documentChanged();
}
#Override
public void insertUpdate(DocumentEvent e)
{
documentChanged();
}
#Override
public void removeUpdate(DocumentEvent e)
{
documentChanged();
}
/*
* A document change may affect the number of displayed lines of text.
* Therefore the lines numbers will also change.
*/
private void documentChanged()
{
// View of the component has not been updated at the time
// the DocumentEvent is fired
SwingUtilities.invokeLater(() ->
{
try
{
int endPos = component.getDocument().getLength();
Rectangle rect = component.modelToView(endPos);
if (rect != null && rect.y != lastHeight)
{
setPreferredWidth();
repaint();
lastHeight = rect.y;
}
} catch (BadLocationException ex)
{ /* nothing to do */ }
});
}
//
// Implement PropertyChangeListener interface
//
#Override
public void propertyChange(PropertyChangeEvent evt)
{
if (evt.getNewValue() instanceof Font)
{
if (updateFont)
{
Font newFont = (Font) evt.getNewValue();
setFont(newFont);
lastDigits = 0;
setPreferredWidth();
} else
{
repaint();
}
}
}
}
Then line numbers can be added like this:
TextLineNumber textLineNumber = new TextLineNumber(sourceCodeArea);
sourceCodeAreaScrollPane.setRowHeaderView(textLineNumber);
I have a table called table and its filled with data, I also have a MessageFormat header I want to use as a header to print the JTable this is the MessageFormat:
MessageFormat header = new MessageFormat("Product: "
+ task.getProductName() + " Job: "
+ task.getJobNumber() + " Task: " + task.getTaskID()
);
I want to print 3 lines in the header, one for Product, Job and Task
the way I print this table is like so:
table.print(JTable.PrintMode.FIT_WIDTH, header, null);
I can't seem to figure out how to print the header in 3 seperate lines, I tried using the \n to make a new line but that doesn't seem to work.
It's gonna be long answer (code wise) because the only solution I found was to implement a custom Printable. Of course I didn't write the following code myself, I mostly copied the code I extracted from the jdk sources and made some adjustments.
Here we are:
This is the way you said you invoke the print method:
DefaultTableModel dtm = new DefaultTableModel(new String[] { "Column 1" }, 1);
JTable table = new JTable(dtm) {
#Override
public Printable getPrintable(PrintMode printMode, MessageFormat headerFormat, MessageFormat footerFormat) {
return new TablePrintable(this, printMode, headerFormat, footerFormat);
}
};
where TablePrintable is the following class (sorry for not being concise here):
static class TablePrintable implements Printable {
private final JTable table;
private final JTableHeader header;
private final TableColumnModel colModel;
private final int totalColWidth;
private final JTable.PrintMode printMode;
private final MessageFormat headerFormat;
private final MessageFormat footerFormat;
private int last = -1;
private int row = 0;
private int col = 0;
private final Rectangle clip = new Rectangle(0, 0, 0, 0);
private final Rectangle hclip = new Rectangle(0, 0, 0, 0);
private final Rectangle tempRect = new Rectangle(0, 0, 0, 0);
private static final int H_F_SPACE = 8;
private static final float HEADER_FONT_SIZE = 18.0f;
private static final float FOOTER_FONT_SIZE = 12.0f;
private final Font headerFont;
private final Font footerFont;
public TablePrintable(JTable table, JTable.PrintMode printMode, MessageFormat headerFormat,
MessageFormat footerFormat) {
this.table = table;
header = table.getTableHeader();
colModel = table.getColumnModel();
totalColWidth = colModel.getTotalColumnWidth();
if (header != null) {
// the header clip height can be set once since it's unchanging
hclip.height = header.getHeight();
}
this.printMode = printMode;
this.headerFormat = headerFormat;
this.footerFormat = footerFormat;
// derive the header and footer font from the table's font
headerFont = table.getFont().deriveFont(Font.BOLD, HEADER_FONT_SIZE);
footerFont = table.getFont().deriveFont(Font.PLAIN, FOOTER_FONT_SIZE);
}
#Override
public int print(Graphics graphics, PageFormat pageFormat, int pageIndex) throws PrinterException {
// for easy access to these values
final int imgWidth = (int) pageFormat.getImageableWidth();
final int imgHeight = (int) pageFormat.getImageableHeight();
if (imgWidth <= 0) {
throw new PrinterException("Width of printable area is too small.");
}
// to pass the page number when formatting the header and footer
// text
Object[] pageNumber = new Object[] { Integer.valueOf(pageIndex + 1) };
// fetch the formatted header text, if any
String headerText = null;
if (headerFormat != null) {
headerText = headerFormat.format(pageNumber);
}
// fetch the formatted footer text, if any
String footerText = null;
if (footerFormat != null) {
footerText = footerFormat.format(pageNumber);
}
// to store the bounds of the header and footer text
Rectangle2D hRect = null;
Rectangle2D fRect = null;
// the amount of vertical space needed for the header and footer
// text
int headerTextSpace = 0;
int footerTextSpace = 0;
// the amount of vertical space available for printing the table
int availableSpace = imgHeight;
// if there's header text, find out how much space is needed for it
// and subtract that from the available space
if (headerText != null) {
graphics.setFont(headerFont);
int nbLines = headerText.split("\n").length;
hRect = graphics.getFontMetrics().getStringBounds(headerText, graphics);
hRect = new Rectangle2D.Double(hRect.getX(), Math.abs(hRect.getY()), hRect.getWidth(),
hRect.getHeight() * nbLines);
headerTextSpace = (int) Math.ceil(hRect.getHeight() * nbLines);
availableSpace -= headerTextSpace + H_F_SPACE;
}
// if there's footer text, find out how much space is needed for it
// and subtract that from the available space
if (footerText != null) {
graphics.setFont(footerFont);
fRect = graphics.getFontMetrics().getStringBounds(footerText, graphics);
footerTextSpace = (int) Math.ceil(fRect.getHeight());
availableSpace -= footerTextSpace + H_F_SPACE;
}
if (availableSpace <= 0) {
throw new PrinterException("Height of printable area is too small.");
}
// depending on the print mode, we may need a scale factor to
// fit the table's entire width on the page
double sf = 1.0D;
if (printMode == JTable.PrintMode.FIT_WIDTH && totalColWidth > imgWidth) {
// if not, we would have thrown an acception previously
assert imgWidth > 0;
// it must be, according to the if-condition, since imgWidth > 0
assert totalColWidth > 1;
sf = (double) imgWidth / (double) totalColWidth;
}
// dictated by the previous two assertions
assert sf > 0;
// This is in a loop for two reasons:
// First, it allows us to catch up in case we're called starting
// with a non-zero pageIndex. Second, we know that we can be called
// for the same page multiple times. The condition of this while
// loop acts as a check, ensuring that we don't attempt to do the
// calculations again when we are called subsequent times for the
// same page.
while (last < pageIndex) {
// if we are finished all columns in all rows
if (row >= table.getRowCount() && col == 0) {
return NO_SUCH_PAGE;
}
// rather than multiplying every row and column by the scale
// factor
// in findNextClip, just pass a width and height that have
// already
// been divided by it
int scaledWidth = (int) (imgWidth / sf);
int scaledHeight = (int) ((availableSpace - hclip.height) / sf);
// calculate the area of the table to be printed for this page
findNextClip(scaledWidth, scaledHeight);
last++;
}
// create a copy of the graphics so we don't affect the one given to
// us
Graphics2D g2d = (Graphics2D) graphics.create();
// translate into the co-ordinate system of the pageFormat
g2d.translate(pageFormat.getImageableX(), pageFormat.getImageableY());
// to save and store the transform
AffineTransform oldTrans;
// if there's footer text, print it at the bottom of the imageable
// area
if (footerText != null) {
oldTrans = g2d.getTransform();
g2d.translate(0, imgHeight - footerTextSpace);
String[] lines = footerText.split("\n");
printText(g2d, lines, fRect, footerFont, imgWidth);
g2d.setTransform(oldTrans);
}
// if there's header text, print it at the top of the imageable area
// and then translate downwards
if (headerText != null) {
String[] lines = headerText.split("\n");
printText(g2d, lines, hRect, headerFont, imgWidth);
g2d.translate(0, headerTextSpace + H_F_SPACE);
}
// constrain the table output to the available space
tempRect.x = 0;
tempRect.y = 0;
tempRect.width = imgWidth;
tempRect.height = availableSpace;
g2d.clip(tempRect);
// if we have a scale factor, scale the graphics object to fit
// the entire width
if (sf != 1.0D) {
g2d.scale(sf, sf);
// otherwise, ensure that the current portion of the table is
// centered horizontally
} else {
int diff = (imgWidth - clip.width) / 2;
g2d.translate(diff, 0);
}
// store the old transform and clip for later restoration
oldTrans = g2d.getTransform();
Shape oldClip = g2d.getClip();
// if there's a table header, print the current section and
// then translate downwards
if (header != null) {
hclip.x = clip.x;
hclip.width = clip.width;
g2d.translate(-hclip.x, 0);
g2d.clip(hclip);
header.print(g2d);
// restore the original transform and clip
g2d.setTransform(oldTrans);
g2d.setClip(oldClip);
// translate downwards
g2d.translate(0, hclip.height);
}
// print the current section of the table
g2d.translate(-clip.x, -clip.y);
g2d.clip(clip);
table.print(g2d);
// restore the original transform and clip
g2d.setTransform(oldTrans);
g2d.setClip(oldClip);
// draw a box around the table
g2d.setColor(Color.BLACK);
g2d.drawRect(0, 0, clip.width, hclip.height + clip.height);
// dispose the graphics copy
g2d.dispose();
return PAGE_EXISTS;
}
private void printText(Graphics2D g2d, String[] lines, Rectangle2D rect, Font font, int imgWidth) {
g2d.setColor(Color.BLACK);
g2d.setFont(font);
for (int i = 0; i < lines.length; i++) {
int tx;
// if the text is small enough to fit, center it
if (rect.getWidth() < imgWidth) {
tx = (int) (imgWidth / 2 - g2d.getFontMetrics().getStringBounds(lines[i], g2d).getWidth() / 2);
// otherwise, if the table is LTR, ensure the left side of
// the text shows; the right can be clipped
} else if (table.getComponentOrientation().isLeftToRight()) {
tx = 0;
// otherwise, ensure the right side of the text shows
} else {
tx = -(int) (Math.ceil(rect.getWidth()) - imgWidth);
}
int ty = (int) Math.ceil(Math.abs(rect.getY() + i * rect.getHeight() / lines.length));
g2d.drawString(lines[i], tx, ty);
}
}
private void findNextClip(int pw, int ph) {
final boolean ltr = table.getComponentOrientation().isLeftToRight();
// if we're ready to start a new set of rows
if (col == 0) {
if (ltr) {
// adjust clip to the left of the first column
clip.x = 0;
} else {
// adjust clip to the right of the first column
clip.x = totalColWidth;
}
// adjust clip to the top of the next set of rows
clip.y += clip.height;
// adjust clip width and height to be zero
clip.width = 0;
clip.height = 0;
// fit as many rows as possible, and at least one
int rowCount = table.getRowCount();
int rowHeight = table.getRowHeight(row);
do {
clip.height += rowHeight;
if (++row >= rowCount) {
break;
}
rowHeight = table.getRowHeight(row);
} while (clip.height + rowHeight <= ph);
}
// we can short-circuit for JTable.PrintMode.FIT_WIDTH since
// we'll always fit all columns on the page
if (printMode == JTable.PrintMode.FIT_WIDTH) {
clip.x = 0;
clip.width = totalColWidth;
return;
}
if (ltr) {
// adjust clip to the left of the next set of columns
clip.x += clip.width;
}
// adjust clip width to be zero
clip.width = 0;
// fit as many columns as possible, and at least one
int colCount = table.getColumnCount();
int colWidth = colModel.getColumn(col).getWidth();
do {
clip.width += colWidth;
if (!ltr) {
clip.x -= colWidth;
}
if (++col >= colCount) {
// reset col to 0 to indicate we're finished all columns
col = 0;
break;
}
colWidth = colModel.getColumn(col).getWidth();
} while (clip.width + colWidth <= pw);
}
}
And here is the result (I hope that's what you expect):
You could try
StringBuilder builder = new StringBuilder();
builder.append("Product: ");
builder.append(task.getProductName());
builder.append(System.getProperty("line.separator"));
builder.append("Job: ");
builder.append(task.getJobNumber());
builder.append(System.getProperty("line.separator"));
builder.append("Task: ");
builder.append(task.getTaskID();
MessageFormat header = new MessageFormat(builder.toString());
If this doesn't work, then you're going to have to set up your own printer job, and layout the header precisely as you want it.
If you use the getPrintable method instead without adding a header/footer text, you can then include/decorate the returned Printable in one where you have more control over the header, and where you can specify multi-line headers. See the javadoc of that method which mentions
It is entirely valid for this Printable to be wrapped inside another in order to create complex reports and documents. You may even request that different pages be rendered into different sized printable areas. The implementation must be prepared to handle this (possibly by doing its layout calculations on the fly). However, providing different heights to each page will likely not work well with PrintMode.NORMAL when it has to spread columns across pages.
I have not enough experience with Printables to help you further on how to actually do this
Basically, the answer of #aymeric is correct: there's no way around a custom printable implementation. A way to do it with slightly less c&p is to have a custom implementation that
takes over header/footer printing
delegates to table printing itself to the default printable
The trick in that approach is to fool the delegate tablePrintable into believing that the page is smaller than it actually is, with a custom pageFormat
more details (and code)
I've utilized two MessageFormat arrays as a neat solution to the problem. You'll find below a printout of the end result:
The code is outlined below:
try
{
PrinterJob job = PrinterJob.getPrinterJob();
MessageFormat[] header = new MessageFormat[6];
// Assign the arrays with 6 String values for the headers
header[0] = new MessageFormat("");
header[1] = new MessageFormat(theExamSelection);
header[2] = new MessageFormat("");
header[3] = new MessageFormat("Scrud 60 - Grade Returns - Random Sample");
header[4] = new MessageFormat("");
header[5] = new MessageFormat(theSubjectSelection+" - "+theLevelSelection+" - "+thePaperSelection);
MessageFormat[] footer = new MessageFormat[4];
// Assign the 4 Strings to the footer array
footer[0] = new MessageFormat("Assistant Examiner Signature:______________ Date:___ /___ /_____ ");
footer[1] = new MessageFormat("");
footer[2] = new MessageFormat("");
footer[3] = new MessageFormat("Advising Examiner Signature:______________ Date:___ /___ /_____ ");
//here you place the JTable to print
// in this case its called randomSample_gradeBreakdown_jTable
// along with the header and footer arrays
job.setPrintable(new PrintTableMultiLine(randomSample_gradeBreakdown_jTable, JTable.PrintMode.FIT_WIDTH, header, footer ));
job.print();
}
catch (java.awt.print.PrinterException e)
{
System.err.format("Cannot print %s%n", e.getMessage());
JOptionPane.showMessageDialog(this,
"Check that your printer is working correctly","PRINT ERROR",JOptionPane.ERROR_MESSAGE
);
}
How do I create an in-memory fully transparent SWT image and draw a black line on it with antialias enabled?
I expect the result to include only black color and alpha values ranging from 0 to 255 due to antialias...
I googled and tried everything that I could... is this possible at all?
This is how I did and it works:
Image src = new Image(null, 16, 16);
ImageData imageData = src.getImageData();
imageData.transparentPixel = imageData.getPixel(0, 0);
src.dispose();
Image icon = new Image(null, imageData);
//draw on the icon with gc
I was able to make this work, although it feels a bit hacky:
Display display = Display.getDefault();
int width = 10;
int height = 10;
Image canvas = new Image(display, width, height);
GC gc = new GC(canvas);
gc.setAntialias(SWT.ON);
// This sets the alpha on the entire canvas to transparent
gc.setAlpha(0);
gc.fillRectangle(0, 0, width, height);
// Reset our alpha and draw a line
gc.setAlpha(255);
gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
gc.drawLine(0, 0, width, height);
// We're done with the GC, so dispose of it
gc.dispose();
ImageData canvasData = canvas.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(canvasData);
// And get rid of the canvas
canvas.dispose();
After this, finalImage can be drawn into a GC with drawImage and the transparent parts will be respected.
I made it by allocating an ImageData, making it transparent then creating the Image from the data :
static Image createTransparentImage(Display display, int width, int height) {
// allocate an image data
ImageData imData = new ImageData(width, height, 24, new PaletteData(0xff0000,0x00ff00, 0x0000ff));
imData.setAlpha(0, 0, 0); // just to force alpha array allocation with the right size
Arrays.fill(imData.alphaData, (byte) 0); // set whole image as transparent
// Initialize image from transparent image data
return new Image(display, imData);
}
To scale with transparency, I've found that I have to manually set the alpha byte array as shown below. So the alpha ends up with nearest-neighbor anti aliasing.
public static Image scaleImage(Device device, Image orig, int scaledWidth, int scaledHeight) {
Rectangle origBounds = orig.getBounds();
if (origBounds.width == scaledWidth && origBounds.height == scaledHeight) {
return orig;
}
ImageData origData = orig.getImageData();
ImageData imData = new ImageData(scaledWidth, scaledHeight, origData.depth, origData.palette);
if (origData.alphaData != null) {
imData.alphaData = new byte[imData.width * imData.height];
for (int row = 0; row < imData.height; row++) {
for (int col = 0; col < imData.width; col++) {
int origRow = row * origData.height / imData.height;
int origCol = col * origData.width / imData.width;
byte origAlpha = origData.alphaData[origRow * origData.width + origCol];
imData.alphaData[row * imData.width + col] = origAlpha;
}
}
}
final Image scaled = new Image(device, imData);
GC gc = new GC(scaled);
gc.setAntialias(SWT.ON);
gc.setInterpolation(SWT.HIGH);
gc.setBackground(device.getSystemColor(SWT.COLOR_WHITE));
gc.fillRectangle(0, 0, scaledWidth, scaledHeight);
gc.drawImage(orig, 0, 0, origBounds.width, origBounds.height, 0, 0, scaledWidth, scaledHeight);
gc.dispose();
return scaled;
}