diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 787177a56..de4d18f48 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -19,6 +19,11 @@ jobs: - run: sudo out/qz-tray-*.run - run: /opt/qz-tray/qz-tray --version - run: ant nsis + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: qz-tray-linux-jdk${{ matrix.java }} + path: out/qz-tray-*.run macos: runs-on: [macos-latest] @@ -42,6 +47,11 @@ jobs: - run: "'/Applications/QZ Tray.app/Contents/MacOS/QZ Tray' --version" - run: ant makeself - run: ant nsis + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: qz-tray-macos-jdk${{ matrix.java }} + path: out/qz-tray-*.pkg windows: runs-on: [windows-latest] @@ -58,3 +68,8 @@ jobs: - run: ant nsis - run: Start-Process -Wait ./out/qz-tray-*.exe -ArgumentList "/S" - run: "&'C:/Program Files/QZ Tray/qz-tray.exe' --wait --version|Out-Null" + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: qz-tray-windows-jdk${{ matrix.java }} + path: out/qz-tray-*.exe diff --git a/src/qz/printer/PrintOptions.java b/src/qz/printer/PrintOptions.java index 1a34bfed6..3074fdbbb 100644 --- a/src/qz/printer/PrintOptions.java +++ b/src/qz/printer/PrintOptions.java @@ -95,6 +95,11 @@ public PrintOptions(JSONObject configOpts, PrintOutput output, PrintingUtilities rawOptions.retainTemp = configOpts.optBoolean("retainTemp", false); } + if (!configOpts.isNull("imageEncoding")) { + String imageEncoding = configOpts.optString("imageEncoding", String.valueOf(ImageEncoding.ESC_STAR)); + rawOptions.imageEncoding = ImageEncoding.valueOf(imageEncoding); + } + //check for pixel options if (!configOpts.isNull("units")) { @@ -423,6 +428,7 @@ public class Raw { private int copies = 1; //Job copies private String jobName = null; //Job name private boolean retainTemp = false; //Retain any temporary files + private ImageEncoding imageEncoding = ImageEncoding.ESC_STAR; //Image encoding public boolean isForceRaw() { @@ -454,6 +460,10 @@ public int getCopies() { public String getJobName(String defaultVal) { return jobName == null || jobName.isEmpty()? defaultVal:jobName; } + + public ImageEncoding getImageEncoding() { + return imageEncoding; + } } /** Pixel printing options */ @@ -749,4 +759,10 @@ public Chromaticity getAsChromaticity() { } } + /** Raw image encoding option */ + public enum ImageEncoding { + ESC_STAR, + GS_V_0, + GS_L + } } diff --git a/src/qz/printer/action/PrintRaw.java b/src/qz/printer/action/PrintRaw.java index aa4a3a52b..fac441abb 100644 --- a/src/qz/printer/action/PrintRaw.java +++ b/src/qz/printer/action/PrintRaw.java @@ -279,6 +279,13 @@ private ImageWrapper getWrapper(BufferedImage img, JSONObject opt, PrintOptions. ImageWrapper iw = new ImageWrapper(img, LanguageType.getType(opt.optString("language"))); iw.setCharset(Charset.forName(destEncoding)); + // Set image encoding + try { + iw.setImageEncoding(PrintOptions.ImageEncoding.valueOf(opt.optString("imageEncoding"))); + } catch (IllegalArgumentException e) { + iw.setImageEncoding(PrintOptions.ImageEncoding.ESC_STAR); + } + //ESC/POS only int density = opt.optInt("dotDensity", -1); if (density == -1) { diff --git a/src/qz/printer/action/raw/ImageWrapper.java b/src/qz/printer/action/raw/ImageWrapper.java index 7ea4514de..8220aeecc 100644 --- a/src/qz/printer/action/raw/ImageWrapper.java +++ b/src/qz/printer/action/raw/ImageWrapper.java @@ -16,12 +16,13 @@ import org.apache.logging.log4j.Logger; import qz.common.ByteArrayBuilder; import qz.exception.InvalidRawImageException; +import qz.printer.PrintOptions.ImageEncoding; +import qz.printer.action.raw.encoder.*; import qz.utils.ByteUtilities; import javax.imageio.ImageIO; import java.awt.*; import java.awt.image.BufferedImage; -import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URL; @@ -84,6 +85,7 @@ public class ImageWrapper { private String logoId = ""; // PGL only, the logo ID private boolean igpDots = false; // PGL only, toggle IGP/PGL default resolution of 72dpi private int dotDensity = 32; // Generally 32 = Single (normal) 33 = Double (higher res) for ESC/POS. Irrelevant for all other languages. + private ImageEncoding imageEncoding = ImageEncoding.ESC_STAR; private boolean legacyMode = false; // Use newlines for ESC/POS spacing; simulates <=2.0.11 behavior @@ -205,6 +207,10 @@ public void setDotDensity(int dotDensity) { this.dotDensity = Math.abs(dotDensity); } + public void setImageEncoding(ImageEncoding imageEncoding) { + this.imageEncoding = imageEncoding; + } + public void setLogoId(String logoId) { this.logoId = logoId; } @@ -335,7 +341,13 @@ public byte[] getImageCommand(JSONObject opt) throws InvalidRawImageException, U switch(languageType) { case ESCP: - appendEpsonSlices(getByteBuffer()); + if (imageEncoding == ImageEncoding.GS_V_0) { + getByteBuffer().append(new GsV0Encoder().encode(bufferedImage)); + } else if (imageEncoding == ImageEncoding.GS_L) { + getByteBuffer().append(new GsLEncoder().encode(bufferedImage)); + } else { + appendEpsonSlices(getByteBuffer()); + } break; case ZPL: String zplHexAsString = ByteUtilities.getHexString(getImageAsIntArray()); diff --git a/src/qz/printer/action/raw/encoder/GsLEncoder.java b/src/qz/printer/action/raw/encoder/GsLEncoder.java new file mode 100644 index 000000000..90d4bb0d9 --- /dev/null +++ b/src/qz/printer/action/raw/encoder/GsLEncoder.java @@ -0,0 +1,128 @@ +package qz.printer.action.raw.encoder; + +import qz.common.ByteArrayBuilder; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; + +public class GsLEncoder implements ImageEncoder { + @Override + public byte[] encode(BufferedImage image) { + ByteArrayBuilder builder = new ByteArrayBuilder(); + if (image == null) return builder.getByteArray(); + + int width = image.getWidth(); + int height = image.getHeight(); + final int sliceHeight = 24; + + for (int y = 0; y < height; y += sliceHeight) { + int slicedHeight = Math.min(sliceHeight, height - y); + + // Create a sliced image from the full image + BufferedImage slicedImage = image.getSubimage(0, y, width, slicedHeight); + + // Append the store graphic command + byte[] storeCommand = generateStoreCommand(slicedImage); + builder.append(storeCommand); + + // Append the print graphic command + byte[] printCommand = generatePrintCommand(); + builder.append(printCommand); + } + + return builder.getByteArray(); + } + + /** + * Generates the store graphic command (GS ( L with fn = 112) for the given image + */ + private static byte[] generateStoreCommand(BufferedImage image) { + int width = image.getWidth(); + int height = image.getHeight(); + + // Convert image to monochrome + BufferedImage monoImage = convertToMonochrome(image); + + // Calculate bytes needed for image data + int bytesPerRow = (width + 7) / 8; // Round up to the nearest byte + byte[] imageData = new byte[bytesPerRow * height]; + + // Convert image to the bit array + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = monoImage.getRGB(x, y); + // If the pixel is black (or dark), set bit to 1 + boolean isBlack = (rgb & 0xFF) < 128; + if (isBlack) { + int byteIndex = y * bytesPerRow + (x / 8); + int bitIndex = 7 - (x % 8); + imageData[byteIndex] |= (byte) (1 << bitIndex); + } + } + } + + // Calculate command parameters + int dataLength = imageData.length + 10; // 10 bytes for parameters + int pL = dataLength & 0xFF; + int pH = (dataLength >> 8) & 0xFF; + int m = 48; // Command header + int fn = 112; // Function 112: Store the graphics data in the print buffer (raster format) + int a = 48; // Normal mode + int bx = 1; // Horizontal scale + int by = 1; // Vertical scale + int c = 49; // Single color + int xL = width & 0xFF; + int xH = (width >> 8) & 0xFF; + int yL = height & 0xFF; + int yH = (height >> 8) & 0xFF; + + // Build command + ByteArrayOutputStream command = new ByteArrayOutputStream(); + command.write(0x1D); // GS + command.write('('); + command.write('L'); + command.write(pL); + command.write(pH); + command.write(m); + command.write(fn); + command.write(a); + command.write(bx); + command.write(by); + command.write(c); + command.write(xL); + command.write(xH); + command.write(yL); + command.write(yH); + command.write(imageData, 0, imageData.length); + + return command.toByteArray(); + } + + /** + * Generates the print graphic command (GS ( L with fn = 50) + */ + public static byte[] generatePrintCommand() { + ByteArrayOutputStream command = new ByteArrayOutputStream(); + command.write(0x1D); // GS + command.write('('); + command.write('L'); + command.write(2); // pL + command.write(0); // pH + command.write(48); // m + command.write(50); // Function 50: Print the graphics data in the print buffer + + return command.toByteArray(); + } + + /** + * Converts a BufferedImage to monochrome (1-bit) format + */ + private static BufferedImage convertToMonochrome(BufferedImage original) { + BufferedImage mono = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = mono.createGraphics(); + g2d.drawImage(original, 0, 0, null); + g2d.dispose(); + return mono; + } +} diff --git a/src/qz/printer/action/raw/encoder/GsV0Encoder.java b/src/qz/printer/action/raw/encoder/GsV0Encoder.java new file mode 100644 index 000000000..d84b3d7c8 --- /dev/null +++ b/src/qz/printer/action/raw/encoder/GsV0Encoder.java @@ -0,0 +1,93 @@ +package qz.printer.action.raw.encoder; + +import qz.common.ByteArrayBuilder; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; + +public class GsV0Encoder implements ImageEncoder { + @Override + public byte[] encode(BufferedImage image) { + ByteArrayBuilder builder = new ByteArrayBuilder(); + if (image == null) return builder.getByteArray(); + + int width = image.getWidth(); + int height = image.getHeight(); + final int sliceHeight = 24; + + for (int y = 0; y < height; y += sliceHeight) { + int slicedHeight = Math.min(sliceHeight, height - y); + + // Create a sliced image from the full image + BufferedImage slicedImage = image.getSubimage(0, y, width, slicedHeight); + + // Append the GS v 0 command for the slice + byte[] command = generateGsV0Command(slicedImage); + builder.append(command); + } + + return builder.getByteArray(); + } + + /** + * Generates the GS v 0 command for the given image slice. + * Command: GS v 0 m xL xH yL yH d1...dk + */ + private byte[] generateGsV0Command(BufferedImage image) { + int width = image.getWidth(); + int height = image.getHeight(); + + // Convert image to monochrome + BufferedImage monoImage = convertToMonochrome(image); + + // Calculate bytes needed for image data + int bytesPerRow = (width + 7) / 8; // Round up to the nearest byte + byte[] imageData = new byte[bytesPerRow * height]; + + // Convert image to the bit array + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int rgb = monoImage.getRGB(x, y); + // If the pixel is black (or dark), set bit to 1 + boolean isBlack = (rgb & 0xFF) < 128; + if (isBlack) { + int byteIndex = y * bytesPerRow + (x / 8); + int bitIndex = 7 - (x % 8); + imageData[byteIndex] |= (byte) (1 << bitIndex); + } + } + } + + // Calculate command parameters + int xL = bytesPerRow & 0xFF; + int xH = (bytesPerRow >> 8) & 0xFF; + int yL = height & 0xFF; + int yH = (height >> 8) & 0xFF; + + // Build command + ByteArrayOutputStream command = new ByteArrayOutputStream(); + command.write(0x1D); // GS + command.write('v'); // 0x76 + command.write('0'); // 0x30 + command.write(0); // m = 0 (normal mode) + command.write(xL); + command.write(xH); + command.write(yL); + command.write(yH); + command.write(imageData, 0, imageData.length); + + return command.toByteArray(); + } + + /** + * Converts a BufferedImage to monochrome (1-bit) format + */ + private static BufferedImage convertToMonochrome(BufferedImage original) { + BufferedImage mono = new BufferedImage(original.getWidth(), original.getHeight(), BufferedImage.TYPE_BYTE_BINARY); + Graphics2D g2d = mono.createGraphics(); + g2d.drawImage(original, 0, 0, null); + g2d.dispose(); + return mono; + } +} diff --git a/src/qz/printer/action/raw/encoder/ImageEncoder.java b/src/qz/printer/action/raw/encoder/ImageEncoder.java new file mode 100644 index 000000000..4df0fc15a --- /dev/null +++ b/src/qz/printer/action/raw/encoder/ImageEncoder.java @@ -0,0 +1,7 @@ +package qz.printer.action.raw.encoder; + +import java.awt.image.BufferedImage; + +public interface ImageEncoder { + byte[] encode(BufferedImage image); +}