diff --git a/pom.xml b/pom.xml index ee638a4..6c21848 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,11 @@ javafx-fxml ${javafx.version} + + org.openjfx + javafx-swing + ${javafx.version} + @@ -34,6 +39,13 @@ 2.10.1 + + + com.itextpdf + itextpdf + 5.5.13.3 + + org.junit.jupiter junit-jupiter-api diff --git a/src/main/java/dev/bytevibe/hyperpoint/Controller.java b/src/main/java/dev/bytevibe/hyperpoint/Controller.java index 21711d0..181737c 100644 --- a/src/main/java/dev/bytevibe/hyperpoint/Controller.java +++ b/src/main/java/dev/bytevibe/hyperpoint/Controller.java @@ -50,6 +50,10 @@ public class Controller implements Initializable { private Button openSlideButton; // 添加打开幻灯片按钮 @FXML private Button saveSlideButton; // 添加保存幻灯片按钮 + @FXML + private Button exportPageButton; // 添加导出页面按钮 + @FXML + private Button exportSlideButton; // 添加导出幻灯片按钮 private Slide currentSlide; private DrawingCanvas drawingCanvas; @@ -430,6 +434,86 @@ public class Controller implements Initializable { } } + /** + * 导出页面为图片 + */ + @FXML + public void onExportPage(ActionEvent actionEvent) { + if (drawingCanvas == null) { + showWarning("请先选择页面", "请先选择一个页面。"); + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("导出页面为图片"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PNG图片 (*.png)", "*.png"), + new FileChooser.ExtensionFilter("JPG图片 (*.jpg)", "*.jpg"), + new FileChooser.ExtensionFilter("所有文件", "*.*") + ); + + SlidePage currentPage = pageListView.getSelectionModel().getSelectedItem(); + if (currentPage != null) { + fileChooser.setInitialFileName(currentPage.getTitle() + ".png"); + } + + stage = (Stage) exportPageButton.getScene().getWindow(); + File file = fileChooser.showSaveDialog(stage); + + if (file != null) { + try { + String format = file.getName().endsWith(".jpg") || file.getName().endsWith(".jpeg") ? "jpg" : "png"; + ExportUtil.exportPageAsImage(drawingCanvas, file, format); + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("成功"); + alert.setHeaderText("页面已导出"); + alert.setContentText("页面已导出到: " + file.getAbsolutePath()); + alert.showAndWait(); + } catch (Exception e) { + showWarning("错误", "无法导出页面: " + e.getMessage()); + e.printStackTrace(); + } + } + } + + /** + * 导出幻灯片为PDF + */ + @FXML + public void onExportSlide(ActionEvent actionEvent) { + if (currentSlide == null) { + showWarning("无幻灯片", "请先创建或打开一个幻灯片。"); + return; + } + + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("导出幻灯片为PDF"); + fileChooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("PDF文件 (*.pdf)", "*.pdf"), + new FileChooser.ExtensionFilter("所有文件", "*.*") + ); + fileChooser.setInitialFileName(currentSlide.getName() + ".pdf"); + + stage = (Stage) exportSlideButton.getScene().getWindow(); + File file = fileChooser.showSaveDialog(stage); + + if (file != null) { + try { + ExportUtil.exportSlideToPDF(currentSlide, file); + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("成功"); + alert.setHeaderText("幻灯片已导出为PDF"); + alert.setContentText("PDF已导出到: " + file.getAbsolutePath()); + alert.showAndWait(); + } catch (Exception e) { + showWarning("错误", "无法导出PDF: " + e.getMessage()); + e.printStackTrace(); + } + } + } + /** * 显示警告对话框 */ diff --git a/src/main/java/dev/bytevibe/hyperpoint/ExportUtil.java b/src/main/java/dev/bytevibe/hyperpoint/ExportUtil.java new file mode 100644 index 0000000..9a07036 --- /dev/null +++ b/src/main/java/dev/bytevibe/hyperpoint/ExportUtil.java @@ -0,0 +1,208 @@ +package dev.bytevibe.hyperpoint; + +import javafx.scene.image.WritableImage; +import javafx.scene.image.PixelReader; +import javafx.scene.layout.Pane; +import com.itextpdf.text.*; +import com.itextpdf.text.pdf.PdfWriter; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * 导出工具类,用于导出页面为图片和导出幻灯片为PDF + */ +public class ExportUtil { + + /** + * 导出页面为图片(PNG或JPG) + * + * @param pane 要导出的页面Pane + * @param file 输出文件 + * @param format 图片格式 "png" 或 "jpg" + * @throws IOException 如果导出失败 + */ + public static void exportPageAsImage(Pane pane, File file, String format) throws IOException { + // 获取Pane的宽度和高度 + double width = pane.getWidth(); + double height = pane.getHeight(); + + // 如果宽度或高度无效,尝试使用布局宽度/高度 + if (width <= 0 || height <= 0) { + width = pane.getLayoutBounds().getWidth(); + height = pane.getLayoutBounds().getHeight(); + } + + // 如果仍然无效,使用固定的默认大小 + if (width <= 0) { + width = 1024; + } + if (height <= 0) { + height = 768; + } + + // 创建WritableImage用于捕获页面内容 + WritableImage writableImage = new WritableImage((int) width, (int) height); + pane.snapshot(null, writableImage); + + // 转换为BufferedImage + BufferedImage bufferedImage = convertFXImageToBufferedImage(writableImage); + + // 写入文件 + String formatName = format.equalsIgnoreCase("jpg") ? "jpg" : "png"; + if (!ImageIO.write(bufferedImage, formatName, file)) { + throw new IOException("无法写入" + formatName + "文件"); + } + } + + /** + * 将JavaFX WritableImage转换为BufferedImage + */ + private static BufferedImage convertFXImageToBufferedImage(WritableImage fxImage) { + int width = (int) fxImage.getWidth(); + int height = (int) fxImage.getHeight(); + BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); + + PixelReader pixelReader = fxImage.getPixelReader(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int argb = pixelReader.getArgb(x, y); + bufferedImage.setRGB(x, y, argb); + } + } + + return bufferedImage; + } + + /** + * 导出整个幻灯片为PDF + * + * @param slide 要导出的幻灯片 + * @param file 输出PDF文件 + * @throws Exception 如果导出失败 + */ + public static void exportSlideToPDF(Slide slide, File file) throws Exception { + if (slide == null || slide.getPages().isEmpty()) { + throw new IOException("幻灯片没有页面"); + } + + // 创建PDF文档 + Document document = new Document(PageSize.A4); + PdfWriter.getInstance(document, new FileOutputStream(file)); + document.open(); + + // 为每一页添加内容 + for (SlidePage page : slide.getPages()) { + // 添加页面标题 + Paragraph title = new Paragraph(page.getTitle(), new com.itextpdf.text.Font( + com.itextpdf.text.Font.FontFamily.HELVETICA, 16, com.itextpdf.text.Font.BOLD)); + title.setSpacingAfter(10); + document.add(title); + + // 添加页面内容(如果有的话) + if (page.getContent() != null && !page.getContent().isEmpty()) { + Paragraph content = new Paragraph(page.getContent(), new com.itextpdf.text.Font( + com.itextpdf.text.Font.FontFamily.HELVETICA, 12)); + content.setSpacingAfter(10); + document.add(content); + } + + // 添加对象列表 + addObjectsToDocument(document, page.getPageContent()); + + // 每页之间添加分页符 + document.newPage(); + } + + document.close(); + } + + /** + * 将页面内容的对象添加到PDF文档 + */ + private static void addObjectsToDocument(Document document, PageContent pageContent) throws DocumentException { + for (DrawableObject obj : pageContent.getDrawableObjects()) { + if (obj instanceof TextObject) { + addTextObjectToDocument(document, (TextObject) obj); + } else if (obj instanceof ShapeObject) { + addShapeObjectToDocument(document, (ShapeObject) obj); + } else if (obj instanceof ImageObject) { + addImageObjectToDocument(document, (ImageObject) obj); + } + } + } + + /** + * 添加文本对象到PDF + */ + private static void addTextObjectToDocument(Document document, TextObject textObj) throws DocumentException { + float fontSize = (float) textObj.getFontSize(); + int fontStyle = com.itextpdf.text.Font.NORMAL; + + if (textObj.getFontStyle().contains("BOLD")) { + fontStyle |= com.itextpdf.text.Font.BOLD; + } + if (textObj.getFontStyle().contains("ITALIC")) { + fontStyle |= com.itextpdf.text.Font.ITALIC; + } + + com.itextpdf.text.Font font = new com.itextpdf.text.Font( + com.itextpdf.text.Font.FontFamily.HELVETICA, fontSize, fontStyle); + + Paragraph paragraph = new Paragraph(textObj.getText(), font); + paragraph.setSpacingAfter(5); + document.add(paragraph); + } + + /** + * 添加形状对象到PDF + */ + private static void addShapeObjectToDocument(Document document, ShapeObject shapeObj) throws DocumentException { + String shapeInfo = "图形: " + shapeObj.getShapeType().name() + + " (位置: " + (int)shapeObj.getX() + ", " + (int)shapeObj.getY() + + " 大小: " + (int)shapeObj.getWidth() + "x" + (int)shapeObj.getHeight() + ")"; + + Paragraph paragraph = new Paragraph(shapeInfo, new com.itextpdf.text.Font( + com.itextpdf.text.Font.FontFamily.HELVETICA, 10)); + paragraph.setSpacingAfter(5); + document.add(paragraph); + } + + /** + * 添加图片对象到PDF + */ + private static void addImageObjectToDocument(Document document, ImageObject imageObj) throws DocumentException { + String imagePath = imageObj.getImagePath(); + if (imagePath != null && !imagePath.isEmpty()) { + try { + // 检查文件是否存在 + File imageFile = new File(imagePath); + if (imageFile.exists()) { + Image img = Image.getInstance(imagePath); + // 限制图片大小 + float maxWidth = document.getPageSize().getWidth() - 40; + float maxHeight = document.getPageSize().getHeight() / 3; + if (img.getWidth() > maxWidth) { + img.scaleToFit(maxWidth, maxHeight); + } + img.setSpacingAfter(10); + document.add(img); + } else { + Paragraph para = new Paragraph("[图片不存在: " + imagePath + "]", + new com.itextpdf.text.Font(com.itextpdf.text.Font.FontFamily.HELVETICA, 10)); + para.setSpacingAfter(5); + document.add(para); + } + } catch (Exception e) { + Paragraph para = new Paragraph("[无法加载图片: " + e.getMessage() + "]", + new com.itextpdf.text.Font(com.itextpdf.text.Font.FontFamily.HELVETICA, 10)); + para.setSpacingAfter(5); + document.add(para); + } + } + } +} + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index fd967a3..d586c38 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -1,9 +1,11 @@ module dev.bytevibe.hyperpoint { requires javafx.controls; requires javafx.fxml; + requires javafx.graphics; requires java.desktop; requires com.google.gson; - + requires itextpdf; + requires javafx.swing; opens dev.bytevibe.hyperpoint to javafx.fxml; exports dev.bytevibe.hyperpoint; diff --git a/src/main/resources/dev/bytevibe/hyperpoint/main.fxml b/src/main/resources/dev/bytevibe/hyperpoint/main.fxml index 5b1c557..ec30974 100644 --- a/src/main/resources/dev/bytevibe/hyperpoint/main.fxml +++ b/src/main/resources/dev/bytevibe/hyperpoint/main.fxml @@ -19,6 +19,8 @@