|
|
|
@@ -5,58 +5,51 @@ import org.apache.pdfbox.rendering.PDFRenderer |
|
|
|
import java.awt.image.BufferedImage |
|
|
|
import java.io.ByteArrayOutputStream |
|
|
|
import java.io.File |
|
|
|
import java.io.OutputStream |
|
|
|
import java.net.Socket |
|
|
|
import javax.imageio.ImageIO |
|
|
|
import org.slf4j.LoggerFactory |
|
|
|
|
|
|
|
class BrotherPrinterUtil { |
|
|
|
|
|
|
|
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) { |
|
|
|
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 -> |
|
|
|
val renderer = PDFRenderer(document) |
|
|
|
|
|
|
|
val totalPages = document.numberOfPages |
|
|
|
repeat(printQty) { copyIndex -> |
|
|
|
println("DEBUG: Printing copy ${copyIndex + 1} of $printQty") |
|
|
|
} |
|
|
|
|
|
|
|
Socket(printerIp, printerPort).use { socket -> |
|
|
|
socket.soTimeout = SOCKET_TIMEOUT_MS |
|
|
|
val os = socket.getOutputStream() |
|
|
|
|
|
|
|
// 1. Start PJL Job |
|
|
|
|
|
|
|
// 1. PJL header (optional; some Brother prefer minimal PJL) |
|
|
|
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.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 { |
|
|
|
val pjl = StringBuilder() |
|
|
|
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) |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 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 width = image.width |
|
|
|
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) { |
|
|
|
val rowBytes = (width + 7) / 8 |
|
|
|
|