This commit is contained in:
root
2025-10-14 02:13:27 +05:00
parent 9086d7be84
commit cf3a4ecde3
5 changed files with 216 additions and 20 deletions
+68
View File
@@ -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);