此文章用于记录怎么简单的使用github.com/OpenPrinting/goipp打印PDF

讲解

配置req payload

用于制作ipp相关参数载荷

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
	req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1)
	req.Operation.Add(goipp.MakeAttribute("attributes-charset",
		goipp.TagCharset, goipp.String("utf-8")))
	req.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
		goipp.TagLanguage, goipp.String("en-US")))
	req.Operation.Add(goipp.MakeAttribute("printer-uri",
		goipp.TagURI, goipp.String(printerURI)))
	req.Operation.Add(goipp.MakeAttribute("requesting-user-name",
		goipp.TagName, goipp.String("John Doe")))
	req.Operation.Add(goipp.MakeAttribute("job-name",
		goipp.TagName, goipp.String("job name"))) // 任务名称
	req.Operation.Add(goipp.MakeAttribute("document-format",
		goipp.TagMimeType, goipp.String("application/pdf"))) // 格式PDF

	payload, err := req.EncodeBytes()
	if err != nil {
		return fmt.Errorf("failed to encode IPP request: %w", err)
	}

加载文件 (根据各自情况处理)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
	// Open document file
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("failed to open PDF file '%s': %w", filePath, err)
	}
	defer file.Close()

	fileSize := int(0)
	if fileInfo, err := file.Stat(); err == nil {
		fileSize = int(fileInfo.Size())
	} else {
		return fmt.Errorf("failed to get file size: %w", err)
	}

	// Build HTTP request
	size := len(payload) + fileSize // 用于 content-length
	body := io.MultiReader(bytes.NewBuffer(payload), file) // 生成最终body载荷

创建 HTTP Client

1
2
3
4
5
6
7
8
9
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 跳过证书验证, 打印机可能会使用自签证书
	}

	// 创建一个使用自定义 Transport 的 HTTP 客户端
	client := &http.Client{
		Transport: tr,
		Timeout:   60 * time.Second,
	}

配置并发送 HTTP 请求

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
	httpReq, err := http.NewRequest(http.MethodPost, printerURI, body)
	if err != nil {
		return fmt.Errorf("failed to create HTTP request: %w", err)
	}
	httpReq.Header.Set("content-length", strconv.Itoa(size))
	httpReq.Header.Set("content-type", goipp.ContentType)
	httpReq.Header.Set("accept", goipp.ContentType)
	httpReq.Header.Set("accept-encoding", "gzip, deflate, identity")

	// Execute HTTP request
	httpRsp, err := client.Do(httpReq)
	if err != nil {
		return fmt.Errorf("failed to send IPP request: %w", err)
	}
	if httpRsp != nil {
		defer httpRsp.Body.Close()
	}

	if httpRsp.StatusCode != 200 {
		return fmt.Errorf("IPP request failed with status code %d", httpRsp.StatusCode)
	}

处理返回结果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
	// Decode IPP response
	rsp := &goipp.Message{}
	err = rsp.Decode(httpRsp.Body)
	if err != nil {
		return fmt.Errorf("failed to decode IPP response: %w", err)
	}

	if goipp.Status(rsp.Code) != goipp.StatusOk {
		err = errors.New(goipp.Status(rsp.Code).String())
		return err
	}
	return nil

代码实现

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package main

import (
	"bytes"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"log"
	"net"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"
	"time"

	"github.com/OpenPrinting/goipp"
)

func PrintPDF(printerURI, filePath, jobName string, copies int, colorMode string) error {
	req := goipp.NewRequest(goipp.DefaultVersion, goipp.OpPrintJob, 1)
	req.Operation.Add(goipp.MakeAttribute("attributes-charset",
		goipp.TagCharset, goipp.String("utf-8")))
	req.Operation.Add(goipp.MakeAttribute("attributes-natural-language",
		goipp.TagLanguage, goipp.String("en-US")))
	req.Operation.Add(goipp.MakeAttribute("printer-uri",
		goipp.TagURI, goipp.String(printerURI)))
	req.Operation.Add(goipp.MakeAttribute("requesting-user-name",
		goipp.TagName, goipp.String("John Doe")))
	req.Operation.Add(goipp.MakeAttribute("job-name",
		goipp.TagName, goipp.String("job name")))
	req.Operation.Add(goipp.MakeAttribute("document-format",
		goipp.TagMimeType, goipp.String("application/pdf"))) // 格式PDF

	payload, err := req.EncodeBytes()
	if err != nil {
		return fmt.Errorf("failed to encode IPP request: %w", err)
	}

	// Open document file
	file, err := os.Open(filePath)
	if err != nil {
		return fmt.Errorf("failed to open PDF file '%s': %w", filePath, err)
	}
	defer file.Close()

	fileSize := int(0)
	if fileInfo, err := file.Stat(); err == nil {
		fileSize = int(fileInfo.Size())
	} else {
		return fmt.Errorf("failed to get file size: %w", err)
	}

	// Build HTTP request
	size := len(payload) + fileSize
	body := io.MultiReader(bytes.NewBuffer(payload), file)

	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // 跳过证书验证, 打印机可能会使用自签证书
	}

	// 创建一个使用自定义 Transport 的 HTTP 客户端
	client := &http.Client{
		Transport: tr,
		Timeout:   60 * time.Second,
	}

	httpReq, err := http.NewRequest(http.MethodPost, printerURI, body)
	if err != nil {
		return fmt.Errorf("failed to create HTTP request: %w", err)
	}
	httpReq.Header.Set("content-length", strconv.Itoa(size))
	httpReq.Header.Set("content-type", goipp.ContentType)
	httpReq.Header.Set("accept", goipp.ContentType)
	httpReq.Header.Set("accept-encoding", "gzip, deflate, identity")

	// Execute HTTP request
	httpRsp, err := client.Do(httpReq)
	if err != nil {
		return fmt.Errorf("failed to send IPP request: %w", err)
	}
	if httpRsp != nil {
		defer httpRsp.Body.Close()
	}

	if httpRsp.StatusCode != 200 {
		return fmt.Errorf("IPP request failed with status code %d", httpRsp.StatusCode)
	}

	// Decode IPP response
	rsp := &goipp.Message{}
	err = rsp.Decode(httpRsp.Body)
	if err != nil {
		return fmt.Errorf("failed to decode IPP response: %w", err)
	}

	if goipp.Status(rsp.Code) != goipp.StatusOk {
		err = errors.New(goipp.Status(rsp.Code).String())
		return err
	}
	return nil
}

