Skip to content

Commit 9f71769

Browse files
committed
[ESMO] fix selection rows not wrapping in PDF export
1 parent e1eec18 commit 9f71769

2 files changed

Lines changed: 491 additions & 158 deletions

File tree

knowage-export/src/main/java/it/eng/knowage/engine/api/export/dashboard/pdf/DashboardPdfExporter.java

Lines changed: 222 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -357,60 +357,91 @@ private void createSelectionsRows(BaseTable table, PDFont font, Map<String, Map<
357357
float baseBottomMargin = 20f;
358358

359359
PDPage page = table.getCurrentPage();
360+
float pageWidth = page.getMediaBox().getWidth();
360361
float pageHeight = page.getMediaBox().getHeight();
362+
float availableTextWidth = pageWidth - (2 * margin);
363+
364+
// Build wrapped lines first so we can paginate correctly
365+
final class SelectionLine {
366+
final String key;
367+
final float keyWidth;
368+
final java.util.List<String> wrappedValueLines;
369+
370+
SelectionLine(String key, float keyWidth, java.util.List<String> wrappedValueLines) {
371+
this.key = key;
372+
this.keyWidth = keyWidth;
373+
this.wrappedValueLines = wrappedValueLines;
374+
}
375+
}
376+
377+
java.util.List<SelectionLine> lines = new java.util.ArrayList<>();
361378

362-
// Count number of selections
363-
int selectionCount = 0;
364379
for (Map.Entry<String, Map<String, JSONArray>> datasetEntry : selections.entrySet()) {
365380
Map<String, JSONArray> selectionContent = datasetEntry.getValue();
366-
if (selectionContent != null && !selectionContent.isEmpty()) {
367-
selectionCount += selectionContent.size();
381+
if (selectionContent == null || selectionContent.isEmpty()) {
382+
continue;
383+
}
384+
385+
for (Map.Entry<String, JSONArray> columnEntry : selectionContent.entrySet()) {
386+
String key = columnEntry.getKey();
387+
key = key.replace("('", "").replace("')", "");
388+
389+
String valueStr = getValues(columnEntry).toString().replace("('", "").replace("')", "");
390+
391+
float keyWidth = font.getStringWidth(key) / 1000f * fontSize;
392+
float prefixWidth = keyWidth + (font.getStringWidth(": ") / 1000f * fontSize);
393+
float valueStartX = margin + prefixWidth;
394+
float valueAvailableWidth = Math.max(10f, (margin + availableTextWidth) - valueStartX);
395+
396+
java.util.List<String> wrappedValues = wrapText(font, fontSize, valueStr, valueAvailableWidth);
397+
if (wrappedValues.isEmpty()) {
398+
wrappedValues = java.util.Collections.singletonList("");
399+
}
400+
401+
lines.add(new SelectionLine(key, keyWidth, wrappedValues));
368402
}
369403
}
370404

371-
if (selectionCount == 0) return;
405+
if (lines.isEmpty()) {
406+
return;
407+
}
408+
409+
int totalLines = 0;
410+
for (SelectionLine sl : lines) {
411+
totalLines += Math.max(1, sl.wrappedValueLines.size());
412+
}
372413

373414
// Calculate where the table actually ends
374415
// Table starts at y=550 (from createBaseTable)
375416
float tableStartY = 550f;
376417
float tableHeight = 0f;
377418

378-
// Sum up all row heights to get total table height
379419
try {
380420
List<Row<PDPage>> rows = table.getRows();
381421
for (Row<PDPage> row : rows) {
382422
tableHeight += row.getHeight();
383423
}
384424
} catch (Exception e) {
385-
// If we can't get rows, use an estimate
386425
logger.warn("Could not calculate exact table bottom");
387426
tableHeight = 0;
388427
}
389428

390-
// Table ends at: startY - totalHeight
391429
float tableEndY = tableStartY - tableHeight;
392430

393-
// Calculate required height for all selections
394-
float requiredHeight = selectionCount * leading;
395-
396-
// Check if there's space below the table
397-
// Available space = tableEndY - baseBottomMargin
431+
float requiredHeight = totalLines * leading;
398432
float availableSpace = tableEndY - baseBottomMargin;
399433
boolean needsNewPage = availableSpace < requiredHeight;
400434

401435
PDPage targetPage;
402-
float topBaseline;
436+
float currentY;
403437

404438
if (needsNewPage) {
405-
// Create a new page for selections
406439
targetPage = new PDPage(page.getMediaBox());
407440
table.document.addPage(targetPage);
408-
// On the new page, start from near the top with margins
409-
topBaseline = pageHeight - margin - leading;
441+
currentY = pageHeight - margin - leading;
410442
} else {
411-
// Position selections immediately after the table
412443
targetPage = page;
413-
topBaseline = tableEndY - leading;
444+
currentY = tableEndY - leading;
414445
}
415446

416447
try (PDPageContentStream contentStream =
@@ -419,57 +450,100 @@ private void createSelectionsRows(BaseTable table, PDFont font, Map<String, Map<
419450
contentStream.setNonStrokingColor(Color.BLACK);
420451
contentStream.setFont(font, fontSize);
421452

422-
float currentY = topBaseline;
453+
for (SelectionLine sl : lines) {
454+
float keyX = margin;
455+
float keyWidth = sl.keyWidth;
456+
float afterKeyX = keyX + keyWidth;
457+
float colonWidth = font.getStringWidth(": ") / 1000f * fontSize;
458+
float valuesX = afterKeyX + colonWidth;
459+
460+
java.util.List<String> wrapped = sl.wrappedValueLines;
461+
int lineCount = Math.max(1, wrapped.size());
462+
463+
for (int i = 0; i < lineCount; i++) {
464+
// page break
465+
if (currentY <= baseBottomMargin + leading) {
466+
targetPage = new PDPage(page.getMediaBox());
467+
table.document.addPage(targetPage);
468+
currentY = pageHeight - margin - leading;
469+
470+
try (PDPageContentStream csNew =
471+
new PDPageContentStream(table.document, targetPage, PDPageContentStream.AppendMode.APPEND, true)) {
472+
csNew.setNonStrokingColor(Color.BLACK);
473+
csNew.setFont(font, fontSize);
474+
475+
for (int j = i; j < lineCount; j++) {
476+
if (j == 0) {
477+
csNew.beginText();
478+
csNew.newLineAtOffset(keyX, currentY);
479+
csNew.showText(sl.key);
480+
csNew.endText();
481+
482+
float underlineY = currentY - 2.0f;
483+
csNew.setLineWidth(0.5f);
484+
csNew.moveTo(keyX, underlineY);
485+
csNew.lineTo(keyX + keyWidth, underlineY);
486+
csNew.stroke();
487+
488+
csNew.beginText();
489+
csNew.newLineAtOffset(afterKeyX, currentY);
490+
csNew.showText(": ");
491+
csNew.endText();
492+
493+
csNew.beginText();
494+
csNew.newLineAtOffset(valuesX, currentY);
495+
csNew.showText(wrapped.get(j));
496+
csNew.endText();
497+
} else {
498+
csNew.beginText();
499+
csNew.newLineAtOffset(valuesX, currentY);
500+
csNew.showText(wrapped.get(j));
501+
csNew.endText();
502+
}
503+
504+
currentY -= leading;
505+
if (currentY <= baseBottomMargin + leading && j + 1 < lineCount) {
506+
targetPage = new PDPage(page.getMediaBox());
507+
table.document.addPage(targetPage);
508+
currentY = pageHeight - margin - leading;
509+
i = j + 1;
510+
break;
511+
}
512+
}
513+
}
423514

424-
for (Map.Entry<String, Map<String, JSONArray>> datasetEntry : selections.entrySet()) {
425-
Map<String, JSONArray> selectionContent = datasetEntry.getValue();
515+
i = lineCount;
516+
break;
517+
}
426518

427-
if (selectionContent == null || selectionContent.isEmpty()) {
428-
continue;
429-
}
519+
if (i == 0) {
520+
contentStream.beginText();
521+
contentStream.newLineAtOffset(keyX, currentY);
522+
contentStream.showText(sl.key);
523+
contentStream.endText();
524+
525+
float underlineY = currentY - 2.0f;
526+
contentStream.setLineWidth(0.5f);
527+
contentStream.moveTo(keyX, underlineY);
528+
contentStream.lineTo(keyX + keyWidth, underlineY);
529+
contentStream.stroke();
530+
531+
contentStream.beginText();
532+
contentStream.newLineAtOffset(afterKeyX, currentY);
533+
contentStream.showText(": ");
534+
contentStream.endText();
535+
536+
contentStream.beginText();
537+
contentStream.newLineAtOffset(valuesX, currentY);
538+
contentStream.showText(wrapped.get(i));
539+
contentStream.endText();
540+
} else {
541+
contentStream.beginText();
542+
contentStream.newLineAtOffset(valuesX, currentY);
543+
contentStream.showText(wrapped.get(i));
544+
contentStream.endText();
545+
}
430546

431-
for (Map.Entry<String, JSONArray> columnEntry : selectionContent.entrySet()) {
432-
String key = columnEntry.getKey();
433-
// Remove 'WA' and spaces from the key
434-
key = key.replace("('", "").replace("')", "");
435-
436-
StringBuilder values = getValues(columnEntry);
437-
String valueStr = values.toString().replace("('", "").replace("')", "");
438-
439-
float currentX = margin;
440-
441-
// Print the key in bold
442-
float keyWidth = font.getStringWidth(key) / 1000f * fontSize;
443-
contentStream.beginText();
444-
contentStream.setFont(font, fontSize);
445-
contentStream.newLineAtOffset(currentX, currentY);
446-
contentStream.showText(key);
447-
contentStream.endText();
448-
contentStream.setFont(font, fontSize);
449-
450-
// Draw underline under the key
451-
float underlineY = currentY - 2.0f;
452-
contentStream.setLineWidth(0.5f);
453-
contentStream.moveTo(currentX, underlineY);
454-
contentStream.lineTo(currentX + keyWidth, underlineY);
455-
contentStream.stroke();
456-
457-
currentX += keyWidth;
458-
459-
// Print ": "
460-
contentStream.beginText();
461-
contentStream.newLineAtOffset(currentX, currentY);
462-
contentStream.showText(": ");
463-
contentStream.endText();
464-
currentX += font.getStringWidth(": ") / 1000f * fontSize;
465-
466-
// Print the values
467-
contentStream.beginText();
468-
contentStream.newLineAtOffset(currentX, currentY);
469-
contentStream.showText(valueStr);
470-
contentStream.endText();
471-
472-
// Move to next line
473547
currentY -= leading;
474548
}
475549
}
@@ -494,6 +568,86 @@ private void createSelectionsRows(BaseTable table, PDFont font, Map<String, Map<
494568
return values;
495569
}
496570

571+
/**
572+
* Wrap text so that each line fits within maxWidth using the provided font metrics.
573+
* Splits by whitespace; if a single token is wider than maxWidth it will be hard-split.
574+
*/
575+
private static java.util.List<String> wrapText(PDFont font, float fontSize, String text, float maxWidth) throws IOException {
576+
java.util.List<String> result = new java.util.ArrayList<>();
577+
if (text == null || text.isEmpty()) {
578+
return result;
579+
}
580+
581+
String[] words = text.split("\\s+");
582+
StringBuilder line = new StringBuilder();
583+
584+
for (String word : words) {
585+
if (word.isEmpty()) {
586+
continue;
587+
}
588+
589+
if (line.length() == 0) {
590+
if (stringWidth(font, fontSize, word) <= maxWidth) {
591+
line.append(word);
592+
} else {
593+
for (String part : hardSplitToken(font, fontSize, word, maxWidth)) {
594+
if (!part.isEmpty()) {
595+
result.add(part);
596+
}
597+
}
598+
}
599+
} else {
600+
String candidate = line + " " + word;
601+
if (stringWidth(font, fontSize, candidate) <= maxWidth) {
602+
line.append(' ').append(word);
603+
} else {
604+
result.add(line.toString());
605+
line.setLength(0);
606+
if (stringWidth(font, fontSize, word) <= maxWidth) {
607+
line.append(word);
608+
} else {
609+
for (String part : hardSplitToken(font, fontSize, word, maxWidth)) {
610+
if (!part.isEmpty()) {
611+
result.add(part);
612+
}
613+
}
614+
}
615+
}
616+
}
617+
}
618+
619+
if (line.length() > 0) {
620+
result.add(line.toString());
621+
}
622+
623+
return result;
624+
}
625+
626+
private static float stringWidth(PDFont font, float fontSize, String text) throws IOException {
627+
return (font.getStringWidth(text) / 1000f) * fontSize;
628+
}
629+
630+
private static java.util.List<String> hardSplitToken(PDFont font, float fontSize, String token, float maxWidth) throws IOException {
631+
java.util.List<String> parts = new java.util.ArrayList<>();
632+
StringBuilder current = new StringBuilder();
633+
for (int i = 0; i < token.length(); i++) {
634+
char c = token.charAt(i);
635+
current.append(c);
636+
if (stringWidth(font, fontSize, current.toString()) > maxWidth) {
637+
current.setLength(current.length() - 1);
638+
if (current.length() > 0) {
639+
parts.add(current.toString());
640+
}
641+
current.setLength(0);
642+
current.append(c);
643+
}
644+
}
645+
if (current.length() > 0) {
646+
parts.add(current.toString());
647+
}
648+
return parts;
649+
}
650+
497651
private void buildRowsAndCols(BaseTable table, JSONObject settings, JSONArray rows, JSONArray columnsOrdered, int numberOfSummaryRows, List<String> summaryRowsLabels, PDFont font, String[] columnDateFormats, Map<String, JSONArray> styles) throws JSONException {
498652

499653
// Check if summary row is enabled
@@ -632,7 +786,7 @@ private void applyWholeRowStyle(int c, List<Boolean> styleCanBeOverriddenByWhole
632786
}
633787
}
634788
}
635-
789+
636790
private void applyStyleToCell(Style style, Cell<PDPage> cell) {
637791
if (!style.getBackgroundColor().isEmpty()) {
638792
cell.setFillColor(getJavaColorFromRGBA(style.getBackgroundColor()));

0 commit comments

Comments
 (0)