Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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-ubuntu
path: out/qz-tray-*.run

macos:
runs-on: [macos-latest]
Expand All @@ -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
path: out/qz-tray-*.pkg

windows:
runs-on: [windows-latest]
Expand All @@ -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
path: out/qz-tray-*.exe
16 changes: 16 additions & 0 deletions src/qz/printer/PrintOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")) {
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -749,4 +759,10 @@ public Chromaticity getAsChromaticity() {
}
}

/** Raw image encoding option */
public enum ImageEncoding {
ESC_STAR,
GS_V_0,
GS_L
}
}
7 changes: 7 additions & 0 deletions src/qz/printer/action/PrintRaw.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 14 additions & 2 deletions src/qz/printer/action/raw/ImageWrapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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());
Expand Down
128 changes: 128 additions & 0 deletions src/qz/printer/action/raw/encoder/GsLEncoder.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
93 changes: 93 additions & 0 deletions src/qz/printer/action/raw/encoder/GsV0Encoder.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
7 changes: 7 additions & 0 deletions src/qz/printer/action/raw/encoder/ImageEncoder.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package qz.printer.action.raw.encoder;

import java.awt.image.BufferedImage;

public interface ImageEncoder {
byte[] encode(BufferedImage image);
}
Loading