// parseAndCleanURI 解析一个可能格式不正确的IPP URI,
// 并返回干净的、可用于建立连接的 host, port, path 和 useTLS.
// 它可以处理主机名中包含未转义空格的情况.
func parseAndCleanURI(rawURI string) (host string, port int, path string, useTLS bool, err error) {
	// 尝试使用标准库直接解析
	parsedURL, err := url.Parse(rawURI)
	if err == nil && parsedURL.Host != "" {
		// 如果解析成功, 直接使用结果
		host = parsedURL.Hostname()
		portStr := parsedURL.Port()
		if portStr == "" {
			port = 631 // IPP默认端口
		} else {
			fmt.Sscanf(portStr, "%d", &port)
		}
		path = parsedURL.Path
		useTLS = (parsedURL.Scheme == "ipps")
		return
	}

	// 如果标准库解析失败 (很可能是因为主机名中有空格等问题), 我们手动拆分
	log.Printf("Standard URI parsing failed: %v. Attempting manual parsing...", err)

	// 1. 提取 scheme
	scheme := "ipp"
	if strings.HasPrefix(rawURI, "ipps://") {
		useTLS = true
		scheme = "ipps"
	}

	// 2. 去掉 scheme 部分
	uriWithoutScheme := strings.TrimPrefix(rawURI, scheme+"://")

	// 3. 查找路径的开始位置 (第一个 '/')
	pathIndex := strings.Index(uriWithoutScheme, "/")
	var hostAndPort string
	if pathIndex == -1 {
		// 没有路径部分
		hostAndPort = uriWithoutScheme
		path = "/" // 默认路径
	} else {
		hostAndPort = uriWithoutScheme[:pathIndex]
		path = uriWithoutScheme[pathIndex:]
	}

	// 4. 从 hostAndPort 中分离 host 和 port
	// net.SplitHostPort 可以很好地处理 IPv6 地址
	host, portStr, err := net.SplitHostPort(hostAndPort)
	if err != nil {
		// 如果分离失败, 说明可能没有指定端口
		host = hostAndPort
		port = 631 // 使用默认端口
	} else {
		fmt.Sscanf(portStr, "%d", &port)
	}

	if host == "" {
		err = fmt.Errorf("could not determine host from URI: %s", rawURI)
	}

	return
}

// --- 完整的可运行示例 ---

func main() {
	// --- 请根据您的实际情况修改以下参数 ---

	// 打印机地址. 您可以通过以下方式找到它:
	// - 在打印机设置中查看IP地址.
	// - 如果是Mac/Linux, 可能是 "ipp://BRWABC123.local:631/ipp/print" 这样的格式.
	// - 对于标准IPP, 路径通常是 /ipp/print 或 /ipp/printer.
	//   如果 "ipp://192.168.1.100" 不工作, 尝试 "ipp://192.168.1.100:631/ipp/print"
	rawPrinterURI := "ipp://localhost:631/printers/L4200_Series"

	// 要打印的PDF文件路径
	pdfFilePath := "test.pdf" // !<-- 修改这里, 确保文件存在

	// 打印任务名称
	jobName := "My Go Test Document"

	// 打印份数
	copies := 1

	// 色彩模式: "color" 或 "monochrome" (黑白)
	colorMode := "color"

	// --- 执行打印 ---
	log.Printf("Sending job '%s' to printer %s...", jobName, rawPrinterURI)
	log.Printf("File: %s, Copies: %d, Mode: %s", pdfFilePath, copies, colorMode)

	log.Printf("Parsing raw printer URI: %s", rawPrinterURI)
	host, port, path, useTLS, err := parseAndCleanURI(rawPrinterURI)
	if err != nil {
		log.Fatalf("❌ Failed to parse printer URI: %v", err)
	}
	log.Printf("Parsed printer details -> Host: %s, Port: %d, Path: %s, UseTLS: %t", host, port, path, useTLS)

	scheme := "http"
	if useTLS {
		scheme = "https"
	}
	printerURI := fmt.Sprintf("%s://%s:%d%s", scheme, host, port, path)

	err = PrintPDF(printerURI, pdfFilePath, jobName, copies, colorMode)
	if err != nil {
		log.Fatalf("❌ Print job failed: %v", err)
	}

	log.Println("Please check your printer queue.")
}