File size: 5,356 Bytes
b43c3a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
import com.google.zxing.BarcodeFormat;
import com.google.zxing.BinaryBitmap;
import com.google.zxing.DecodeHintType;
import com.google.zxing.LuminanceSource;
import com.google.zxing.MultiFormatReader;
import com.google.zxing.ReaderException;
import com.google.zxing.Result;
import com.google.zxing.ResultMetadataType;
import com.google.zxing.common.HybridBinarizer;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * ZXing core 3.5.2 as Android's Settings app calls it (SettingsLib QrCamera): QR only, HybridBinarizer,
 * decodeWithState on the frame, then on the inverted frame; no TRY_HARDER.
 * stdin: "id<TAB>path" lines, each an 8-bit gray PNG. stdout: one JSON line per input, flushed, with every byte
 * form of the content ZXing returns (its text as UTF-8 and, where it fits, ISO-8859-1, and its raw byte
 * segments), base64-encoded: a read counts when one of them is the picture's content.
 */
public class ZxingServer {

    /** A plain gray plane, as SettingsLib's QrYuvLuminanceSource hands ZXing the camera's Y plane. */
    static final class GraySource extends LuminanceSource {
        private final byte[] data;

        GraySource(byte[] data, int width, int height) {
            super(width, height);
            this.data = data;
        }

        @Override
        public byte[] getRow(int y, byte[] row) {
            int w = getWidth();
            if (row == null || row.length < w) row = new byte[w];
            System.arraycopy(data, y * w, row, 0, w);
            return row;
        }

        @Override
        public byte[] getMatrix() {
            return data;
        }
    }

    static Result decode(MultiFormatReader reader, LuminanceSource source) {
        try {
            return reader.decodeWithState(new BinaryBitmap(new HybridBinarizer(source)));
        } catch (ReaderException e) {
            return null;
        } finally {
            reader.reset();
        }
    }

    @SuppressWarnings("unchecked")
    static Set<String> contents(Result r) throws Exception {
        Base64.Encoder b64 = Base64.getEncoder();
        Set<String> out = new LinkedHashSet<>();
        String text = r.getText() == null ? "" : r.getText();
        out.add(b64.encodeToString(text.getBytes(StandardCharsets.UTF_8)));
        if (StandardCharsets.ISO_8859_1.newEncoder().canEncode(text)) {
            out.add(b64.encodeToString(text.getBytes(StandardCharsets.ISO_8859_1)));
        }
        Map<ResultMetadataType, Object> md = r.getResultMetadata();
        if (md != null && md.get(ResultMetadataType.BYTE_SEGMENTS) instanceof List) {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            for (byte[] seg : (List<byte[]>) md.get(ResultMetadataType.BYTE_SEGMENTS)) bos.write(seg);
            if (bos.size() > 0) out.add(b64.encodeToString(bos.toByteArray()));
        }
        return out;
    }

    public static void main(String[] args) throws Exception {
        Map<DecodeHintType, Object> hints = new EnumMap<>(DecodeHintType.class);
        hints.put(DecodeHintType.POSSIBLE_FORMATS, List.of(BarcodeFormat.QR_CODE));
        MultiFormatReader reader = new MultiFormatReader();
        reader.setHints(hints);

        PrintStream out = new PrintStream(new FileOutputStream(FileDescriptor.out), true, StandardCharsets.UTF_8);
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8));
        String line;
        while ((line = in.readLine()) != null) {
            String[] parts = line.split("\t", 2);
            if (parts.length < 2 || !parts[0].matches("[0-9]+")) continue;
            String id = parts[0];
            BufferedImage img;
            try {
                img = ImageIO.read(new File(parts[1]));
            } catch (Exception e) {
                img = null;
            }
            if (img == null || img.getType() != BufferedImage.TYPE_BYTE_GRAY) {
                out.println("{\"id\":" + id + ",\"error\":\"not an 8-bit gray PNG\"}");
                continue;
            }
            int w = img.getWidth();
            int h = img.getHeight();
            byte[] y = (byte[]) img.getRaster().getDataElements(0, 0, w, h, null);
            LuminanceSource src = new GraySource(y, w, h);
            Result r = decode(reader, src);
            boolean inverted = false;
            if (r == null) {  // QrCamera: "Check color inversion QR code"
                r = decode(reader, src.invert());
                inverted = r != null;
            }
            StringBuilder sb = new StringBuilder();
            sb.append("{\"id\":").append(id).append(",\"inverted\":").append(inverted).append(",\"contents\":[");
            if (r != null) {
                int i = 0;
                for (String c : contents(r)) sb.append(i++ > 0 ? "," : "").append('"').append(c).append('"');
            }
            sb.append("]}");
            out.println(sb);
        }
    }
}