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
);
}
Related
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;
}
In java swing JTable, I want to print multiple header with new lines from MessageFormat.
MessageFormat header = new MessageFormat("Products Details" + '\n' + "xyz suppliers");
MessageFormat footer = new MessageFormat("Page{1,number,integer}");
tbl_country.print(JTable.PrintMode.FIT_WIDTH, header, footer);
I had try lot of codes but new lines header does not work.
Finally i found answer to print multiple header with new line of JTable data as below:-
My TablePrintable.java
import javax.swing.table.*;
import java.awt.*;
import java.awt.print.*;
import java.awt.geom.*;
import java.text.MessageFormat;
import javax.swing.JTable;
/**
* An implementation of <code>Printable</code> for printing
* <code>JTable</code>s.
* <p>
* This implementation spreads table rows naturally in sequence across multiple
* pages, fitting as many rows as possible per page. The distribution of
* columns, on the other hand, is controlled by a printing mode parameter passed
* to the constructor. When <code>JTable.PrintMode.NORMAL</code> is used, the
* implementation handles columns in a similar manner to how it handles rows,
* spreading them across multiple pages (in an order consistent with the table's
* <code>ComponentOrientation</code>). When
* <code>JTable.PrintMode.FIT_WIDTH</code> is given, the implementation scales
* the output smaller if necessary, to ensure that all columns fit on the page.
* (Note that width and height are scaled equally, ensuring that the aspect
* ratio remains the same).
* <p>
* The portion of table printed on each page is headed by the appropriate
* section of the table's <code>JTableHeader</code>.
* <p>
* Header and footer text can be added to the output by providing
* <code>MessageFormat</code> instances to the constructor. The printing code
* requests Strings from the formats by calling their <code>format</code> method
* with a single parameter: an <code>Object</code> array containing a single
* element of type <code>Integer</code>, representing the current page number.
* <p>
* There are certain circumstances where this <code>Printable</code> cannot fit
* items appropriately, resulting in clipped output. These are:
* <ul>
* <li>In any mode, when the header or footer text is too wide to fit completely
* in the printable area. The implementation prints as much of the text as
* possible starting from the beginning, as determined by the table's
* <code>ComponentOrientation</code>.
* <li>In any mode, when a row is too tall to fit in the printable area. The
* upper most portion of the row is printed and no lower border is shown.
* <li>In <code>JTable.PrintMode.NORMAL</code> when a column is too wide to fit
* in the printable area. The center of the column is printed and no left and
* right borders are shown.
* </ul>
* <p>
* It is entirely valid for a developer to wrap this <code>Printable</code>
* inside another in order to create complex reports and documents. They may
* even request that different pages be rendered into different sized printable
* areas. The implementation was designed to handle this by performing most of
* its calculations on the fly. However, providing different sizes works best
* when <code>JTable.PrintMode.FIT_WIDTH</code> is used, or when only the
* printable width is changed between pages. This is because when it is printing
* a set of rows in <code>JTable.PrintMode.NORMAL</code> and the implementation
* determines a need to distribute columns across pages, it assumes that all of
* those rows will fit on each subsequent page needed to fit the columns.
* <p>
* It is the responsibility of the developer to ensure that the table is not
* modified in any way after this <code>Printable</code> is created (invalid
* modifications include changes in: size, renderers, or underlying data). The
* behavior of this <code>Printable</code> is undefined if the table is changed
* at any time after creation.
*
* #author Shannon Hickey
* #version 1.41 11/17/05
*/
class MyTablePrintable implements Printable {
/**
* The table to print.
*/
private JTable table;
/**
* For quick reference to the table's header.
*/
private JTableHeader header;
/**
* For quick reference to the table's column model.
*/
private TableColumnModel colModel;
/**
* To save multiple calculations of total column width.
*/
private int totalColWidth;
/**
* The printing mode of this printable.
*/
private JTable.PrintMode printMode;
/**
* Provides the header text for the table.
*/
private MessageFormat[] headerFormat;
/**
* Provides the footer text for the table.
*/
private MessageFormat[] footerFormat;
/**
* The most recent page index asked to print.
*/
private int last = -1;
/**
* The next row to print.
*/
private int row = 0;
/**
* The next column to print.
*/
private int col = 0;
/**
* Used to store an area of the table to be printed.
*/
private final Rectangle clip = new Rectangle(0, 0, 0, 0);
/**
* Used to store an area of the table's header to be printed.
*/
private final Rectangle hclip = new Rectangle(0, 0, 0, 0);
/**
* Saves the creation of multiple rectangles.
*/
private final Rectangle tempRect = new Rectangle(0, 0, 0, 0);
/**
* Vertical space to leave between table and header/footer text.
*/
private static final int H_F_SPACE = 8;
/**
* Font size for the header text.
*/
private static final float HEADER_FONT_SIZE = 15.0f;
/**
* Font size for the footer text.
*/
private static final float FOOTER_FONT_SIZE = 10.0f;
/**
* The font to use in rendering header text.
*/
private Font headerFont;
/**
* The font to use in rendering footer text.
*/
private Font footerFont;
/**
* Create a new <code>TablePrintable</code> for the given
* <code>JTable</code>. Header and footer text can be specified using the
* two <code>MessageFormat</code> parameters. When called upon to provide a
* String, each format is given the current page number.
*
* #param table the table to print
* #param printMode the printing mode for this printable
* #param headerFormat a <code>MessageFormat</code> specifying the text to
* be used in printing a header, or null for none
* #param footerFormat a <code>MessageFormat</code> specifying the text to
* be used in printing a footer, or null for none
* #throws IllegalArgumentException if passed an invalid print mode
*/
public MyTablePrintable(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);
}
/**
* Prints the specified page of the table into the given {#link Graphics}
* context, in the specified format.
*
* #param graphics the context into which the page is drawn
* #param pageFormat the size and orientation of the page being drawn
* #param pageIndex the zero based index of the page to be drawn
* #return PAGE_EXISTS if the page is rendered successfully, or NO_SUCH_PAGE
* if a non-existent page index is specified
* #throws PrinterException if an error causes printing to be aborted
*/
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[]{new Integer(pageIndex + 1)};
// fetch the formatted header text, if any
String[] headerText = null;
if (headerFormat != null) {
headerText = new String[headerFormat.length];
for (int i = 0; i < headerFormat.length; i++) {
headerText[i] = headerFormat[i].format(pageNumber);
}
}
// fetch the formatted footer text, if any
String[] footerText = null;
if (footerFormat != null) {
footerText = new String[footerFormat.length];
for (int i = 0; i < footerFormat.length; i++) {
footerText[i] = footerFormat[i].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 = 15;
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);
hRect = new Rectangle2D[headerText.length];
for (int i = 0; i < headerText.length; i++) {
hRect[i] = graphics.getFontMetrics().getStringBounds(headerText[i], graphics);
headerTextSpace += (int) Math.ceil(hRect[i].getHeight());
}
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 = new Rectangle2D[footerText.length];
for (int i = 0; i < footerText.length; i++) {
fRect[i] = graphics.getFontMetrics().getStringBounds(footerText[i], graphics);
footerTextSpace += (int) Math.ceil(fRect[i].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++;
}
// translate into the co-ordinate system of the pageFormat
Graphics2D g2d = (Graphics2D) graphics;
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);
for (int i = 0; i < footerText.length; i++) {
printText(g2d, footerText[i], fRect[i], footerFont, i, 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) {
for (int i = 0; i < headerText.length; i++) {
printText(g2d, headerText[i], hRect[i], headerFont, i, 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);
return PAGE_EXISTS;
}
/**
* A helper method that encapsulates common code for rendering the header
* and footer text.
*
* #param g2d the graphics to draw into
* #param text the text to draw, non null
* #param rect the bounding rectangle for this text, as calculated at the
* given font, non null
* #param font the font to draw the text in, non null
* #param imgWidth the width of the area to draw into
*/
private void printText(Graphics2D g2d,
String text,
Rectangle2D rect,
Font font,
int textIndex,
int imgWidth) {
//int tx = -(int)(Math.ceil(rect.getWidth()) - imgWidth); // for right allign
int tx = 0; // for left allign
int ty = textIndex * (int) Math.ceil(Math.abs(rect.getY()));
g2d.setColor(Color.BLACK);
g2d.setFont(font);
g2d.drawString(text, tx, ty);
}
/**
* Calculate the area of the table to be printed for the next page. This
* should only be called if there are rows and columns left to print.
*
* To avoid an infinite loop in printing, this will always put at least one
* cell on each page.
*
* #param pw the width of the area to print in
* #param ph the height of the area to print in
*/
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);
}
}
To print while print buttom clicked
String name = lblCompanyName.getText();
String address = lblAddress.getText();
try {
PrinterJob job = PrinterJob.getPrinterJob();
MessageFormat[] header = new MessageFormat[3];
header[0] = new MessageFormat("line 1");
header[1] = new MessageFormat(" " + name);
header[2] = new MessageFormat(" " + address);
MessageFormat[] footer = new MessageFormat[2];
footer[0] = new MessageFormat("");
footer[1] = new MessageFormat("-{1}-");
job.setPrintable(new MyTablePrintable(table, JTable.PrintMode.FIT_WIDTH, header, footer));
job.printDialog();
job.print();
} catch (Exception ex) {
}
result
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'm currently in the process of converting Java to JavaScript and need to change the colour of some images.
Right now each image is loaded within an Image class, an image looks like this:
It's a PNG which works as a character set, the data sent through is mapped to each character in the image.
The existing Java code looks like this:
class VDColorFilter extends RGBImageFilter
{
int fg;
int bg;
final int[] colors;
public VDColorFilter(final int fgc, final int bgc) {
super();
this.colors = new int[] { 0, 16711680, 65280, 16776960, 255, 16711935, 65535, 16777215 };
this.fg = fgc;
this.bg = bgc;
this.canFilterIndexColorModel = true;
}
public int filterRGB(final int x, final int y, int rgb) {
if (rgb == -1) {
rgb = (0xFF000000 | this.colors[this.bg]);
}
else if (rgb == -16777216) {
rgb = (0xFF000000 | this.colors[this.fg]);
}
return rgb;
}
}
I want to be able to do the same thing to my images, but in JavaScript. I don't have much experience with Java, so I'm unsure on how the filterRGB actually applies the RGB result, against the colors array.
Of course, this is only tinting the black of the image, not the white.
Are there any libraries out there which mimic this? If not, what is my best way of achieving the same result?
You can filter an image using getImageData() and putImageData(). This will require cross-origin resource sharing (CORS) to be fulfilled, e.g. the image comes from the same server as the page (a security mechanism in the browser).
If that part is OK, lets do an example using your image -
The best would be if your images had an alpha channel instead of white background. This would allow you to use composite operators to change the colors directly without having to parse the pixels.
You can do this two ways:
Punch out the background once and for all, then use composite operator (recommended)
Replace all black pixels with the color
With the first approach you only have to parse the pixels once. Every time you need to change the colors just use a composite operator (see demo 2 below).
Using Composite Operator
Here is a way to punch out the background first. We will be using a unsigned 32-bit buffer for this as this is faster than using a byte-array.
We can convert the byte-buffer by using the view's buffer and create a second view for it:
var data32 = new Uint32Array(idata.data.buffer);
See code below for details:
var img = new Image();
img.crossOrigin = "";
img.onload = punchOut;
img.src = "//i.imgur.com/8NWz72w.png";
function punchOut() {
var canvas = document.createElement("canvas"),
ctx = canvas.getContext("2d");
document.body.appendChild(this);
document.body.appendChild(canvas);
// set canvas size = image size
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
// draw in image
ctx.drawImage(this, 0, 0);
// get pixel data
var idata = ctx.getImageData(0, 0, canvas.width, canvas.height),
data32 = new Uint32Array(idata.data.buffer), // create a uint32 buffer
i = 0, len = data32.length;
while(i < len) {
if (data32[i] !== 0xff000000) data32[i] = 0; // if not black, set transparent
i++
}
ctx.putImageData(idata, 0, 0); // put pixels back on canvas
}
body {background:#aaa}
Now that we have a transparent image we can use composite modes to alter its colors. The mode we need to use is "source-atop":
var img = new Image();
img.crossOrigin = ""; img.onload = punchOut;
img.src = "//i.imgur.com/8NWz72w.png";
function punchOut() {
var canvas = document.querySelector("canvas"), ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0, 0);
var idata = ctx.getImageData(0, 0, canvas.width, canvas.height),
data32 = new Uint32Array(idata.data.buffer), i = 0, len = data32.length;
while(i < len) {if (data32[i] !== 0xff000000) data32[i] = 0; i++}
ctx.putImageData(idata, 0, 0);
// NEW PART --------------- (see previous demo for detail of the code above)
// alter color using composite mode
// This will replace existing non-transparent pixels with the next drawn object
ctx.globalCompositeOperation = "source-atop";
function setColor() {
for (var y = 0; y < 16; y++) {
for (var x = 0; x < 6; x++) {
var cw = (canvas.width - 1) / 6,
ch = (canvas.height - 1) / 16,
cx = cw * x,
cy = ch * y;
// set the desired color using fillStyle, here: using HSL just to make cycle
ctx.fillStyle = "hsl(" + (Math.random() * 360) + ", 100%, 80%)";
// fill the area with the new color, due to comp. mode only existing pixels
// will be changed
ctx.fillRect(cx+1, cy+1, cw-1, ch-1);
}
}
}
setInterval(setColor, 100);
// to reset comp. mode, use:
//ctx.globalCompositeOperation = "source-over";
}
body {background:#333}
<canvas></canvas>
And finally, use drawImage() to pick each letter based on mapping and cell calculations for each char (see for example the previous answer I gave you for drawImage usage).
Define a char map using a string
Find the letter using the map and indexOf()
Calculate the index of the map to x and y in the image
Use drawImage() to draw that letter to the x/y position in the output canvas
Random letters
var img = new Image();
img.crossOrigin = ""; img.onload = punchOut;
img.src = "http://i.imgur.com/8NWz72w.png";
function punchOut() {
var canvas = document.querySelector("canvas"), ctx = canvas.getContext("2d");
canvas.width = this.naturalWidth;
canvas.height = this.naturalHeight;
ctx.drawImage(this, 0, 0);
var idata = ctx.getImageData(0, 0, canvas.width, canvas.height),
data32 = new Uint32Array(idata.data.buffer), i = 0, len = data32.length;
while(i < len) {if (data32[i] !== 0xff000000) data32[i] = 0; i++}
ctx.putImageData(idata, 0, 0);
ctx.globalCompositeOperation = "source-atop";
function setColor() {
for (var y = 0; y < 16; y++) {
for (var x = 0; x < 6; x++) {
var cw = (canvas.width - 1) / 6,
ch = (canvas.height - 1) / 16,
cx = cw * x,
cy = ch * y;
ctx.fillStyle = "hsl(" + (Math.random() * 360) + ", 100%, 80%)";
ctx.fillRect(cx+1, cy+1, cw-1, ch-1);
}
}
}
setColor();
// NEW PART --------------- (see previous demo for detail of the code above)
var dcanvas = document.createElement("canvas"), xpos = 0;
ctx = dcanvas.getContext("2d");
document.body.appendChild(dcanvas);
for(var i = 0; i < 16; i++) {
var cw = (canvas.width - 1) / 6,
ch = (canvas.height - 1) / 16,
cx = cw * ((Math.random() * 6)|0), // random x
cy = ch * ((Math.random() * 16)|0); // random y
ctx.drawImage(canvas, cx+1, cy+1, cw-1, ch-1, xpos, 0, cw-1, ch-1);
xpos += 16;
}
}
body {background:#333}
<canvas></canvas>
I render text with a custom table cell renderer and I want to trim the text drawn if it would exceed the current cell width, switching to a "This text is to long..." kind of representation, where the three dots at the end never leaves the bounding box. My current algorithm is the following:
FontMetrics fm0 = g2.getFontMetrics();
int h = fm0.getHeight();
int ellw = fm0.stringWidth("\u2026");
int titleWidth = fm0.stringWidth(se.title);
if (titleWidth > getWidth()) {
for (int i = se.title.length() - 1; i > 0; i--) {
String tstr = se.title.substring(0, i);
int tstrw = fm0.stringWidth(tstr);
if (tstrw + ellw < getWidth()) {
g2.drawString(tstr, 2, h);
g2.drawString("\u2026", 2 + tstrw, h);
break;
}
}
} else {
g2.drawString(se.title, 2, h);
}
Is there a better way of determining how many characters should I drawString() before switching to the ... (u2026) character?
This is the default behaviour of the default renderer for a table so I'm not sure I understand the reason for doing this.
But I've created a renderer for dots on the left ("... text is too long") which shows a more complete way to do what you want since it takes into account a possible Border on the renderer and the intercell spacing of the renderer. Check out the LeftDotRenderer.
I can't think of another way to achieve the desired effect, but you should be able to replace the 2 drawString() calls with one. It's been a while since I've written Java but I think the following should work.
BTW, I changed the variable names so I can read the code. :p
FontMetrics metrics = g2.getFontMetrics();
int fontHeight = metrics.getHeight();
int titleWidth = metrics.stringWidth(se.title);
if (titleWidth > getWidth()) {
String titleString;
for (int i = se.title.length() - 1; i > 0; i --) {
titleString = se.title.substring(0, i) + "\u2026";
titleWidth = metrics.stringWidth(titleString);
if (titleWidth < getWidth()) {
g2.drawString(titleString, 2, fontHeight);
break;
}
}
} else {
g2.drawString(se.title, 2, fontHeight);
}