| @@ -7,6 +7,8 @@ import org.springframework.stereotype.Service | |||||
| import java.awt.Color | import java.awt.Color | ||||
| import java.awt.Font | import java.awt.Font | ||||
| import java.awt.Graphics2D | |||||
| import java.awt.RenderingHints | |||||
| import java.awt.image.BufferedImage | import java.awt.image.BufferedImage | ||||
| import java.io.ByteArrayOutputStream | import java.io.ByteArrayOutputStream | ||||
| import java.util.zip.ZipEntry | import java.util.zip.ZipEntry | ||||
| @@ -20,6 +22,9 @@ import java.io.PrintWriter | |||||
| import java.io.DataOutputStream | import java.io.DataOutputStream | ||||
| import java.nio.charset.Charset | import java.nio.charset.Charset | ||||
| // 1. Data class to store both the raw bytes and the width for XML injection | |||||
| data class BitmapResult(val bytes: ByteArray, val width: Int) | |||||
| @Service | @Service | ||||
| open class PlasticBagPrinterService( | open class PlasticBagPrinterService( | ||||
| val jobOrderRepository: JobOrderRepository, | val jobOrderRepository: JobOrderRepository, | ||||
| @@ -28,65 +33,105 @@ open class PlasticBagPrinterService( | |||||
| val baos = ByteArrayOutputStream() | val baos = ByteArrayOutputStream() | ||||
| ZipOutputStream(baos).use { zos -> | ZipOutputStream(baos).use { zos -> | ||||
| // Use unique names based on the Lot Number | |||||
| // Generate Bitmaps and capture their widths | |||||
| val productBmp = createMonochromeBitmap(productName, 704) | |||||
| val codeBmp = createMonochromeBitmap(itemCode, 1173) | |||||
| val expBmp = createMonochromeBitmap(expiryDate, 1173) | |||||
| val qrBmp = createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 1000) | |||||
| val nameFile = "${lotNo}_product.bmp" | val nameFile = "${lotNo}_product.bmp" | ||||
| val codeFile = "${lotNo}_code.bmp" | |||||
| val expFile = "${lotNo}_expiry.bmp" | val expFile = "${lotNo}_expiry.bmp" | ||||
| val qrFile = "${lotNo}_qr.bmp" | val qrFile = "${lotNo}_qr.bmp" | ||||
| // 1. Generate Bitmaps | |||||
| addToZip(zos, nameFile, createMonochromeBitmap(productName, 5365, 704)) | |||||
| addToZip(zos, expFile, createMonochromeBitmap(expiryDate, 4203, 1173)) | |||||
| addToZip(zos, qrFile, createQrCodeBitmap("$itemCode|$lotNo|$expiryDate", 1000)) | |||||
| // Now "bytes" property is resolved | |||||
| addToZip(zos, nameFile, productBmp.bytes) | |||||
| addToZip(zos, codeFile, codeBmp.bytes) | |||||
| addToZip(zos, expFile, expBmp.bytes) | |||||
| addToZip(zos, qrFile, qrBmp.bytes) | |||||
| // 2. Generate the .image file with dynamic references | |||||
| // 2. Generate the .image file with XML headers and mandatory X40 tags | |||||
| val imageXml = """ | val imageXml = """ | ||||
| <?xml version="1.0" encoding="utf-8"?> | |||||
| <Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray"> | <Legend type="Image.V1" PaperColor="White" BackgroundColor="DarkGray"> | ||||
| <DeviceInfo DeviceURI="NGEDriver[SDX40c.cadb94e3-677c-4bcb-9c80-88bf9526d6fe]/"><DeviceName /></DeviceInfo> | |||||
| <Mirrored>false</Mirrored> | |||||
| <Orientation>DEG_0</Orientation> | |||||
| <Width>5300</Width><Height>7100</Height> | |||||
| <FieldList> | <FieldList> | ||||
| <Logo> | |||||
| <Name>PRODUCT_NAME</Name> | |||||
| <Logo type="Logo.V1"> | |||||
| <Name>LOGO</Name> | |||||
| <Geometry><X>0</X><Y>250</Y></Geometry> | <Geometry><X>0</X><Y>250</Y></Geometry> | ||||
| <FieldColor>BLACK</FieldColor> | |||||
| <FileName>$nameFile</FileName> | <FileName>$nameFile</FileName> | ||||
| <Width>${productBmp.width}</Width> | |||||
| <Height>704</Height> | |||||
| </Logo> | </Logo> | ||||
| <Logo> | |||||
| <Name>EXPIRY_DATE</Name> | |||||
| <Logo type="Logo.V1"> | |||||
| <Name>LOGO_2</Name> | |||||
| <Geometry><X>1000</X><Y>1250</Y></Geometry> | |||||
| <FieldColor>BLACK</FieldColor> | |||||
| <FileName>$codeFile</FileName> | |||||
| <Width>${codeBmp.width}</Width> | |||||
| <Height>1173</Height> | |||||
| </Logo> | |||||
| <Logo type="Logo.V1"> | |||||
| <Name>LOGO_3</Name> | |||||
| <Geometry><X>500</X><Y>2500</Y></Geometry> | <Geometry><X>500</X><Y>2500</Y></Geometry> | ||||
| <FieldColor>BLACK</FieldColor> | |||||
| <FileName>$expFile</FileName> | <FileName>$expFile</FileName> | ||||
| <Width>${expBmp.width}</Width> | |||||
| <Height>1173</Height> | |||||
| </Logo> | </Logo> | ||||
| <Logo> | |||||
| <Name>QR_CODE</Name> | |||||
| <Logo type="Logo.V1"> | |||||
| <Name>LOGO_4</Name> | |||||
| <Geometry><X>750</X><Y>3750</Y></Geometry> | <Geometry><X>750</X><Y>3750</Y></Geometry> | ||||
| <FieldColor>BLACK</FieldColor> | |||||
| <FileName>$qrFile</FileName> | <FileName>$qrFile</FileName> | ||||
| <Width>${qrBmp.width}</Width> | |||||
| <Height>1000</Height> | |||||
| </Logo> | </Logo> | ||||
| </FieldList> | </FieldList> | ||||
| </Legend> | </Legend> | ||||
| """.trimIndent() | """.trimIndent() | ||||
| addToZip(zos, "$lotNo.image", imageXml.toByteArray()) | |||||
| addToZip(zos, "$lotNo.image", imageXml.toByteArray(Charsets.UTF_8)) | |||||
| // 3. Generate the .job file pointing to the new .image [cite: 2] | |||||
| val jobXml = "<Job><ImageFileName>$lotNo.image</ImageFileName></Job>" | |||||
| addToZip(zos, "$lotNo.job", jobXml.toByteArray()) | |||||
| val jobXml = """<?xml version="1.0" encoding="utf-8"?><Job><ImageFileName>$lotNo.image</ImageFileName></Job>""" | |||||
| addToZip(zos, "$lotNo.job", jobXml.toByteArray(Charsets.UTF_8)) | |||||
| } | } | ||||
| return baos.toByteArray() | return baos.toByteArray() | ||||
| } | } | ||||
| private fun createMonochromeBitmap(text: String, width: Int, height: Int): ByteArray { | |||||
| val image = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY) // Essential for printers | |||||
| val g = image.createGraphics() | |||||
| g.color = Color.WHITE | |||||
| g.fillRect(0, 0, width, height) | |||||
| g.color = Color.BLACK | |||||
| g.font = Font("Arial", Font.BOLD, 400) | |||||
| g.drawString(text, 50, height - 200) | |||||
| g.dispose() | |||||
| private fun createMonochromeBitmap(text: String, maxHeight: Int): BitmapResult { | |||||
| val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) | |||||
| val g2dTemp = tempImg.createGraphics() | |||||
| val font = Font("SimSun", Font.BOLD, (maxHeight * 0.8).toInt()) | |||||
| g2dTemp.font = font | |||||
| val metrics = g2dTemp.getFontMetrics(font) | |||||
| val width = metrics.stringWidth(text).let { if (it < 1) 1 else it } | |||||
| val height = maxHeight | |||||
| g2dTemp.dispose() | |||||
| val finalImg = BufferedImage(width, height, BufferedImage.TYPE_BYTE_BINARY) | |||||
| val g2d = finalImg.createGraphics() | |||||
| g2d.color = Color.WHITE | |||||
| g2d.fillRect(0, 0, width, height) | |||||
| g2d.color = Color.BLACK | |||||
| g2d.font = font | |||||
| g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_OFF) | |||||
| g2d.drawString(text, 0, metrics.ascent) | |||||
| g2d.dispose() | |||||
| val baos = ByteArrayOutputStream() | val baos = ByteArrayOutputStream() | ||||
| ImageIO.write(image, "bmp", baos) | |||||
| return baos.toByteArray() | |||||
| ImageIO.write(finalImg, "bmp", baos) | |||||
| return BitmapResult(baos.toByteArray(), width) | |||||
| } | } | ||||
| private fun createQrCodeBitmap(content: String, size: Int): ByteArray { | |||||
| val bitMatrix = QRCodeWriter().encode(content, BarcodeFormat.QR_CODE, size, size) | |||||
| private fun createQrCodeBitmap(content: String, size: Int): BitmapResult { | |||||
| val writer = QRCodeWriter() | |||||
| val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, size, size) | |||||
| val image = BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY) | val image = BufferedImage(size, size, BufferedImage.TYPE_BYTE_BINARY) | ||||
| for (x in 0 until size) { | for (x in 0 until size) { | ||||
| for (y in 0 until size) { | for (y in 0 until size) { | ||||
| @@ -95,11 +140,13 @@ open class PlasticBagPrinterService( | |||||
| } | } | ||||
| val baos = ByteArrayOutputStream() | val baos = ByteArrayOutputStream() | ||||
| ImageIO.write(image, "bmp", baos) | ImageIO.write(image, "bmp", baos) | ||||
| return baos.toByteArray() | |||||
| // For QR code, width and height are the same (size) | |||||
| return BitmapResult(baos.toByteArray(), size) | |||||
| } | } | ||||
| private fun addToZip(zos: ZipOutputStream, fileName: String, content: ByteArray) { | |||||
| zos.putNextEntry(ZipEntry(fileName)) | |||||
| private fun addToZip(zos: ZipOutputStream, entryName: String, content: ByteArray) { | |||||
| val entry = ZipEntry(entryName) | |||||
| zos.putNextEntry(entry) | |||||
| zos.write(content) | zos.write(content) | ||||
| zos.closeEntry() | zos.closeEntry() | ||||
| } | } | ||||