diff --git a/hutool-swing/src/main/java/cn/hutool/v7/swing/DesktopUtil.java b/hutool-swing/src/main/java/cn/hutool/v7/swing/DesktopUtil.java index ddc2b50fec..0fe68473a3 100644 --- a/hutool-swing/src/main/java/cn/hutool/v7/swing/DesktopUtil.java +++ b/hutool-swing/src/main/java/cn/hutool/v7/swing/DesktopUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2013-2025 Hutool Team and hutool.cn + * Copyright (c) 2013-2026 Hutool Team and hutool.cn * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintAttributeBuilder.java b/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintAttributeBuilder.java new file mode 100644 index 0000000000..a9b2d93248 --- /dev/null +++ b/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintAttributeBuilder.java @@ -0,0 +1,358 @@ +/* + * Copyright (c) 2013-2026 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.v7.swing; + +import cn.hutool.v7.core.array.ArrayUtil; +import cn.hutool.v7.core.lang.Console; +import cn.hutool.v7.core.lang.builder.Builder; +import cn.hutool.v7.core.util.ObjUtil; + +import javax.print.PrintService; +import javax.print.attribute.Attribute; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.*; +import java.io.Serial; +import java.util.*; +import java.util.stream.Collectors; + +/** + * 智能打印配置器:根据打印机能力动态构建 PrintRequestAttributeSet + *

+ * 使用示例: + *

<@code
+ * PrintService printer = findPrinter("MyPrinter");
+ * PrintRequestAttributeSet attrs = new SmartPrintConfigurator(printer)
+ *     .copies(2)
+ *     .a4Paper()
+ *     .landscape()
+ *     .duplex()
+ *     .monochrome()
+ *     .collated()
+ *     .highQuality()
+ *     .build();
+ * }
+ */ +public class PrintAttributeBuilder implements Builder { + @Serial + private static final long serialVersionUID = 1L; + + private final PrintService printer; + private final HashPrintRequestAttributeSet attributes; + private final Set> supportedAttrs; + + // 缓存支持的属性值(提升性能) + private final Map, Object> supportedValuesCache = new HashMap<>(); + + /** + * 构造 + * + * @param printer 打印机对象,不能为空 + */ + @SuppressWarnings("unchecked") + public PrintAttributeBuilder(final PrintService printer) { + this.printer = Objects.requireNonNull(printer, "printer 不能为空"); + this.attributes = new HashPrintRequestAttributeSet(); + this.supportedAttrs = Arrays.stream(printer.getSupportedAttributeCategories()) + .filter(Objects::nonNull) + .filter(Attribute.class::isAssignableFrom) + .map(c -> (Class) c) // 安全转型 + .collect(Collectors.toUnmodifiableSet()); + } + + // ------------------ 配置方法(链式调用) ------------------ + + /** + * 设置份数(Copies)— 所有打印机均支持 + * + * @param n 份数,至少为 1 + * @return this,用于链式调用 + */ + public PrintAttributeBuilder copies(final int n) { + attributes.add(new Copies(Math.max(1, n))); + return this; + } + + /** + * 设置作业名 + * + * @param name 作业名,可为空字符串但不应为null + * @return this,用于链式调用 + */ + public PrintAttributeBuilder jobName(final String name) { + attributes.add(new JobName(name, null)); + return this; + } + + /** + * 强制 A4 纸(若不支持则尝试 Letter 或忽略) + * + * @return this,用于链式调用 + */ + public PrintAttributeBuilder a4Paper() { + if (support(MediaSizeName.class)) { + final MediaSizeName[] sizes = getSupportedValues(MediaSizeName.class); + if (ArrayUtil.contains(sizes, MediaSizeName.ISO_A4)) { + attributes.add(MediaSizeName.ISO_A4); + } else if (ArrayUtil.contains(sizes, MediaSizeName.NA_LETTER)) { + // 降级到 Letter(美标) + attributes.add(MediaSizeName.NA_LETTER); + } + } + return this; + } + + /** + * 设置横向打印方向 + *

+ * 如果打印机不支持横向打印,将输出警告信息并保持默认的纵向打印方向 + *

+ * + * @return 当前PrintAttributeBuilder实例,支持链式调用 + */ + public PrintAttributeBuilder landscape() { + if (support(OrientationRequested.class) && + supportsValue(OrientationRequested.LANDSCAPE)) { + attributes.add(OrientationRequested.LANDSCAPE); + } + return this; + } + + /** + * 设置纵向打印模式(默认,通常无需显式调用) + *

+ * 将打印方向设置为纵向模式,这是大多数打印任务的默认方向。 + * 如果打印机支持该方向,则会在打印属性中添加 PORTRAIT 方向设置。 + *

+ * + * @return 当前 PrintAttributeBuilder 实例,支持链式调用 + */ + public PrintAttributeBuilder portrait() { + if (support(OrientationRequested.class) && + supportsValue(OrientationRequested.PORTRAIT)) { + attributes.add(OrientationRequested.PORTRAIT); + } + return this; + } + + /** + * 双面打印(若支持则启用,否则自动降级为单面并告警) + * + * @return this + */ + public PrintAttributeBuilder duplex() { + if (support(Sides.class)) { + final Sides[] sides = getSupportedValues(Sides.class); + if (Arrays.asList(sides).contains(Sides.DUPLEX)) { + attributes.add(Sides.DUPLEX); + } else if (Arrays.asList(sides).contains(Sides.TUMBLE)) { + attributes.add(Sides.TUMBLE); + } else { + attributes.add(Sides.ONE_SIDED); + } + } else { + attributes.add(Sides.ONE_SIDED); + } + return this; + } + + /** + * 强制单面(覆盖可能的默认双面) + * + * @return this,用于链式调用 + */ + public PrintAttributeBuilder oneSided() { + attributes.add(Sides.ONE_SIDED); + return this; + } + + /** + * 黑白打印(若支持彩色/黑白切换;否则保留默认) + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder monochrome() { + if (support(Chromaticity.class) && + supportsValue(Chromaticity.MONOCHROME)) { + attributes.add(Chromaticity.MONOCHROME); + } + return this; + } + + /** + * 彩色打印(若支持) + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder color() { + if (support(Chromaticity.class) && + supportsValue(Chromaticity.COLOR)) { + attributes.add(Chromaticity.COLOR); + } + return this; + } + + /** + * 分套打印(collate) + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder collated() { + if (support(SheetCollate.class) && + supportsValue(SheetCollate.COLLATED)) { + attributes.add(SheetCollate.COLLATED); + } + return this; + } + + /** + * 不分套 + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder uncollated() { + if (support(SheetCollate.class) && + supportsValue(SheetCollate.UNCOLLATED)) { + attributes.add(SheetCollate.UNCOLLATED); + } + return this; + } + + /** + * 高质量打印(若支持) + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder highQuality() { + if (support(PrintQuality.class) && + supportsValue(PrintQuality.HIGH)) { + attributes.add(PrintQuality.HIGH); + } + return this; + } + + /** + * 草稿模式(省墨) + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder draftMode() { + if (support(PrintQuality.class) && + supportsValue(PrintQuality.DRAFT)) { + attributes.add(PrintQuality.DRAFT); + } + return this; + } + + /** + * 指定进纸托盘为手动进纸 + * + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder manualTray() { + if (support(MediaTray.class) && + supportsValue(MediaTray.MANUAL)) { + attributes.add(MediaTray.MANUAL); + } + return this; + } + + /** + * 设置页码范围 + * + * @param from 开始页码(包含) + * @param to 结束页码(包含) + * @return this,用于链式调用。 + */ + public PrintAttributeBuilder pageRange(final int from, final int to) { + // PageRanges 通常都支持 + attributes.add(new PageRanges(Math.max(1, from), Math.max(from, to))); + return this; + } + + /** + * 构建最终的属性集 + */ + public PrintRequestAttributeSet build() { + return ObjUtil.clone(this.attributes); + } + + /** + * 打印当前配置摘要(调试用) + */ + public void printSummary() { + Console.log("🖨️ 打印配置摘要 [打印机: " + printer.getName() + "]"); + for (final Attribute attr : attributes.toArray()) { + Console.log(" • " + attr.getCategory().getSimpleName() + " = " + attr); + } + } + + @Override + public String toString() { + return "PrintAttributeBuilder{" + + "printer=" + printer.getName() + + ", attributes=" + Arrays.toString(attributes.toArray()) + + '}'; + } + + /** + * 快速获取支持的能力摘要 + */ + public void printCapabilities() { + Console.log("🔍 打印机能力检测: " + printer.getName()); + for (final Class c : this.supportedAttrs) { + final Object vals = printer.getSupportedAttributeValues(c, null, null); + if (vals instanceof final Attribute[] arr) { + Console.log(" ✅ " + c.getSimpleName() + ": " + Arrays.toString(arr)); + } else { + Console.log(" ✅ " + c.getSimpleName() + " (值类型: " + (vals == null ? "null" : vals.getClass().getSimpleName()) + ")"); + } + } + } + + private boolean support(final Class attrClass) { + return supportedAttrs.contains(attrClass); + } + + /** + * 判断是否支持某个属性 + * + * @param attrClass 属性类 + * @param 属性类 + * @return 是否支持 + */ + @SuppressWarnings("unchecked") + private T[] getSupportedValues(final Class attrClass) { + return (T[]) supportedValuesCache.computeIfAbsent(attrClass, k -> + printer.getSupportedAttributeValues(k, null, null)); + } + + /** + * 判断是否支持某个属性值 + * + * @param value 属性值 + * @return 是否支持 + */ + private boolean supportsValue(final Attribute value) { + final Object vals = printer.getSupportedAttributeValues( + value.getCategory(), null, null); + if (vals instanceof final Attribute[] arr) { + return Arrays.asList(arr).contains(value); + } + return false; + } +} diff --git a/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintUtil.java b/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintUtil.java new file mode 100644 index 0000000000..46105a9b0a --- /dev/null +++ b/hutool-swing/src/main/java/cn/hutool/v7/swing/PrintUtil.java @@ -0,0 +1,118 @@ +package cn.hutool.v7.swing; + +import cn.hutool.v7.core.text.StrUtil; + +import javax.print.PrintService; +import javax.print.attribute.HashPrintRequestAttributeSet; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.*; +import java.awt.print.Printable; +import java.awt.print.PrinterException; +import java.awt.print.PrinterJob; + +/** + * 打印机工具类 + * + * @author Looly + * @since 7.0.0 + */ +public class PrintUtil { + + /** + * 获取所有可用的打印机 + * + * @return 打印机数组 + */ + public static PrintService[] getPrinters() { + return PrinterJob.lookupPrintServices(); + } + + /** + * 获取默认打印机 + * + * @return 默认打印机 + */ + public static PrintService getDefaultPrinter() { + return PrinterJob.getPrinterJob().getPrintService(); + } + + /** + * 根据名称获取打印机 + * + * @param printerName 打印机名称 + * @return 对应的打印机,未找到返回null + */ + public static PrintService getPrinter(String printerName) { + PrintService[] printers = getPrinters(); + for (PrintService printer : printers) { + if (printer.getName().equals(printerName)) { + return printer; + } + } + return null; + } + + /** + * 检查打印机是否可用 + * + * @param printer 打印机 + * @return 是否可用 + */ + public static boolean isPrinterAvailable(PrintService printer) { + try { + PrinterJob job = PrinterJob.getPrinterJob(); + job.setPrintService(printer); + return true; + } catch (PrinterException e) { + return false; + } + } + + /** + * 打印内容(使用默认打印机) + * + * @param printable 可打印内容 + */ + public static void print(Printable printable) { + print(printable, null, null); + } + + /** + * 打印内容 + * + * @param printable 可打印内容 + * @param printerName 打印机名称(可选) + * @param attributes 打印属性(可选) + */ + public static void print(Printable printable, String printerName, PrintRequestAttributeSet attributes) { + try { + PrinterJob job = PrinterJob.getPrinterJob(); + + if (StrUtil.isNotEmpty(printerName)) { + PrintService printer = getPrinter(printerName); + if (printer != null) { + job.setPrintService(printer); + } + } + + if (attributes == null) { + attributes = new HashPrintRequestAttributeSet(); + // 打印纸张大小 + attributes.add(MediaSizeName.ISO_A4); + // 打印方向 + attributes.add(OrientationRequested.PORTRAIT); + // 打印质量 + attributes.add(PrintQuality.NORMAL); + // 打印颜色 + attributes.add(Chromaticity.COLOR); + // 打印份数 + attributes.add(new Copies(1)); + } + + job.setPrintable(printable); + job.print(attributes); + } catch (PrinterException e) { + throw new SwingException(e); + } + } +} diff --git a/hutool-swing/src/main/java/cn/hutool/v7/swing/SwingException.java b/hutool-swing/src/main/java/cn/hutool/v7/swing/SwingException.java new file mode 100644 index 0000000000..81f0ade05a --- /dev/null +++ b/hutool-swing/src/main/java/cn/hutool/v7/swing/SwingException.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2013-2025 Hutool Team and hutool.cn + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.v7.swing; + +import cn.hutool.v7.core.exception.HutoolException; + +import java.io.Serial; + +/** + * Swing异常 + * + * @author Looly + */ +public class SwingException extends HutoolException { + @Serial + private static final long serialVersionUID = 1L; + + /** + * 构造 + * + * @param e 异常 + */ + public SwingException(final Throwable e) { + super(e); + } + + /** + * 构造 + * + * @param message 消息 + */ + public SwingException(final String message) { + super(message); + } + + /** + * 构造 + * + * @param messageTemplate 消息模板 + * @param params 参数 + */ + public SwingException(final String messageTemplate, final Object... params) { + super(messageTemplate, params); + } + + /** + * 构造 + * + * @param message 消息 + * @param cause 被包装的子异常 + */ + public SwingException(final String message, final Throwable cause) { + super(message, cause); + } + + /** + * 构造 + * + * @param message 消息 + * @param cause 被包装的子异常 + * @param enableSuppression 是否启用抑制 + * @param writableStackTrace 堆栈跟踪是否应该是可写的 + */ + public SwingException(final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + /** + * 构造 + * + * @param cause 被包装的子异常 + * @param messageTemplate 消息模板 + * @param params 参数 + */ + public SwingException(final Throwable cause, final String messageTemplate, final Object... params) { + super(cause, messageTemplate, params); + } +} diff --git a/hutool-swing/src/test/java/cn/hutool/v7/swing/PrintAttributeBuilderTest.java b/hutool-swing/src/test/java/cn/hutool/v7/swing/PrintAttributeBuilderTest.java new file mode 100644 index 0000000000..7454b2c2c5 --- /dev/null +++ b/hutool-swing/src/test/java/cn/hutool/v7/swing/PrintAttributeBuilderTest.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2026 Hutool Team. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hutool.v7.swing; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import javax.print.PrintService; +import javax.print.attribute.Attribute; +import javax.print.attribute.PrintRequestAttributeSet; +import javax.print.attribute.standard.*; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") +class PrintAttributeBuilderTest { + private PrintService testPrinter; + private Set> supportedAttributes; + + @BeforeEach + void setUp() { + supportedAttributes = new HashSet<>(); + testPrinter = PrintUtil.getDefaultPrinter(); + } + + @Test + void testConstructorWithNullPrinter() { + assertThrows(NullPointerException.class, () -> new PrintAttributeBuilder(null)); + } + + @Test + void testCopies() { + // 测试份数设置 + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.copies(3); + final PrintRequestAttributeSet attributes = builder.build(); + + final Copies copies = (Copies) attributes.get(Copies.class); + assertNotNull(copies); + assertEquals(3, copies.getValue()); + } + + @Test + void testCopiesWithInvalidValue() { + // 测试无效份数(小于1) + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.copies(0); + final PrintRequestAttributeSet attributes = builder.build(); + + final Copies copies = (Copies) attributes.get(Copies.class); + assertNotNull(copies); + assertEquals(1, copies.getValue()); // 应该自动修正为1 + } + + @Test + void testJobName() { + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.jobName("Test Job"); + final PrintRequestAttributeSet attributes = builder.build(); + + final JobName jobName = (JobName) attributes.get(JobName.class); + assertNotNull(jobName); + assertEquals("Test Job", jobName.getValue()); + } + + @Test + void testA4PaperWhenNotSupported() { + // 确保不支持MediaSizeName + supportedAttributes.clear(); + + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.a4Paper(); + final PrintRequestAttributeSet attributes = builder.build(); + + assertNull(attributes.get(MediaSizeName.class)); + } + + @Test + void testLandscapeWhenSupported() { + supportedAttributes.add(OrientationRequested.class); + + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.landscape(); + final PrintRequestAttributeSet attributes = builder.build(); + + assertEquals(OrientationRequested.LANDSCAPE, attributes.get(OrientationRequested.class)); + } + + @Test + void testMonochromeWhenSupported() { + supportedAttributes.add(Chromaticity.class); + + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.monochrome(); + final PrintRequestAttributeSet attributes = builder.build(); + + assertEquals(Chromaticity.MONOCHROME, attributes.get(Chromaticity.class)); + } + + @Test + void testPageRange() { + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.pageRange(3, 5); + final PrintRequestAttributeSet attributes = builder.build(); + + final PageRanges pageRanges = (PageRanges) attributes.get(PageRanges.class); + assertNotNull(pageRanges); + assertArrayEquals(new int[][]{{3, 5}}, pageRanges.getMembers()); + } + + @Test + void testBuildReturnsClone() { + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.copies(2); + final PrintRequestAttributeSet attributes1 = builder.build(); + final PrintRequestAttributeSet attributes2 = builder.build(); + + assertNotSame(attributes1, attributes2); + assertEquals(attributes1, attributes2); + } + + @Test + void testOneSided() { + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.oneSided(); + final PrintRequestAttributeSet attributes = builder.build(); + + final Sides sides = (Sides) attributes.get(Sides.class); + assertNotNull(sides); + assertEquals(Sides.ONE_SIDED, sides); + } + + @Test + void testColorWhenSupported() { + supportedAttributes.add(Chromaticity.class); + + final PrintAttributeBuilder builder = new PrintAttributeBuilder(testPrinter); + builder.color(); + final PrintRequestAttributeSet attributes = builder.build(); + + assertEquals(Chromaticity.COLOR, attributes.get(Chromaticity.class)); + } +}