add PrintUtil

This commit is contained in:
Looly
2026-01-06 00:50:57 +08:00
parent 0eee7d27a7
commit c8219464c6
5 changed files with 728 additions and 1 deletions

View File

@@ -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.

View File

@@ -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
* <p>
* 使用示例:
* <pre><@code
* PrintService printer = findPrinter("MyPrinter");
* PrintRequestAttributeSet attrs = new SmartPrintConfigurator(printer)
* .copies(2)
* .a4Paper()
* .landscape()
* .duplex()
* .monochrome()
* .collated()
* .highQuality()
* .build();
* }</pre>
*/
public class PrintAttributeBuilder implements Builder<PrintRequestAttributeSet> {
@Serial
private static final long serialVersionUID = 1L;
private final PrintService printer;
private final HashPrintRequestAttributeSet attributes;
private final Set<Class<? extends Attribute>> supportedAttrs;
// 缓存支持的属性值(提升性能)
private final Map<Class<? extends Attribute>, 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<? extends Attribute>) 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;
}
/**
* 设置横向打印方向
* <p>
* 如果打印机不支持横向打印,将输出警告信息并保持默认的纵向打印方向
* </p>
*
* @return 当前PrintAttributeBuilder实例支持链式调用
*/
public PrintAttributeBuilder landscape() {
if (support(OrientationRequested.class) &&
supportsValue(OrientationRequested.LANDSCAPE)) {
attributes.add(OrientationRequested.LANDSCAPE);
}
return this;
}
/**
* 设置纵向打印模式(默认,通常无需显式调用)
* <p>
* 将打印方向设置为纵向模式,这是大多数打印任务的默认方向。
* 如果打印机支持该方向,则会在打印属性中添加 PORTRAIT 方向设置。
* </p>
*
* @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<? extends Attribute> 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<? extends Attribute> attrClass) {
return supportedAttrs.contains(attrClass);
}
/**
* 判断是否支持某个属性
*
* @param attrClass 属性类
* @param <T> 属性类
* @return 是否支持
*/
@SuppressWarnings("unchecked")
private <T extends Attribute> T[] getSupportedValues(final Class<T> 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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<Class<? extends Attribute>> 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));
}
}