.
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) {
|
if (isRussian) {
|
||||||
return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. "
|
return "Ты — эксперт по визуализации данных. На основе следующего JSON, сгенерируй полный и валидный SVG-код для диаграммы. "
|
||||||
+
|
+
|
||||||
"SVG должен быть стильным и читаемым, с подписями на русском языке. \n" +
|
"SVG должен быть стильным, читаемым и оптимизированным для PDF, с подписями на русском языке. \n" +
|
||||||
"КРИТИЧЕСКИ ВАЖНЫЕ ПРАВИЛА:\n" +
|
"КРИТИЧЕСКИ ВАЖНЫЕ ПРАВИЛА:\n" +
|
||||||
"1. ВСЕ значения атрибутов width, height, x, y, r ДОЛЖНЫ быть СТРОГО ПОЛОЖИТЕЛЬНЫМИ числами (> 0).\n"
|
"1. ВСЕ значения атрибутов width, height, x, y, r ДОЛЖНЫ быть СТРОГО ПОЛОЖИТЕЛЬНЫМИ числами (> 0).\n"
|
||||||
+
|
+
|
||||||
@@ -194,13 +194,23 @@ public class OpenAiChartService {
|
|||||||
+
|
+
|
||||||
"4. Проверь каждый элемент перед отправкой - отрицательные значения приведут к ошибке конвертации.\n"
|
"4. Проверь каждый элемент перед отправкой - отрицательные значения приведут к ошибке конвертации.\n"
|
||||||
+
|
+
|
||||||
"5. Не добавляй никаких комментариев или объяснений.\n" +
|
"5. ОПТИМИЗАЦИЯ ДЛЯ PDF:\n" +
|
||||||
"6. Верни ТОЛЬКО SVG-код, начиная с тега <svg> и заканчивая </svg>.\n\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---\n" +
|
||||||
json + "\n---";
|
json + "\n---";
|
||||||
} else {
|
} else {
|
||||||
return "You are a data visualization expert that generates clean, valid SVG code.\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 bar chart.\n\n" +
|
"Based on the following JSON, generate a complete and valid SVG code for a chart.\n\n" +
|
||||||
|
|
||||||
"CRITICAL RULES - FOLLOW EXACTLY:\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" +
|
"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" +
|
"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" +
|
"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" +
|
"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" +
|
"6. PDF OPTIMIZATION:\n" +
|
||||||
"7. Return ONLY the raw SVG code, starting with <svg> and ending with </svg>. No explanations or markdown.\n\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" +
|
"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.xwpf.usermodel.XWPFRun;
|
||||||
import org.apache.poi.util.Units;
|
import org.apache.poi.util.Units;
|
||||||
import com.lowagie.text.Document;
|
import com.lowagie.text.Document;
|
||||||
|
import com.lowagie.text.Element;
|
||||||
import com.lowagie.text.Image;
|
import com.lowagie.text.Image;
|
||||||
import com.lowagie.text.Paragraph;
|
import com.lowagie.text.Paragraph;
|
||||||
import com.lowagie.text.pdf.PdfWriter;
|
import com.lowagie.text.pdf.PdfWriter;
|
||||||
@@ -406,7 +407,25 @@ public class ReportGenerationService {
|
|||||||
byte[] piePng = buildSentimentChart(sentiment);
|
byte[] piePng = buildSentimentChart(sentiment);
|
||||||
if (piePng != null) {
|
if (piePng != null) {
|
||||||
Image img = Image.getInstance(piePng);
|
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);
|
pdf.add(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,32 +208,114 @@ public class ResearchPdfService {
|
|||||||
Chapter chapter = new Chapter(title, 3);
|
Chapter chapter = new Chapter(title, 3);
|
||||||
chapter.setNumberDepth(0);
|
chapter.setNumberDepth(0);
|
||||||
|
|
||||||
for (byte[] imageData : chartImages) {
|
for (int i = 0; i < chartImages.size(); i++) {
|
||||||
try {
|
try {
|
||||||
Image chartImage = Image.getInstance(imageData);
|
Image chartImage = Image.getInstance(chartImages.get(i));
|
||||||
|
|
||||||
// --- УЛУЧШЕННАЯ ЛОГИКА МАСШТАБИРОВАНИЯ ---
|
// --- УЛУЧШЕННАЯ ЛОГИКА МАСШТАБИРОВАНИЯ ---
|
||||||
float availableWidth = document.getPageSize().getWidth() - document.leftMargin()
|
float pageWidth = document.getPageSize().getWidth();
|
||||||
- document.rightMargin();
|
float pageHeight = document.getPageSize().getHeight();
|
||||||
float availableHeight = document.getPageSize().getHeight() - document.topMargin()
|
float leftMargin = document.leftMargin();
|
||||||
- document.bottomMargin() - 50; // -50 для доп. отступа
|
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);
|
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) {
|
} catch (Exception e) {
|
||||||
System.err.println("Не удалось добавить изображение диаграммы в PDF: " + e.getMessage());
|
System.err.println("Не удалось добавить изображение диаграммы в PDF: " + e.getMessage());
|
||||||
|
logger.error("Error adding chart image to PDF", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
document.add(chapter);
|
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 {
|
private void addTitlePage(Document document, String query) throws DocumentException {
|
||||||
// Этот метод был в порядке, оставляем без изменений
|
// Этот метод был в порядке, оставляем без изменений
|
||||||
Paragraph title = new Paragraph(query, titleFont);
|
Paragraph title = new Paragraph(query, titleFont);
|
||||||
|
|||||||
@@ -36,9 +36,16 @@ public class SvgToPngConverter {
|
|||||||
// 1. Санитизируем SVG код перед конвертацией
|
// 1. Санитизируем SVG код перед конвертацией
|
||||||
String sanitizedSvg = sanitizeSvg(svgCode);
|
String sanitizedSvg = sanitizeSvg(svgCode);
|
||||||
|
|
||||||
// 2. Создаем транскодер для PNG
|
// 2. Создаем транскодер для PNG с улучшенными настройками
|
||||||
PNGTranscoder transcoder = new PNGTranscoder();
|
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-код)
|
// 3. Устанавливаем входные данные (наш SVG-код)
|
||||||
InputStream inputStream = new ByteArrayInputStream(sanitizedSvg.getBytes(StandardCharsets.UTF_8));
|
InputStream inputStream = new ByteArrayInputStream(sanitizedSvg.getBytes(StandardCharsets.UTF_8));
|
||||||
TranscoderInput input = new TranscoderInput(inputStream);
|
TranscoderInput input = new TranscoderInput(inputStream);
|
||||||
|
|||||||
Reference in New Issue
Block a user