|
|
|
@@ -23,11 +23,13 @@ import java.net.InetSocketAddress |
|
|
|
import java.io.PrintWriter |
|
|
|
import java.io.DataOutputStream |
|
|
|
import java.nio.charset.Charset |
|
|
|
import java.nio.charset.StandardCharsets |
|
|
|
|
|
|
|
import java.io.BufferedReader |
|
|
|
import java.io.InputStreamReader |
|
|
|
import java.net.ConnectException |
|
|
|
import java.net.SocketTimeoutException |
|
|
|
import org.springframework.core.io.ClassPathResource |
|
|
|
|
|
|
|
// Data class to store bitmap bytes + width (for XML) |
|
|
|
data class BitmapResult(val bytes: ByteArray, val width: Int) |
|
|
|
@@ -183,46 +185,38 @@ open class PlasticBagPrinterService( |
|
|
|
val packagingJobOrders = normalizedJobOrders.filter { it.jobOrderId in allowedJobOrderIds } |
|
|
|
require(packagingJobOrders.isNotEmpty()) { "No 包裝 process job orders found for export" } |
|
|
|
|
|
|
|
val normalizedCodes = packagingJobOrders |
|
|
|
.map { it.itemCode } |
|
|
|
.distinct() |
|
|
|
|
|
|
|
val sql = """ |
|
|
|
select code, filename |
|
|
|
from onpack_qr |
|
|
|
where code in (:itemCodes) |
|
|
|
order by code asc |
|
|
|
""".trimIndent() |
|
|
|
|
|
|
|
val rows = jdbcDao.queryForList( |
|
|
|
sql, |
|
|
|
mapOf("itemCodes" to normalizedCodes), |
|
|
|
) |
|
|
|
|
|
|
|
require(rows.isNotEmpty()) { "No OnPack QR records found for the selected date" } |
|
|
|
val exportItems = packagingJobOrders |
|
|
|
.groupBy { it.itemCode.trim().lowercase() } |
|
|
|
.mapNotNull { (codeLower, orders) -> |
|
|
|
val order = orders.firstOrNull() ?: return@mapNotNull null |
|
|
|
val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(order.jobOrderId) |
|
|
|
?: return@mapNotNull null |
|
|
|
val itemId = stockInLine.item?.id ?: return@mapNotNull null |
|
|
|
val stockInLineId = stockInLine.id ?: return@mapNotNull null |
|
|
|
Triple(codeLower, itemId, stockInLineId) |
|
|
|
} |
|
|
|
|
|
|
|
val filenameByCode = rows.associate { row -> |
|
|
|
row["code"]?.toString()?.trim().orEmpty() to row["filename"]?.toString()?.trim().orEmpty() |
|
|
|
} |
|
|
|
require(exportItems.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } |
|
|
|
|
|
|
|
val baos = ByteArrayOutputStream() |
|
|
|
ZipOutputStream(baos).use { zos -> |
|
|
|
val addedEntries = linkedSetOf<String>() |
|
|
|
packagingJobOrders.forEach { jobOrder -> |
|
|
|
val filename = filenameByCode[jobOrder.itemCode].orEmpty() |
|
|
|
if (filename.isBlank()) return@forEach |
|
|
|
|
|
|
|
val stockInLine = stockInLineRepository.findFirstByJobOrder_IdAndDeletedFalse(jobOrder.jobOrderId) |
|
|
|
?: return@forEach |
|
|
|
val itemId = stockInLine.item?.id ?: return@forEach |
|
|
|
val stockInLineId = stockInLine.id ?: return@forEach |
|
|
|
exportItems.forEach { (codeLower, itemId, stockInLineId) -> |
|
|
|
val imageTemplate = loadOnPackImageTemplateOrNull(codeLower) ?: return@forEach |
|
|
|
|
|
|
|
val qrContent = """{"itemId": $itemId, "stockInLineId": $stockInLineId}""" |
|
|
|
// Trim 90% of top/bottom/side whitespace: keep 4px padding per side (was 40) → totalSize = contentSize + 8 |
|
|
|
val bmp = createQrCodeBitmap(qrContent, 600, 600 + 8) |
|
|
|
val zipEntryName = buildUniqueZipEntryName(filename, addedEntries) |
|
|
|
if (!addedEntries.add(zipEntryName)) return@forEach |
|
|
|
addToZip(zos, zipEntryName, bmp.bytes) |
|
|
|
// Reduce top/bottom whitespace by 90% for exported QR images (40px -> 4px). |
|
|
|
val bmp = createQrCodeBitmap(qrContent, contentSize = 600, horizontalPadding = 40, verticalPadding = 4) |
|
|
|
val qrBmpFileName = "${codeLower}qr.bmp" |
|
|
|
val imageFileName = "$codeLower.image" |
|
|
|
val imageContent = withOnPackLogo4Bmp(imageTemplate, qrBmpFileName) |
|
|
|
|
|
|
|
if (addedEntries.add(qrBmpFileName)) { |
|
|
|
addToZip(zos, qrBmpFileName, bmp.bytes) |
|
|
|
} |
|
|
|
if (addedEntries.add(imageFileName)) { |
|
|
|
addToZip(zos, imageFileName, imageContent) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
require(addedEntries.isNotEmpty()) { "No OnPack QR files could be generated for the selected date" } |
|
|
|
@@ -231,6 +225,24 @@ open class PlasticBagPrinterService( |
|
|
|
return baos.toByteArray() |
|
|
|
} |
|
|
|
|
|
|
|
private fun loadOnPackImageTemplateOrNull(codeLower: String): ByteArray? { |
|
|
|
val resourcePath = "onpack2030/${codeLower}.image" |
|
|
|
val resource = ClassPathResource(resourcePath) |
|
|
|
if (!resource.exists()) return null |
|
|
|
return resource.inputStream.use { it.readBytes() } |
|
|
|
} |
|
|
|
|
|
|
|
private fun withOnPackLogo4Bmp(imageBytes: ByteArray, qrBmpFileName: String): ByteArray { |
|
|
|
// Use ISO-8859-1 one-byte mapping so all original bytes are preserved, |
|
|
|
// while replacing only ASCII XML fragment for LOGO_4 filename. |
|
|
|
val oneByteText = String(imageBytes, StandardCharsets.ISO_8859_1) |
|
|
|
val replaced = oneByteText.replace( |
|
|
|
Regex("""(<Name>\s*LOGO_4\s*</Name>[\s\S]*?<FileName>)([^<]+)(</FileName>)"""), |
|
|
|
"$1$qrBmpFileName$3", |
|
|
|
) |
|
|
|
return replaced.toByteArray(StandardCharsets.ISO_8859_1) |
|
|
|
} |
|
|
|
|
|
|
|
private fun createMonochromeBitmap(text: String, targetHeight: Int): BitmapResult { |
|
|
|
// Step 1: Measure text width with temporary image |
|
|
|
val tempImg = BufferedImage(1, 1, BufferedImage.TYPE_BYTE_BINARY) |
|
|
|
@@ -364,22 +376,29 @@ open class PlasticBagPrinterService( |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private fun createQrCodeBitmap(content: String, contentSize: Int, totalSize: Int = contentSize + 80): BitmapResult { |
|
|
|
if (totalSize < contentSize) throw IllegalArgumentException("totalSize must be >= contentSize") |
|
|
|
private fun createQrCodeBitmap( |
|
|
|
content: String, |
|
|
|
contentSize: Int, |
|
|
|
horizontalPadding: Int = 40, |
|
|
|
verticalPadding: Int = 40, |
|
|
|
): BitmapResult { |
|
|
|
require(horizontalPadding >= 0) { "horizontalPadding must be >= 0" } |
|
|
|
require(verticalPadding >= 0) { "verticalPadding must be >= 0" } |
|
|
|
val totalWidth = contentSize + (horizontalPadding * 2) |
|
|
|
val totalHeight = contentSize + (verticalPadding * 2) |
|
|
|
|
|
|
|
val writer = QRCodeWriter() |
|
|
|
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, contentSize, contentSize) |
|
|
|
|
|
|
|
val image = BufferedImage(totalSize, totalSize, BufferedImage.TYPE_BYTE_BINARY) |
|
|
|
val image = BufferedImage(totalWidth, totalHeight, BufferedImage.TYPE_BYTE_BINARY) |
|
|
|
val g = image.createGraphics() |
|
|
|
g.color = Color.WHITE |
|
|
|
g.fillRect(0, 0, totalSize, totalSize) |
|
|
|
g.fillRect(0, 0, totalWidth, totalHeight) |
|
|
|
|
|
|
|
val offset = (totalSize - contentSize) / 2 |
|
|
|
for (x in 0 until contentSize) { |
|
|
|
for (y in 0 until contentSize) { |
|
|
|
if (bitMatrix.get(x, y)) { |
|
|
|
image.setRGB(x + offset, y + offset, Color.BLACK.rgb) |
|
|
|
image.setRGB(x + horizontalPadding, y + verticalPadding, Color.BLACK.rgb) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|