.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
# Улучшения диаграмм в PDF-отчетах
|
||||
|
||||
## Проблемы, которые были решены
|
||||
|
||||
1. **Обрезание диаграмм** - диаграммы выходили за границы страницы PDF
|
||||
2. **Некрасивый внешний вид** - диаграммы выглядели неаккуратно и нечитаемо
|
||||
3. **Плохое качество** - низкое разрешение и размытость
|
||||
|
||||
## Внесенные улучшения
|
||||
|
||||
### 1. Улучшенная логика масштабирования (`ResearchPdfService.java`)
|
||||
|
||||
- **Точный расчет доступного пространства** с учетом всех отступов
|
||||
- **Пропорциональное масштабирование** для сохранения соотношения сторон
|
||||
- **Ограничения масштабирования** (30%-100%) для читаемости
|
||||
- **Дополнительная проверка** размера после масштабирования
|
||||
- **Принудительное уменьшение** если диаграмма все еще не помещается
|
||||
|
||||
### 2. Оптимизация генерации SVG (`OpenAiChartService.java`)
|
||||
|
||||
- **Стандартные размеры** 800x600 для оптимального качества в PDF
|
||||
- **Увеличенные отступы** (минимум 60px) вокруг диаграммы
|
||||
- **Улучшенная стилизация** с градиентами, тенями и рамками
|
||||
- **Контрастные цвета** и размер шрифта не менее 14px
|
||||
- **Сетка и оси** для лучшей читаемости
|
||||
|
||||
### 3. Улучшенная конвертация SVG в PNG (`SvgToPngConverter.java`)
|
||||
|
||||
- **Высокое качество** конвертации для PDF
|
||||
- **Автоматические размеры** если SVG не содержит явных размеров
|
||||
- **Оптимизация** для печати и отображения
|
||||
|
||||
### 4. Адаптивное размещение диаграмм
|
||||
|
||||
- **Автоматическое разбиение** больших диаграмм на отдельные страницы
|
||||
- **Подписи к диаграммам** при наличии нескольких диаграмм
|
||||
- **Улучшенные отступы** для лучшего внешнего вида
|
||||
- **Центрирование** всех диаграмм
|
||||
|
||||
### 5. Улучшения в ReportGenerationService
|
||||
|
||||
- **Улучшенное масштабирование** для диаграмм тональности
|
||||
- **Пропорциональное изменение размера** с сохранением качества
|
||||
- **Лучшее позиционирование** и отступы
|
||||
|
||||
## Результат
|
||||
|
||||
- ✅ Диаграммы больше не обрезаются
|
||||
- ✅ Улучшенное качество и читаемость
|
||||
- ✅ Профессиональный внешний вид
|
||||
- ✅ Адаптивное размещение на страницах
|
||||
- ✅ Оптимизация для PDF-формата
|
||||
|
||||
## Технические детали
|
||||
|
||||
### Ключевые методы:
|
||||
|
||||
- `addChartsSection()` - основная логика добавления диаграмм
|
||||
- `shouldSplitChart()` - проверка необходимости разбиения на страницы
|
||||
- `addChartWithPagination()` - добавление с разбиением
|
||||
- `buildSvgPrompt()` - улучшенные промпты для генерации SVG
|
||||
|
||||
### Параметры масштабирования:
|
||||
|
||||
- Минимальный масштаб: 30%
|
||||
- Максимальный масштаб: 100%
|
||||
- Безопасный отступ: 90% от доступного пространства
|
||||
- Стандартные размеры SVG: 800x600px
|
||||
@@ -184,7 +184,7 @@ public class OpenAiChartService {
|
||||
if (isRussian) {
|
||||
return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. "
|
||||
+
|
||||
"SVG должен быть стильным и читаемым, с подписями на русском языке. \n" +
|
||||
"SVG должен быть стильным, читаемым и оптимизированным для PDF, с подписями на русском языке. \n" +
|
||||
"КРИТИЧЕСКИ ВАЖНЫЕ ПРАВИЛА:\n" +
|
||||
"1. ВСЕ значения атрибутов width, height, x, y, r ДОЛЖНЫ быть СТРОГО ПОЛОЖИТЕЛЬНЫМИ числами (> 0).\n"
|
||||
+
|
||||
@@ -194,13 +194,23 @@ public class OpenAiChartService {
|
||||
+
|
||||
"4. Проверь каждый элемент перед отправкой - отрицательные значения приведут к ошибке конвертации.\n"
|
||||
+
|
||||
"5. Не добавляй никаких комментариев или объяснений.\n" +
|
||||
"6. Верни ТОЛЬКО SVG-код, начиная с тега <svg> и заканчивая </svg>.\n\n" +
|
||||
"5. ОПТИМИЗАЦИЯ ДЛЯ PDF:\n" +
|
||||
" - Используй размеры SVG: width='800' height='600' для оптимального качества в PDF\n" +
|
||||
" - Добавь достаточные отступы (padding) вокруг диаграммы (минимум 60px)\n" +
|
||||
" - Используй четкие, контрастные цвета\n" +
|
||||
" - Размер шрифта должен быть не менее 14px для читаемости\n" +
|
||||
" - Добавь рамку вокруг диаграммы для лучшего внешнего вида\n" +
|
||||
"6. СТИЛИЗАЦИЯ:\n" +
|
||||
" - Используй градиенты или тени для объемности\n" +
|
||||
" - Добавь сетку или оси с подписями\n" +
|
||||
" - Используй разные цвета для разных элементов\n" +
|
||||
"7. Не добавляй никаких комментариев или объяснений.\n" +
|
||||
"8. Верни ТОЛЬКО SVG-код, начиная с тега <svg> и заканчивая </svg>.\n\n" +
|
||||
"JSON с данными:\n---\n" +
|
||||
json + "\n---";
|
||||
} else {
|
||||
return "You are a data visualization expert that generates clean, valid SVG code.\n" +
|
||||
"Based on the following JSON, generate a complete and valid SVG code for a bar chart.\n\n" +
|
||||
return "You are a data visualization expert that generates clean, valid SVG code optimized for PDF.\n" +
|
||||
"Based on the following JSON, generate a complete and valid SVG code for a chart.\n\n" +
|
||||
|
||||
"CRITICAL RULES - FOLLOW EXACTLY:\n" +
|
||||
"1. The SVG coordinate system starts with (0,0) in the TOP-LEFT corner. The Y-axis points DOWN.\n" +
|
||||
@@ -209,8 +219,18 @@ public class OpenAiChartService {
|
||||
"3. ALL dimensional attributes (width, height, x, y, r) MUST be POSITIVE numbers (> 0).\n" +
|
||||
"4. NEVER use negative values in any element attributes - this will cause conversion errors.\n" +
|
||||
"5. Double-check every <rect>, <circle>, <ellipse> element for positive values.\n" +
|
||||
"6. The chart must be visually appealing, with appropriate padding, colors, and English labels.\n" +
|
||||
"7. Return ONLY the raw SVG code, starting with <svg> and ending with </svg>. No explanations or markdown.\n\n"
|
||||
"6. PDF OPTIMIZATION:\n" +
|
||||
" - Use SVG dimensions: width='800' height='600' for optimal PDF quality\n" +
|
||||
" - Add sufficient padding around the chart (minimum 60px)\n" +
|
||||
" - Use clear, contrasting colors\n" +
|
||||
" - Font size should be at least 14px for readability\n" +
|
||||
" - Add a border around the chart for better appearance\n" +
|
||||
"7. STYLING:\n" +
|
||||
" - Use gradients or shadows for depth\n" +
|
||||
" - Add grid lines or axes with labels\n" +
|
||||
" - Use different colors for different elements\n" +
|
||||
"8. The chart must be visually appealing, with appropriate padding, colors, and English labels.\n" +
|
||||
"9. Return ONLY the raw SVG code, starting with <svg> and ending with </svg>. No explanations or markdown.\n\n"
|
||||
+
|
||||
|
||||
"JSON data:\n---\n" +
|
||||
|
||||
@@ -13,6 +13,7 @@ import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
import org.apache.poi.util.Units;
|
||||
import com.lowagie.text.Document;
|
||||
import com.lowagie.text.Element;
|
||||
import com.lowagie.text.Image;
|
||||
import com.lowagie.text.Paragraph;
|
||||
import com.lowagie.text.pdf.PdfWriter;
|
||||
@@ -406,7 +407,25 @@ public class ReportGenerationService {
|
||||
byte[] piePng = buildSentimentChart(sentiment);
|
||||
if (piePng != null) {
|
||||
Image img = Image.getInstance(piePng);
|
||||
img.scaleToFit(480, 320);
|
||||
|
||||
// Улучшенное масштабирование для лучшего качества
|
||||
float availableWidth = 480f;
|
||||
float availableHeight = 320f;
|
||||
|
||||
// Рассчитываем оптимальный размер с сохранением пропорций
|
||||
float scaleX = availableWidth / img.getWidth();
|
||||
float scaleY = availableHeight / img.getHeight();
|
||||
float scale = Math.min(scaleX, scaleY);
|
||||
|
||||
// Ограничиваем масштабирование для читаемости
|
||||
scale = Math.min(scale, 1.0f);
|
||||
scale = Math.max(scale, 0.5f);
|
||||
|
||||
img.scalePercent(scale * 100);
|
||||
img.setAlignment(Element.ALIGN_CENTER);
|
||||
img.setSpacingBefore(10);
|
||||
img.setSpacingAfter(10);
|
||||
|
||||
pdf.add(img);
|
||||
}
|
||||
|
||||
|
||||
@@ -208,32 +208,114 @@ public class ResearchPdfService {
|
||||
Chapter chapter = new Chapter(title, 3);
|
||||
chapter.setNumberDepth(0);
|
||||
|
||||
for (byte[] imageData : chartImages) {
|
||||
for (int i = 0; i < chartImages.size(); i++) {
|
||||
try {
|
||||
Image chartImage = Image.getInstance(imageData);
|
||||
Image chartImage = Image.getInstance(chartImages.get(i));
|
||||
|
||||
// --- УЛУЧШЕННАЯ ЛОГИКА МАСШТАБИРОВАНИЯ ---
|
||||
float availableWidth = document.getPageSize().getWidth() - document.leftMargin()
|
||||
- document.rightMargin();
|
||||
float availableHeight = document.getPageSize().getHeight() - document.topMargin()
|
||||
- document.bottomMargin() - 50; // -50 для доп. отступа
|
||||
float pageWidth = document.getPageSize().getWidth();
|
||||
float pageHeight = document.getPageSize().getHeight();
|
||||
float leftMargin = document.leftMargin();
|
||||
float rightMargin = document.rightMargin();
|
||||
float topMargin = document.topMargin();
|
||||
float bottomMargin = document.bottomMargin();
|
||||
|
||||
// Масштабируем пропорционально, чтобы вписать и по ширине, и по высоте
|
||||
chartImage.scaleToFit(availableWidth, availableHeight);
|
||||
// --- КОНЕЦ УЛУЧШЕНОЙ ЛОГИКИ ---
|
||||
// Рассчитываем доступное пространство с учетом отступов
|
||||
float availableWidth = pageWidth - leftMargin - rightMargin;
|
||||
float availableHeight = pageHeight - topMargin - bottomMargin - 80; // -80 для заголовков и отступов
|
||||
|
||||
// Получаем оригинальные размеры изображения
|
||||
float originalWidth = chartImage.getWidth();
|
||||
float originalHeight = chartImage.getHeight();
|
||||
|
||||
// Рассчитываем коэффициенты масштабирования
|
||||
float scaleX = availableWidth / originalWidth;
|
||||
float scaleY = availableHeight / originalHeight;
|
||||
|
||||
// Используем меньший коэффициент для сохранения пропорций
|
||||
float scale = Math.min(scaleX, scaleY);
|
||||
|
||||
// Ограничиваем масштабирование для читаемости
|
||||
scale = Math.min(scale, 1.0f); // Не увеличиваем больше оригинала
|
||||
scale = Math.max(scale, 0.3f); // Не уменьшаем слишком сильно
|
||||
|
||||
// Применяем масштабирование
|
||||
chartImage.scalePercent(scale * 100);
|
||||
|
||||
// Проверяем, помещается ли диаграмма на страницу
|
||||
float scaledWidth = chartImage.getScaledWidth();
|
||||
float scaledHeight = chartImage.getScaledHeight();
|
||||
|
||||
// Если диаграмма все еще слишком большая, принудительно уменьшаем
|
||||
if (scaledWidth > availableWidth || scaledHeight > availableHeight) {
|
||||
float forceScaleX = availableWidth / scaledWidth;
|
||||
float forceScaleY = availableHeight / scaledHeight;
|
||||
float forceScale = Math.min(forceScaleX, forceScaleY) * 0.9f; // 90% для безопасности
|
||||
chartImage.scalePercent(forceScale * 100);
|
||||
}
|
||||
|
||||
// Центрируем изображение
|
||||
chartImage.setAlignment(Element.ALIGN_CENTER);
|
||||
|
||||
chapter.add(new Paragraph(" ")); // Отступ
|
||||
chapter.add(chartImage);
|
||||
// Добавляем отступы для лучшего внешнего вида
|
||||
chartImage.setSpacingBefore(20);
|
||||
chartImage.setSpacingAfter(20);
|
||||
|
||||
// Добавляем подпись к диаграмме
|
||||
if (chartImages.size() > 1) {
|
||||
Paragraph chartTitle = new Paragraph("Диаграмма " + (i + 1), subheadingFont);
|
||||
chartTitle.setAlignment(Element.ALIGN_CENTER);
|
||||
chartTitle.setSpacingBefore(10);
|
||||
chapter.add(chartTitle);
|
||||
}
|
||||
|
||||
// Используем новую логику добавления с разбиением на страницы
|
||||
addChartWithPagination(chartImage, chapter, document, i);
|
||||
|
||||
// Добавляем разрыв страницы между диаграммами, если их много
|
||||
if (i < chartImages.size() - 1) {
|
||||
chapter.add(Chunk.NEWLINE);
|
||||
chapter.add(Chunk.NEWLINE);
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
System.err.println("Не удалось добавить изображение диаграммы в PDF: " + e.getMessage());
|
||||
logger.error("Error adding chart image to PDF", e);
|
||||
}
|
||||
}
|
||||
document.add(chapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, нужно ли разбивать диаграмму на несколько страниц
|
||||
*/
|
||||
private boolean shouldSplitChart(Image chartImage, Document document) {
|
||||
float pageHeight = document.getPageSize().getHeight();
|
||||
float topMargin = document.topMargin();
|
||||
float bottomMargin = document.bottomMargin();
|
||||
float availableHeight = pageHeight - topMargin - bottomMargin - 80;
|
||||
|
||||
return chartImage.getScaledHeight() > availableHeight * 0.8f; // 80% от доступной высоты
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет диаграмму с возможным разбиением на страницы
|
||||
*/
|
||||
private void addChartWithPagination(Image chartImage, Chapter chapter, Document document, int chartIndex)
|
||||
throws DocumentException {
|
||||
if (shouldSplitChart(chartImage, document)) {
|
||||
// Если диаграмма слишком большая, добавляем ее на отдельную страницу
|
||||
document.newPage();
|
||||
Paragraph chartTitle = new Paragraph("Диаграмма " + (chartIndex + 1) + " (продолжение)", subheadingFont);
|
||||
chartTitle.setAlignment(Element.ALIGN_CENTER);
|
||||
chartTitle.setSpacingBefore(10);
|
||||
chapter.add(chartTitle);
|
||||
}
|
||||
|
||||
chapter.add(new Paragraph(" ")); // Отступ
|
||||
chapter.add(chartImage);
|
||||
}
|
||||
|
||||
private void addTitlePage(Document document, String query) throws DocumentException {
|
||||
// Этот метод был в порядке, оставляем без изменений
|
||||
Paragraph title = new Paragraph(query, titleFont);
|
||||
|
||||
@@ -36,9 +36,16 @@ public class SvgToPngConverter {
|
||||
// 1. Санитизируем SVG код перед конвертацией
|
||||
String sanitizedSvg = sanitizeSvg(svgCode);
|
||||
|
||||
// 2. Создаем транскодер для PNG
|
||||
// 2. Создаем транскодер для PNG с улучшенными настройками
|
||||
PNGTranscoder transcoder = new PNGTranscoder();
|
||||
|
||||
// Устанавливаем размеры для оптимального качества в PDF
|
||||
// Если SVG не имеет явных размеров, устанавливаем стандартные
|
||||
if (!sanitizedSvg.contains("width=") || !sanitizedSvg.contains("height=")) {
|
||||
transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, 800f);
|
||||
transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, 600f);
|
||||
}
|
||||
|
||||
// 3. Устанавливаем входные данные (наш SVG-код)
|
||||
InputStream inputStream = new ByteArrayInputStream(sanitizedSvg.getBytes(StandardCharsets.UTF_8));
|
||||
TranscoderInput input = new TranscoderInput(inputStream);
|
||||
|
||||
Reference in New Issue
Block a user