I'm not a big fan of asking this kind of questions but well, it's been 3 whole days now trying to solve this bug on my code.
I know it's a logic problem and I know how to solve it in my mind, but when it comes to translate my ideas into code I just can't make it work as I want.
I'm working on a contract endorsement (a modification of a contract) which compares the data from 2 tables and if any of them has changed then it paints only that information.
Sometimes the information that changes such as conditions grows up or lowers in length, here's where the problem lies.
I made an algorithm which gets the minimum Y coord once it finished painting the information but when I change page Y coord must be reseted to 700f
and start painting again from there.
The code I'm using was first made by another programmer who's not here anymore, I don't know how to do an MCVE with PDF's.
But here are the methods I think may help you helping me.
An example of how I compare and paint information on the PDF:
sOffA
and sOffE
are objects from a Class called SubscriptionOffer
and A
stands for Agreement
while E
for Endorsement
. Endorsement is a copy of the table Agreement
on MySQL but with the information modified. Probably this is irrelevant information, probably not.
if (!nullOrEmpty(sOffA.getGeneralConditions()) &&
!nullOrEmpty(sOffE.getGeneralConditions())) {
if (!getStringValue(sOffA.getGeneralConditions()).
equals(getStringValue(sOffE.getGeneralConditions()))) {
minYs[0] = pdf.rText(LEFT_MARGIN, y, 10,
constants.generalConditions(),
getStringValue(sOffA.
getGeneralConditions()));
minYs[1] = pdf.rText(HALF_PAGE, y, 10,
constants.generalConditions(),
getStringValue(sOffE.
getGeneralConditions()));
y = checkY(pdf, minYs);
}
} else if (nullOrEmpty(sOffA.getGeneralConditions()) &&
!nullOrEmpty(sOffE.getGeneralConditions())) {
minYs[0] = pdf.rText(LEFT_MARGIN, y, 10, "", "");
minYs[1] = pdf.rText(HALF_PAGE, y, 10,
constants.generalConditions(),
getStringValue(sOffE.
getGeneralConditions()));
y = checkY(pdf, minYs);
} else if (!nullOrEmpty(sOffA.getGeneralConditions()) &&
nullOrEmpty(sOffE.getGeneralConditions())) {
minYs[0] = pdf.rText(LEFT_MARGIN, y, 10,
constants.generalConditions(),
getStringValue(sOffA.
getGeneralConditions()));
minYs[1] = pdf.rText(HALF_PAGE, y, 10, "", "");
y = checkY(pdf, minYs);
}
The array I use is this one:
private float[] minYs = new float[] {700, 700, 700, 700, 700, 700, 700,
700, 700};
This is the method checkY
, it checks which Y on the array (as seen above) is the lowest one, then it restarts the whole array back to 700f for all elements. And after that it checks if the space below the lowest Y coord is enough to paint next item.
private float checkY(PdfRenderingEndorsement pdf, float... ys) throws Exception {
float y2 = getMinY(pdf, ys);
for (int i = 0; i < ys.length; i++) {
ys[i] = 700;
}
y2 = pdf.checkContentStream(y2, 5, 10);
return y2;
}
This method is where I guess my logic problem is, and as you can see I've tried diferent things, the original algorithm was only the for-each
part then I've tried different things w/o success.
private float getMinY(PdfRenderingEndorsement pdf, float... ys) {
float result = 700f;
float lowest1 = 0, lowest2 = 0;
for (int i = 0; i < ys.length; i++) {
for (int j = 0; j < ys.length; j++) {
if (ys[j] > ys[i]) {
float aux = ys[i];
ys[i] = ys[j];
ys[j] = aux;
//lowest1 = ys[i];
//lowest2 = ys[j];
}
}
}
if (ys.length > 1) {
lowest1 = ys[0];
lowest2 = ys[1];
}
LOGGER.trace("lowest1: " + lowest1);
LOGGER.trace("lowest2: " + lowest2);
LOGGER.trace("newPage " + pdf.getNewPage());
/*if(pdf.getNewPage()) {
return lowest1 > lowest2 ? lowest2 : lowest1;
}
*/
/*for (float y : ys) {
if (y < result) {
result = y;
}
}*/
return lowest1 > lowest2 && pdf.getNewPage() ? lowest1 : lowest2;
//return lowest1;
}
The methods where I get String value of the objects and check if they're null or empty:
private boolean nullOrEmpty(String s) {
return s == null || s.isEmpty();
}
private String getStringValue(Object o) {
if (o == null) {
return "";
}
return getStringValue(o, null);
}
private String getStringValue(Object o, Class< ? extends Unit> clazz) {
if (o instanceof Boolean) {
Boolean bd = (Boolean) o;
if (bd) {
return constants.getString("dbeditorYes");
} else {
return constants.getString("dbeditorNo");
}
} else if (o instanceof Date) {
Date date = (Date) o;
DateFormat df = new SimpleDateFormat(DateConstants.DATE_FORMAT);
return df.format(date);
} else if (o instanceof Enum) {
Enum en = (Enum) o;
return constants.enumMap().get(en.toString());
} else if (o instanceof Integer) {
Integer integer = (Integer) o;
if (clazz == null) {
return String.valueOf(integer);
} else {
Unit entry = unitSvc.get(clazz, integer);
String[] params = entry.toString().split("\\|");
String label = params.length == 1 ? params[0] : params[1];
return label;
}
} else if (o instanceof BigDecimal) {
BigDecimal bd = (BigDecimal) o;
DecimalFormat df = new DecimalFormat("#,##0");
df = new DecimalFormat("#,##0.00");
return df.format(bd);
} else if (o instanceof Float) {
Float bd = (Float) o;
DecimalFormat df = new DecimalFormat("#,##0");
df = new DecimalFormat("#,##0.00");
return df.format(bd);
} else if (o instanceof String) {
String td = (String) o;
return td;
}
return "";
}
All of the above methods correspond to PdfEndorsement
class.
And this is PdfRenderingEndorsement
class which is the class where actually the data is painted (this is the complete class since all methods are used):
public class PdfRenderingEndorsement {
private static final Logger LOGGER = Logger.
getLogger(PdfRenderingEndorsement.class);
private static final float BOTTOM_MARGIN = 60;
private static final int DESC_WIDTH = 269; //For description fields
private static final int FIELD_WIDTH = 70;
private static final int FIELD_WIDTH2 = 60;
private static final int FIELD_WIDTH3 = 60;
private static final int FIELD1 = 112;
private static final int VALUE1 = 112;
private static final int FIELD2 = 80;
private static final int VALUE2 = 80;
private static final int VALUE_WIDTH = 80;
private static final int VALUE_WIDTH2 = 60;
private static final int VALUE_WIDTH3 = 80;
private static final int HALF_WIDTH = 325;
private static final int TEXT_WIDTH = 410; //For text fields
private final RwaConstants constants = ConstantsGetter.getInstance();
private final PDDocument doc;
private final String logoPath;
private final String[] header;
private int count = 0;
private boolean newPage;
private PDPageContentStream content;
/**
* Empty constructor. Used only to initialize the rendering class and call
* it's methods.
*/
public PdfRenderingEndorsement(PDDocument doc, String logoPath,
String[] header) {
this.doc = doc;
this.logoPath = logoPath;
this.header = header;
}
public float checkContentStream2(float y, int lines, int space)
throws Exception {
float newY = checkYCoord2(y, lines, space);
if (newY == 700) {
if (content != null) {
content.close();
}
File file = new File(logoPath);
PDJpeg logoImg = new PDJpeg(doc, new FileInputStream(file));
PDPage page = new PDPage(PDPage.PAGE_SIZE_LETTER);
doc.addPage(page);
content = new PDPageContentStream(doc, page);
content.drawImage(logoImg, 50, 720);
rHeader();
}
return newY;
}
private float checkYCoord2(float y, int lines, int space) {
float newY = y;
for (int i = 0; i < lines; i++) {
if ((newY - space) <= BOTTOM_MARGIN) {
newY = 700f;
return newY;
} else {
newY = newY - space;
}
}
return y;
}
public boolean getNewPage() {
return newPage;
}
public float checkContentStream(float y) throws Exception {
float newY = checkYCoord(y, 1, 10);
if (newY == 700) {
if (content != null) {
content.close();
}
File file = new File(logoPath);
PDJpeg logoImg = new PDJpeg(doc, new FileInputStream(file));
PDPage page = new PDPage(PDPage.PAGE_SIZE_LETTER);
doc.addPage(page);
content = new PDPageContentStream(doc, page);
content.drawImage(logoImg, 50, 720);
rHeader();
}
return newY;
}
public float checkYCoord(float y, int lines, int space) {
float newY = y;
for (int i = 0; i < lines; i++) {
if ((newY - space) <= BOTTOM_MARGIN) {
newY = 700f;
return newY;
} else {
newY = newY - space;
}
}
return y;
}
public float checkContentStream(float y, int lines, int space)
throws Exception {
float newY = checkYCoord(y, lines, space);
if (newY == 700) {
if (content != null) {
content.close();
}
File file = new File(logoPath);
PDJpeg logoImg = new PDJpeg(doc, new FileInputStream(file));
PDPage page = new PDPage(PDPage.PAGE_SIZE_LETTER);
doc.addPage(page);
content = new PDPageContentStream(doc, page);
content.drawImage(logoImg, 50, 720);
rHeader();
}
return newY;
}
public void closeContentStream() throws Exception {
if (content != null) {
content.close();
}
}
/**
* Renders the header for slip documents.
*/
public void rHeader() throws Exception {
float y = 760f;
content.setLineWidth(.5f);
content.setFont(PDType1Font.TIMES_ROMAN, 9);
content.setNonStrokingColor(Color.GRAY);
content.drawLine(50, 710, 562, 710);
y = rText(150, y + 19, 10, constants.endorsement(), null,
TEXT_WIDTH, 0);
y = rText(150, y + 9, 10, header[0], null, TEXT_WIDTH, 0);
y = rText(150, y + 9, 10, header[1], null, TEXT_WIDTH, 0);
y = rText(150, y + 9, 10, header[2], null, TEXT_WIDTH, 0);
y = rText(150, y + 9, 10, header[3], null, TEXT_WIDTH, 0);
content.setNonStrokingColor(Color.BLACK);
content.setFont(PDType1Font.TIMES_ROMAN, 9);
}
public float rText(float x, float y, int space, String labelField,
String value)
throws Exception {
return rText(x, y, space, labelField, value, FIELD_WIDTH,
HALF_WIDTH - 2 * FIELD_WIDTH - 10);
}
public float rTextLR(float x, float y, int space, String labelField,
String value)
throws Exception {
return rText(x, y, space, labelField, value, 0,
HALF_WIDTH - 2 * FIELD_WIDTH - 10);
}
public float rText(float x, float y, int space, String labelField,
String value, int fieldWidth)
throws Exception {
if (fieldWidth == 0) {
return rText(x, y, space, labelField, value, FIELD_WIDTH2,
VALUE_WIDTH2);
} else if (fieldWidth == 1) {
return rText(x, y, space, labelField, value, FIELD_WIDTH3,
VALUE_WIDTH3);
} else if (fieldWidth == 2) {
return rText(x, y, space, labelField, value, TEXT_WIDTH,
TEXT_WIDTH);
}
return y;
}
public float getFieldSize(int fs) {
switch(fs) {
case 1:
return (FIELD_WIDTH + VALUE_WIDTH);
case 2:
return (FIELD_WIDTH + DESC_WIDTH);
case 3:
return (FIELD_WIDTH + TEXT_WIDTH);
case 4:
return (HALF_WIDTH - FIELD_WIDTH);
case 5:
return (FIELD_WIDTH + TEXT_WIDTH);
case 6:
return (FIELD_WIDTH + TEXT_WIDTH);
case 7:
return (FIELD1 + 19) / 2;
case 8:
return (FIELD2 + 19) / 2;
default:
return 0;
}
}
public void paintLinesH(float y) throws Exception {
content.drawLine(49, y - 6, 327, y - 6);
content.drawLine(335, y - 6, 563, y - 6);
}
public void paintLinesV(float x, float yMax, float yMin)
throws Exception {
content.drawLine(x - 1, yMax - 6, x - 1, yMin - 6);
}
public float rText(float x, float y, int space, String labelField,
String value, int fieldWidth, int valueWidth)
throws Exception {
PDFont font = PDType1Font.TIMES_BOLD;
content.setFont(font, 9);
float y1 = 0f;
float y2 = 0f;
if (value == null) {
return rText(labelField, fieldWidth, x, y - 19, space, font, false);
} else {
if (labelField == null) {
font = PDType1Font.TIMES_ROMAN;
content.setFont(font, 9);
return rText(value, valueWidth, x, y - 19, space, font, true);
} else {
y1 = rText(labelField, fieldWidth, x, y - 30, space, font,
false);
font = PDType1Font.TIMES_ROMAN;
content.setFont(font, 9);
float y3 = y;
y2 = rText(value, valueWidth, x + fieldWidth + 10, y - 30,
space, font, true);
if (y3 < y2) {
return y2;
} else {
if (y1 >= y2) {
return y2;
} else {
return y1;
}
}
}
}
}
private ArrayList<String> getRows(String text, int width, PDFont font)
throws Exception {
float textWidth = font.getStringWidth(text) / 1000f * 9f;
ArrayList<String> result = Lists.newArrayList();
if (textWidth < width) {
result.add(text);
return result;
}
float spaceWidth = font.getStringWidth(" ") / 1000f * 9f;
String[] paragraphs = text.split("\n|\r\n|\r");
for (String paragraph : paragraphs) {
float pWidth = font.getStringWidth(paragraph) / 1000f * 9f;
if (pWidth < width) {
result.add(paragraph);
continue;
}
float widthCount = 0f;
String[] words = paragraph.trim().split(" ");
StringBuilder sb = new StringBuilder();
for (int j = 0; j < words.length; j++) {
if (words[j].trim().length() == 0) {
continue;
}
float wWidth = font.getStringWidth(words[j]) / 1000f * 9f;
float totalWidth = widthCount + wWidth + spaceWidth;
if (totalWidth < width + spaceWidth) {
sb.append(words[j]);
sb.append(" ");
widthCount = totalWidth;
} else {
result.add(sb.toString().trim());
sb = new StringBuilder();
sb.append(words[j]);
sb.append(" ");
widthCount = totalWidth - widthCount;
}
}
result.add(sb.toString().trim());
}
return result;
}
private float rText(String text, int width, float x, float y, int space,
PDFont font, boolean isValue) throws Exception {
float newY = y;
int rowHeight = 0;
newPage = false;
ArrayList<String> rowList = getRows(text, width, font);
if (isValue) {
for (String row : rowList) {
if (rowHeight >= 10) {
newY = checkContentStream(newY - 10);
newY = newY == 700 ? 680 : newY;
if (newY <= 700 && !newPage) {
newPage = true;
}
rowHeight = newY == 680 ? 0 : rowHeight;
}
content.beginText();
content.moveTextPositionByAmount(x, newY);
content.drawString(row);
content.endText();
rowHeight = rowHeight + 10;
}
} else {
for (String row : rowList) {
content.beginText();
content.moveTextPositionByAmount(x, newY - rowHeight);
content.drawString(row);
content.endText();
rowHeight = rowHeight + 10;
}
newY -= (rowHeight - 10);
}
return newY;
}
}
This is a PDF Example of the output with the original (for-each
) method.
If you can't see the PDF let me know but here are also some Screenshots from it:
This is a PDF Example of the output with the non-commented code on getMinY
method.
And from this modification here's the output:
As you can see the 1st output "jumps" into the next page, because let's say the text on the right on "Texto de Poliza" ended on Y coord 680, 670 or something like that but on the left I painted an empty field ("")
which ended in 90 or 100 something near of those numbers.
Then it compares 100 < 670 ? Yes, then I take 50 to 100 and it's lower than my BOTTOM_MARGIN
(which is 60), so it closes actual page (which is now where the text ended but thinks it's on the page before it) and creates a new one.
I've asked a similar question almost a year before here, these Classes are almost a copy from those files but this bug on my logic comes only with these files because data is handled a bit different.
Well after that wall of text I hope someone could read it and actually give me a hand, maybe I'm missing an important part but can't find it atm.
Thanks in advance.
EDIT
After adding @mkl's answer to my methods I find that when no information changed in both sides, it creates a gap and it doesn't look good.
This is how I'm sending data to PdfRenderingEndorsementAlternative
:
for (String[] data: sOppData) {
//float y = renderer.getPreviousBandBase;
for (int i = 0; i < data.length - 2; i += 3) {
if (!nullOrEmpty(data[i + 1]) &&
!nullOrEmpty(data[i + 2])) {
if (!data[i + 1].equals(data[i + 2])) {
renderer.
render(new BandColumn(leftHalfPageField,
data[i], data[i + 1]),
new BandColumn(rightHalfPageField,
data[i], data[i + 2])
);
}
} else if (nullOrEmpty(data[i + 1]) &&
!nullOrEmpty(data[i + 2])) {
renderer.
render(new BandColumn(leftHalfPageField,
"", ""),
new BandColumn(rightHalfPageField,
data[i], data[i + 2])
);
} else if (!nullOrEmpty(data[i + 1]) &&
nullOrEmpty(data[i + 2])) {
renderer.
render(new BandColumn(leftHalfPageField,
data[i], data[i + 1]),
new BandColumn(rightHalfPageField,
"", "")
);
}
}
//float y2 = renderer.getPreviousBandBase();
/*if (y2 < y)
renderer.gap(20);
*/
renderer.gap(20);
}
Are the above commented validations correct or would I be going back to past methods that originated bugs? Should I add getPreviousBandBase()
method on PdfRenderingEndorsementAlternative or think in another way to do it directly on render()
method?
And this is how I get data that currently has the same info in both sides.
An example of this would be:
sOppA.getContractName()
and sOppE.getContractName()
are both "Hello World"
and as both are equal then it would leave a gap as seen on screenshot.
private ArrayList<String []> renderSubscriptionOpportunity(
SubscriptionOpp sOppA, SubscriptionOpp sOppE,
ArrayList<Location> lcA, ArrayList<Location> lcE)
throws Exception {
ArrayList <String[]> sOppData = new ArrayList<String[]>();
sOppData.add(new String[] {constants.currencyId(),
getStringValue(sOppA.getCurrencyId(),
Currency.class),
getStringValue(sOppE.getCurrencyId(),
Currency.class)});
sOppData.add(new String[] {constants.contractName(),
getStringValue(sOppA.getContractName()),
getStringValue(sOppE.getContractName())});
sOppData.add(new String[] {constants.mainActivityId(),
getStringValue(sOppA.getMainActivityId(),
MainActivity.class),
getStringValue(sOppE.getMainActivityId(),
MainActivity.class)});
//here add location table
if (lcA.size() > 1 && lcE.size() > 1) {
int lastIdA = 0;
int lastIdE = 0;
int size = lcA.size() >= lcE.size() ? lcA.size() : lcE.size();
LOGGER.trace("size: " + size + " lcA.size(): " + lcA.size() +
" lcE.size(): " + lcE.size());
for (int pos = 1; pos < lcA.size(); pos++) {
StringBuilder aSb = new StringBuilder();
StringBuilder eSb = new StringBuilder();
String valueA = "";
String valueE = "";
if (pos < lcA.size()) {
Country countryA = unitSvc.get(Country.class,
lcA.get(pos).getCountryId());
LOGGER.trace("Entro1");
if (countryA.getId() != lastIdA) {
aSb.append(countryA.getName());
lastIdA = countryA.getId();
} else {
aSb.append("");
}
} else {
aSb.append("");
}
if (pos < lcE.size()) {
Country countryE = unitSvc.get(Country.class,
lcE.get(pos).getCountryId());
LOGGER.trace("Entro2");
if (countryE.getId() != lastIdE) {
eSb.append(countryE.getName());
lastIdE = countryE.getId();
} else {
eSb.append("");
}
} else {
eSb.append("");
}
valueA = aSb.toString();
valueE = eSb.toString();
sOppData.add(new String[] {pos == 1 ? constants.countryId() :
"", valueA, valueE});
}
}
return sOppData;
}
As already hinted at in a comment (actually already in a comment to your former question) I think that the whole architecture of your rendering class needs an overhaul. Based on your
PdfRenderingEndorsement
I created the following classPdfRenderingEndorsementAlternative
which represents an alternative approach at that rendering:This class is based on the concept of bands, i.e. horizontal stripes of content, with any number of columns containing field names and / or field values.
It can be used like this:
As you see, the caller does not have to care anymore about y positions, everything is done in the renderer class. And the result:
I used the data from your first sample PDF as input, and this is the result:
As you see, no page jumps and no overlapping texts, either.