| @@ -5,58 +5,51 @@ import org.apache.pdfbox.rendering.PDFRenderer | |||||
| import java.awt.image.BufferedImage | import java.awt.image.BufferedImage | ||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||
| import java.io.File | import java.io.File | ||||
| import java.io.OutputStream | |||||
| import java.net.Socket | import java.net.Socket | ||||
| import javax.imageio.ImageIO | |||||
| import org.slf4j.LoggerFactory | |||||
| class BrotherPrinterUtil { | class BrotherPrinterUtil { | ||||
| companion object { | companion object { | ||||
| private const val SOCKET_TIMEOUT_MS = 120_000 // 2 min for large jobs; Brother may be slow to accept | |||||
| private const val BROTHER_DPI = 200 // Lower DPI = smaller job; some Brother models buffer better at 200 | |||||
| private val log = LoggerFactory.getLogger(BrotherPrinterUtil::class.java) | |||||
| /** | /** | ||||
| * //Usage | |||||
| * BrotherPrinterUtil.printToBrother( | |||||
| pdfFile = File("document.pdf"), | |||||
| printerIp = "192.168.1.50", | |||||
| printQty = 1 | |||||
| ) | |||||
| * Sends a PDF to a Brother DCP-1610W by rendering it to a PCL-compatible format. | |||||
| * Sends a PDF to a Brother (e.g. DCP-1610W) by rendering to PCL raster over port 9100. | |||||
| * Uses socket timeout and flush-per-page so the printer has time to accept data. | |||||
| */ | */ | ||||
| fun printToBrother(pdfFile: File, printerIp: String, printerPort: Int = 9100, printQty: Int = 1) { | fun printToBrother(pdfFile: File, printerIp: String, printerPort: Int = 9100, printQty: Int = 1) { | ||||
| if (!pdfFile.exists()) throw IllegalArgumentException("File not found.") | |||||
| if (!pdfFile.exists()) throw IllegalArgumentException("File not found: ${pdfFile.absolutePath}") | |||||
| println("DEBUG: PDF file size: ${pdfFile.length()} bytes") | |||||
| log.info("Brother print: file=${pdfFile.name} size=${pdfFile.length()} ip=$printerIp port=$printerPort copies=$printQty") | |||||
| PDDocument.load(pdfFile).use { document -> | PDDocument.load(pdfFile).use { document -> | ||||
| val renderer = PDFRenderer(document) | val renderer = PDFRenderer(document) | ||||
| val totalPages = document.numberOfPages | val totalPages = document.numberOfPages | ||||
| repeat(printQty) { copyIndex -> | |||||
| println("DEBUG: Printing copy ${copyIndex + 1} of $printQty") | |||||
| } | |||||
| Socket(printerIp, printerPort).use { socket -> | Socket(printerIp, printerPort).use { socket -> | ||||
| socket.soTimeout = SOCKET_TIMEOUT_MS | |||||
| val os = socket.getOutputStream() | val os = socket.getOutputStream() | ||||
| // 1. Start PJL Job | |||||
| // 1. PJL header (optional; some Brother prefer minimal PJL) | |||||
| os.write(getPjlHeader(printQty)) | os.write(getPjlHeader(printQty)) | ||||
| os.flush() | |||||
| // 2. Render each page as a 300 DPI image | |||||
| for (pageIndex in 0 until document.numberOfPages) { | |||||
| val image = renderer.renderImageWithDPI(pageIndex, 300f, org.apache.pdfbox.rendering.ImageType.BINARY) | |||||
| // 3. Convert Image to PCL Raster Data | |||||
| val pclData = convertImageToPcl(image) | |||||
| os.write(pclData) | |||||
| // Form Feed (Move to next page) | |||||
| os.write("\u000C".toByteArray()) | |||||
| repeat(printQty) { copyIndex -> | |||||
| for (pageIndex in 0 until totalPages) { | |||||
| val image = renderer.renderImageWithDPI(pageIndex, BROTHER_DPI.toFloat(), org.apache.pdfbox.rendering.ImageType.BINARY) | |||||
| val pclData = convertImageToPcl(image, BROTHER_DPI) | |||||
| os.write(pclData) | |||||
| os.write("\u000C".toByteArray(Charsets.US_ASCII)) // Form feed | |||||
| os.flush() // Flush per page so Brother receives incrementally | |||||
| } | |||||
| } | } | ||||
| // 4. End Job | |||||
| os.write("\u001B%-12345X".toByteArray(Charsets.US_ASCII)) | os.write("\u001B%-12345X".toByteArray(Charsets.US_ASCII)) | ||||
| os.flush() | os.flush() | ||||
| println("DEBUG: Print job sent to printer") | |||||
| log.info("Brother print job sent successfully") | |||||
| } | } | ||||
| } | } | ||||
| } | } | ||||
| @@ -64,24 +57,25 @@ class BrotherPrinterUtil { | |||||
| private fun getPjlHeader(qty: Int): ByteArray { | private fun getPjlHeader(qty: Int): ByteArray { | ||||
| val pjl = StringBuilder() | val pjl = StringBuilder() | ||||
| pjl.append("\u001B%-12345X@PJL\r\n") | pjl.append("\u001B%-12345X@PJL\r\n") | ||||
| pjl.append("@PJL SET COPIES=$qty\r\n") | |||||
| pjl.append("@PJL ENTER LANGUAGE=PCL\r\n") // Tell printer to expect PCL graphics, not PDF | |||||
| pjl.append("@PJL SET COPIES=1\r\n") // We send copies in app; some Brother ignore COPIES or mis-handle it | |||||
| pjl.append("@PJL ENTER LANGUAGE=PCL\r\n") | |||||
| return pjl.toString().toByteArray(Charsets.US_ASCII) | return pjl.toString().toByteArray(Charsets.US_ASCII) | ||||
| } | } | ||||
| /** | /** | ||||
| * Converts a BufferedImage into PCL Level 3/5 Raster Graphics. | |||||
| * This is the "magic" that allows a budget printer to print a PDF. | |||||
| * Converts a BufferedImage into PCL raster graphics for Brother. | |||||
| * Uses given DPI (e.g. 200) to keep job size smaller for printers with limited buffer. | |||||
| */ | */ | ||||
| private fun convertImageToPcl(image: BufferedImage): ByteArray { | |||||
| private fun convertImageToPcl(image: BufferedImage, dpi: Int): ByteArray { | |||||
| val out = ByteArrayOutputStream() | val out = ByteArrayOutputStream() | ||||
| val width = image.width | val width = image.width | ||||
| val height = image.height | val height = image.height | ||||
| // PCL: Start Graphics at 300 DPI | |||||
| out.write("\u001B*t300R".toByteArray()) | |||||
| // PCL: Start Raster Graphics | |||||
| out.write("\u001B*r1A".toByteArray()) | |||||
| // PCL reset (Brother often needs clean state) | |||||
| out.write("\u001BE".toByteArray(Charsets.US_ASCII)) // Esc E = reset | |||||
| // PCL: Set resolution and start raster | |||||
| out.write("\u001B*t${dpi}R".toByteArray(Charsets.US_ASCII)) | |||||
| out.write("\u001B*r1A".toByteArray(Charsets.US_ASCII)) | |||||
| for (y in 0 until height) { | for (y in 0 until height) { | ||||
| val rowBytes = (width + 7) / 8 | val rowBytes = (width + 7) / 8 | ||||