diff --git a/CHART_IMPROVEMENTS.md b/CHART_IMPROVEMENTS.md new file mode 100644 index 0000000..45f1d30 --- /dev/null +++ b/CHART_IMPROVEMENTS.md @@ -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 diff --git a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java index 6e6487e..545c581 100644 --- a/src/main/java/kz/konturai/parser/service/OpenAiChartService.java +++ b/src/main/java/kz/konturai/parser/service/OpenAiChartService.java @@ -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-код, начиная с тега и заканчивая .\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-код, начиная с тега и заканчивая .\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 , , 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 and ending with . 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 and ending with . No explanations or markdown.\n\n" + "JSON data:\n---\n" + diff --git a/src/main/java/kz/konturai/parser/service/ReportGenerationService.java b/src/main/java/kz/konturai/parser/service/ReportGenerationService.java index 95f4f70..f19ac61 100644 --- a/src/main/java/kz/konturai/parser/service/ReportGenerationService.java +++ b/src/main/java/kz/konturai/parser/service/ReportGenerationService.java @@ -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); } diff --git a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java index df074fd..4c5519a 100644 --- a/src/main/java/kz/konturai/parser/service/ResearchPdfService.java +++ b/src/main/java/kz/konturai/parser/service/ResearchPdfService.java @@ -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); diff --git a/src/main/java/kz/konturai/parser/util/SvgToPngConverter.java b/src/main/java/kz/konturai/parser/util/SvgToPngConverter.java index 86b0eaa..8891ccc 100644 --- a/src/main/java/kz/konturai/parser/util/SvgToPngConverter.java +++ b/src/main/java/kz/konturai/parser/util/SvgToPngConverter.java @@ -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);