From fcc36ed82b95c962e9b196513a5fcbdd4aff55b4 Mon Sep 17 00:00:00 2001 From: Takeya Yuki Date: Fri, 1 Sep 2017 15:13:47 +0800 Subject: [PATCH] Add Google ZXing Core as Built-in --- .../java/com/google/zxing/BarcodeFormat.java | 77 ++ .../main/java/com/google/zxing/Binarizer.java | 87 ++ .../java/com/google/zxing/BinaryBitmap.java | 150 ++++ .../com/google/zxing/ChecksumException.java | 47 ++ .../java/com/google/zxing/DecodeHintType.java | 122 +++ .../main/java/com/google/zxing/Dimension.java | 62 ++ .../java/com/google/zxing/EncodeHintType.java | 104 +++ .../com/google/zxing/FormatException.java | 47 ++ .../google/zxing/InvertedLuminanceSource.java | 88 ++ .../com/google/zxing/LuminanceSource.java | 157 ++++ .../com/google/zxing/MultiFormatReader.java | 180 ++++ .../com/google/zxing/MultiFormatWriter.java | 105 +++ .../com/google/zxing/NotFoundException.java | 40 + .../zxing/PlanarYUVLuminanceSource.java | 168 ++++ .../com/google/zxing/RGBLuminanceSource.java | 136 +++ .../main/java/com/google/zxing/Reader.java | 69 ++ .../com/google/zxing/ReaderException.java | 47 ++ .../main/java/com/google/zxing/Result.java | 153 ++++ .../com/google/zxing/ResultMetadataType.java | 97 +++ .../java/com/google/zxing/ResultPoint.java | 130 +++ .../com/google/zxing/ResultPointCallback.java | 29 + .../main/java/com/google/zxing/Writer.java | 59 ++ .../com/google/zxing/WriterException.java | 38 + .../zxing/aztec/AztecDetectorResult.java | 58 ++ .../com/google/zxing/aztec/AztecReader.java | 122 +++ .../com/google/zxing/aztec/AztecWriter.java | 96 +++ .../google/zxing/aztec/decoder/Decoder.java | 366 ++++++++ .../google/zxing/aztec/detector/Detector.java | 601 ++++++++++++++ .../google/zxing/aztec/encoder/AztecCode.java | 89 ++ .../zxing/aztec/encoder/BinaryShiftToken.java | 60 ++ .../google/zxing/aztec/encoder/Encoder.java | 346 ++++++++ .../zxing/aztec/encoder/HighLevelEncoder.java | 307 +++++++ .../zxing/aztec/encoder/SimpleToken.java | 45 + .../com/google/zxing/aztec/encoder/State.java | 169 ++++ .../com/google/zxing/aztec/encoder/Token.java | 46 ++ .../result/AbstractDoCoMoResultParser.java | 39 + .../result/AddressBookAUResultParser.java | 91 ++ .../result/AddressBookDoCoMoResultParser.java | 92 +++ .../result/AddressBookParsedResult.java | 220 +++++ .../client/result/BizcardResultParser.java | 100 +++ .../result/BookmarkDoCoMoResultParser.java | 41 + .../client/result/CalendarParsedResult.java | 259 ++++++ .../result/EmailAddressParsedResult.java | 99 +++ .../result/EmailAddressResultParser.java | 85 ++ .../result/EmailDoCoMoResultParser.java | 64 ++ .../result/ExpandedProductParsedResult.java | 207 +++++ .../result/ExpandedProductResultParser.java | 217 +++++ .../zxing/client/result/GeoParsedResult.java | 104 +++ .../zxing/client/result/GeoResultParser.java | 73 ++ .../zxing/client/result/ISBNParsedResult.java | 42 + .../zxing/client/result/ISBNResultParser.java | 50 ++ .../zxing/client/result/ParsedResult.java | 67 ++ .../zxing/client/result/ParsedResultType.java | 40 + .../client/result/ProductParsedResult.java | 52 ++ .../client/result/ProductResultParser.java | 55 ++ .../zxing/client/result/ResultParser.java | 259 ++++++ .../client/result/SMSMMSResultParser.java | 109 +++ .../zxing/client/result/SMSParsedResult.java | 114 +++ .../client/result/SMSTOMMSTOResultParser.java | 52 ++ .../zxing/client/result/SMTPResultParser.java | 54 ++ .../zxing/client/result/TelParsedResult.java | 57 ++ .../zxing/client/result/TelResultParser.java | 42 + .../zxing/client/result/TextParsedResult.java | 49 ++ .../zxing/client/result/URIParsedResult.java | 92 +++ .../zxing/client/result/URIResultParser.java | 63 ++ .../client/result/URLTOResultParser.java | 45 + .../client/result/VCardResultParser.java | 360 ++++++++ .../client/result/VEventResultParser.java | 119 +++ .../zxing/client/result/VINParsedResult.java | 106 +++ .../zxing/client/result/VINResultParser.java | 209 +++++ .../zxing/client/result/WifiParsedResult.java | 69 ++ .../zxing/client/result/WifiResultParser.java | 51 ++ .../com/google/zxing/common/BitArray.java | 357 ++++++++ .../com/google/zxing/common/BitMatrix.java | 481 +++++++++++ .../com/google/zxing/common/BitSource.java | 111 +++ .../google/zxing/common/CharacterSetECI.java | 118 +++ .../google/zxing/common/DecoderResult.java | 152 ++++ .../zxing/common/DefaultGridSampler.java | 88 ++ .../google/zxing/common/DetectorResult.java | 46 ++ .../common/GlobalHistogramBinarizer.java | 203 +++++ .../com/google/zxing/common/GridSampler.java | 173 ++++ .../google/zxing/common/HybridBinarizer.java | 237 ++++++ .../zxing/common/PerspectiveTransform.java | 156 ++++ .../com/google/zxing/common/StringUtils.java | 197 +++++ .../zxing/common/detector/MathUtils.java | 78 ++ .../detector/MonochromeRectangleDetector.java | 217 +++++ .../detector/WhiteRectangleDetector.java | 330 ++++++++ .../zxing/common/reedsolomon/GenericGF.java | 166 ++++ .../common/reedsolomon/GenericGFPoly.java | 264 ++++++ .../reedsolomon/ReedSolomonDecoder.java | 190 +++++ .../reedsolomon/ReedSolomonEncoder.java | 74 ++ .../reedsolomon/ReedSolomonException.java | 31 + .../zxing/datamatrix/DataMatrixReader.java | 161 ++++ .../zxing/datamatrix/DataMatrixWriter.java | 180 ++++ .../datamatrix/decoder/BitMatrixParser.java | 440 ++++++++++ .../zxing/datamatrix/decoder/DataBlock.java | 118 +++ .../decoder/DecodedBitStreamParser.java | 525 ++++++++++++ .../zxing/datamatrix/decoder/Decoder.java | 125 +++ .../zxing/datamatrix/decoder/Version.java | 237 ++++++ .../zxing/datamatrix/detector/Detector.java | 440 ++++++++++ .../datamatrix/encoder/ASCIIEncoder.java | 82 ++ .../datamatrix/encoder/Base256Encoder.java | 75 ++ .../zxing/datamatrix/encoder/C40Encoder.java | 178 ++++ .../encoder/DataMatrixSymbolInfo144.java | 35 + .../datamatrix/encoder/DefaultPlacement.java | 198 +++++ .../datamatrix/encoder/EdifactEncoder.java | 138 ++++ .../zxing/datamatrix/encoder/Encoder.java | 25 + .../datamatrix/encoder/EncoderContext.java | 134 +++ .../datamatrix/encoder/ErrorCorrection.java | 184 +++++ .../datamatrix/encoder/HighLevelEncoder.java | 446 ++++++++++ .../zxing/datamatrix/encoder/SymbolInfo.java | 236 ++++++ .../datamatrix/encoder/SymbolShapeHint.java | 29 + .../zxing/datamatrix/encoder/TextEncoder.java | 85 ++ .../zxing/datamatrix/encoder/X12Encoder.java | 93 +++ .../google/zxing/maxicode/MaxiCodeReader.java | 125 +++ .../maxicode/decoder/BitMatrixParser.java | 88 ++ .../decoder/DecodedBitStreamParser.java | 207 +++++ .../zxing/maxicode/decoder/Decoder.java | 114 +++ .../google/zxing/multi/ByQuadrantReader.java | 115 +++ .../multi/GenericMultipleBarcodeReader.java | 180 ++++ .../zxing/multi/MultipleBarcodeReader.java | 39 + .../zxing/multi/qrcode/QRCodeMultiReader.java | 181 ++++ .../multi/qrcode/detector/MultiDetector.java | 73 ++ .../detector/MultiFinderPatternFinder.java | 311 +++++++ .../com/google/zxing/oned/CodaBarReader.java | 345 ++++++++ .../com/google/zxing/oned/CodaBarWriter.java | 130 +++ .../com/google/zxing/oned/Code128Reader.java | 539 ++++++++++++ .../com/google/zxing/oned/Code128Writer.java | 255 ++++++ .../com/google/zxing/oned/Code39Reader.java | 324 ++++++++ .../com/google/zxing/oned/Code39Writer.java | 89 ++ .../com/google/zxing/oned/Code93Reader.java | 287 +++++++ .../com/google/zxing/oned/Code93Writer.java | 128 +++ .../com/google/zxing/oned/EAN13Reader.java | 138 ++++ .../com/google/zxing/oned/EAN13Writer.java | 109 +++ .../com/google/zxing/oned/EAN8Reader.java | 75 ++ .../com/google/zxing/oned/EAN8Writer.java | 106 +++ .../zxing/oned/EANManufacturerOrgSupport.java | 171 ++++ .../java/com/google/zxing/oned/ITFReader.java | 352 ++++++++ .../java/com/google/zxing/oned/ITFWriter.java | 76 ++ .../zxing/oned/MultiFormatOneDReader.java | 112 +++ .../zxing/oned/MultiFormatUPCEANReader.java | 123 +++ .../com/google/zxing/oned/OneDReader.java | 296 +++++++ .../zxing/oned/OneDimensionalCodeWriter.java | 129 +++ .../com/google/zxing/oned/UPCAReader.java | 86 ++ .../com/google/zxing/oned/UPCAWriter.java | 55 ++ .../zxing/oned/UPCEANExtension2Support.java | 112 +++ .../zxing/oned/UPCEANExtension5Support.java | 181 ++++ .../zxing/oned/UPCEANExtensionSupport.java | 40 + .../com/google/zxing/oned/UPCEANReader.java | 403 +++++++++ .../com/google/zxing/oned/UPCEANWriter.java | 34 + .../com/google/zxing/oned/UPCEReader.java | 182 ++++ .../com/google/zxing/oned/UPCEWriter.java | 104 +++ .../zxing/oned/rss/AbstractRSSReader.java | 140 ++++ .../google/zxing/oned/rss/DataCharacter.java | 59 ++ .../google/zxing/oned/rss/FinderPattern.java | 65 ++ .../java/com/google/zxing/oned/rss/Pair.java | 41 + .../google/zxing/oned/rss/RSS14Reader.java | 473 +++++++++++ .../com/google/zxing/oned/rss/RSSUtils.java | 129 +++ .../oned/rss/expanded/BitArrayBuilder.java | 85 ++ .../zxing/oned/rss/expanded/ExpandedPair.java | 104 +++ .../zxing/oned/rss/expanded/ExpandedRow.java | 76 ++ .../oned/rss/expanded/RSSExpandedReader.java | 779 ++++++++++++++++++ .../expanded/decoders/AI013103decoder.java | 49 ++ .../expanded/decoders/AI01320xDecoder.java | 57 ++ .../expanded/decoders/AI01392xDecoder.java | 68 ++ .../expanded/decoders/AI01393xDecoder.java | 78 ++ .../expanded/decoders/AI013x0x1xDecoder.java | 108 +++ .../expanded/decoders/AI013x0xDecoder.java | 57 ++ .../expanded/decoders/AI01AndOtherAIs.java | 58 ++ .../rss/expanded/decoders/AI01decoder.java | 81 ++ .../expanded/decoders/AI01weightDecoder.java | 60 ++ .../decoders/AbstractExpandedDecoder.java | 93 +++ .../rss/expanded/decoders/AnyAIDecoder.java | 50 ++ .../expanded/decoders/BlockParsedResult.java | 54 ++ .../decoders/CurrentParsingState.java | 83 ++ .../rss/expanded/decoders/DecodedChar.java | 52 ++ .../expanded/decoders/DecodedInformation.java | 64 ++ .../rss/expanded/decoders/DecodedNumeric.java | 77 ++ .../rss/expanded/decoders/DecodedObject.java | 44 + .../rss/expanded/decoders/FieldParser.java | 291 +++++++ .../decoders/GeneralAppIdDecoder.java | 469 +++++++++++ .../com/google/zxing/pdf417/PDF417Common.java | 463 +++++++++++ .../com/google/zxing/pdf417/PDF417Reader.java | 135 +++ .../zxing/pdf417/PDF417ResultMetadata.java | 61 ++ .../com/google/zxing/pdf417/PDF417Writer.java | 177 ++++ .../zxing/pdf417/decoder/BarcodeMetadata.java | 58 ++ .../zxing/pdf417/decoder/BarcodeValue.java | 68 ++ .../zxing/pdf417/decoder/BoundingBox.java | 176 ++++ .../google/zxing/pdf417/decoder/Codeword.java | 84 ++ .../decoder/DecodedBitStreamParser.java | 677 +++++++++++++++ .../zxing/pdf417/decoder/DetectionResult.java | 296 +++++++ .../pdf417/decoder/DetectionResultColumn.java | 96 +++ .../DetectionResultRowIndicatorColumn.java | 267 ++++++ .../pdf417/decoder/PDF417CodewordDecoder.java | 120 +++ .../pdf417/decoder/PDF417ScanningDecoder.java | 632 ++++++++++++++ .../pdf417/decoder/ec/ErrorCorrection.java | 187 +++++ .../zxing/pdf417/decoder/ec/ModulusGF.java | 112 +++ .../zxing/pdf417/decoder/ec/ModulusPoly.java | 262 ++++++ .../zxing/pdf417/detector/Detector.java | 340 ++++++++ .../pdf417/detector/PDF417DetectorResult.java | 45 + .../zxing/pdf417/encoder/BarcodeMatrix.java | 82 ++ .../zxing/pdf417/encoder/BarcodeRow.java | 85 ++ .../zxing/pdf417/encoder/Compaction.java | 29 + .../zxing/pdf417/encoder/Dimensions.java | 54 ++ .../google/zxing/pdf417/encoder/PDF417.java | 768 +++++++++++++++++ .../pdf417/encoder/PDF417ErrorCorrection.java | 204 +++++ .../encoder/PDF417HighLevelEncoder.java | 583 +++++++++++++ .../com/google/zxing/qrcode/QRCodeReader.java | 222 +++++ .../com/google/zxing/qrcode/QRCodeWriter.java | 118 +++ .../zxing/qrcode/decoder/BitMatrixParser.java | 245 ++++++ .../zxing/qrcode/decoder/DataBlock.java | 122 +++ .../google/zxing/qrcode/decoder/DataMask.java | 141 ++++ .../decoder/DecodedBitStreamParser.java | 360 ++++++++ .../google/zxing/qrcode/decoder/Decoder.java | 193 +++++ .../qrcode/decoder/ErrorCorrectionLevel.java | 60 ++ .../qrcode/decoder/FormatInformation.java | 157 ++++ .../com/google/zxing/qrcode/decoder/Mode.java | 102 +++ .../qrcode/decoder/QRCodeDecoderMetaData.java | 57 ++ .../google/zxing/qrcode/decoder/Version.java | 578 +++++++++++++ .../qrcode/detector/AlignmentPattern.java | 59 ++ .../detector/AlignmentPatternFinder.java | 277 +++++++ .../zxing/qrcode/detector/Detector.java | 405 +++++++++ .../zxing/qrcode/detector/FinderPattern.java | 82 ++ .../qrcode/detector/FinderPatternFinder.java | 680 +++++++++++++++ .../qrcode/detector/FinderPatternInfo.java | 49 ++ .../zxing/qrcode/encoder/BlockPair.java | 37 + .../zxing/qrcode/encoder/ByteMatrix.java | 99 +++ .../google/zxing/qrcode/encoder/Encoder.java | 610 ++++++++++++++ .../google/zxing/qrcode/encoder/MaskUtil.java | 222 +++++ .../zxing/qrcode/encoder/MatrixUtil.java | 476 +++++++++++ .../google/zxing/qrcode/encoder/QRCode.java | 108 +++ 231 files changed, 37955 insertions(+) create mode 100644 rubylib/src/main/java/com/google/zxing/BarcodeFormat.java create mode 100644 rubylib/src/main/java/com/google/zxing/Binarizer.java create mode 100644 rubylib/src/main/java/com/google/zxing/BinaryBitmap.java create mode 100644 rubylib/src/main/java/com/google/zxing/ChecksumException.java create mode 100644 rubylib/src/main/java/com/google/zxing/DecodeHintType.java create mode 100644 rubylib/src/main/java/com/google/zxing/Dimension.java create mode 100644 rubylib/src/main/java/com/google/zxing/EncodeHintType.java create mode 100644 rubylib/src/main/java/com/google/zxing/FormatException.java create mode 100644 rubylib/src/main/java/com/google/zxing/InvertedLuminanceSource.java create mode 100644 rubylib/src/main/java/com/google/zxing/LuminanceSource.java create mode 100644 rubylib/src/main/java/com/google/zxing/MultiFormatReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/MultiFormatWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/NotFoundException.java create mode 100644 rubylib/src/main/java/com/google/zxing/PlanarYUVLuminanceSource.java create mode 100644 rubylib/src/main/java/com/google/zxing/RGBLuminanceSource.java create mode 100644 rubylib/src/main/java/com/google/zxing/Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/ReaderException.java create mode 100644 rubylib/src/main/java/com/google/zxing/Result.java create mode 100644 rubylib/src/main/java/com/google/zxing/ResultMetadataType.java create mode 100644 rubylib/src/main/java/com/google/zxing/ResultPoint.java create mode 100644 rubylib/src/main/java/com/google/zxing/ResultPointCallback.java create mode 100644 rubylib/src/main/java/com/google/zxing/Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/WriterException.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/AztecDetectorResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/AztecReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/AztecWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/decoder/Decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/detector/Detector.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/AztecCode.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/BinaryShiftToken.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/HighLevelEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/SimpleToken.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/State.java create mode 100644 rubylib/src/main/java/com/google/zxing/aztec/encoder/Token.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/AbstractDoCoMoResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/AddressBookAUResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/AddressBookDoCoMoResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/AddressBookParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/BizcardResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/BookmarkDoCoMoResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/CalendarParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/EmailAddressParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/EmailAddressResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/EmailDoCoMoResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/GeoParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/GeoResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ISBNParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ISBNResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ParsedResultType.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ProductParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ProductResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/ResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/SMSMMSResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/SMSParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/SMSTOMMSTOResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/SMTPResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/TelParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/TelResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/TextParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/URIParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/URIResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/URLTOResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/VCardResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/VEventResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/VINParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/VINResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/WifiParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/client/result/WifiResultParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/BitArray.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/BitMatrix.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/BitSource.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/CharacterSetECI.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/DecoderResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/DefaultGridSampler.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/DetectorResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/GlobalHistogramBinarizer.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/GridSampler.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/HybridBinarizer.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/PerspectiveTransform.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/StringUtils.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/detector/MathUtils.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/detector/MonochromeRectangleDetector.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/detector/WhiteRectangleDetector.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGF.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGFPoly.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonException.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/decoder/BitMatrixParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DataBlock.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Version.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/detector/Detector.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ASCIIEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Base256Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/C40Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DataMatrixSymbolInfo144.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DefaultPlacement.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EdifactEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EncoderContext.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ErrorCorrection.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/HighLevelEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolInfo.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolShapeHint.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/TextEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/datamatrix/encoder/X12Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/maxicode/MaxiCodeReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/maxicode/decoder/BitMatrixParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/maxicode/decoder/DecodedBitStreamParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/maxicode/decoder/Decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/ByQuadrantReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/GenericMultipleBarcodeReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/MultipleBarcodeReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/qrcode/QRCodeMultiReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiDetector.java create mode 100644 rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/CodaBarReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/CodaBarWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code128Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code128Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code39Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code39Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code93Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/Code93Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/EAN13Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/EAN13Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/EAN8Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/EAN8Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/EANManufacturerOrgSupport.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/ITFReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/ITFWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/MultiFormatOneDReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/MultiFormatUPCEANReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/OneDReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCAReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCAWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension2Support.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension5Support.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEANExtensionSupport.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEANReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEANWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/UPCEWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/AbstractRSSReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/DataCharacter.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/FinderPattern.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/Pair.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/RSS14Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/RSSUtils.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedPair.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedRow.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/GeneralAppIdDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/PDF417Common.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/PDF417Reader.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/PDF417ResultMetadata.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/PDF417Writer.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeMetadata.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeValue.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/BoundingBox.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/Codeword.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultColumn.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultRowIndicatorColumn.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417CodewordDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417ScanningDecoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ErrorCorrection.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusGF.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusPoly.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/detector/Detector.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/detector/PDF417DetectorResult.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeMatrix.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeRow.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/Compaction.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/Dimensions.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417ErrorCorrection.java create mode 100644 rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/QRCodeReader.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/QRCodeWriter.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/BitMatrixParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataBlock.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/Decoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/FormatInformation.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/Mode.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/QRCodeDecoderMetaData.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/decoder/Version.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPattern.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/Detector.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPattern.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternFinder.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternInfo.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/BlockPair.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/ByteMatrix.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/MatrixUtil.java create mode 100644 rubylib/src/main/java/com/google/zxing/qrcode/encoder/QRCode.java diff --git a/rubylib/src/main/java/com/google/zxing/BarcodeFormat.java b/rubylib/src/main/java/com/google/zxing/BarcodeFormat.java new file mode 100644 index 0000000..7f6a0ef --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/BarcodeFormat.java @@ -0,0 +1,77 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Enumerates barcode formats known to this package. Please keep alphabetized. + * + * @author Sean Owen + */ +public enum BarcodeFormat { + + /** Aztec 2D barcode format. */ + AZTEC, + + /** CODABAR 1D format. */ + CODABAR, + + /** Code 39 1D format. */ + CODE_39, + + /** Code 93 1D format. */ + CODE_93, + + /** Code 128 1D format. */ + CODE_128, + + /** Data Matrix 2D barcode format. */ + DATA_MATRIX, + + /** EAN-8 1D format. */ + EAN_8, + + /** EAN-13 1D format. */ + EAN_13, + + /** ITF (Interleaved Two of Five) 1D format. */ + ITF, + + /** MaxiCode 2D barcode format. */ + MAXICODE, + + /** PDF417 format. */ + PDF_417, + + /** QR Code 2D barcode format. */ + QR_CODE, + + /** RSS 14 */ + RSS_14, + + /** RSS EXPANDED */ + RSS_EXPANDED, + + /** UPC-A 1D format. */ + UPC_A, + + /** UPC-E 1D format. */ + UPC_E, + + /** UPC/EAN extension format. Not a stand-alone format. */ + UPC_EAN_EXTENSION + +} diff --git a/rubylib/src/main/java/com/google/zxing/Binarizer.java b/rubylib/src/main/java/com/google/zxing/Binarizer.java new file mode 100644 index 0000000..02af083 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/Binarizer.java @@ -0,0 +1,87 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.common.BitArray; +import com.google.zxing.common.BitMatrix; + +/** + * This class hierarchy provides a set of methods to convert luminance data to 1 bit data. + * It allows the algorithm to vary polymorphically, for example allowing a very expensive + * thresholding technique for servers and a fast one for mobile. It also permits the implementation + * to vary, e.g. a JNI version for Android and a Java fallback version for other platforms. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public abstract class Binarizer { + + private final LuminanceSource source; + + protected Binarizer(LuminanceSource source) { + this.source = source; + } + + public final LuminanceSource getLuminanceSource() { + return source; + } + + /** + * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return + * cached data. Callers should assume this method is expensive and call it as seldom as possible. + * This method is intended for decoding 1D barcodes and may choose to apply sharpening. + * For callers which only examine one row of pixels at a time, the same BitArray should be reused + * and passed in with each call for performance. However it is legal to keep more than one row + * at a time if needed. + * + * @param y The row to fetch, which must be in [0, bitmap height) + * @param row An optional preallocated array. If null or too small, it will be ignored. + * If used, the Binarizer will call BitArray.clear(). Always use the returned object. + * @return The array of bits for this row (true means black). + * @throws NotFoundException if row can't be binarized + */ + public abstract BitArray getBlackRow(int y, BitArray row) throws NotFoundException; + + /** + * Converts a 2D array of luminance data to 1 bit data. As above, assume this method is expensive + * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or + * may not apply sharpening. Therefore, a row from this matrix may not be identical to one + * fetched using getBlackRow(), so don't mix and match between them. + * + * @return The 2D array of bits for the image (true means black). + * @throws NotFoundException if image can't be binarized to make a matrix + */ + public abstract BitMatrix getBlackMatrix() throws NotFoundException; + + /** + * Creates a new object with the same type as this Binarizer implementation, but with pristine + * state. This is needed because Binarizer implementations may be stateful, e.g. keeping a cache + * of 1 bit data. See Effective Java for why we can't use Java's clone() method. + * + * @param source The LuminanceSource this Binarizer will operate on. + * @return A new concrete Binarizer implementation object. + */ + public abstract Binarizer createBinarizer(LuminanceSource source); + + public final int getWidth() { + return source.getWidth(); + } + + public final int getHeight() { + return source.getHeight(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/BinaryBitmap.java b/rubylib/src/main/java/com/google/zxing/BinaryBitmap.java new file mode 100644 index 0000000..c1ef8a1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/BinaryBitmap.java @@ -0,0 +1,150 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.common.BitArray; +import com.google.zxing.common.BitMatrix; + +/** + * This class is the core bitmap class used by ZXing to represent 1 bit data. Reader objects + * accept a BinaryBitmap and attempt to decode it. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class BinaryBitmap { + + private final Binarizer binarizer; + private BitMatrix matrix; + + public BinaryBitmap(Binarizer binarizer) { + if (binarizer == null) { + throw new IllegalArgumentException("Binarizer must be non-null."); + } + this.binarizer = binarizer; + } + + /** + * @return The width of the bitmap. + */ + public int getWidth() { + return binarizer.getWidth(); + } + + /** + * @return The height of the bitmap. + */ + public int getHeight() { + return binarizer.getHeight(); + } + + /** + * Converts one row of luminance data to 1 bit data. May actually do the conversion, or return + * cached data. Callers should assume this method is expensive and call it as seldom as possible. + * This method is intended for decoding 1D barcodes and may choose to apply sharpening. + * + * @param y The row to fetch, which must be in [0, bitmap height) + * @param row An optional preallocated array. If null or too small, it will be ignored. + * If used, the Binarizer will call BitArray.clear(). Always use the returned object. + * @return The array of bits for this row (true means black). + * @throws NotFoundException if row can't be binarized + */ + public BitArray getBlackRow(int y, BitArray row) throws NotFoundException { + return binarizer.getBlackRow(y, row); + } + + /** + * Converts a 2D array of luminance data to 1 bit. As above, assume this method is expensive + * and do not call it repeatedly. This method is intended for decoding 2D barcodes and may or + * may not apply sharpening. Therefore, a row from this matrix may not be identical to one + * fetched using getBlackRow(), so don't mix and match between them. + * + * @return The 2D array of bits for the image (true means black). + * @throws NotFoundException if image can't be binarized to make a matrix + */ + public BitMatrix getBlackMatrix() throws NotFoundException { + // The matrix is created on demand the first time it is requested, then cached. There are two + // reasons for this: + // 1. This work will never be done if the caller only installs 1D Reader objects, or if a + // 1D Reader finds a barcode before the 2D Readers run. + // 2. This work will only be done once even if the caller installs multiple 2D Readers. + if (matrix == null) { + matrix = binarizer.getBlackMatrix(); + } + return matrix; + } + + /** + * @return Whether this bitmap can be cropped. + */ + public boolean isCropSupported() { + return binarizer.getLuminanceSource().isCropSupported(); + } + + /** + * Returns a new object with cropped image data. Implementations may keep a reference to the + * original data rather than a copy. Only callable if isCropSupported() is true. + * + * @param left The left coordinate, which must be in [0,getWidth()) + * @param top The top coordinate, which must be in [0,getHeight()) + * @param width The width of the rectangle to crop. + * @param height The height of the rectangle to crop. + * @return A cropped version of this object. + */ + public BinaryBitmap crop(int left, int top, int width, int height) { + LuminanceSource newSource = binarizer.getLuminanceSource().crop(left, top, width, height); + return new BinaryBitmap(binarizer.createBinarizer(newSource)); + } + + /** + * @return Whether this bitmap supports counter-clockwise rotation. + */ + public boolean isRotateSupported() { + return binarizer.getLuminanceSource().isRotateSupported(); + } + + /** + * Returns a new object with rotated image data by 90 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public BinaryBitmap rotateCounterClockwise() { + LuminanceSource newSource = binarizer.getLuminanceSource().rotateCounterClockwise(); + return new BinaryBitmap(binarizer.createBinarizer(newSource)); + } + + /** + * Returns a new object with rotated image data by 45 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public BinaryBitmap rotateCounterClockwise45() { + LuminanceSource newSource = binarizer.getLuminanceSource().rotateCounterClockwise45(); + return new BinaryBitmap(binarizer.createBinarizer(newSource)); + } + + @Override + public String toString() { + try { + return getBlackMatrix().toString(); + } catch (NotFoundException e) { + return ""; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/ChecksumException.java b/rubylib/src/main/java/com/google/zxing/ChecksumException.java new file mode 100644 index 0000000..c5acbe3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/ChecksumException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Thrown when a barcode was successfully detected and decoded, but + * was not returned because its checksum feature failed. + * + * @author Sean Owen + */ +public final class ChecksumException extends ReaderException { + + private static final ChecksumException INSTANCE = new ChecksumException(); + static { + INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless + } + + private ChecksumException() { + // do nothing + } + + private ChecksumException(Throwable cause) { + super(cause); + } + + public static ChecksumException getChecksumInstance() { + return isStackTrace ? new ChecksumException() : INSTANCE; + } + + public static ChecksumException getChecksumInstance(Throwable cause) { + return isStackTrace ? new ChecksumException(cause) : INSTANCE; + } +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/DecodeHintType.java b/rubylib/src/main/java/com/google/zxing/DecodeHintType.java new file mode 100644 index 0000000..11f579e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/DecodeHintType.java @@ -0,0 +1,122 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import java.util.List; + +/** + * Encapsulates a type of hint that a caller may pass to a barcode reader to help it + * more quickly or accurately decode it. It is up to implementations to decide what, + * if anything, to do with the information that is supplied. + * + * @author Sean Owen + * @author dswitkin@google.com (Daniel Switkin) + * @see Reader#decode(BinaryBitmap,java.util.Map) + */ +public enum DecodeHintType { + + /** + * Unspecified, application-specific hint. Maps to an unspecified {@link Object}. + */ + OTHER(Object.class), + + /** + * Image is a pure monochrome image of a barcode. Doesn't matter what it maps to; + * use {@link Boolean#TRUE}. + */ + PURE_BARCODE(Void.class), + + /** + * Image is known to be of one of a few possible formats. + * Maps to a {@link List} of {@link BarcodeFormat}s. + */ + POSSIBLE_FORMATS(List.class), + + /** + * Spend more time to try to find a barcode; optimize for accuracy, not speed. + * Doesn't matter what it maps to; use {@link Boolean#TRUE}. + */ + TRY_HARDER(Void.class), + + /** + * Specifies what character encoding to use when decoding, where applicable (type String) + */ + CHARACTER_SET(String.class), + + /** + * Allowed lengths of encoded data -- reject anything else. Maps to an {@code int[]}. + */ + ALLOWED_LENGTHS(int[].class), + + /** + * Assume Code 39 codes employ a check digit. Doesn't matter what it maps to; + * use {@link Boolean#TRUE}. + */ + ASSUME_CODE_39_CHECK_DIGIT(Void.class), + + /** + * Assume the barcode is being processed as a GS1 barcode, and modify behavior as needed. + * For example this affects FNC1 handling for Code 128 (aka GS1-128). Doesn't matter what it maps to; + * use {@link Boolean#TRUE}. + */ + ASSUME_GS1(Void.class), + + /** + * If true, return the start and end digits in a Codabar barcode instead of stripping them. They + * are alpha, whereas the rest are numeric. By default, they are stripped, but this causes them + * to not be. Doesn't matter what it maps to; use {@link Boolean#TRUE}. + */ + RETURN_CODABAR_START_END(Void.class), + + /** + * The caller needs to be notified via callback when a possible {@link ResultPoint} + * is found. Maps to a {@link ResultPointCallback}. + */ + NEED_RESULT_POINT_CALLBACK(ResultPointCallback.class), + + + /** + * Allowed extension lengths for EAN or UPC barcodes. Other formats will ignore this. + * Maps to an {@code int[]} of the allowed extension lengths, for example [2], [5], or [2, 5]. + * If it is optional to have an extension, do not set this hint. If this is set, + * and a UPC or EAN barcode is found but an extension is not, then no result will be returned + * at all. + */ + ALLOWED_EAN_EXTENSIONS(int[].class), + + // End of enumeration values. + ; + + /** + * Data type the hint is expecting. + * Among the possible values the {@link Void} stands out as being used for + * hints that do not expect a value to be supplied (flag hints). Such hints + * will possibly have their value ignored, or replaced by a + * {@link Boolean#TRUE}. Hint suppliers should probably use + * {@link Boolean#TRUE} as directed by the actual hint documentation. + */ + private final Class valueType; + + DecodeHintType(Class valueType) { + this.valueType = valueType; + } + + public Class getValueType() { + return valueType; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/Dimension.java b/rubylib/src/main/java/com/google/zxing/Dimension.java new file mode 100644 index 0000000..dd40d06 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/Dimension.java @@ -0,0 +1,62 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Simply encapsulates a width and height. + */ +public final class Dimension { + + private final int width; + private final int height; + + public Dimension(int width, int height) { + if (width < 0 || height < 0) { + throw new IllegalArgumentException(); + } + this.width = width; + this.height = height; + } + + public int getWidth() { + return width; + } + + public int getHeight() { + return height; + } + + @Override + public boolean equals(Object other) { + if (other instanceof Dimension) { + Dimension d = (Dimension) other; + return width == d.width && height == d.height; + } + return false; + } + + @Override + public int hashCode() { + return width * 32713 + height; + } + + @Override + public String toString() { + return width + "x" + height; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/EncodeHintType.java b/rubylib/src/main/java/com/google/zxing/EncodeHintType.java new file mode 100644 index 0000000..424d2b6 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/EncodeHintType.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * These are a set of hints that you may pass to Writers to specify their behavior. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public enum EncodeHintType { + + /** + * Specifies what degree of error correction to use, for example in QR Codes. + * Type depends on the encoder. For example for QR codes it's type + * {@link com.google.zxing.qrcode.decoder.ErrorCorrectionLevel ErrorCorrectionLevel}. + * For Aztec it is of type {@link Integer}, representing the minimal percentage of error correction words. + * For PDF417 it is of type {@link Integer}, valid values being 0 to 8. + * In all cases, it can also be a {@link String} representation of the desired value as well. + * Note: an Aztec symbol should have a minimum of 25% EC words. + */ + ERROR_CORRECTION, + + /** + * Specifies what character encoding to use where applicable (type {@link String}) + */ + CHARACTER_SET, + + /** + * Specifies the matrix shape for Data Matrix (type {@link com.google.zxing.datamatrix.encoder.SymbolShapeHint}) + */ + DATA_MATRIX_SHAPE, + + /** + * Specifies a minimum barcode size (type {@link Dimension}). Only applicable to Data Matrix now. + * + * @deprecated use width/height params in + * {@link com.google.zxing.datamatrix.DataMatrixWriter#encode(String, BarcodeFormat, int, int)} + */ + @Deprecated + MIN_SIZE, + + /** + * Specifies a maximum barcode size (type {@link Dimension}). Only applicable to Data Matrix now. + * + * @deprecated without replacement + */ + @Deprecated + MAX_SIZE, + + /** + * Specifies margin, in pixels, to use when generating the barcode. The meaning can vary + * by format; for example it controls margin before and after the barcode horizontally for + * most 1D formats. (Type {@link Integer}, or {@link String} representation of the integer value). + */ + MARGIN, + + /** + * Specifies whether to use compact mode for PDF417 (type {@link Boolean}, or "true" or "false" + * {@link String} value). + */ + PDF417_COMPACT, + + /** + * Specifies what compaction mode to use for PDF417 (type + * {@link com.google.zxing.pdf417.encoder.Compaction Compaction} or {@link String} value of one of its + * enum values). + */ + PDF417_COMPACTION, + + /** + * Specifies the minimum and maximum number of rows and columns for PDF417 (type + * {@link com.google.zxing.pdf417.encoder.Dimensions Dimensions}). + */ + PDF417_DIMENSIONS, + + /** + * Specifies the required number of layers for an Aztec code. + * A negative number (-1, -2, -3, -4) specifies a compact Aztec code. + * 0 indicates to use the minimum number of layers (the default). + * A positive number (1, 2, .. 32) specifies a normal (non-compact) Aztec code. + * (Type {@link Integer}, or {@link String} representation of the integer value). + */ + AZTEC_LAYERS, + + /** + * Specifies the exact version of QR code to be encoded. + * (Type {@link Integer}, or {@link String} representation of the integer value). + */ + QR_VERSION, +} diff --git a/rubylib/src/main/java/com/google/zxing/FormatException.java b/rubylib/src/main/java/com/google/zxing/FormatException.java new file mode 100644 index 0000000..ebd8008 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/FormatException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Thrown when a barcode was successfully detected, but some aspect of + * the content did not conform to the barcode's format rules. This could have + * been due to a mis-detection. + * + * @author Sean Owen + */ +public final class FormatException extends ReaderException { + + private static final FormatException INSTANCE = new FormatException(); + static { + INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless + } + + private FormatException() { + } + + private FormatException(Throwable cause) { + super(cause); + } + + public static FormatException getFormatInstance() { + return isStackTrace ? new FormatException() : INSTANCE; + } + + public static FormatException getFormatInstance(Throwable cause) { + return isStackTrace ? new FormatException(cause) : INSTANCE; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/InvertedLuminanceSource.java b/rubylib/src/main/java/com/google/zxing/InvertedLuminanceSource.java new file mode 100644 index 0000000..a64e15d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/InvertedLuminanceSource.java @@ -0,0 +1,88 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * A wrapper implementation of {@link LuminanceSource} which inverts the luminances it returns -- black becomes + * white and vice versa, and each value becomes (255-value). + * + * @author Sean Owen + */ +public final class InvertedLuminanceSource extends LuminanceSource { + + private final LuminanceSource delegate; + + public InvertedLuminanceSource(LuminanceSource delegate) { + super(delegate.getWidth(), delegate.getHeight()); + this.delegate = delegate; + } + + @Override + public byte[] getRow(int y, byte[] row) { + row = delegate.getRow(y, row); + int width = getWidth(); + for (int i = 0; i < width; i++) { + row[i] = (byte) (255 - (row[i] & 0xFF)); + } + return row; + } + + @Override + public byte[] getMatrix() { + byte[] matrix = delegate.getMatrix(); + int length = getWidth() * getHeight(); + byte[] invertedMatrix = new byte[length]; + for (int i = 0; i < length; i++) { + invertedMatrix[i] = (byte) (255 - (matrix[i] & 0xFF)); + } + return invertedMatrix; + } + + @Override + public boolean isCropSupported() { + return delegate.isCropSupported(); + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new InvertedLuminanceSource(delegate.crop(left, top, width, height)); + } + + @Override + public boolean isRotateSupported() { + return delegate.isRotateSupported(); + } + + /** + * @return original delegate {@link LuminanceSource} since invert undoes itself + */ + @Override + public LuminanceSource invert() { + return delegate; + } + + @Override + public LuminanceSource rotateCounterClockwise() { + return new InvertedLuminanceSource(delegate.rotateCounterClockwise()); + } + + @Override + public LuminanceSource rotateCounterClockwise45() { + return new InvertedLuminanceSource(delegate.rotateCounterClockwise45()); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/LuminanceSource.java b/rubylib/src/main/java/com/google/zxing/LuminanceSource.java new file mode 100644 index 0000000..1946d02 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/LuminanceSource.java @@ -0,0 +1,157 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * The purpose of this class hierarchy is to abstract different bitmap implementations across + * platforms into a standard interface for requesting greyscale luminance values. The interface + * only provides immutable methods; therefore crop and rotation create copies. This is to ensure + * that one Reader does not modify the original luminance source and leave it in an unknown state + * for other Readers in the chain. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public abstract class LuminanceSource { + + private final int width; + private final int height; + + protected LuminanceSource(int width, int height) { + this.width = width; + this.height = height; + } + + /** + * Fetches one row of luminance data from the underlying platform's bitmap. Values range from + * 0 (black) to 255 (white). Because Java does not have an unsigned byte type, callers will have + * to bitwise and with 0xff for each value. It is preferable for implementations of this method + * to only fetch this row rather than the whole image, since no 2D Readers may be installed and + * getMatrix() may never be called. + * + * @param y The row to fetch, which must be in [0,getHeight()) + * @param row An optional preallocated array. If null or too small, it will be ignored. + * Always use the returned object, and ignore the .length of the array. + * @return An array containing the luminance data. + */ + public abstract byte[] getRow(int y, byte[] row); + + /** + * Fetches luminance data for the underlying bitmap. Values should be fetched using: + * {@code int luminance = array[y * width + x] & 0xff} + * + * @return A row-major 2D array of luminance values. Do not use result.length as it may be + * larger than width * height bytes on some platforms. Do not modify the contents + * of the result. + */ + public abstract byte[] getMatrix(); + + /** + * @return The width of the bitmap. + */ + public final int getWidth() { + return width; + } + + /** + * @return The height of the bitmap. + */ + public final int getHeight() { + return height; + } + + /** + * @return Whether this subclass supports cropping. + */ + public boolean isCropSupported() { + return false; + } + + /** + * Returns a new object with cropped image data. Implementations may keep a reference to the + * original data rather than a copy. Only callable if isCropSupported() is true. + * + * @param left The left coordinate, which must be in [0,getWidth()) + * @param top The top coordinate, which must be in [0,getHeight()) + * @param width The width of the rectangle to crop. + * @param height The height of the rectangle to crop. + * @return A cropped version of this object. + */ + public LuminanceSource crop(int left, int top, int width, int height) { + throw new UnsupportedOperationException("This luminance source does not support cropping."); + } + + /** + * @return Whether this subclass supports counter-clockwise rotation. + */ + public boolean isRotateSupported() { + return false; + } + + /** + * @return a wrapper of this {@code LuminanceSource} which inverts the luminances it returns -- black becomes + * white and vice versa, and each value becomes (255-value). + */ + public LuminanceSource invert() { + return new InvertedLuminanceSource(this); + } + + /** + * Returns a new object with rotated image data by 90 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public LuminanceSource rotateCounterClockwise() { + throw new UnsupportedOperationException("This luminance source does not support rotation by 90 degrees."); + } + + /** + * Returns a new object with rotated image data by 45 degrees counterclockwise. + * Only callable if {@link #isRotateSupported()} is true. + * + * @return A rotated version of this object. + */ + public LuminanceSource rotateCounterClockwise45() { + throw new UnsupportedOperationException("This luminance source does not support rotation by 45 degrees."); + } + + @Override + public final String toString() { + byte[] row = new byte[width]; + StringBuilder result = new StringBuilder(height * (width + 1)); + for (int y = 0; y < height; y++) { + row = getRow(y, row); + for (int x = 0; x < width; x++) { + int luminance = row[x] & 0xFF; + char c; + if (luminance < 0x40) { + c = '#'; + } else if (luminance < 0x80) { + c = '+'; + } else if (luminance < 0xC0) { + c = '.'; + } else { + c = ' '; + } + result.append(c); + } + result.append('\n'); + } + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/MultiFormatReader.java b/rubylib/src/main/java/com/google/zxing/MultiFormatReader.java new file mode 100644 index 0000000..fe6f4ce --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/MultiFormatReader.java @@ -0,0 +1,180 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.aztec.AztecReader; +import com.google.zxing.datamatrix.DataMatrixReader; +import com.google.zxing.maxicode.MaxiCodeReader; +import com.google.zxing.oned.MultiFormatOneDReader; +import com.google.zxing.pdf417.PDF417Reader; +import com.google.zxing.qrcode.QRCodeReader; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * MultiFormatReader is a convenience class and the main entry point into the library for most uses. + * By default it attempts to decode all barcode formats that the library supports. Optionally, you + * can provide a hints object to request different behavior, for example only decoding QR codes. + * + * @author Sean Owen + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class MultiFormatReader implements Reader { + + private Map hints; + private Reader[] readers; + + /** + * This version of decode honors the intent of Reader.decode(BinaryBitmap) in that it + * passes null as a hint to the decoders. However, that makes it inefficient to call repeatedly. + * Use setHints() followed by decodeWithState() for continuous scan applications. + * + * @param image The pixel data to decode + * @return The contents of the image + * @throws NotFoundException Any errors which occurred + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException { + setHints(null); + return decodeInternal(image); + } + + /** + * Decode an image using the hints provided. Does not honor existing state. + * + * @param image The pixel data to decode + * @param hints The hints to use, clearing the previous state. + * @return The contents of the image + * @throws NotFoundException Any errors which occurred + */ + @Override + public Result decode(BinaryBitmap image, Map hints) throws NotFoundException { + setHints(hints); + return decodeInternal(image); + } + + /** + * Decode an image using the state set up by calling setHints() previously. Continuous scan + * clients will get a large speed increase by using this instead of decode(). + * + * @param image The pixel data to decode + * @return The contents of the image + * @throws NotFoundException Any errors which occurred + */ + public Result decodeWithState(BinaryBitmap image) throws NotFoundException { + // Make sure to set up the default state so we don't crash + if (readers == null) { + setHints(null); + } + return decodeInternal(image); + } + + /** + * This method adds state to the MultiFormatReader. By setting the hints once, subsequent calls + * to decodeWithState(image) can reuse the same set of readers without reallocating memory. This + * is important for performance in continuous scan clients. + * + * @param hints The set of hints to use for subsequent calls to decode(image) + */ + public void setHints(Map hints) { + this.hints = hints; + + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + @SuppressWarnings("unchecked") + Collection formats = + hints == null ? null : (Collection) hints.get(DecodeHintType.POSSIBLE_FORMATS); + Collection readers = new ArrayList<>(); + if (formats != null) { + boolean addOneDReader = + formats.contains(BarcodeFormat.UPC_A) || + formats.contains(BarcodeFormat.UPC_E) || + formats.contains(BarcodeFormat.EAN_13) || + formats.contains(BarcodeFormat.EAN_8) || + formats.contains(BarcodeFormat.CODABAR) || + formats.contains(BarcodeFormat.CODE_39) || + formats.contains(BarcodeFormat.CODE_93) || + formats.contains(BarcodeFormat.CODE_128) || + formats.contains(BarcodeFormat.ITF) || + formats.contains(BarcodeFormat.RSS_14) || + formats.contains(BarcodeFormat.RSS_EXPANDED); + // Put 1D readers upfront in "normal" mode + if (addOneDReader && !tryHarder) { + readers.add(new MultiFormatOneDReader(hints)); + } + if (formats.contains(BarcodeFormat.QR_CODE)) { + readers.add(new QRCodeReader()); + } + if (formats.contains(BarcodeFormat.DATA_MATRIX)) { + readers.add(new DataMatrixReader()); + } + if (formats.contains(BarcodeFormat.AZTEC)) { + readers.add(new AztecReader()); + } + if (formats.contains(BarcodeFormat.PDF_417)) { + readers.add(new PDF417Reader()); + } + if (formats.contains(BarcodeFormat.MAXICODE)) { + readers.add(new MaxiCodeReader()); + } + // At end in "try harder" mode + if (addOneDReader && tryHarder) { + readers.add(new MultiFormatOneDReader(hints)); + } + } + if (readers.isEmpty()) { + if (!tryHarder) { + readers.add(new MultiFormatOneDReader(hints)); + } + + readers.add(new QRCodeReader()); + readers.add(new DataMatrixReader()); + readers.add(new AztecReader()); + readers.add(new PDF417Reader()); + readers.add(new MaxiCodeReader()); + + if (tryHarder) { + readers.add(new MultiFormatOneDReader(hints)); + } + } + this.readers = readers.toArray(new Reader[readers.size()]); + } + + @Override + public void reset() { + if (readers != null) { + for (Reader reader : readers) { + reader.reset(); + } + } + } + + private Result decodeInternal(BinaryBitmap image) throws NotFoundException { + if (readers != null) { + for (Reader reader : readers) { + try { + return reader.decode(image, hints); + } catch (ReaderException re) { + // continue + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/MultiFormatWriter.java b/rubylib/src/main/java/com/google/zxing/MultiFormatWriter.java new file mode 100644 index 0000000..19c29a4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/MultiFormatWriter.java @@ -0,0 +1,105 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.aztec.AztecWriter; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.datamatrix.DataMatrixWriter; +import com.google.zxing.oned.CodaBarWriter; +import com.google.zxing.oned.Code128Writer; +import com.google.zxing.oned.Code39Writer; +import com.google.zxing.oned.Code93Writer; +import com.google.zxing.oned.EAN13Writer; +import com.google.zxing.oned.EAN8Writer; +import com.google.zxing.oned.ITFWriter; +import com.google.zxing.oned.UPCAWriter; +import com.google.zxing.oned.UPCEWriter; +import com.google.zxing.pdf417.PDF417Writer; +import com.google.zxing.qrcode.QRCodeWriter; + +import java.util.Map; + +/** + * This is a factory class which finds the appropriate Writer subclass for the BarcodeFormat + * requested and encodes the barcode with the supplied contents. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class MultiFormatWriter implements Writer { + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height) throws WriterException { + return encode(contents, format, width, height, null); + } + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, int height, + Map hints) throws WriterException { + + Writer writer; + switch (format) { + case EAN_8: + writer = new EAN8Writer(); + break; + case UPC_E: + writer = new UPCEWriter(); + break; + case EAN_13: + writer = new EAN13Writer(); + break; + case UPC_A: + writer = new UPCAWriter(); + break; + case QR_CODE: + writer = new QRCodeWriter(); + break; + case CODE_39: + writer = new Code39Writer(); + break; + case CODE_93: + writer = new Code93Writer(); + break; + case CODE_128: + writer = new Code128Writer(); + break; + case ITF: + writer = new ITFWriter(); + break; + case PDF_417: + writer = new PDF417Writer(); + break; + case CODABAR: + writer = new CodaBarWriter(); + break; + case DATA_MATRIX: + writer = new DataMatrixWriter(); + break; + case AZTEC: + writer = new AztecWriter(); + break; + default: + throw new IllegalArgumentException("No encoder available for format " + format); + } + return writer.encode(contents, format, width, height, hints); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/NotFoundException.java b/rubylib/src/main/java/com/google/zxing/NotFoundException.java new file mode 100644 index 0000000..863526a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/NotFoundException.java @@ -0,0 +1,40 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Thrown when a barcode was not found in the image. It might have been + * partially detected but could not be confirmed. + * + * @author Sean Owen + */ +public final class NotFoundException extends ReaderException { + + private static final NotFoundException INSTANCE = new NotFoundException(); + static { + INSTANCE.setStackTrace(NO_TRACE); // since it's meaningless + } + + private NotFoundException() { + // do nothing + } + + public static NotFoundException getNotFoundInstance() { + return INSTANCE; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/PlanarYUVLuminanceSource.java b/rubylib/src/main/java/com/google/zxing/PlanarYUVLuminanceSource.java new file mode 100644 index 0000000..cecff3e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/PlanarYUVLuminanceSource.java @@ -0,0 +1,168 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * This object extends LuminanceSource around an array of YUV data returned from the camera driver, + * with the option to crop to a rectangle within the full data. This can be used to exclude + * superfluous pixels around the perimeter and speed up decoding. + * + * It works for any pixel format where the Y channel is planar and appears first, including + * YCbCr_420_SP and YCbCr_422_SP. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class PlanarYUVLuminanceSource extends LuminanceSource { + + private static final int THUMBNAIL_SCALE_FACTOR = 2; + + private final byte[] yuvData; + private final int dataWidth; + private final int dataHeight; + private final int left; + private final int top; + + public PlanarYUVLuminanceSource(byte[] yuvData, + int dataWidth, + int dataHeight, + int left, + int top, + int width, + int height, + boolean reverseHorizontal) { + super(width, height); + + if (left + width > dataWidth || top + height > dataHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + + this.yuvData = yuvData; + this.dataWidth = dataWidth; + this.dataHeight = dataHeight; + this.left = left; + this.top = top; + if (reverseHorizontal) { + reverseHorizontal(width, height); + } + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + int width = getWidth(); + if (row == null || row.length < width) { + row = new byte[width]; + } + int offset = (y + top) * dataWidth + left; + System.arraycopy(yuvData, offset, row, 0, width); + return row; + } + + @Override + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + + // If the caller asks for the entire underlying image, save the copy and give them the + // original data. The docs specifically warn that result.length must be ignored. + if (width == dataWidth && height == dataHeight) { + return yuvData; + } + + int area = width * height; + byte[] matrix = new byte[area]; + int inputOffset = top * dataWidth + left; + + // If the width matches the full width of the underlying data, perform a single copy. + if (width == dataWidth) { + System.arraycopy(yuvData, inputOffset, matrix, 0, area); + return matrix; + } + + // Otherwise copy one cropped row at a time. + for (int y = 0; y < height; y++) { + int outputOffset = y * width; + System.arraycopy(yuvData, inputOffset, matrix, outputOffset, width); + inputOffset += dataWidth; + } + return matrix; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new PlanarYUVLuminanceSource(yuvData, + dataWidth, + dataHeight, + this.left + left, + this.top + top, + width, + height, + false); + } + + public int[] renderThumbnail() { + int width = getWidth() / THUMBNAIL_SCALE_FACTOR; + int height = getHeight() / THUMBNAIL_SCALE_FACTOR; + int[] pixels = new int[width * height]; + byte[] yuv = yuvData; + int inputOffset = top * dataWidth + left; + + for (int y = 0; y < height; y++) { + int outputOffset = y * width; + for (int x = 0; x < width; x++) { + int grey = yuv[inputOffset + x * THUMBNAIL_SCALE_FACTOR] & 0xff; + pixels[outputOffset + x] = 0xFF000000 | (grey * 0x00010101); + } + inputOffset += dataWidth * THUMBNAIL_SCALE_FACTOR; + } + return pixels; + } + + /** + * @return width of image from {@link #renderThumbnail()} + */ + public int getThumbnailWidth() { + return getWidth() / THUMBNAIL_SCALE_FACTOR; + } + + /** + * @return height of image from {@link #renderThumbnail()} + */ + public int getThumbnailHeight() { + return getHeight() / THUMBNAIL_SCALE_FACTOR; + } + + private void reverseHorizontal(int width, int height) { + byte[] yuvData = this.yuvData; + for (int y = 0, rowStart = top * dataWidth + left; y < height; y++, rowStart += dataWidth) { + int middle = rowStart + width / 2; + for (int x1 = rowStart, x2 = rowStart + width - 1; x1 < middle; x1++, x2--) { + byte temp = yuvData[x1]; + yuvData[x1] = yuvData[x2]; + yuvData[x2] = temp; + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/RGBLuminanceSource.java b/rubylib/src/main/java/com/google/zxing/RGBLuminanceSource.java new file mode 100644 index 0000000..3950ce8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/RGBLuminanceSource.java @@ -0,0 +1,136 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * This class is used to help decode images from files which arrive as RGB data from + * an ARGB pixel array. It does not support rotation. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Betaminos + */ +public final class RGBLuminanceSource extends LuminanceSource { + + private final byte[] luminances; + private final int dataWidth; + private final int dataHeight; + private final int left; + private final int top; + + public RGBLuminanceSource(int width, int height, int[] pixels) { + super(width, height); + + dataWidth = width; + dataHeight = height; + left = 0; + top = 0; + + // In order to measure pure decoding speed, we convert the entire image to a greyscale array + // up front, which is the same as the Y channel of the YUVLuminanceSource in the real app. + // + // Total number of pixels suffices, can ignore shape + int size = width * height; + luminances = new byte[size]; + for (int offset = 0; offset < size; offset++) { + int pixel = pixels[offset]; + int r = (pixel >> 16) & 0xff; // red + int g2 = (pixel >> 7) & 0x1fe; // 2 * green + int b = pixel & 0xff; // blue + // Calculate green-favouring average cheaply + luminances[offset] = (byte) ((r + g2 + b) / 4); + } + } + + private RGBLuminanceSource(byte[] pixels, + int dataWidth, + int dataHeight, + int left, + int top, + int width, + int height) { + super(width, height); + if (left + width > dataWidth || top + height > dataHeight) { + throw new IllegalArgumentException("Crop rectangle does not fit within image data."); + } + this.luminances = pixels; + this.dataWidth = dataWidth; + this.dataHeight = dataHeight; + this.left = left; + this.top = top; + } + + @Override + public byte[] getRow(int y, byte[] row) { + if (y < 0 || y >= getHeight()) { + throw new IllegalArgumentException("Requested row is outside the image: " + y); + } + int width = getWidth(); + if (row == null || row.length < width) { + row = new byte[width]; + } + int offset = (y + top) * dataWidth + left; + System.arraycopy(luminances, offset, row, 0, width); + return row; + } + + @Override + public byte[] getMatrix() { + int width = getWidth(); + int height = getHeight(); + + // If the caller asks for the entire underlying image, save the copy and give them the + // original data. The docs specifically warn that result.length must be ignored. + if (width == dataWidth && height == dataHeight) { + return luminances; + } + + int area = width * height; + byte[] matrix = new byte[area]; + int inputOffset = top * dataWidth + left; + + // If the width matches the full width of the underlying data, perform a single copy. + if (width == dataWidth) { + System.arraycopy(luminances, inputOffset, matrix, 0, area); + return matrix; + } + + // Otherwise copy one cropped row at a time. + for (int y = 0; y < height; y++) { + int outputOffset = y * width; + System.arraycopy(luminances, inputOffset, matrix, outputOffset, width); + inputOffset += dataWidth; + } + return matrix; + } + + @Override + public boolean isCropSupported() { + return true; + } + + @Override + public LuminanceSource crop(int left, int top, int width, int height) { + return new RGBLuminanceSource(luminances, + dataWidth, + dataHeight, + this.left + left, + this.top + top, + width, + height); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/Reader.java b/rubylib/src/main/java/com/google/zxing/Reader.java new file mode 100644 index 0000000..0e6af10 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/Reader.java @@ -0,0 +1,69 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import java.util.Map; + +/** + * Implementations of this interface can decode an image of a barcode in some format into + * the String it encodes. For example, {@link com.google.zxing.qrcode.QRCodeReader} can + * decode a QR code. The decoder may optionally receive hints from the caller which may help + * it decode more quickly or accurately. + * + * See {@link MultiFormatReader}, which attempts to determine what barcode + * format is present within the image as well, and then decodes it accordingly. + * + * @author Sean Owen + * @author dswitkin@google.com (Daniel Switkin) + */ +public interface Reader { + + /** + * Locates and decodes a barcode in some format within an image. + * + * @param image image of barcode to decode + * @return String which the barcode encodes + * @throws NotFoundException if no potential barcode is found + * @throws ChecksumException if a potential barcode is found but does not pass its checksum + * @throws FormatException if a potential barcode is found but format is invalid + */ + Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException; + + /** + * Locates and decodes a barcode in some format within an image. This method also accepts + * hints, each possibly associated to some data, which may help the implementation decode. + * + * @param image image of barcode to decode + * @param hints passed as a {@link Map} from {@link DecodeHintType} + * to arbitrary data. The + * meaning of the data depends upon the hint type. The implementation may or may not do + * anything with these hints. + * @return String which the barcode encodes + * @throws NotFoundException if no potential barcode is found + * @throws ChecksumException if a potential barcode is found but does not pass its checksum + * @throws FormatException if a potential barcode is found but format is invalid + */ + Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, ChecksumException, FormatException; + + /** + * Resets any internal state the implementation has after a decode, to prepare it + * for reuse. + */ + void reset(); + +} diff --git a/rubylib/src/main/java/com/google/zxing/ReaderException.java b/rubylib/src/main/java/com/google/zxing/ReaderException.java new file mode 100644 index 0000000..32bbcda --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/ReaderException.java @@ -0,0 +1,47 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * The general exception class throw when something goes wrong during decoding of a barcode. + * This includes, but is not limited to, failing checksums / error correction algorithms, being + * unable to locate finder timing patterns, and so on. + * + * @author Sean Owen + */ +public abstract class ReaderException extends Exception { + + // disable stack traces when not running inside test units + protected static final boolean isStackTrace = + System.getProperty("surefire.test.class.path") != null; + protected static final StackTraceElement[] NO_TRACE = new StackTraceElement[0]; + + ReaderException() { + // do nothing + } + + ReaderException(Throwable cause) { + super(cause); + } + + // Prevent stack traces from being taken + @Override + public final synchronized Throwable fillInStackTrace() { + return null; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/Result.java b/rubylib/src/main/java/com/google/zxing/Result.java new file mode 100644 index 0000000..3df435f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/Result.java @@ -0,0 +1,153 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import java.util.EnumMap; +import java.util.Map; + +/** + *

Encapsulates the result of decoding a barcode within an image.

+ * + * @author Sean Owen + */ +public final class Result { + + private final String text; + private final byte[] rawBytes; + private final int numBits; + private ResultPoint[] resultPoints; + private final BarcodeFormat format; + private Map resultMetadata; + private final long timestamp; + + public Result(String text, + byte[] rawBytes, + ResultPoint[] resultPoints, + BarcodeFormat format) { + this(text, rawBytes, resultPoints, format, System.currentTimeMillis()); + } + + public Result(String text, + byte[] rawBytes, + ResultPoint[] resultPoints, + BarcodeFormat format, + long timestamp) { + this(text, rawBytes, rawBytes == null ? 0 : 8 * rawBytes.length, + resultPoints, format, timestamp); + } + + public Result(String text, + byte[] rawBytes, + int numBits, + ResultPoint[] resultPoints, + BarcodeFormat format, + long timestamp) { + this.text = text; + this.rawBytes = rawBytes; + this.numBits = numBits; + this.resultPoints = resultPoints; + this.format = format; + this.resultMetadata = null; + this.timestamp = timestamp; + } + + /** + * @return raw text encoded by the barcode + */ + public String getText() { + return text; + } + + /** + * @return raw bytes encoded by the barcode, if applicable, otherwise {@code null} + */ + public byte[] getRawBytes() { + return rawBytes; + } + + /** + * @return how many bits of {@link #getRawBytes()} are valid; typically 8 times its length + * @since 3.3.0 + */ + public int getNumBits() { + return numBits; + } + + /** + * @return points related to the barcode in the image. These are typically points + * identifying finder patterns or the corners of the barcode. The exact meaning is + * specific to the type of barcode that was decoded. + */ + public ResultPoint[] getResultPoints() { + return resultPoints; + } + + /** + * @return {@link BarcodeFormat} representing the format of the barcode that was decoded + */ + public BarcodeFormat getBarcodeFormat() { + return format; + } + + /** + * @return {@link Map} mapping {@link ResultMetadataType} keys to values. May be + * {@code null}. This contains optional metadata about what was detected about the barcode, + * like orientation. + */ + public Map getResultMetadata() { + return resultMetadata; + } + + public void putMetadata(ResultMetadataType type, Object value) { + if (resultMetadata == null) { + resultMetadata = new EnumMap<>(ResultMetadataType.class); + } + resultMetadata.put(type, value); + } + + public void putAllMetadata(Map metadata) { + if (metadata != null) { + if (resultMetadata == null) { + resultMetadata = metadata; + } else { + resultMetadata.putAll(metadata); + } + } + } + + public void addResultPoints(ResultPoint[] newPoints) { + ResultPoint[] oldPoints = resultPoints; + if (oldPoints == null) { + resultPoints = newPoints; + } else if (newPoints != null && newPoints.length > 0) { + ResultPoint[] allPoints = new ResultPoint[oldPoints.length + newPoints.length]; + System.arraycopy(oldPoints, 0, allPoints, 0, oldPoints.length); + System.arraycopy(newPoints, 0, allPoints, oldPoints.length, newPoints.length); + resultPoints = allPoints; + } + } + + public long getTimestamp() { + return timestamp; + } + + @Override + public String toString() { + return text; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/ResultMetadataType.java b/rubylib/src/main/java/com/google/zxing/ResultMetadataType.java new file mode 100644 index 0000000..67c5363 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/ResultMetadataType.java @@ -0,0 +1,97 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Represents some type of metadata about the result of the decoding that the decoder + * wishes to communicate back to the caller. + * + * @author Sean Owen + */ +public enum ResultMetadataType { + + /** + * Unspecified, application-specific metadata. Maps to an unspecified {@link Object}. + */ + OTHER, + + /** + * Denotes the likely approximate orientation of the barcode in the image. This value + * is given as degrees rotated clockwise from the normal, upright orientation. + * For example a 1D barcode which was found by reading top-to-bottom would be + * said to have orientation "90". This key maps to an {@link Integer} whose + * value is in the range [0,360). + */ + ORIENTATION, + + /** + *

2D barcode formats typically encode text, but allow for a sort of 'byte mode' + * which is sometimes used to encode binary data. While {@link Result} makes available + * the complete raw bytes in the barcode for these formats, it does not offer the bytes + * from the byte segments alone.

+ * + *

This maps to a {@link java.util.List} of byte arrays corresponding to the + * raw bytes in the byte segments in the barcode, in order.

+ */ + BYTE_SEGMENTS, + + /** + * Error correction level used, if applicable. The value type depends on the + * format, but is typically a String. + */ + ERROR_CORRECTION_LEVEL, + + /** + * For some periodicals, indicates the issue number as an {@link Integer}. + */ + ISSUE_NUMBER, + + /** + * For some products, indicates the suggested retail price in the barcode as a + * formatted {@link String}. + */ + SUGGESTED_PRICE, + + /** + * For some products, the possible country of manufacture as a {@link String} denoting the + * ISO country code. Some map to multiple possible countries, like "US/CA". + */ + POSSIBLE_COUNTRY, + + /** + * For some products, the extension text + */ + UPC_EAN_EXTENSION, + + /** + * PDF417-specific metadata + */ + PDF417_EXTRA_METADATA, + + /** + * If the code format supports structured append and the current scanned code is part of one then the + * sequence number is given with it. + */ + STRUCTURED_APPEND_SEQUENCE, + + /** + * If the code format supports structured append and the current scanned code is part of one then the + * parity is given with it. + */ + STRUCTURED_APPEND_PARITY, + +} diff --git a/rubylib/src/main/java/com/google/zxing/ResultPoint.java b/rubylib/src/main/java/com/google/zxing/ResultPoint.java new file mode 100644 index 0000000..9bd8cd2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/ResultPoint.java @@ -0,0 +1,130 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.common.detector.MathUtils; + +/** + *

Encapsulates a point of interest in an image containing a barcode. Typically, this + * would be the location of a finder pattern or the corner of the barcode, for example.

+ * + * @author Sean Owen + */ +public class ResultPoint { + + private final float x; + private final float y; + + public ResultPoint(float x, float y) { + this.x = x; + this.y = y; + } + + public final float getX() { + return x; + } + + public final float getY() { + return y; + } + + @Override + public final boolean equals(Object other) { + if (other instanceof ResultPoint) { + ResultPoint otherPoint = (ResultPoint) other; + return x == otherPoint.x && y == otherPoint.y; + } + return false; + } + + @Override + public final int hashCode() { + return 31 * Float.floatToIntBits(x) + Float.floatToIntBits(y); + } + + @Override + public final String toString() { + return "(" + x + ',' + y + ')'; + } + + /** + * Orders an array of three ResultPoints in an order [A,B,C] such that AB is less than AC + * and BC is less than AC, and the angle between BC and BA is less than 180 degrees. + * + * @param patterns array of three {@code ResultPoint} to order + */ + public static void orderBestPatterns(ResultPoint[] patterns) { + + // Find distances between pattern centers + float zeroOneDistance = distance(patterns[0], patterns[1]); + float oneTwoDistance = distance(patterns[1], patterns[2]); + float zeroTwoDistance = distance(patterns[0], patterns[2]); + + ResultPoint pointA; + ResultPoint pointB; + ResultPoint pointC; + // Assume one closest to other two is B; A and C will just be guesses at first + if (oneTwoDistance >= zeroOneDistance && oneTwoDistance >= zeroTwoDistance) { + pointB = patterns[0]; + pointA = patterns[1]; + pointC = patterns[2]; + } else if (zeroTwoDistance >= oneTwoDistance && zeroTwoDistance >= zeroOneDistance) { + pointB = patterns[1]; + pointA = patterns[0]; + pointC = patterns[2]; + } else { + pointB = patterns[2]; + pointA = patterns[0]; + pointC = patterns[1]; + } + + // Use cross product to figure out whether A and C are correct or flipped. + // This asks whether BC x BA has a positive z component, which is the arrangement + // we want for A, B, C. If it's negative, then we've got it flipped around and + // should swap A and C. + if (crossProductZ(pointA, pointB, pointC) < 0.0f) { + ResultPoint temp = pointA; + pointA = pointC; + pointC = temp; + } + + patterns[0] = pointA; + patterns[1] = pointB; + patterns[2] = pointC; + } + + /** + * @param pattern1 first pattern + * @param pattern2 second pattern + * @return distance between two points + */ + public static float distance(ResultPoint pattern1, ResultPoint pattern2) { + return MathUtils.distance(pattern1.x, pattern1.y, pattern2.x, pattern2.y); + } + + /** + * Returns the z component of the cross product between vectors BC and BA. + */ + private static float crossProductZ(ResultPoint pointA, + ResultPoint pointB, + ResultPoint pointC) { + float bX = pointB.x; + float bY = pointB.y; + return ((pointC.x - bX) * (pointA.y - bY)) - ((pointC.y - bY) * (pointA.x - bX)); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/ResultPointCallback.java b/rubylib/src/main/java/com/google/zxing/ResultPointCallback.java new file mode 100644 index 0000000..0c85410 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/ResultPointCallback.java @@ -0,0 +1,29 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * Callback which is invoked when a possible result point (significant + * point in the barcode image such as a corner) is found. + * + * @see DecodeHintType#NEED_RESULT_POINT_CALLBACK + */ +public interface ResultPointCallback { + + void foundPossibleResultPoint(ResultPoint point); + +} diff --git a/rubylib/src/main/java/com/google/zxing/Writer.java b/rubylib/src/main/java/com/google/zxing/Writer.java new file mode 100644 index 0000000..f405fd8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/Writer.java @@ -0,0 +1,59 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * The base class for all objects which encode/generate a barcode image. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public interface Writer { + + /** + * Encode a barcode using the default settings. + * + * @param contents The contents to encode in the barcode + * @param format The barcode format to generate + * @param width The preferred width in pixels + * @param height The preferred height in pixels + * @return {@link BitMatrix} representing encoded barcode image + * @throws WriterException if contents cannot be encoded legally in a format + */ + BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException; + + /** + * @param contents The contents to encode in the barcode + * @param format The barcode format to generate + * @param width The preferred width in pixels + * @param height The preferred height in pixels + * @param hints Additional parameters to supply to the encoder + * @return {@link BitMatrix} representing encoded barcode image + * @throws WriterException if contents cannot be encoded legally in a format + */ + BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) + throws WriterException; + +} diff --git a/rubylib/src/main/java/com/google/zxing/WriterException.java b/rubylib/src/main/java/com/google/zxing/WriterException.java new file mode 100644 index 0000000..d2a37bc --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/WriterException.java @@ -0,0 +1,38 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing; + +/** + * A base class which covers the range of exceptions which may occur when encoding a barcode using + * the Writer framework. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class WriterException extends Exception { + + public WriterException() { + } + + public WriterException(String message) { + super(message); + } + + public WriterException(Throwable cause) { + super(cause); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/AztecDetectorResult.java b/rubylib/src/main/java/com/google/zxing/aztec/AztecDetectorResult.java new file mode 100644 index 0000000..8341578 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/AztecDetectorResult.java @@ -0,0 +1,58 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec; + +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; + +/** + *

Extends {@link DetectorResult} with more information specific to the Aztec format, + * like the number of layers and whether it's compact.

+ * + * @author Sean Owen + */ +public final class AztecDetectorResult extends DetectorResult { + + private final boolean compact; + private final int nbDatablocks; + private final int nbLayers; + + public AztecDetectorResult(BitMatrix bits, + ResultPoint[] points, + boolean compact, + int nbDatablocks, + int nbLayers) { + super(bits, points); + this.compact = compact; + this.nbDatablocks = nbDatablocks; + this.nbLayers = nbLayers; + } + + public int getNbLayers() { + return nbLayers; + } + + public int getNbDatablocks() { + return nbDatablocks; + } + + public boolean isCompact() { + return compact; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/AztecReader.java b/rubylib/src/main/java/com/google/zxing/aztec/AztecReader.java new file mode 100644 index 0000000..c553e0a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/AztecReader.java @@ -0,0 +1,122 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.aztec.decoder.Decoder; +import com.google.zxing.aztec.detector.Detector; +import com.google.zxing.common.DecoderResult; + +import java.util.List; +import java.util.Map; + +/** + * This implementation can detect and decode Aztec codes in an image. + * + * @author David Olivier + */ +public final class AztecReader implements Reader { + + /** + * Locates and decodes a Data Matrix code in an image. + * + * @return a String representing the content encoded by the Data Matrix code + * @throws NotFoundException if a Data Matrix code cannot be found + * @throws FormatException if a Data Matrix code cannot be decoded + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException { + return decode(image, null); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, FormatException { + + NotFoundException notFoundException = null; + FormatException formatException = null; + Detector detector = new Detector(image.getBlackMatrix()); + ResultPoint[] points = null; + DecoderResult decoderResult = null; + try { + AztecDetectorResult detectorResult = detector.detect(false); + points = detectorResult.getPoints(); + decoderResult = new Decoder().decode(detectorResult); + } catch (NotFoundException e) { + notFoundException = e; + } catch (FormatException e) { + formatException = e; + } + if (decoderResult == null) { + try { + AztecDetectorResult detectorResult = detector.detect(true); + points = detectorResult.getPoints(); + decoderResult = new Decoder().decode(detectorResult); + } catch (NotFoundException | FormatException e) { + if (notFoundException != null) { + throw notFoundException; + } + if (formatException != null) { + throw formatException; + } + throw e; + } + } + + if (hints != null) { + ResultPointCallback rpcb = (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + if (rpcb != null) { + for (ResultPoint point : points) { + rpcb.foundPossibleResultPoint(point); + } + } + } + + Result result = new Result(decoderResult.getText(), + decoderResult.getRawBytes(), + decoderResult.getNumBits(), + points, + BarcodeFormat.AZTEC, + System.currentTimeMillis()); + + List byteSegments = decoderResult.getByteSegments(); + if (byteSegments != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments); + } + String ecLevel = decoderResult.getECLevel(); + if (ecLevel != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); + } + + return result; + } + + @Override + public void reset() { + // do nothing + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/AztecWriter.java b/rubylib/src/main/java/com/google/zxing/aztec/AztecWriter.java new file mode 100644 index 0000000..9eb4190 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/AztecWriter.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.aztec.encoder.AztecCode; +import com.google.zxing.aztec.encoder.Encoder; +import com.google.zxing.common.BitMatrix; + +import java.nio.charset.Charset; +import java.util.Map; + +/** + * Renders an Aztec code as a {@link BitMatrix}. + */ +public final class AztecWriter implements Writer { + + private static final Charset DEFAULT_CHARSET = Charset.forName("ISO-8859-1"); + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) { + return encode(contents, format, width, height, null); + } + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, Map hints) { + Charset charset = DEFAULT_CHARSET; + int eccPercent = Encoder.DEFAULT_EC_PERCENT; + int layers = Encoder.DEFAULT_AZTEC_LAYERS; + if (hints != null) { + if (hints.containsKey(EncodeHintType.CHARACTER_SET)) { + charset = Charset.forName(hints.get(EncodeHintType.CHARACTER_SET).toString()); + } + if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) { + eccPercent = Integer.parseInt(hints.get(EncodeHintType.ERROR_CORRECTION).toString()); + } + if (hints.containsKey(EncodeHintType.AZTEC_LAYERS)) { + layers = Integer.parseInt(hints.get(EncodeHintType.AZTEC_LAYERS).toString()); + } + } + return encode(contents, format, width, height, charset, eccPercent, layers); + } + + private static BitMatrix encode(String contents, BarcodeFormat format, + int width, int height, + Charset charset, int eccPercent, int layers) { + if (format != BarcodeFormat.AZTEC) { + throw new IllegalArgumentException("Can only encode AZTEC, but got " + format); + } + AztecCode aztec = Encoder.encode(contents.getBytes(charset), eccPercent, layers); + return renderResult(aztec, width, height); + } + + private static BitMatrix renderResult(AztecCode code, int width, int height) { + BitMatrix input = code.getMatrix(); + if (input == null) { + throw new IllegalStateException(); + } + int inputWidth = input.getWidth(); + int inputHeight = input.getHeight(); + int outputWidth = Math.max(width, inputWidth); + int outputHeight = Math.max(height, inputHeight); + + int multiple = Math.min(outputWidth / inputWidth, outputHeight / inputHeight); + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + int topPadding = (outputHeight - (inputHeight * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + + for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) { + // Write the contents of this row of the barcode + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (input.get(inputX, inputY)) { + output.setRegion(outputX, outputY, multiple, multiple); + } + } + } + return output; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/decoder/Decoder.java b/rubylib/src/main/java/com/google/zxing/aztec/decoder/Decoder.java new file mode 100644 index 0000000..d456dd1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/decoder/Decoder.java @@ -0,0 +1,366 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.aztec.AztecDetectorResult; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +import java.util.Arrays; + +/** + *

The main class which implements Aztec Code decoding -- as opposed to locating and extracting + * the Aztec Code from an image.

+ * + * @author David Olivier + */ +public final class Decoder { + + private enum Table { + UPPER, + LOWER, + MIXED, + DIGIT, + PUNCT, + BINARY + } + + private static final String[] UPPER_TABLE = { + "CTRL_PS", " ", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", + "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "CTRL_LL", "CTRL_ML", "CTRL_DL", "CTRL_BS" + }; + + private static final String[] LOWER_TABLE = { + "CTRL_PS", " ", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", + "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "CTRL_US", "CTRL_ML", "CTRL_DL", "CTRL_BS" + }; + + private static final String[] MIXED_TABLE = { + "CTRL_PS", " ", "\1", "\2", "\3", "\4", "\5", "\6", "\7", "\b", "\t", "\n", + "\13", "\f", "\r", "\33", "\34", "\35", "\36", "\37", "@", "\\", "^", "_", + "`", "|", "~", "\177", "CTRL_LL", "CTRL_UL", "CTRL_PL", "CTRL_BS" + }; + + private static final String[] PUNCT_TABLE = { + "", "\r", "\r\n", ". ", ", ", ": ", "!", "\"", "#", "$", "%", "&", "'", "(", ")", + "*", "+", ",", "-", ".", "/", ":", ";", "<", "=", ">", "?", "[", "]", "{", "}", "CTRL_UL" + }; + + private static final String[] DIGIT_TABLE = { + "CTRL_PS", " ", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ",", ".", "CTRL_UL", "CTRL_US" + }; + + private AztecDetectorResult ddata; + + public DecoderResult decode(AztecDetectorResult detectorResult) throws FormatException { + ddata = detectorResult; + BitMatrix matrix = detectorResult.getBits(); + boolean[] rawbits = extractBits(matrix); + boolean[] correctedBits = correctBits(rawbits); + byte[] rawBytes = convertBoolArrayToByteArray(correctedBits); + String result = getEncodedData(correctedBits); + DecoderResult decoderResult = new DecoderResult(rawBytes, result, null, null); + decoderResult.setNumBits(correctedBits.length); + return decoderResult; + } + + // This method is used for testing the high-level encoder + public static String highLevelDecode(boolean[] correctedBits) { + return getEncodedData(correctedBits); + } + + /** + * Gets the string encoded in the aztec code bits + * + * @return the decoded string + */ + private static String getEncodedData(boolean[] correctedBits) { + int endIndex = correctedBits.length; + Table latchTable = Table.UPPER; // table most recently latched to + Table shiftTable = Table.UPPER; // table to use for the next read + StringBuilder result = new StringBuilder(20); + int index = 0; + while (index < endIndex) { + if (shiftTable == Table.BINARY) { + if (endIndex - index < 5) { + break; + } + int length = readCode(correctedBits, index, 5); + index += 5; + if (length == 0) { + if (endIndex - index < 11) { + break; + } + length = readCode(correctedBits, index, 11) + 31; + index += 11; + } + for (int charCount = 0; charCount < length; charCount++) { + if (endIndex - index < 8) { + index = endIndex; // Force outer loop to exit + break; + } + int code = readCode(correctedBits, index, 8); + result.append((char) code); + index += 8; + } + // Go back to whatever mode we had been in + shiftTable = latchTable; + } else { + int size = shiftTable == Table.DIGIT ? 4 : 5; + if (endIndex - index < size) { + break; + } + int code = readCode(correctedBits, index, size); + index += size; + String str = getCharacter(shiftTable, code); + if (str.startsWith("CTRL_")) { + // Table changes + // ISO/IEC 24778:2008 prescribes ending a shift sequence in the mode from which it was invoked. + // That's including when that mode is a shift. + // Our test case dlusbs.png for issue #642 exercises that. + latchTable = shiftTable; // Latch the current mode, so as to return to Upper after U/S B/S + shiftTable = getTable(str.charAt(5)); + if (str.charAt(6) == 'L') { + latchTable = shiftTable; + } + } else { + result.append(str); + // Go back to whatever mode we had been in + shiftTable = latchTable; + } + } + } + return result.toString(); + } + + /** + * gets the table corresponding to the char passed + */ + private static Table getTable(char t) { + switch (t) { + case 'L': + return Table.LOWER; + case 'P': + return Table.PUNCT; + case 'M': + return Table.MIXED; + case 'D': + return Table.DIGIT; + case 'B': + return Table.BINARY; + case 'U': + default: + return Table.UPPER; + } + } + + /** + * Gets the character (or string) corresponding to the passed code in the given table + * + * @param table the table used + * @param code the code of the character + */ + private static String getCharacter(Table table, int code) { + switch (table) { + case UPPER: + return UPPER_TABLE[code]; + case LOWER: + return LOWER_TABLE[code]; + case MIXED: + return MIXED_TABLE[code]; + case PUNCT: + return PUNCT_TABLE[code]; + case DIGIT: + return DIGIT_TABLE[code]; + default: + // Should not reach here. + throw new IllegalStateException("Bad table"); + } + } + + /** + *

Performs RS error correction on an array of bits.

+ * + * @return the corrected array + * @throws FormatException if the input contains too many errors + */ + private boolean[] correctBits(boolean[] rawbits) throws FormatException { + GenericGF gf; + int codewordSize; + + if (ddata.getNbLayers() <= 2) { + codewordSize = 6; + gf = GenericGF.AZTEC_DATA_6; + } else if (ddata.getNbLayers() <= 8) { + codewordSize = 8; + gf = GenericGF.AZTEC_DATA_8; + } else if (ddata.getNbLayers() <= 22) { + codewordSize = 10; + gf = GenericGF.AZTEC_DATA_10; + } else { + codewordSize = 12; + gf = GenericGF.AZTEC_DATA_12; + } + + int numDataCodewords = ddata.getNbDatablocks(); + int numCodewords = rawbits.length / codewordSize; + if (numCodewords < numDataCodewords) { + throw FormatException.getFormatInstance(); + } + int offset = rawbits.length % codewordSize; + + int[] dataWords = new int[numCodewords]; + for (int i = 0; i < numCodewords; i++, offset += codewordSize) { + dataWords[i] = readCode(rawbits, offset, codewordSize); + } + + try { + ReedSolomonDecoder rsDecoder = new ReedSolomonDecoder(gf); + rsDecoder.decode(dataWords, numCodewords - numDataCodewords); + } catch (ReedSolomonException ex) { + throw FormatException.getFormatInstance(ex); + } + + // Now perform the unstuffing operation. + // First, count how many bits are going to be thrown out as stuffing + int mask = (1 << codewordSize) - 1; + int stuffedBits = 0; + for (int i = 0; i < numDataCodewords; i++) { + int dataWord = dataWords[i]; + if (dataWord == 0 || dataWord == mask) { + throw FormatException.getFormatInstance(); + } else if (dataWord == 1 || dataWord == mask - 1) { + stuffedBits++; + } + } + // Now, actually unpack the bits and remove the stuffing + boolean[] correctedBits = new boolean[numDataCodewords * codewordSize - stuffedBits]; + int index = 0; + for (int i = 0; i < numDataCodewords; i++) { + int dataWord = dataWords[i]; + if (dataWord == 1 || dataWord == mask - 1) { + // next codewordSize-1 bits are all zeros or all ones + Arrays.fill(correctedBits, index, index + codewordSize - 1, dataWord > 1); + index += codewordSize - 1; + } else { + for (int bit = codewordSize - 1; bit >= 0; --bit) { + correctedBits[index++] = (dataWord & (1 << bit)) != 0; + } + } + } + return correctedBits; + } + + /** + * Gets the array of bits from an Aztec Code matrix + * + * @return the array of bits + */ + private boolean[] extractBits(BitMatrix matrix) { + boolean compact = ddata.isCompact(); + int layers = ddata.getNbLayers(); + int baseMatrixSize = (compact ? 11 : 14) + layers * 4; // not including alignment lines + int[] alignmentMap = new int[baseMatrixSize]; + boolean[] rawbits = new boolean[totalBitsInLayer(layers, compact)]; + + if (compact) { + for (int i = 0; i < alignmentMap.length; i++) { + alignmentMap[i] = i; + } + } else { + int matrixSize = baseMatrixSize + 1 + 2 * ((baseMatrixSize / 2 - 1) / 15); + int origCenter = baseMatrixSize / 2; + int center = matrixSize / 2; + for (int i = 0; i < origCenter; i++) { + int newOffset = i + i / 15; + alignmentMap[origCenter - i - 1] = center - newOffset - 1; + alignmentMap[origCenter + i] = center + newOffset + 1; + } + } + for (int i = 0, rowOffset = 0; i < layers; i++) { + int rowSize = (layers - i) * 4 + (compact ? 9 : 12); + // The top-left most point of this layer is (not including alignment lines) + int low = i * 2; + // The bottom-right most point of this layer is (not including alignment lines) + int high = baseMatrixSize - 1 - low; + // We pull bits from the two 2 x rowSize columns and two rowSize x 2 rows + for (int j = 0; j < rowSize; j++) { + int columnOffset = j * 2; + for (int k = 0; k < 2; k++) { + // left column + rawbits[rowOffset + columnOffset + k] = + matrix.get(alignmentMap[low + k], alignmentMap[low + j]); + // bottom row + rawbits[rowOffset + 2 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[low + j], alignmentMap[high - k]); + // right column + rawbits[rowOffset + 4 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[high - k], alignmentMap[high - j]); + // top row + rawbits[rowOffset + 6 * rowSize + columnOffset + k] = + matrix.get(alignmentMap[high - j], alignmentMap[low + k]); + } + } + rowOffset += rowSize * 8; + } + return rawbits; + } + + /** + * Reads a code of given length and at given index in an array of bits + */ + private static int readCode(boolean[] rawbits, int startIndex, int length) { + int res = 0; + for (int i = startIndex; i < startIndex + length; i++) { + res <<= 1; + if (rawbits[i]) { + res |= 0x01; + } + } + return res; + } + + /** + * Reads a code of length 8 in an array of bits, padding with zeros + */ + private static byte readByte(boolean[] rawbits, int startIndex) { + int n = rawbits.length - startIndex; + if (n >= 8) { + return (byte) readCode(rawbits, startIndex, 8); + } + return (byte) (readCode(rawbits, startIndex, n) << (8 - n)); + } + + /** + * Packs a bit array into bytes, most significant bit first + */ + static byte[] convertBoolArrayToByteArray(boolean[] boolArr) { + byte[] byteArr = new byte[(boolArr.length + 7) / 8]; + for (int i = 0; i < byteArr.length; i++) { + byteArr[i] = readByte(boolArr, 8 * i); + } + return byteArr; + } + + private static int totalBitsInLayer(int layers, boolean compact) { + return ((compact ? 88 : 112) + 16 * layers) * layers; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/detector/Detector.java b/rubylib/src/main/java/com/google/zxing/aztec/detector/Detector.java new file mode 100644 index 0000000..89adcd8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/detector/Detector.java @@ -0,0 +1,601 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.aztec.AztecDetectorResult; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.GridSampler; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.common.detector.WhiteRectangleDetector; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +/** + * Encapsulates logic that can detect an Aztec Code in an image, even if the Aztec Code + * is rotated or skewed, or partially obscured. + * + * @author David Olivier + * @author Frank Yellin + */ +public final class Detector { + + private static final int[] EXPECTED_CORNER_BITS = { + 0xee0, // 07340 XXX .XX X.. ... + 0x1dc, // 00734 ... XXX .XX X.. + 0x83b, // 04073 X.. ... XXX .XX + 0x707, // 03407 .XX X.. ... XXX + }; + + private final BitMatrix image; + + private boolean compact; + private int nbLayers; + private int nbDataBlocks; + private int nbCenterLayers; + private int shift; + + public Detector(BitMatrix image) { + this.image = image; + } + + public AztecDetectorResult detect() throws NotFoundException { + return detect(false); + } + + /** + * Detects an Aztec Code in an image. + * + * @param isMirror if true, image is a mirror-image of original + * @return {@link AztecDetectorResult} encapsulating results of detecting an Aztec Code + * @throws NotFoundException if no Aztec Code can be found + */ + public AztecDetectorResult detect(boolean isMirror) throws NotFoundException { + + // 1. Get the center of the aztec matrix + Point pCenter = getMatrixCenter(); + + // 2. Get the center points of the four diagonal points just outside the bull's eye + // [topRight, bottomRight, bottomLeft, topLeft] + ResultPoint[] bullsEyeCorners = getBullsEyeCorners(pCenter); + + if (isMirror) { + ResultPoint temp = bullsEyeCorners[0]; + bullsEyeCorners[0] = bullsEyeCorners[2]; + bullsEyeCorners[2] = temp; + } + + // 3. Get the size of the matrix and other parameters from the bull's eye + extractParameters(bullsEyeCorners); + + // 4. Sample the grid + BitMatrix bits = sampleGrid(image, + bullsEyeCorners[shift % 4], + bullsEyeCorners[(shift + 1) % 4], + bullsEyeCorners[(shift + 2) % 4], + bullsEyeCorners[(shift + 3) % 4]); + + // 5. Get the corners of the matrix. + ResultPoint[] corners = getMatrixCornerPoints(bullsEyeCorners); + + return new AztecDetectorResult(bits, corners, compact, nbDataBlocks, nbLayers); + } + + /** + * Extracts the number of data layers and data blocks from the layer around the bull's eye. + * + * @param bullsEyeCorners the array of bull's eye corners + * @throws NotFoundException in case of too many errors or invalid parameters + */ + private void extractParameters(ResultPoint[] bullsEyeCorners) throws NotFoundException { + if (!isValid(bullsEyeCorners[0]) || !isValid(bullsEyeCorners[1]) || + !isValid(bullsEyeCorners[2]) || !isValid(bullsEyeCorners[3])) { + throw NotFoundException.getNotFoundInstance(); + } + int length = 2 * nbCenterLayers; + // Get the bits around the bull's eye + int[] sides = { + sampleLine(bullsEyeCorners[0], bullsEyeCorners[1], length), // Right side + sampleLine(bullsEyeCorners[1], bullsEyeCorners[2], length), // Bottom + sampleLine(bullsEyeCorners[2], bullsEyeCorners[3], length), // Left side + sampleLine(bullsEyeCorners[3], bullsEyeCorners[0], length) // Top + }; + + // bullsEyeCorners[shift] is the corner of the bulls'eye that has three + // orientation marks. + // sides[shift] is the row/column that goes from the corner with three + // orientation marks to the corner with two. + shift = getRotation(sides, length); + + // Flatten the parameter bits into a single 28- or 40-bit long + long parameterData = 0; + for (int i = 0; i < 4; i++) { + int side = sides[(shift + i) % 4]; + if (compact) { + // Each side of the form ..XXXXXXX. where Xs are parameter data + parameterData <<= 7; + parameterData += (side >> 1) & 0x7F; + } else { + // Each side of the form ..XXXXX.XXXXX. where Xs are parameter data + parameterData <<= 10; + parameterData += ((side >> 2) & (0x1f << 5)) + ((side >> 1) & 0x1F); + } + } + + // Corrects parameter data using RS. Returns just the data portion + // without the error correction. + int correctedData = getCorrectedParameterData(parameterData, compact); + + if (compact) { + // 8 bits: 2 bits layers and 6 bits data blocks + nbLayers = (correctedData >> 6) + 1; + nbDataBlocks = (correctedData & 0x3F) + 1; + } else { + // 16 bits: 5 bits layers and 11 bits data blocks + nbLayers = (correctedData >> 11) + 1; + nbDataBlocks = (correctedData & 0x7FF) + 1; + } + } + + private static int getRotation(int[] sides, int length) throws NotFoundException { + // In a normal pattern, we expect to See + // ** .* D A + // * * + // + // . * + // .. .. C B + // + // Grab the 3 bits from each of the sides the form the locator pattern and concatenate + // into a 12-bit integer. Start with the bit at A + int cornerBits = 0; + for (int side : sides) { + // XX......X where X's are orientation marks + int t = ((side >> (length - 2)) << 1) + (side & 1); + cornerBits = (cornerBits << 3) + t; + } + // Mov the bottom bit to the top, so that the three bits of the locator pattern at A are + // together. cornerBits is now: + // 3 orientation bits at A || 3 orientation bits at B || ... || 3 orientation bits at D + cornerBits = ((cornerBits & 1) << 11) + (cornerBits >> 1); + // The result shift indicates which element of BullsEyeCorners[] goes into the top-left + // corner. Since the four rotation values have a Hamming distance of 8, we + // can easily tolerate two errors. + for (int shift = 0; shift < 4; shift++) { + if (Integer.bitCount(cornerBits ^ EXPECTED_CORNER_BITS[shift]) <= 2) { + return shift; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Corrects the parameter bits using Reed-Solomon algorithm. + * + * @param parameterData parameter bits + * @param compact true if this is a compact Aztec code + * @throws NotFoundException if the array contains too many errors + */ + private static int getCorrectedParameterData(long parameterData, boolean compact) throws NotFoundException { + int numCodewords; + int numDataCodewords; + + if (compact) { + numCodewords = 7; + numDataCodewords = 2; + } else { + numCodewords = 10; + numDataCodewords = 4; + } + + int numECCodewords = numCodewords - numDataCodewords; + int[] parameterWords = new int[numCodewords]; + for (int i = numCodewords - 1; i >= 0; --i) { + parameterWords[i] = (int) parameterData & 0xF; + parameterData >>= 4; + } + try { + ReedSolomonDecoder rsDecoder = new ReedSolomonDecoder(GenericGF.AZTEC_PARAM); + rsDecoder.decode(parameterWords, numECCodewords); + } catch (ReedSolomonException ignored) { + throw NotFoundException.getNotFoundInstance(); + } + // Toss the error correction. Just return the data as an integer + int result = 0; + for (int i = 0; i < numDataCodewords; i++) { + result = (result << 4) + parameterWords[i]; + } + return result; + } + + /** + * Finds the corners of a bull-eye centered on the passed point. + * This returns the centers of the diagonal points just outside the bull's eye + * Returns [topRight, bottomRight, bottomLeft, topLeft] + * + * @param pCenter Center point + * @return The corners of the bull-eye + * @throws NotFoundException If no valid bull-eye can be found + */ + private ResultPoint[] getBullsEyeCorners(Point pCenter) throws NotFoundException { + + Point pina = pCenter; + Point pinb = pCenter; + Point pinc = pCenter; + Point pind = pCenter; + + boolean color = true; + + for (nbCenterLayers = 1; nbCenterLayers < 9; nbCenterLayers++) { + Point pouta = getFirstDifferent(pina, color, 1, -1); + Point poutb = getFirstDifferent(pinb, color, 1, 1); + Point poutc = getFirstDifferent(pinc, color, -1, 1); + Point poutd = getFirstDifferent(pind, color, -1, -1); + + //d a + // + //c b + + if (nbCenterLayers > 2) { + float q = distance(poutd, pouta) * nbCenterLayers / (distance(pind, pina) * (nbCenterLayers + 2)); + if (q < 0.75 || q > 1.25 || !isWhiteOrBlackRectangle(pouta, poutb, poutc, poutd)) { + break; + } + } + + pina = pouta; + pinb = poutb; + pinc = poutc; + pind = poutd; + + color = !color; + } + + if (nbCenterLayers != 5 && nbCenterLayers != 7) { + throw NotFoundException.getNotFoundInstance(); + } + + compact = nbCenterLayers == 5; + + // Expand the square by .5 pixel in each direction so that we're on the border + // between the white square and the black square + ResultPoint pinax = new ResultPoint(pina.getX() + 0.5f, pina.getY() - 0.5f); + ResultPoint pinbx = new ResultPoint(pinb.getX() + 0.5f, pinb.getY() + 0.5f); + ResultPoint pincx = new ResultPoint(pinc.getX() - 0.5f, pinc.getY() + 0.5f); + ResultPoint pindx = new ResultPoint(pind.getX() - 0.5f, pind.getY() - 0.5f); + + // Expand the square so that its corners are the centers of the points + // just outside the bull's eye. + return expandSquare(new ResultPoint[]{pinax, pinbx, pincx, pindx}, + 2 * nbCenterLayers - 3, + 2 * nbCenterLayers); + } + + /** + * Finds a candidate center point of an Aztec code from an image + * + * @return the center point + */ + private Point getMatrixCenter() { + + ResultPoint pointA; + ResultPoint pointB; + ResultPoint pointC; + ResultPoint pointD; + + //Get a white rectangle that can be the border of the matrix in center bull's eye or + try { + + ResultPoint[] cornerPoints = new WhiteRectangleDetector(image).detect(); + pointA = cornerPoints[0]; + pointB = cornerPoints[1]; + pointC = cornerPoints[2]; + pointD = cornerPoints[3]; + + } catch (NotFoundException e) { + + // This exception can be in case the initial rectangle is white + // In that case, surely in the bull's eye, we try to expand the rectangle. + int cx = image.getWidth() / 2; + int cy = image.getHeight() / 2; + pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint(); + pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint(); + pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint(); + pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint(); + + } + + //Compute the center of the rectangle + int cx = MathUtils.round((pointA.getX() + pointD.getX() + pointB.getX() + pointC.getX()) / 4.0f); + int cy = MathUtils.round((pointA.getY() + pointD.getY() + pointB.getY() + pointC.getY()) / 4.0f); + + // Redetermine the white rectangle starting from previously computed center. + // This will ensure that we end up with a white rectangle in center bull's eye + // in order to compute a more accurate center. + try { + ResultPoint[] cornerPoints = new WhiteRectangleDetector(image, 15, cx, cy).detect(); + pointA = cornerPoints[0]; + pointB = cornerPoints[1]; + pointC = cornerPoints[2]; + pointD = cornerPoints[3]; + } catch (NotFoundException e) { + // This exception can be in case the initial rectangle is white + // In that case we try to expand the rectangle. + pointA = getFirstDifferent(new Point(cx + 7, cy - 7), false, 1, -1).toResultPoint(); + pointB = getFirstDifferent(new Point(cx + 7, cy + 7), false, 1, 1).toResultPoint(); + pointC = getFirstDifferent(new Point(cx - 7, cy + 7), false, -1, 1).toResultPoint(); + pointD = getFirstDifferent(new Point(cx - 7, cy - 7), false, -1, -1).toResultPoint(); + } + + // Recompute the center of the rectangle + cx = MathUtils.round((pointA.getX() + pointD.getX() + pointB.getX() + pointC.getX()) / 4.0f); + cy = MathUtils.round((pointA.getY() + pointD.getY() + pointB.getY() + pointC.getY()) / 4.0f); + + return new Point(cx, cy); + } + + /** + * Gets the Aztec code corners from the bull's eye corners and the parameters. + * + * @param bullsEyeCorners the array of bull's eye corners + * @return the array of aztec code corners + */ + private ResultPoint[] getMatrixCornerPoints(ResultPoint[] bullsEyeCorners) { + return expandSquare(bullsEyeCorners, 2 * nbCenterLayers, getDimension()); + } + + /** + * Creates a BitMatrix by sampling the provided image. + * topLeft, topRight, bottomRight, and bottomLeft are the centers of the squares on the + * diagonal just outside the bull's eye. + */ + private BitMatrix sampleGrid(BitMatrix image, + ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomRight, + ResultPoint bottomLeft) throws NotFoundException { + + GridSampler sampler = GridSampler.getInstance(); + int dimension = getDimension(); + + float low = dimension / 2.0f - nbCenterLayers; + float high = dimension / 2.0f + nbCenterLayers; + + return sampler.sampleGrid(image, + dimension, + dimension, + low, low, // topleft + high, low, // topright + high, high, // bottomright + low, high, // bottomleft + topLeft.getX(), topLeft.getY(), + topRight.getX(), topRight.getY(), + bottomRight.getX(), bottomRight.getY(), + bottomLeft.getX(), bottomLeft.getY()); + } + + /** + * Samples a line. + * + * @param p1 start point (inclusive) + * @param p2 end point (exclusive) + * @param size number of bits + * @return the array of bits as an int (first bit is high-order bit of result) + */ + private int sampleLine(ResultPoint p1, ResultPoint p2, int size) { + int result = 0; + + float d = distance(p1, p2); + float moduleSize = d / size; + float px = p1.getX(); + float py = p1.getY(); + float dx = moduleSize * (p2.getX() - p1.getX()) / d; + float dy = moduleSize * (p2.getY() - p1.getY()) / d; + for (int i = 0; i < size; i++) { + if (image.get(MathUtils.round(px + i * dx), MathUtils.round(py + i * dy))) { + result |= 1 << (size - i - 1); + } + } + return result; + } + + /** + * @return true if the border of the rectangle passed in parameter is compound of white points only + * or black points only + */ + private boolean isWhiteOrBlackRectangle(Point p1, + Point p2, + Point p3, + Point p4) { + + int corr = 3; + + p1 = new Point(p1.getX() - corr, p1.getY() + corr); + p2 = new Point(p2.getX() - corr, p2.getY() - corr); + p3 = new Point(p3.getX() + corr, p3.getY() - corr); + p4 = new Point(p4.getX() + corr, p4.getY() + corr); + + int cInit = getColor(p4, p1); + + if (cInit == 0) { + return false; + } + + int c = getColor(p1, p2); + + if (c != cInit) { + return false; + } + + c = getColor(p2, p3); + + if (c != cInit) { + return false; + } + + c = getColor(p3, p4); + + return c == cInit; + + } + + /** + * Gets the color of a segment + * + * @return 1 if segment more than 90% black, -1 if segment is more than 90% white, 0 else + */ + private int getColor(Point p1, Point p2) { + float d = distance(p1, p2); + float dx = (p2.getX() - p1.getX()) / d; + float dy = (p2.getY() - p1.getY()) / d; + int error = 0; + + float px = p1.getX(); + float py = p1.getY(); + + boolean colorModel = image.get(p1.getX(), p1.getY()); + + int iMax = (int) Math.ceil(d); + for (int i = 0; i < iMax; i++) { + px += dx; + py += dy; + if (image.get(MathUtils.round(px), MathUtils.round(py)) != colorModel) { + error++; + } + } + + float errRatio = error / d; + + if (errRatio > 0.1f && errRatio < 0.9f) { + return 0; + } + + return (errRatio <= 0.1f) == colorModel ? 1 : -1; + } + + /** + * Gets the coordinate of the first point with a different color in the given direction + */ + private Point getFirstDifferent(Point init, boolean color, int dx, int dy) { + int x = init.getX() + dx; + int y = init.getY() + dy; + + while (isValid(x, y) && image.get(x, y) == color) { + x += dx; + y += dy; + } + + x -= dx; + y -= dy; + + while (isValid(x, y) && image.get(x, y) == color) { + x += dx; + } + x -= dx; + + while (isValid(x, y) && image.get(x, y) == color) { + y += dy; + } + y -= dy; + + return new Point(x, y); + } + + /** + * Expand the square represented by the corner points by pushing out equally in all directions + * + * @param cornerPoints the corners of the square, which has the bull's eye at its center + * @param oldSide the original length of the side of the square in the target bit matrix + * @param newSide the new length of the size of the square in the target bit matrix + * @return the corners of the expanded square + */ + private static ResultPoint[] expandSquare(ResultPoint[] cornerPoints, float oldSide, float newSide) { + float ratio = newSide / (2 * oldSide); + float dx = cornerPoints[0].getX() - cornerPoints[2].getX(); + float dy = cornerPoints[0].getY() - cornerPoints[2].getY(); + float centerx = (cornerPoints[0].getX() + cornerPoints[2].getX()) / 2.0f; + float centery = (cornerPoints[0].getY() + cornerPoints[2].getY()) / 2.0f; + + ResultPoint result0 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy); + ResultPoint result2 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy); + + dx = cornerPoints[1].getX() - cornerPoints[3].getX(); + dy = cornerPoints[1].getY() - cornerPoints[3].getY(); + centerx = (cornerPoints[1].getX() + cornerPoints[3].getX()) / 2.0f; + centery = (cornerPoints[1].getY() + cornerPoints[3].getY()) / 2.0f; + ResultPoint result1 = new ResultPoint(centerx + ratio * dx, centery + ratio * dy); + ResultPoint result3 = new ResultPoint(centerx - ratio * dx, centery - ratio * dy); + + return new ResultPoint[]{result0, result1, result2, result3}; + } + + private boolean isValid(int x, int y) { + return x >= 0 && x < image.getWidth() && y > 0 && y < image.getHeight(); + } + + private boolean isValid(ResultPoint point) { + int x = MathUtils.round(point.getX()); + int y = MathUtils.round(point.getY()); + return isValid(x, y); + } + + private static float distance(Point a, Point b) { + return MathUtils.distance(a.getX(), a.getY(), b.getX(), b.getY()); + } + + private static float distance(ResultPoint a, ResultPoint b) { + return MathUtils.distance(a.getX(), a.getY(), b.getX(), b.getY()); + } + + private int getDimension() { + if (compact) { + return 4 * nbLayers + 11; + } + if (nbLayers <= 4) { + return 4 * nbLayers + 15; + } + return 4 * nbLayers + 2 * ((nbLayers - 4) / 8 + 1) + 15; + } + + static final class Point { + private final int x; + private final int y; + + ResultPoint toResultPoint() { + return new ResultPoint(getX(), getY()); + } + + Point(int x, int y) { + this.x = x; + this.y = y; + } + + int getX() { + return x; + } + + int getY() { + return y; + } + + @Override + public String toString() { + return "<" + x + ' ' + y + '>'; + } + } +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/AztecCode.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/AztecCode.java new file mode 100644 index 0000000..6426d4e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/AztecCode.java @@ -0,0 +1,89 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitMatrix; + +/** + * Aztec 2D code representation + * + * @author Rustam Abdullaev + */ +public final class AztecCode { + + private boolean compact; + private int size; + private int layers; + private int codeWords; + private BitMatrix matrix; + + /** + * @return {@code true} if compact instead of full mode + */ + public boolean isCompact() { + return compact; + } + + public void setCompact(boolean compact) { + this.compact = compact; + } + + /** + * @return size in pixels (width and height) + */ + public int getSize() { + return size; + } + + public void setSize(int size) { + this.size = size; + } + + /** + * @return number of levels + */ + public int getLayers() { + return layers; + } + + public void setLayers(int layers) { + this.layers = layers; + } + + /** + * @return number of data codewords + */ + public int getCodeWords() { + return codeWords; + } + + public void setCodeWords(int codeWords) { + this.codeWords = codeWords; + } + + /** + * @return the symbol image + */ + public BitMatrix getMatrix() { + return matrix; + } + + public void setMatrix(BitMatrix matrix) { + this.matrix = matrix; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/BinaryShiftToken.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/BinaryShiftToken.java new file mode 100644 index 0000000..61abaaf --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/BinaryShiftToken.java @@ -0,0 +1,60 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitArray; + +final class BinaryShiftToken extends Token { + + private final short binaryShiftStart; + private final short binaryShiftByteCount; + + BinaryShiftToken(Token previous, + int binaryShiftStart, + int binaryShiftByteCount) { + super(previous); + this.binaryShiftStart = (short) binaryShiftStart; + this.binaryShiftByteCount = (short) binaryShiftByteCount; + } + + @Override + public void appendTo(BitArray bitArray, byte[] text) { + for (int i = 0; i < binaryShiftByteCount; i++) { + if (i == 0 || (i == 31 && binaryShiftByteCount <= 62)) { + // We need a header before the first character, and before + // character 31 when the total byte code is <= 62 + bitArray.appendBits(31, 5); // BINARY_SHIFT + if (binaryShiftByteCount > 62) { + bitArray.appendBits(binaryShiftByteCount - 31, 16); + } else if (i == 0) { + // 1 <= binaryShiftByteCode <= 62 + bitArray.appendBits(Math.min(binaryShiftByteCount, 31), 5); + } else { + // 32 <= binaryShiftCount <= 62 and i == 31 + bitArray.appendBits(binaryShiftByteCount - 31, 5); + } + } + bitArray.appendBits(text[binaryShiftStart + i], 8); + } + } + + @Override + public String toString() { + return "<" + binaryShiftStart + "::" + (binaryShiftStart + binaryShiftByteCount - 1) + '>'; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/Encoder.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/Encoder.java new file mode 100644 index 0000000..4fdb2eb --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/Encoder.java @@ -0,0 +1,346 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitArray; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; + +/** + * Generates Aztec 2D barcodes. + * + * @author Rustam Abdullaev + */ +public final class Encoder { + + public static final int DEFAULT_EC_PERCENT = 33; // default minimal percentage of error check words + public static final int DEFAULT_AZTEC_LAYERS = 0; + private static final int MAX_NB_BITS = 32; + private static final int MAX_NB_BITS_COMPACT = 4; + + private static final int[] WORD_SIZE = { + 4, 6, 6, 8, 8, 8, 8, 8, 8, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12 + }; + + private Encoder() { + } + + /** + * Encodes the given binary content as an Aztec symbol + * + * @param data input data string + * @return Aztec symbol matrix with metadata + */ + public static AztecCode encode(byte[] data) { + return encode(data, DEFAULT_EC_PERCENT, DEFAULT_AZTEC_LAYERS); + } + + /** + * Encodes the given binary content as an Aztec symbol + * + * @param data input data string + * @param minECCPercent minimal percentage of error check words (According to ISO/IEC 24778:2008, + * a minimum of 23% + 3 words is recommended) + * @param userSpecifiedLayers if non-zero, a user-specified value for the number of layers + * @return Aztec symbol matrix with metadata + */ + public static AztecCode encode(byte[] data, int minECCPercent, int userSpecifiedLayers) { + // High-level encode + BitArray bits = new HighLevelEncoder(data).encode(); + + // stuff bits and choose symbol size + int eccBits = bits.getSize() * minECCPercent / 100 + 11; + int totalSizeBits = bits.getSize() + eccBits; + boolean compact; + int layers; + int totalBitsInLayer; + int wordSize; + BitArray stuffedBits; + if (userSpecifiedLayers != DEFAULT_AZTEC_LAYERS) { + compact = userSpecifiedLayers < 0; + layers = Math.abs(userSpecifiedLayers); + if (layers > (compact ? MAX_NB_BITS_COMPACT : MAX_NB_BITS)) { + throw new IllegalArgumentException( + String.format("Illegal value %s for layers", userSpecifiedLayers)); + } + totalBitsInLayer = totalBitsInLayer(layers, compact); + wordSize = WORD_SIZE[layers]; + int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize); + stuffedBits = stuffBits(bits, wordSize); + if (stuffedBits.getSize() + eccBits > usableBitsInLayers) { + throw new IllegalArgumentException("Data to large for user specified layer"); + } + if (compact && stuffedBits.getSize() > wordSize * 64) { + // Compact format only allows 64 data words, though C4 can hold more words than that + throw new IllegalArgumentException("Data to large for user specified layer"); + } + } else { + wordSize = 0; + stuffedBits = null; + // We look at the possible table sizes in the order Compact1, Compact2, Compact3, + // Compact4, Normal4,... Normal(i) for i < 4 isn't typically used since Compact(i+1) + // is the same size, but has more data. + for (int i = 0; ; i++) { + if (i > MAX_NB_BITS) { + throw new IllegalArgumentException("Data too large for an Aztec code"); + } + compact = i <= 3; + layers = compact ? i + 1 : i; + totalBitsInLayer = totalBitsInLayer(layers, compact); + if (totalSizeBits > totalBitsInLayer) { + continue; + } + // [Re]stuff the bits if this is the first opportunity, or if the + // wordSize has changed + if (wordSize != WORD_SIZE[layers]) { + wordSize = WORD_SIZE[layers]; + stuffedBits = stuffBits(bits, wordSize); + } + int usableBitsInLayers = totalBitsInLayer - (totalBitsInLayer % wordSize); + if (compact && stuffedBits.getSize() > wordSize * 64) { + // Compact format only allows 64 data words, though C4 can hold more words than that + continue; + } + if (stuffedBits.getSize() + eccBits <= usableBitsInLayers) { + break; + } + } + } + BitArray messageBits = generateCheckWords(stuffedBits, totalBitsInLayer, wordSize); + + // generate mode message + int messageSizeInWords = stuffedBits.getSize() / wordSize; + BitArray modeMessage = generateModeMessage(compact, layers, messageSizeInWords); + + // allocate symbol + int baseMatrixSize = (compact ? 11 : 14) + layers * 4; // not including alignment lines + int[] alignmentMap = new int[baseMatrixSize]; + int matrixSize; + if (compact) { + // no alignment marks in compact mode, alignmentMap is a no-op + matrixSize = baseMatrixSize; + for (int i = 0; i < alignmentMap.length; i++) { + alignmentMap[i] = i; + } + } else { + matrixSize = baseMatrixSize + 1 + 2 * ((baseMatrixSize / 2 - 1) / 15); + int origCenter = baseMatrixSize / 2; + int center = matrixSize / 2; + for (int i = 0; i < origCenter; i++) { + int newOffset = i + i / 15; + alignmentMap[origCenter - i - 1] = center - newOffset - 1; + alignmentMap[origCenter + i] = center + newOffset + 1; + } + } + BitMatrix matrix = new BitMatrix(matrixSize); + + // draw data bits + for (int i = 0, rowOffset = 0; i < layers; i++) { + int rowSize = (layers - i) * 4 + (compact ? 9 : 12); + for (int j = 0; j < rowSize; j++) { + int columnOffset = j * 2; + for (int k = 0; k < 2; k++) { + if (messageBits.get(rowOffset + columnOffset + k)) { + matrix.set(alignmentMap[i * 2 + k], alignmentMap[i * 2 + j]); + } + if (messageBits.get(rowOffset + rowSize * 2 + columnOffset + k)) { + matrix.set(alignmentMap[i * 2 + j], alignmentMap[baseMatrixSize - 1 - i * 2 - k]); + } + if (messageBits.get(rowOffset + rowSize * 4 + columnOffset + k)) { + matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - k], alignmentMap[baseMatrixSize - 1 - i * 2 - j]); + } + if (messageBits.get(rowOffset + rowSize * 6 + columnOffset + k)) { + matrix.set(alignmentMap[baseMatrixSize - 1 - i * 2 - j], alignmentMap[i * 2 + k]); + } + } + } + rowOffset += rowSize * 8; + } + + // draw mode message + drawModeMessage(matrix, compact, matrixSize, modeMessage); + + // draw alignment marks + if (compact) { + drawBullsEye(matrix, matrixSize / 2, 5); + } else { + drawBullsEye(matrix, matrixSize / 2, 7); + for (int i = 0, j = 0; i < baseMatrixSize / 2 - 1; i += 15, j += 16) { + for (int k = (matrixSize / 2) & 1; k < matrixSize; k += 2) { + matrix.set(matrixSize / 2 - j, k); + matrix.set(matrixSize / 2 + j, k); + matrix.set(k, matrixSize / 2 - j); + matrix.set(k, matrixSize / 2 + j); + } + } + } + + AztecCode aztec = new AztecCode(); + aztec.setCompact(compact); + aztec.setSize(matrixSize); + aztec.setLayers(layers); + aztec.setCodeWords(messageSizeInWords); + aztec.setMatrix(matrix); + return aztec; + } + + private static void drawBullsEye(BitMatrix matrix, int center, int size) { + for (int i = 0; i < size; i += 2) { + for (int j = center - i; j <= center + i; j++) { + matrix.set(j, center - i); + matrix.set(j, center + i); + matrix.set(center - i, j); + matrix.set(center + i, j); + } + } + matrix.set(center - size, center - size); + matrix.set(center - size + 1, center - size); + matrix.set(center - size, center - size + 1); + matrix.set(center + size, center - size); + matrix.set(center + size, center - size + 1); + matrix.set(center + size, center + size - 1); + } + + static BitArray generateModeMessage(boolean compact, int layers, int messageSizeInWords) { + BitArray modeMessage = new BitArray(); + if (compact) { + modeMessage.appendBits(layers - 1, 2); + modeMessage.appendBits(messageSizeInWords - 1, 6); + modeMessage = generateCheckWords(modeMessage, 28, 4); + } else { + modeMessage.appendBits(layers - 1, 5); + modeMessage.appendBits(messageSizeInWords - 1, 11); + modeMessage = generateCheckWords(modeMessage, 40, 4); + } + return modeMessage; + } + + private static void drawModeMessage(BitMatrix matrix, boolean compact, int matrixSize, BitArray modeMessage) { + int center = matrixSize / 2; + if (compact) { + for (int i = 0; i < 7; i++) { + int offset = center - 3 + i; + if (modeMessage.get(i)) { + matrix.set(offset, center - 5); + } + if (modeMessage.get(i + 7)) { + matrix.set(center + 5, offset); + } + if (modeMessage.get(20 - i)) { + matrix.set(offset, center + 5); + } + if (modeMessage.get(27 - i)) { + matrix.set(center - 5, offset); + } + } + } else { + for (int i = 0; i < 10; i++) { + int offset = center - 5 + i + i / 5; + if (modeMessage.get(i)) { + matrix.set(offset, center - 7); + } + if (modeMessage.get(i + 10)) { + matrix.set(center + 7, offset); + } + if (modeMessage.get(29 - i)) { + matrix.set(offset, center + 7); + } + if (modeMessage.get(39 - i)) { + matrix.set(center - 7, offset); + } + } + } + } + + private static BitArray generateCheckWords(BitArray bitArray, int totalBits, int wordSize) { + // bitArray is guaranteed to be a multiple of the wordSize, so no padding needed + int messageSizeInWords = bitArray.getSize() / wordSize; + ReedSolomonEncoder rs = new ReedSolomonEncoder(getGF(wordSize)); + int totalWords = totalBits / wordSize; + int[] messageWords = bitsToWords(bitArray, wordSize, totalWords); + rs.encode(messageWords, totalWords - messageSizeInWords); + int startPad = totalBits % wordSize; + BitArray messageBits = new BitArray(); + messageBits.appendBits(0, startPad); + for (int messageWord : messageWords) { + messageBits.appendBits(messageWord, wordSize); + } + return messageBits; + } + + private static int[] bitsToWords(BitArray stuffedBits, int wordSize, int totalWords) { + int[] message = new int[totalWords]; + int i; + int n; + for (i = 0, n = stuffedBits.getSize() / wordSize; i < n; i++) { + int value = 0; + for (int j = 0; j < wordSize; j++) { + value |= stuffedBits.get(i * wordSize + j) ? (1 << wordSize - j - 1) : 0; + } + message[i] = value; + } + return message; + } + + private static GenericGF getGF(int wordSize) { + switch (wordSize) { + case 4: + return GenericGF.AZTEC_PARAM; + case 6: + return GenericGF.AZTEC_DATA_6; + case 8: + return GenericGF.AZTEC_DATA_8; + case 10: + return GenericGF.AZTEC_DATA_10; + case 12: + return GenericGF.AZTEC_DATA_12; + default: + throw new IllegalArgumentException("Unsupported word size " + wordSize); + } + } + + static BitArray stuffBits(BitArray bits, int wordSize) { + BitArray out = new BitArray(); + + int n = bits.getSize(); + int mask = (1 << wordSize) - 2; + for (int i = 0; i < n; i += wordSize) { + int word = 0; + for (int j = 0; j < wordSize; j++) { + if (i + j >= n || bits.get(i + j)) { + word |= 1 << (wordSize - 1 - j); + } + } + if ((word & mask) == mask) { + out.appendBits(word & mask, wordSize); + i--; + } else if ((word & mask) == 0) { + out.appendBits(word | 1, wordSize); + i--; + } else { + out.appendBits(word, wordSize); + } + } + return out; + } + + private static int totalBitsInLayer(int layers, boolean compact) { + return ((compact ? 88 : 112) + 16 * layers) * layers; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/HighLevelEncoder.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/HighLevelEncoder.java new file mode 100644 index 0000000..8cf514e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/HighLevelEncoder.java @@ -0,0 +1,307 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; + +/** + * This produces nearly optimal encodings of text into the first-level of + * encoding used by Aztec code. + * + * It uses a dynamic algorithm. For each prefix of the string, it determines + * a set of encodings that could lead to this prefix. We repeatedly add a + * character and generate a new set of optimal encodings until we have read + * through the entire input. + * + * @author Frank Yellin + * @author Rustam Abdullaev + */ +public final class HighLevelEncoder { + + static final String[] MODE_NAMES = {"UPPER", "LOWER", "DIGIT", "MIXED", "PUNCT"}; + + static final int MODE_UPPER = 0; // 5 bits + static final int MODE_LOWER = 1; // 5 bits + static final int MODE_DIGIT = 2; // 4 bits + static final int MODE_MIXED = 3; // 5 bits + static final int MODE_PUNCT = 4; // 5 bits + + // The Latch Table shows, for each pair of Modes, the optimal method for + // getting from one mode to another. In the worst possible case, this can + // be up to 14 bits. In the best possible case, we are already there! + // The high half-word of each entry gives the number of bits. + // The low half-word of each entry are the actual bits necessary to change + static final int[][] LATCH_TABLE = { + { + 0, + (5 << 16) + 28, // UPPER -> LOWER + (5 << 16) + 30, // UPPER -> DIGIT + (5 << 16) + 29, // UPPER -> MIXED + (10 << 16) + (29 << 5) + 30, // UPPER -> MIXED -> PUNCT + }, + { + (9 << 16) + (30 << 4) + 14, // LOWER -> DIGIT -> UPPER + 0, + (5 << 16) + 30, // LOWER -> DIGIT + (5 << 16) + 29, // LOWER -> MIXED + (10 << 16) + (29 << 5) + 30, // LOWER -> MIXED -> PUNCT + }, + { + (4 << 16) + 14, // DIGIT -> UPPER + (9 << 16) + (14 << 5) + 28, // DIGIT -> UPPER -> LOWER + 0, + (9 << 16) + (14 << 5) + 29, // DIGIT -> UPPER -> MIXED + (14 << 16) + (14 << 10) + (29 << 5) + 30, + // DIGIT -> UPPER -> MIXED -> PUNCT + }, + { + (5 << 16) + 29, // MIXED -> UPPER + (5 << 16) + 28, // MIXED -> LOWER + (10 << 16) + (29 << 5) + 30, // MIXED -> UPPER -> DIGIT + 0, + (5 << 16) + 30, // MIXED -> PUNCT + }, + { + (5 << 16) + 31, // PUNCT -> UPPER + (10 << 16) + (31 << 5) + 28, // PUNCT -> UPPER -> LOWER + (10 << 16) + (31 << 5) + 30, // PUNCT -> UPPER -> DIGIT + (10 << 16) + (31 << 5) + 29, // PUNCT -> UPPER -> MIXED + 0, + }, + }; + + // A reverse mapping from [mode][char] to the encoding for that character + // in that mode. An entry of 0 indicates no mapping exists. + private static final int[][] CHAR_MAP = new int[5][256]; + static { + CHAR_MAP[MODE_UPPER][' '] = 1; + for (int c = 'A'; c <= 'Z'; c++) { + CHAR_MAP[MODE_UPPER][c] = c - 'A' + 2; + } + CHAR_MAP[MODE_LOWER][' '] = 1; + for (int c = 'a'; c <= 'z'; c++) { + CHAR_MAP[MODE_LOWER][c] = c - 'a' + 2; + } + CHAR_MAP[MODE_DIGIT][' '] = 1; + for (int c = '0'; c <= '9'; c++) { + CHAR_MAP[MODE_DIGIT][c] = c - '0' + 2; + } + CHAR_MAP[MODE_DIGIT][','] = 12; + CHAR_MAP[MODE_DIGIT]['.'] = 13; + int[] mixedTable = { + '\0', ' ', '\1', '\2', '\3', '\4', '\5', '\6', '\7', '\b', '\t', '\n', + '\13', '\f', '\r', '\33', '\34', '\35', '\36', '\37', '@', '\\', '^', + '_', '`', '|', '~', '\177' + }; + for (int i = 0; i < mixedTable.length; i++) { + CHAR_MAP[MODE_MIXED][mixedTable[i]] = i; + } + int[] punctTable = { + '\0', '\r', '\0', '\0', '\0', '\0', '!', '\'', '#', '$', '%', '&', '\'', + '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', + '[', ']', '{', '}' + }; + for (int i = 0; i < punctTable.length; i++) { + if (punctTable[i] > 0) { + CHAR_MAP[MODE_PUNCT][punctTable[i]] = i; + } + } + } + + // A map showing the available shift codes. (The shifts to BINARY are not + // shown + static final int[][] SHIFT_TABLE = new int[6][6]; // mode shift codes, per table + static { + for (int[] table : SHIFT_TABLE) { + Arrays.fill(table, -1); + } + SHIFT_TABLE[MODE_UPPER][MODE_PUNCT] = 0; + + SHIFT_TABLE[MODE_LOWER][MODE_PUNCT] = 0; + SHIFT_TABLE[MODE_LOWER][MODE_UPPER] = 28; + + SHIFT_TABLE[MODE_MIXED][MODE_PUNCT] = 0; + + SHIFT_TABLE[MODE_DIGIT][MODE_PUNCT] = 0; + SHIFT_TABLE[MODE_DIGIT][MODE_UPPER] = 15; + } + + private final byte[] text; + + public HighLevelEncoder(byte[] text) { + this.text = text; + } + + /** + * @return text represented by this encoder encoded as a {@link BitArray} + */ + public BitArray encode() { + Collection states = Collections.singletonList(State.INITIAL_STATE); + for (int index = 0; index < text.length; index++) { + int pairCode; + int nextChar = index + 1 < text.length ? text[index + 1] : 0; + switch (text[index]) { + case '\r': + pairCode = nextChar == '\n' ? 2 : 0; + break; + case '.' : + pairCode = nextChar == ' ' ? 3 : 0; + break; + case ',' : + pairCode = nextChar == ' ' ? 4 : 0; + break; + case ':' : + pairCode = nextChar == ' ' ? 5 : 0; + break; + default: + pairCode = 0; + } + if (pairCode > 0) { + // We have one of the four special PUNCT pairs. Treat them specially. + // Get a new set of states for the two new characters. + states = updateStateListForPair(states, index, pairCode); + index++; + } else { + // Get a new set of states for the new character. + states = updateStateListForChar(states, index); + } + } + // We are left with a set of states. Find the shortest one. + State minState = Collections.min(states, new Comparator() { + @Override + public int compare(State a, State b) { + return a.getBitCount() - b.getBitCount(); + } + }); + // Convert it to a bit array, and return. + return minState.toBitArray(text); + } + + // We update a set of states for a new character by updating each state + // for the new character, merging the results, and then removing the + // non-optimal states. + private Collection updateStateListForChar(Iterable states, int index) { + Collection result = new LinkedList<>(); + for (State state : states) { + updateStateForChar(state, index, result); + } + return simplifyStates(result); + } + + // Return a set of states that represent the possible ways of updating this + // state for the next character. The resulting set of states are added to + // the "result" list. + private void updateStateForChar(State state, int index, Collection result) { + char ch = (char) (text[index] & 0xFF); + boolean charInCurrentTable = CHAR_MAP[state.getMode()][ch] > 0; + State stateNoBinary = null; + for (int mode = 0; mode <= MODE_PUNCT; mode++) { + int charInMode = CHAR_MAP[mode][ch]; + if (charInMode > 0) { + if (stateNoBinary == null) { + // Only create stateNoBinary the first time it's required. + stateNoBinary = state.endBinaryShift(index); + } + // Try generating the character by latching to its mode + if (!charInCurrentTable || mode == state.getMode() || mode == MODE_DIGIT) { + // If the character is in the current table, we don't want to latch to + // any other mode except possibly digit (which uses only 4 bits). Any + // other latch would be equally successful *after* this character, and + // so wouldn't save any bits. + State latchState = stateNoBinary.latchAndAppend(mode, charInMode); + result.add(latchState); + } + // Try generating the character by switching to its mode. + if (!charInCurrentTable && SHIFT_TABLE[state.getMode()][mode] >= 0) { + // It never makes sense to temporarily shift to another mode if the + // character exists in the current mode. That can never save bits. + State shiftState = stateNoBinary.shiftAndAppend(mode, charInMode); + result.add(shiftState); + } + } + } + if (state.getBinaryShiftByteCount() > 0 || CHAR_MAP[state.getMode()][ch] == 0) { + // It's never worthwhile to go into binary shift mode if you're not already + // in binary shift mode, and the character exists in your current mode. + // That can never save bits over just outputting the char in the current mode. + State binaryState = state.addBinaryShiftChar(index); + result.add(binaryState); + } + } + + private static Collection updateStateListForPair(Iterable states, int index, int pairCode) { + Collection result = new LinkedList<>(); + for (State state : states) { + updateStateForPair(state, index, pairCode, result); + } + return simplifyStates(result); + } + + private static void updateStateForPair(State state, int index, int pairCode, Collection result) { + State stateNoBinary = state.endBinaryShift(index); + // Possibility 1. Latch to MODE_PUNCT, and then append this code + result.add(stateNoBinary.latchAndAppend(MODE_PUNCT, pairCode)); + if (state.getMode() != MODE_PUNCT) { + // Possibility 2. Shift to MODE_PUNCT, and then append this code. + // Every state except MODE_PUNCT (handled above) can shift + result.add(stateNoBinary.shiftAndAppend(MODE_PUNCT, pairCode)); + } + if (pairCode == 3 || pairCode == 4) { + // both characters are in DIGITS. Sometimes better to just add two digits + State digitState = stateNoBinary + .latchAndAppend(MODE_DIGIT, 16 - pairCode) // period or comma in DIGIT + .latchAndAppend(MODE_DIGIT, 1); // space in DIGIT + result.add(digitState); + } + if (state.getBinaryShiftByteCount() > 0) { + // It only makes sense to do the characters as binary if we're already + // in binary mode. + State binaryState = state.addBinaryShiftChar(index).addBinaryShiftChar(index + 1); + result.add(binaryState); + } + } + + private static Collection simplifyStates(Iterable states) { + List result = new LinkedList<>(); + for (State newState : states) { + boolean add = true; + for (Iterator iterator = result.iterator(); iterator.hasNext();) { + State oldState = iterator.next(); + if (oldState.isBetterThanOrEqualTo(newState)) { + add = false; + break; + } + if (newState.isBetterThanOrEqualTo(oldState)) { + iterator.remove(); + } + } + if (add) { + result.add(newState); + } + } + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/SimpleToken.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/SimpleToken.java new file mode 100644 index 0000000..d3b0c45 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/SimpleToken.java @@ -0,0 +1,45 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitArray; + +final class SimpleToken extends Token { + + // For normal words, indicates value and bitCount + private final short value; + private final short bitCount; + + SimpleToken(Token previous, int value, int bitCount) { + super(previous); + this.value = (short) value; + this.bitCount = (short) bitCount; + } + + @Override + void appendTo(BitArray bitArray, byte[] text) { + bitArray.appendBits(value, bitCount); + } + + @Override + public String toString() { + int value = this.value & ((1 << bitCount) - 1); + value |= 1 << bitCount; + return '<' + Integer.toBinaryString(value | (1 << bitCount)).substring(1) + '>'; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/State.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/State.java new file mode 100644 index 0000000..d582a74 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/State.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import java.util.Deque; +import java.util.LinkedList; + +import com.google.zxing.common.BitArray; + +/** + * State represents all information about a sequence necessary to generate the current output. + * Note that a state is immutable. + */ +final class State { + + static final State INITIAL_STATE = new State(Token.EMPTY, HighLevelEncoder.MODE_UPPER, 0, 0); + + // The current mode of the encoding (or the mode to which we'll return if + // we're in Binary Shift mode. + private final int mode; + // The list of tokens that we output. If we are in Binary Shift mode, this + // token list does *not* yet included the token for those bytes + private final Token token; + // If non-zero, the number of most recent bytes that should be output + // in Binary Shift mode. + private final int binaryShiftByteCount; + // The total number of bits generated (including Binary Shift). + private final int bitCount; + + private State(Token token, int mode, int binaryBytes, int bitCount) { + this.token = token; + this.mode = mode; + this.binaryShiftByteCount = binaryBytes; + this.bitCount = bitCount; + // Make sure we match the token + //int binaryShiftBitCount = (binaryShiftByteCount * 8) + + // (binaryShiftByteCount == 0 ? 0 : + // binaryShiftByteCount <= 31 ? 10 : + // binaryShiftByteCount <= 62 ? 20 : 21); + //assert this.bitCount == token.getTotalBitCount() + binaryShiftBitCount; + } + + int getMode() { + return mode; + } + + Token getToken() { + return token; + } + + int getBinaryShiftByteCount() { + return binaryShiftByteCount; + } + + int getBitCount() { + return bitCount; + } + + // Create a new state representing this state with a latch to a (not + // necessary different) mode, and then a code. + State latchAndAppend(int mode, int value) { + //assert binaryShiftByteCount == 0; + int bitCount = this.bitCount; + Token token = this.token; + if (mode != this.mode) { + int latch = HighLevelEncoder.LATCH_TABLE[this.mode][mode]; + token = token.add(latch & 0xFFFF, latch >> 16); + bitCount += latch >> 16; + } + int latchModeBitCount = mode == HighLevelEncoder.MODE_DIGIT ? 4 : 5; + token = token.add(value, latchModeBitCount); + return new State(token, mode, 0, bitCount + latchModeBitCount); + } + + // Create a new state representing this state, with a temporary shift + // to a different mode to output a single value. + State shiftAndAppend(int mode, int value) { + //assert binaryShiftByteCount == 0 && this.mode != mode; + Token token = this.token; + int thisModeBitCount = this.mode == HighLevelEncoder.MODE_DIGIT ? 4 : 5; + // Shifts exist only to UPPER and PUNCT, both with tokens size 5. + token = token.add(HighLevelEncoder.SHIFT_TABLE[this.mode][mode], thisModeBitCount); + token = token.add(value, 5); + return new State(token, this.mode, 0, this.bitCount + thisModeBitCount + 5); + } + + // Create a new state representing this state, but an additional character + // output in Binary Shift mode. + State addBinaryShiftChar(int index) { + Token token = this.token; + int mode = this.mode; + int bitCount = this.bitCount; + if (this.mode == HighLevelEncoder.MODE_PUNCT || this.mode == HighLevelEncoder.MODE_DIGIT) { + //assert binaryShiftByteCount == 0; + int latch = HighLevelEncoder.LATCH_TABLE[mode][HighLevelEncoder.MODE_UPPER]; + token = token.add(latch & 0xFFFF, latch >> 16); + bitCount += latch >> 16; + mode = HighLevelEncoder.MODE_UPPER; + } + int deltaBitCount = + (binaryShiftByteCount == 0 || binaryShiftByteCount == 31) ? 18 : + (binaryShiftByteCount == 62) ? 9 : 8; + State result = new State(token, mode, binaryShiftByteCount + 1, bitCount + deltaBitCount); + if (result.binaryShiftByteCount == 2047 + 31) { + // The string is as long as it's allowed to be. We should end it. + result = result.endBinaryShift(index + 1); + } + return result; + } + + // Create the state identical to this one, but we are no longer in + // Binary Shift mode. + State endBinaryShift(int index) { + if (binaryShiftByteCount == 0) { + return this; + } + Token token = this.token; + token = token.addBinaryShift(index - binaryShiftByteCount, binaryShiftByteCount); + //assert token.getTotalBitCount() == this.bitCount; + return new State(token, mode, 0, this.bitCount); + } + + // Returns true if "this" state is better (or equal) to be in than "that" + // state under all possible circumstances. + boolean isBetterThanOrEqualTo(State other) { + int mySize = this.bitCount + (HighLevelEncoder.LATCH_TABLE[this.mode][other.mode] >> 16); + if (other.binaryShiftByteCount > 0 && + (this.binaryShiftByteCount == 0 || this.binaryShiftByteCount > other.binaryShiftByteCount)) { + mySize += 10; // Cost of entering Binary Shift mode. + } + return mySize <= other.bitCount; + } + + BitArray toBitArray(byte[] text) { + // Reverse the tokens, so that they are in the order that they should + // be output + Deque symbols = new LinkedList<>(); + for (Token token = endBinaryShift(text.length).token; token != null; token = token.getPrevious()) { + symbols.addFirst(token); + } + BitArray bitArray = new BitArray(); + // Add each token to the result. + for (Token symbol : symbols) { + symbol.appendTo(bitArray, text); + } + //assert bitArray.getSize() == this.bitCount; + return bitArray; + } + + @Override + public String toString() { + return String.format("%s bits=%d bytes=%d", HighLevelEncoder.MODE_NAMES[mode], bitCount, binaryShiftByteCount); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/aztec/encoder/Token.java b/rubylib/src/main/java/com/google/zxing/aztec/encoder/Token.java new file mode 100644 index 0000000..c61bc57 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/aztec/encoder/Token.java @@ -0,0 +1,46 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.aztec.encoder; + +import com.google.zxing.common.BitArray; + +abstract class Token { + + static final Token EMPTY = new SimpleToken(null, 0, 0); + + private final Token previous; + + Token(Token previous) { + this.previous = previous; + } + + final Token getPrevious() { + return previous; + } + + final Token add(int value, int bitCount) { + return new SimpleToken(this, value, bitCount); + } + + final Token addBinaryShift(int start, int byteCount) { + //int bitCount = (byteCount * 8) + (byteCount <= 31 ? 10 : byteCount <= 62 ? 20 : 21); + return new BinaryShiftToken(this, start, byteCount); + } + + abstract void appendTo(BitArray bitArray, byte[] text); + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/AbstractDoCoMoResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/AbstractDoCoMoResultParser.java new file mode 100644 index 0000000..3d2c1d2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/AbstractDoCoMoResultParser.java @@ -0,0 +1,39 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + *

See + * + * DoCoMo's documentation about the result types represented by subclasses of this class.

+ * + *

Thanks to Jeff Griffin for proposing rewrite of these classes that relies less + * on exception-based mechanisms during parsing.

+ * + * @author Sean Owen + */ +abstract class AbstractDoCoMoResultParser extends ResultParser { + + static String[] matchDoCoMoPrefixedField(String prefix, String rawText, boolean trim) { + return matchPrefixedField(prefix, rawText, ';', trim); + } + + static String matchSingleDoCoMoPrefixedField(String prefix, String rawText, boolean trim) { + return matchSinglePrefixedField(prefix, rawText, ';', trim); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/AddressBookAUResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookAUResultParser.java new file mode 100644 index 0000000..cf7f96e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookAUResultParser.java @@ -0,0 +1,91 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implements KDDI AU's address book format. See + * + * http://www.au.kddi.com/ezfactory/tec/two_dimensions/index.html. + * (Thanks to Yuzo for translating!) + * + * @author Sean Owen + */ +public final class AddressBookAUResultParser extends ResultParser { + + @Override + public AddressBookParsedResult parse(Result result) { + String rawText = getMassagedText(result); + // MEMORY is mandatory; seems like a decent indicator, as does end-of-record separator CR/LF + if (!rawText.contains("MEMORY") || !rawText.contains("\r\n")) { + return null; + } + + // NAME1 and NAME2 have specific uses, namely written name and pronunciation, respectively. + // Therefore we treat them specially instead of as an array of names. + String name = matchSinglePrefixedField("NAME1:", rawText, '\r', true); + String pronunciation = matchSinglePrefixedField("NAME2:", rawText, '\r', true); + + String[] phoneNumbers = matchMultipleValuePrefix("TEL", 3, rawText, true); + String[] emails = matchMultipleValuePrefix("MAIL", 3, rawText, true); + String note = matchSinglePrefixedField("MEMORY:", rawText, '\r', false); + String address = matchSinglePrefixedField("ADD:", rawText, '\r', true); + String[] addresses = address == null ? null : new String[] {address}; + return new AddressBookParsedResult(maybeWrap(name), + null, + pronunciation, + phoneNumbers, + null, + emails, + null, + null, + note, + addresses, + null, + null, + null, + null, + null, + null); + } + + private static String[] matchMultipleValuePrefix(String prefix, + int max, + String rawText, + boolean trim) { + List values = null; + for (int i = 1; i <= max; i++) { + String value = matchSinglePrefixedField(prefix + i + ':', rawText, '\r', trim); + if (value == null) { + break; + } + if (values == null) { + values = new ArrayList<>(max); // lazy init + } + values.add(value); + } + if (values == null) { + return null; + } + return values.toArray(new String[values.size()]); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/AddressBookDoCoMoResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookDoCoMoResultParser.java new file mode 100644 index 0000000..ad507ef --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookDoCoMoResultParser.java @@ -0,0 +1,92 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + * Implements the "MECARD" address book entry format. + * + * Supported keys: N, SOUND, TEL, EMAIL, NOTE, ADR, BDAY, URL, plus ORG + * Unsupported keys: TEL-AV, NICKNAME + * + * Except for TEL, multiple values for keys are also not supported; + * the first one found takes precedence. + * + * Our understanding of the MECARD format is based on this document: + * + * http://www.mobicode.org.tw/files/OMIA%20Mobile%20Bar%20Code%20Standard%20v3.2.1.doc + * + * @author Sean Owen + */ +public final class AddressBookDoCoMoResultParser extends AbstractDoCoMoResultParser { + + @Override + public AddressBookParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("MECARD:")) { + return null; + } + String[] rawName = matchDoCoMoPrefixedField("N:", rawText, true); + if (rawName == null) { + return null; + } + String name = parseName(rawName[0]); + String pronunciation = matchSingleDoCoMoPrefixedField("SOUND:", rawText, true); + String[] phoneNumbers = matchDoCoMoPrefixedField("TEL:", rawText, true); + String[] emails = matchDoCoMoPrefixedField("EMAIL:", rawText, true); + String note = matchSingleDoCoMoPrefixedField("NOTE:", rawText, false); + String[] addresses = matchDoCoMoPrefixedField("ADR:", rawText, true); + String birthday = matchSingleDoCoMoPrefixedField("BDAY:", rawText, true); + if (!isStringOfDigits(birthday, 8)) { + // No reason to throw out the whole card because the birthday is formatted wrong. + birthday = null; + } + String[] urls = matchDoCoMoPrefixedField("URL:", rawText, true); + + // Although ORG may not be strictly legal in MECARD, it does exist in VCARD and we might as well + // honor it when found in the wild. + String org = matchSingleDoCoMoPrefixedField("ORG:", rawText, true); + + return new AddressBookParsedResult(maybeWrap(name), + null, + pronunciation, + phoneNumbers, + null, + emails, + null, + null, + note, + addresses, + null, + org, + birthday, + null, + urls, + null); + } + + private static String parseName(String name) { + int comma = name.indexOf(','); + if (comma >= 0) { + // Format may be last,first; switch it around + return name.substring(comma + 1) + ' ' + name.substring(0, comma); + } + return name; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/AddressBookParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookParsedResult.java new file mode 100644 index 0000000..0738339 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/AddressBookParsedResult.java @@ -0,0 +1,220 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes contact information, like that in an address book + * entry. + * + * @author Sean Owen + */ +public final class AddressBookParsedResult extends ParsedResult { + + private final String[] names; + private final String[] nicknames; + private final String pronunciation; + private final String[] phoneNumbers; + private final String[] phoneTypes; + private final String[] emails; + private final String[] emailTypes; + private final String instantMessenger; + private final String note; + private final String[] addresses; + private final String[] addressTypes; + private final String org; + private final String birthday; + private final String title; + private final String[] urls; + private final String[] geo; + + public AddressBookParsedResult(String[] names, + String[] phoneNumbers, + String[] phoneTypes, + String[] emails, + String[] emailTypes, + String[] addresses, + String[] addressTypes) { + this(names, + null, + null, + phoneNumbers, + phoneTypes, + emails, + emailTypes, + null, + null, + addresses, + addressTypes, + null, + null, + null, + null, + null); + } + + public AddressBookParsedResult(String[] names, + String[] nicknames, + String pronunciation, + String[] phoneNumbers, + String[] phoneTypes, + String[] emails, + String[] emailTypes, + String instantMessenger, + String note, + String[] addresses, + String[] addressTypes, + String org, + String birthday, + String title, + String[] urls, + String[] geo) { + super(ParsedResultType.ADDRESSBOOK); + if (phoneNumbers != null && phoneTypes != null && phoneNumbers.length != phoneTypes.length) { + throw new IllegalArgumentException("Phone numbers and types lengths differ"); + } + if (emails != null && emailTypes != null && emails.length != emailTypes.length) { + throw new IllegalArgumentException("Emails and types lengths differ"); + } + if (addresses != null && addressTypes != null && addresses.length != addressTypes.length) { + throw new IllegalArgumentException("Addresses and types lengths differ"); + } + this.names = names; + this.nicknames = nicknames; + this.pronunciation = pronunciation; + this.phoneNumbers = phoneNumbers; + this.phoneTypes = phoneTypes; + this.emails = emails; + this.emailTypes = emailTypes; + this.instantMessenger = instantMessenger; + this.note = note; + this.addresses = addresses; + this.addressTypes = addressTypes; + this.org = org; + this.birthday = birthday; + this.title = title; + this.urls = urls; + this.geo = geo; + } + + public String[] getNames() { + return names; + } + + public String[] getNicknames() { + return nicknames; + } + + /** + * In Japanese, the name is written in kanji, which can have multiple readings. Therefore a hint + * is often provided, called furigana, which spells the name phonetically. + * + * @return The pronunciation of the getNames() field, often in hiragana or katakana. + */ + public String getPronunciation() { + return pronunciation; + } + + public String[] getPhoneNumbers() { + return phoneNumbers; + } + + /** + * @return optional descriptions of the type of each phone number. It could be like "HOME", but, + * there is no guaranteed or standard format. + */ + public String[] getPhoneTypes() { + return phoneTypes; + } + + public String[] getEmails() { + return emails; + } + + /** + * @return optional descriptions of the type of each e-mail. It could be like "WORK", but, + * there is no guaranteed or standard format. + */ + public String[] getEmailTypes() { + return emailTypes; + } + + public String getInstantMessenger() { + return instantMessenger; + } + + public String getNote() { + return note; + } + + public String[] getAddresses() { + return addresses; + } + + /** + * @return optional descriptions of the type of each e-mail. It could be like "WORK", but, + * there is no guaranteed or standard format. + */ + public String[] getAddressTypes() { + return addressTypes; + } + + public String getTitle() { + return title; + } + + public String getOrg() { + return org; + } + + public String[] getURLs() { + return urls; + } + + /** + * @return birthday formatted as yyyyMMdd (e.g. 19780917) + */ + public String getBirthday() { + return birthday; + } + + /** + * @return a location as a latitude/longitude pair + */ + public String[] getGeo() { + return geo; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(100); + maybeAppend(names, result); + maybeAppend(nicknames, result); + maybeAppend(pronunciation, result); + maybeAppend(title, result); + maybeAppend(org, result); + maybeAppend(addresses, result); + maybeAppend(phoneNumbers, result); + maybeAppend(emails, result); + maybeAppend(instantMessenger, result); + maybeAppend(urls, result); + maybeAppend(birthday, result); + maybeAppend(geo, result); + maybeAppend(note, result); + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/BizcardResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/BizcardResultParser.java new file mode 100644 index 0000000..1d588d4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/BizcardResultParser.java @@ -0,0 +1,100 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.ArrayList; +import java.util.List; + +/** + * Implements the "BIZCARD" address book entry format, though this has been + * largely reverse-engineered from examples observed in the wild -- still + * looking for a definitive reference. + * + * @author Sean Owen + */ +public final class BizcardResultParser extends AbstractDoCoMoResultParser { + + // Yes, we extend AbstractDoCoMoResultParser since the format is very much + // like the DoCoMo MECARD format, but this is not technically one of + // DoCoMo's proposed formats + + @Override + public AddressBookParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("BIZCARD:")) { + return null; + } + String firstName = matchSingleDoCoMoPrefixedField("N:", rawText, true); + String lastName = matchSingleDoCoMoPrefixedField("X:", rawText, true); + String fullName = buildName(firstName, lastName); + String title = matchSingleDoCoMoPrefixedField("T:", rawText, true); + String org = matchSingleDoCoMoPrefixedField("C:", rawText, true); + String[] addresses = matchDoCoMoPrefixedField("A:", rawText, true); + String phoneNumber1 = matchSingleDoCoMoPrefixedField("B:", rawText, true); + String phoneNumber2 = matchSingleDoCoMoPrefixedField("M:", rawText, true); + String phoneNumber3 = matchSingleDoCoMoPrefixedField("F:", rawText, true); + String email = matchSingleDoCoMoPrefixedField("E:", rawText, true); + + return new AddressBookParsedResult(maybeWrap(fullName), + null, + null, + buildPhoneNumbers(phoneNumber1, phoneNumber2, phoneNumber3), + null, + maybeWrap(email), + null, + null, + null, + addresses, + null, + org, + null, + title, + null, + null); + } + + private static String[] buildPhoneNumbers(String number1, + String number2, + String number3) { + List numbers = new ArrayList<>(3); + if (number1 != null) { + numbers.add(number1); + } + if (number2 != null) { + numbers.add(number2); + } + if (number3 != null) { + numbers.add(number3); + } + int size = numbers.size(); + if (size == 0) { + return null; + } + return numbers.toArray(new String[size]); + } + + private static String buildName(String firstName, String lastName) { + if (firstName == null) { + return lastName; + } else { + return lastName == null ? firstName : firstName + ' ' + lastName; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/BookmarkDoCoMoResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/BookmarkDoCoMoResultParser.java new file mode 100644 index 0000000..a729239 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/BookmarkDoCoMoResultParser.java @@ -0,0 +1,41 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + * @author Sean Owen + */ +public final class BookmarkDoCoMoResultParser extends AbstractDoCoMoResultParser { + + @Override + public URIParsedResult parse(Result result) { + String rawText = result.getText(); + if (!rawText.startsWith("MEBKM:")) { + return null; + } + String title = matchSingleDoCoMoPrefixedField("TITLE:", rawText, true); + String[] rawUri = matchDoCoMoPrefixedField("URL:", rawText, true); + if (rawUri == null) { + return null; + } + String uri = rawUri[0]; + return URIResultParser.isBasicallyValidURI(uri) ? new URIParsedResult(uri, title) : null; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/CalendarParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/CalendarParsedResult.java new file mode 100644 index 0000000..7b478a3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/CalendarParsedResult.java @@ -0,0 +1,259 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.Locale; +import java.util.TimeZone; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Represents a parsed result that encodes a calendar event at a certain time, optionally + * with attendees and a location. + * + * @author Sean Owen + */ +public final class CalendarParsedResult extends ParsedResult { + + private static final Pattern RFC2445_DURATION = + Pattern.compile("P(?:(\\d+)W)?(?:(\\d+)D)?(?:T(?:(\\d+)H)?(?:(\\d+)M)?(?:(\\d+)S)?)?"); + private static final long[] RFC2445_DURATION_FIELD_UNITS = { + 7 * 24 * 60 * 60 * 1000L, // 1 week + 24 * 60 * 60 * 1000L, // 1 day + 60 * 60 * 1000L, // 1 hour + 60 * 1000L, // 1 minute + 1000L, // 1 second + }; + + private static final Pattern DATE_TIME = Pattern.compile("[0-9]{8}(T[0-9]{6}Z?)?"); + + private final String summary; + private final long start; + private final boolean startAllDay; + private final long end; + private final boolean endAllDay; + private final String location; + private final String organizer; + private final String[] attendees; + private final String description; + private final double latitude; + private final double longitude; + + public CalendarParsedResult(String summary, + String startString, + String endString, + String durationString, + String location, + String organizer, + String[] attendees, + String description, + double latitude, + double longitude) { + super(ParsedResultType.CALENDAR); + this.summary = summary; + + try { + this.start = parseDate(startString); + } catch (ParseException pe) { + throw new IllegalArgumentException(pe.toString()); + } + + if (endString == null) { + long durationMS = parseDurationMS(durationString); + end = durationMS < 0L ? -1L : start + durationMS; + } else { + try { + this.end = parseDate(endString); + } catch (ParseException pe) { + throw new IllegalArgumentException(pe.toString()); + } + } + + this.startAllDay = startString.length() == 8; + this.endAllDay = endString != null && endString.length() == 8; + + this.location = location; + this.organizer = organizer; + this.attendees = attendees; + this.description = description; + this.latitude = latitude; + this.longitude = longitude; + } + + public String getSummary() { + return summary; + } + + /** + * @return start time + * @deprecated use {@link #getStartTimestamp()} + */ + @Deprecated + public Date getStart() { + return new Date(start); + } + + /** + * @return start time + * @see #getEndTimestamp() + */ + public long getStartTimestamp() { + return start; + } + + /** + * @return true if start time was specified as a whole day + */ + public boolean isStartAllDay() { + return startAllDay; + } + + /** + * @return event end {@link Date}, or {@code null} if event has no duration + * @deprecated use {@link #getEndTimestamp()} + */ + @Deprecated + public Date getEnd() { + return end < 0L ? null : new Date(end); + } + + /** + * @return event end {@link Date}, or -1 if event has no duration + * @see #getStartTimestamp() + */ + public long getEndTimestamp() { + return end; + } + + /** + * @return true if end time was specified as a whole day + */ + public boolean isEndAllDay() { + return endAllDay; + } + + public String getLocation() { + return location; + } + + public String getOrganizer() { + return organizer; + } + + public String[] getAttendees() { + return attendees; + } + + public String getDescription() { + return description; + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(100); + maybeAppend(summary, result); + maybeAppend(format(startAllDay, start), result); + maybeAppend(format(endAllDay, end), result); + maybeAppend(location, result); + maybeAppend(organizer, result); + maybeAppend(attendees, result); + maybeAppend(description, result); + return result.toString(); + } + + /** + * Parses a string as a date. RFC 2445 allows the start and end fields to be of type DATE (e.g. 20081021) + * or DATE-TIME (e.g. 20081021T123000 for local time, or 20081021T123000Z for UTC). + * + * @param when The string to parse + * @throws ParseException if not able to parse as a date + */ + private static long parseDate(String when) throws ParseException { + if (!DATE_TIME.matcher(when).matches()) { + throw new ParseException(when, 0); + } + if (when.length() == 8) { + // Show only year/month/day + DateFormat format = new SimpleDateFormat("yyyyMMdd", Locale.ENGLISH); + // For dates without a time, for purposes of interacting with Android, the resulting timestamp + // needs to be midnight of that day in GMT. See: + // http://code.google.com/p/android/issues/detail?id=8330 + format.setTimeZone(TimeZone.getTimeZone("GMT")); + return format.parse(when).getTime(); + } + // The when string can be local time, or UTC if it ends with a Z + if (when.length() == 16 && when.charAt(15) == 'Z') { + long milliseconds = parseDateTimeString(when.substring(0, 15)); + Calendar calendar = new GregorianCalendar(); + // Account for time zone difference + milliseconds += calendar.get(Calendar.ZONE_OFFSET); + // Might need to correct for daylight savings time, but use target time since + // now might be in DST but not then, or vice versa + calendar.setTime(new Date(milliseconds)); + return milliseconds + calendar.get(Calendar.DST_OFFSET); + } + return parseDateTimeString(when); + } + + private static String format(boolean allDay, long date) { + if (date < 0L) { + return null; + } + DateFormat format = allDay + ? DateFormat.getDateInstance(DateFormat.MEDIUM) + : DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM); + return format.format(date); + } + + private static long parseDurationMS(CharSequence durationString) { + if (durationString == null) { + return -1L; + } + Matcher m = RFC2445_DURATION.matcher(durationString); + if (!m.matches()) { + return -1L; + } + long durationMS = 0L; + for (int i = 0; i < RFC2445_DURATION_FIELD_UNITS.length; i++) { + String fieldValue = m.group(i + 1); + if (fieldValue != null) { + durationMS += RFC2445_DURATION_FIELD_UNITS[i] * Integer.parseInt(fieldValue); + } + } + return durationMS; + } + + private static long parseDateTimeString(String dateTimeString) throws ParseException { + DateFormat format = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.ENGLISH); + return format.parse(dateTimeString).getTime(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressParsedResult.java new file mode 100644 index 0000000..a5bcd44 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressParsedResult.java @@ -0,0 +1,99 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes an email message including recipients, subject + * and body text. + * + * @author Sean Owen + */ +public final class EmailAddressParsedResult extends ParsedResult { + + private final String[] tos; + private final String[] ccs; + private final String[] bccs; + private final String subject; + private final String body; + + EmailAddressParsedResult(String to) { + this(new String[] {to}, null, null, null, null); + } + + EmailAddressParsedResult(String[] tos, + String[] ccs, + String[] bccs, + String subject, + String body) { + super(ParsedResultType.EMAIL_ADDRESS); + this.tos = tos; + this.ccs = ccs; + this.bccs = bccs; + this.subject = subject; + this.body = body; + } + + /** + * @return first elements of {@link #getTos()} or {@code null} if none + * @deprecated use {@link #getTos()} + */ + @Deprecated + public String getEmailAddress() { + return tos == null || tos.length == 0 ? null : tos[0]; + } + + public String[] getTos() { + return tos; + } + + public String[] getCCs() { + return ccs; + } + + public String[] getBCCs() { + return bccs; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + /** + * @return "mailto:" + * @deprecated without replacement + */ + @Deprecated + public String getMailtoURI() { + return "mailto:"; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(30); + maybeAppend(tos, result); + maybeAppend(ccs, result); + maybeAppend(bccs, result); + maybeAppend(subject, result); + maybeAppend(body, result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressResultParser.java new file mode 100644 index 0000000..18c4f7a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/EmailAddressResultParser.java @@ -0,0 +1,85 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.Map; +import java.util.regex.Pattern; + +/** + * Represents a result that encodes an e-mail address, either as a plain address + * like "joe@example.org" or a mailto: URL like "mailto:joe@example.org". + * + * @author Sean Owen + */ +public final class EmailAddressResultParser extends ResultParser { + + private static final Pattern COMMA = Pattern.compile(","); + + @Override + public EmailAddressParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (rawText.startsWith("mailto:") || rawText.startsWith("MAILTO:")) { + // If it starts with mailto:, assume it is definitely trying to be an email address + String hostEmail = rawText.substring(7); + int queryStart = hostEmail.indexOf('?'); + if (queryStart >= 0) { + hostEmail = hostEmail.substring(0, queryStart); + } + try { + hostEmail = urlDecode(hostEmail); + } catch (IllegalArgumentException iae) { + return null; + } + String[] tos = null; + if (!hostEmail.isEmpty()) { + tos = COMMA.split(hostEmail); + } + Map nameValues = parseNameValuePairs(rawText); + String[] ccs = null; + String[] bccs = null; + String subject = null; + String body = null; + if (nameValues != null) { + if (tos == null) { + String tosString = nameValues.get("to"); + if (tosString != null) { + tos = COMMA.split(tosString); + } + } + String ccString = nameValues.get("cc"); + if (ccString != null) { + ccs = COMMA.split(ccString); + } + String bccString = nameValues.get("bcc"); + if (bccString != null) { + bccs = COMMA.split(bccString); + } + subject = nameValues.get("subject"); + body = nameValues.get("body"); + } + return new EmailAddressParsedResult(tos, ccs, bccs, subject, body); + } else { + if (!EmailDoCoMoResultParser.isBasicallyValidEmailAddress(rawText)) { + return null; + } + return new EmailAddressParsedResult(rawText); + } + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/EmailDoCoMoResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/EmailDoCoMoResultParser.java new file mode 100644 index 0000000..fc4c284 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/EmailDoCoMoResultParser.java @@ -0,0 +1,64 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.regex.Pattern; + +/** + * Implements the "MATMSG" email message entry format. + * + * Supported keys: TO, SUB, BODY + * + * @author Sean Owen + */ +public final class EmailDoCoMoResultParser extends AbstractDoCoMoResultParser { + + private static final Pattern ATEXT_ALPHANUMERIC = Pattern.compile("[a-zA-Z0-9@.!#$%&'*+\\-/=?^_`{|}~]+"); + + @Override + public EmailAddressParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("MATMSG:")) { + return null; + } + String[] tos = matchDoCoMoPrefixedField("TO:", rawText, true); + if (tos == null) { + return null; + } + for (String to : tos) { + if (!isBasicallyValidEmailAddress(to)) { + return null; + } + } + String subject = matchSingleDoCoMoPrefixedField("SUB:", rawText, false); + String body = matchSingleDoCoMoPrefixedField("BODY:", rawText, false); + return new EmailAddressParsedResult(tos, null, null, subject, body); + } + + /** + * This implements only the most basic checking for an email address's validity -- that it contains + * an '@' and contains no characters disallowed by RFC 2822. This is an overly lenient definition of + * validity. We want to generally be lenient here since this class is only intended to encapsulate what's + * in a barcode, not "judge" it. + */ + static boolean isBasicallyValidEmailAddress(String email) { + return email != null && ATEXT_ALPHANUMERIC.matcher(email).matches() && email.indexOf('@') >= 0; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductParsedResult.java new file mode 100644 index 0000000..7585159 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductParsedResult.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.client.result; + +import java.util.Map; + +/** + * Represents a parsed result that encodes extended product information as encoded + * by the RSS format, like weight, price, dates, etc. + * + * @author Antonio Manuel Benjumea Conde, Servinform, S.A. + * @author Agustín Delgado, Servinform, S.A. + */ +public final class ExpandedProductParsedResult extends ParsedResult { + + public static final String KILOGRAM = "KG"; + public static final String POUND = "LB"; + + private final String rawText; + private final String productID; + private final String sscc; + private final String lotNumber; + private final String productionDate; + private final String packagingDate; + private final String bestBeforeDate; + private final String expirationDate; + private final String weight; + private final String weightType; + private final String weightIncrement; + private final String price; + private final String priceIncrement; + private final String priceCurrency; + // For AIS that not exist in this object + private final Map uncommonAIs; + + public ExpandedProductParsedResult(String rawText, + String productID, + String sscc, + String lotNumber, + String productionDate, + String packagingDate, + String bestBeforeDate, + String expirationDate, + String weight, + String weightType, + String weightIncrement, + String price, + String priceIncrement, + String priceCurrency, + Map uncommonAIs) { + super(ParsedResultType.PRODUCT); + this.rawText = rawText; + this.productID = productID; + this.sscc = sscc; + this.lotNumber = lotNumber; + this.productionDate = productionDate; + this.packagingDate = packagingDate; + this.bestBeforeDate = bestBeforeDate; + this.expirationDate = expirationDate; + this.weight = weight; + this.weightType = weightType; + this.weightIncrement = weightIncrement; + this.price = price; + this.priceIncrement = priceIncrement; + this.priceCurrency = priceCurrency; + this.uncommonAIs = uncommonAIs; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ExpandedProductParsedResult)) { + return false; + } + + ExpandedProductParsedResult other = (ExpandedProductParsedResult) o; + + return equalsOrNull(productID, other.productID) + && equalsOrNull(sscc, other.sscc) + && equalsOrNull(lotNumber, other.lotNumber) + && equalsOrNull(productionDate, other.productionDate) + && equalsOrNull(bestBeforeDate, other.bestBeforeDate) + && equalsOrNull(expirationDate, other.expirationDate) + && equalsOrNull(weight, other.weight) + && equalsOrNull(weightType, other.weightType) + && equalsOrNull(weightIncrement, other.weightIncrement) + && equalsOrNull(price, other.price) + && equalsOrNull(priceIncrement, other.priceIncrement) + && equalsOrNull(priceCurrency, other.priceCurrency) + && equalsOrNull(uncommonAIs, other.uncommonAIs); + } + + private static boolean equalsOrNull(Object o1, Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + + @Override + public int hashCode() { + int hash = 0; + hash ^= hashNotNull(productID); + hash ^= hashNotNull(sscc); + hash ^= hashNotNull(lotNumber); + hash ^= hashNotNull(productionDate); + hash ^= hashNotNull(bestBeforeDate); + hash ^= hashNotNull(expirationDate); + hash ^= hashNotNull(weight); + hash ^= hashNotNull(weightType); + hash ^= hashNotNull(weightIncrement); + hash ^= hashNotNull(price); + hash ^= hashNotNull(priceIncrement); + hash ^= hashNotNull(priceCurrency); + hash ^= hashNotNull(uncommonAIs); + return hash; + } + + private static int hashNotNull(Object o) { + return o == null ? 0 : o.hashCode(); + } + + public String getRawText() { + return rawText; + } + + public String getProductID() { + return productID; + } + + public String getSscc() { + return sscc; + } + + public String getLotNumber() { + return lotNumber; + } + + public String getProductionDate() { + return productionDate; + } + + public String getPackagingDate() { + return packagingDate; + } + + public String getBestBeforeDate() { + return bestBeforeDate; + } + + public String getExpirationDate() { + return expirationDate; + } + + public String getWeight() { + return weight; + } + + public String getWeightType() { + return weightType; + } + + public String getWeightIncrement() { + return weightIncrement; + } + + public String getPrice() { + return price; + } + + public String getPriceIncrement() { + return priceIncrement; + } + + public String getPriceCurrency() { + return priceCurrency; + } + + public Map getUncommonAIs() { + return uncommonAIs; + } + + @Override + public String getDisplayResult() { + return String.valueOf(rawText); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductResultParser.java new file mode 100644 index 0000000..c021587 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ExpandedProductResultParser.java @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.client.result; + +import java.util.HashMap; +import java.util.Map; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; + +/** + * Parses strings of digits that represent a RSS Extended code. + * + * @author Antonio Manuel Benjumea Conde, Servinform, S.A. + * @author Agustín Delgado, Servinform, S.A. + */ +public final class ExpandedProductResultParser extends ResultParser { + + @Override + public ExpandedProductParsedResult parse(Result result) { + BarcodeFormat format = result.getBarcodeFormat(); + if (format != BarcodeFormat.RSS_EXPANDED) { + // ExtendedProductParsedResult NOT created. Not a RSS Expanded barcode + return null; + } + String rawText = getMassagedText(result); + + String productID = null; + String sscc = null; + String lotNumber = null; + String productionDate = null; + String packagingDate = null; + String bestBeforeDate = null; + String expirationDate = null; + String weight = null; + String weightType = null; + String weightIncrement = null; + String price = null; + String priceIncrement = null; + String priceCurrency = null; + Map uncommonAIs = new HashMap<>(); + + int i = 0; + + while (i < rawText.length()) { + String ai = findAIvalue(i, rawText); + if (ai == null) { + // Error. Code doesn't match with RSS expanded pattern + // ExtendedProductParsedResult NOT created. Not match with RSS Expanded pattern + return null; + } + i += ai.length() + 2; + String value = findValue(i, rawText); + i += value.length(); + + switch (ai) { + case "00": + sscc = value; + break; + case "01": + productID = value; + break; + case "10": + lotNumber = value; + break; + case "11": + productionDate = value; + break; + case "13": + packagingDate = value; + break; + case "15": + bestBeforeDate = value; + break; + case "17": + expirationDate = value; + break; + case "3100": + case "3101": + case "3102": + case "3103": + case "3104": + case "3105": + case "3106": + case "3107": + case "3108": + case "3109": + weight = value; + weightType = ExpandedProductParsedResult.KILOGRAM; + weightIncrement = ai.substring(3); + break; + case "3200": + case "3201": + case "3202": + case "3203": + case "3204": + case "3205": + case "3206": + case "3207": + case "3208": + case "3209": + weight = value; + weightType = ExpandedProductParsedResult.POUND; + weightIncrement = ai.substring(3); + break; + case "3920": + case "3921": + case "3922": + case "3923": + price = value; + priceIncrement = ai.substring(3); + break; + case "3930": + case "3931": + case "3932": + case "3933": + if (value.length() < 4) { + // The value must have more of 3 symbols (3 for currency and + // 1 at least for the price) + // ExtendedProductParsedResult NOT created. Not match with RSS Expanded pattern + return null; + } + price = value.substring(3); + priceCurrency = value.substring(0, 3); + priceIncrement = ai.substring(3); + break; + default: + // No match with common AIs + uncommonAIs.put(ai, value); + break; + } + } + + return new ExpandedProductParsedResult(rawText, + productID, + sscc, + lotNumber, + productionDate, + packagingDate, + bestBeforeDate, + expirationDate, + weight, + weightType, + weightIncrement, + price, + priceIncrement, + priceCurrency, + uncommonAIs); + } + + private static String findAIvalue(int i, String rawText) { + char c = rawText.charAt(i); + // First character must be a open parenthesis.If not, ERROR + if (c != '(') { + return null; + } + + CharSequence rawTextAux = rawText.substring(i + 1); + + StringBuilder buf = new StringBuilder(); + for (int index = 0; index < rawTextAux.length(); index++) { + char currentChar = rawTextAux.charAt(index); + if (currentChar == ')') { + return buf.toString(); + } + if (currentChar < '0' || currentChar > '9') { + return null; + } + buf.append(currentChar); + } + return buf.toString(); + } + + private static String findValue(int i, String rawText) { + StringBuilder buf = new StringBuilder(); + String rawTextAux = rawText.substring(i); + + for (int index = 0; index < rawTextAux.length(); index++) { + char c = rawTextAux.charAt(index); + if (c == '(') { + // We look for a new AI. If it doesn't exist (ERROR), we continue + // with the iteration + if (findAIvalue(index, rawTextAux) != null) { + break; + } + buf.append('('); + } else { + buf.append(c); + } + } + return buf.toString(); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/GeoParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/GeoParsedResult.java new file mode 100644 index 0000000..3a2cae6 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/GeoParsedResult.java @@ -0,0 +1,104 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes a geographic coordinate, with latitude, + * longitude and altitude. + * + * @author Sean Owen + */ +public final class GeoParsedResult extends ParsedResult { + + private final double latitude; + private final double longitude; + private final double altitude; + private final String query; + + GeoParsedResult(double latitude, double longitude, double altitude, String query) { + super(ParsedResultType.GEO); + this.latitude = latitude; + this.longitude = longitude; + this.altitude = altitude; + this.query = query; + } + + public String getGeoURI() { + StringBuilder result = new StringBuilder(); + result.append("geo:"); + result.append(latitude); + result.append(','); + result.append(longitude); + if (altitude > 0) { + result.append(','); + result.append(altitude); + } + if (query != null) { + result.append('?'); + result.append(query); + } + return result.toString(); + } + + /** + * @return latitude in degrees + */ + public double getLatitude() { + return latitude; + } + + /** + * @return longitude in degrees + */ + public double getLongitude() { + return longitude; + } + + /** + * @return altitude in meters. If not specified, in the geo URI, returns 0.0 + */ + public double getAltitude() { + return altitude; + } + + /** + * @return query string associated with geo URI or null if none exists + */ + public String getQuery() { + return query; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(20); + result.append(latitude); + result.append(", "); + result.append(longitude); + if (altitude > 0.0) { + result.append(", "); + result.append(altitude); + result.append('m'); + } + if (query != null) { + result.append(" ("); + result.append(query); + result.append(')'); + } + return result.toString(); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/GeoResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/GeoResultParser.java new file mode 100644 index 0000000..fb9cb07 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/GeoResultParser.java @@ -0,0 +1,73 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses a "geo:" URI result, which specifies a location on the surface of + * the Earth as well as an optional altitude above the surface. See + * + * http://tools.ietf.org/html/draft-mayrhofer-geo-uri-00. + * + * @author Sean Owen + */ +public final class GeoResultParser extends ResultParser { + + private static final Pattern GEO_URL_PATTERN = + Pattern.compile("geo:([\\-0-9.]+),([\\-0-9.]+)(?:,([\\-0-9.]+))?(?:\\?(.*))?", Pattern.CASE_INSENSITIVE); + + @Override + public GeoParsedResult parse(Result result) { + CharSequence rawText = getMassagedText(result); + Matcher matcher = GEO_URL_PATTERN.matcher(rawText); + if (!matcher.matches()) { + return null; + } + + String query = matcher.group(4); + + double latitude; + double longitude; + double altitude; + try { + latitude = Double.parseDouble(matcher.group(1)); + if (latitude > 90.0 || latitude < -90.0) { + return null; + } + longitude = Double.parseDouble(matcher.group(2)); + if (longitude > 180.0 || longitude < -180.0) { + return null; + } + if (matcher.group(3) == null) { + altitude = 0.0; + } else { + altitude = Double.parseDouble(matcher.group(3)); + if (altitude < 0.0) { + return null; + } + } + } catch (NumberFormatException ignored) { + return null; + } + return new GeoParsedResult(latitude, longitude, altitude, query); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ISBNParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/ISBNParsedResult.java new file mode 100644 index 0000000..ebf11c5 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ISBNParsedResult.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes a product ISBN number. + * + * @author jbreiden@google.com (Jeff Breidenbach) + */ +public final class ISBNParsedResult extends ParsedResult { + + private final String isbn; + + ISBNParsedResult(String isbn) { + super(ParsedResultType.ISBN); + this.isbn = isbn; + } + + public String getISBN() { + return isbn; + } + + @Override + public String getDisplayResult() { + return isbn; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ISBNResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/ISBNResultParser.java new file mode 100644 index 0000000..e957dd0 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ISBNResultParser.java @@ -0,0 +1,50 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; + +/** + * Parses strings of digits that represent a ISBN. + * + * @author jbreiden@google.com (Jeff Breidenbach) + */ +public final class ISBNResultParser extends ResultParser { + + /** + * See ISBN-13 For Dummies + */ + @Override + public ISBNParsedResult parse(Result result) { + BarcodeFormat format = result.getBarcodeFormat(); + if (format != BarcodeFormat.EAN_13) { + return null; + } + String rawText = getMassagedText(result); + int length = rawText.length(); + if (length != 13) { + return null; + } + if (!rawText.startsWith("978") && !rawText.startsWith("979")) { + return null; + } + + return new ISBNParsedResult(rawText); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/ParsedResult.java new file mode 100644 index 0000000..17660e2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ParsedResult.java @@ -0,0 +1,67 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + *

Abstract class representing the result of decoding a barcode, as more than + * a String -- as some type of structured data. This might be a subclass which represents + * a URL, or an e-mail address. {@link ResultParser#parseResult(com.google.zxing.Result)} will turn a raw + * decoded string into the most appropriate type of structured representation.

+ * + *

Thanks to Jeff Griffin for proposing rewrite of these classes that relies less + * on exception-based mechanisms during parsing.

+ * + * @author Sean Owen + */ +public abstract class ParsedResult { + + private final ParsedResultType type; + + protected ParsedResult(ParsedResultType type) { + this.type = type; + } + + public final ParsedResultType getType() { + return type; + } + + public abstract String getDisplayResult(); + + @Override + public final String toString() { + return getDisplayResult(); + } + + public static void maybeAppend(String value, StringBuilder result) { + if (value != null && !value.isEmpty()) { + // Don't add a newline before the first value + if (result.length() > 0) { + result.append('\n'); + } + result.append(value); + } + } + + public static void maybeAppend(String[] values, StringBuilder result) { + if (values != null) { + for (String value : values) { + maybeAppend(value, result); + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ParsedResultType.java b/rubylib/src/main/java/com/google/zxing/client/result/ParsedResultType.java new file mode 100644 index 0000000..c74d545 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ParsedResultType.java @@ -0,0 +1,40 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents the type of data encoded by a barcode -- from plain text, to a + * URI, to an e-mail address, etc. + * + * @author Sean Owen + */ +public enum ParsedResultType { + + ADDRESSBOOK, + EMAIL_ADDRESS, + PRODUCT, + URI, + TEXT, + GEO, + TEL, + SMS, + CALENDAR, + WIFI, + ISBN, + VIN, + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ProductParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/ProductParsedResult.java new file mode 100644 index 0000000..66a5c9b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ProductParsedResult.java @@ -0,0 +1,52 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes a product by an identifier of some kind. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ProductParsedResult extends ParsedResult { + + private final String productID; + private final String normalizedProductID; + + ProductParsedResult(String productID) { + this(productID, productID); + } + + ProductParsedResult(String productID, String normalizedProductID) { + super(ParsedResultType.PRODUCT); + this.productID = productID; + this.normalizedProductID = normalizedProductID; + } + + public String getProductID() { + return productID; + } + + public String getNormalizedProductID() { + return normalizedProductID; + } + + @Override + public String getDisplayResult() { + return productID; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ProductResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/ProductResultParser.java new file mode 100644 index 0000000..bed1a7d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ProductResultParser.java @@ -0,0 +1,55 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; +import com.google.zxing.oned.UPCEReader; + +/** + * Parses strings of digits that represent a UPC code. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ProductResultParser extends ResultParser { + + // Treat all UPC and EAN variants as UPCs, in the sense that they are all product barcodes. + @Override + public ProductParsedResult parse(Result result) { + BarcodeFormat format = result.getBarcodeFormat(); + if (!(format == BarcodeFormat.UPC_A || format == BarcodeFormat.UPC_E || + format == BarcodeFormat.EAN_8 || format == BarcodeFormat.EAN_13)) { + return null; + } + String rawText = getMassagedText(result); + if (!isStringOfDigits(rawText, rawText.length())) { + return null; + } + // Not actually checking the checksum again here + + String normalizedProductID; + // Expand UPC-E for purposes of searching + if (format == BarcodeFormat.UPC_E && rawText.length() == 8) { + normalizedProductID = UPCEReader.convertUPCEtoUPCA(rawText); + } else { + normalizedProductID = rawText; + } + + return new ProductParsedResult(rawText, normalizedProductID); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/ResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/ResultParser.java new file mode 100644 index 0000000..c86f1f7 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/ResultParser.java @@ -0,0 +1,259 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + *

Abstract class representing the result of decoding a barcode, as more than + * a String -- as some type of structured data. This might be a subclass which represents + * a URL, or an e-mail address. {@link #parseResult(Result)} will turn a raw + * decoded string into the most appropriate type of structured representation.

+ * + *

Thanks to Jeff Griffin for proposing rewrite of these classes that relies less + * on exception-based mechanisms during parsing.

+ * + * @author Sean Owen + */ +public abstract class ResultParser { + + private static final ResultParser[] PARSERS = { + new BookmarkDoCoMoResultParser(), + new AddressBookDoCoMoResultParser(), + new EmailDoCoMoResultParser(), + new AddressBookAUResultParser(), + new VCardResultParser(), + new BizcardResultParser(), + new VEventResultParser(), + new EmailAddressResultParser(), + new SMTPResultParser(), + new TelResultParser(), + new SMSMMSResultParser(), + new SMSTOMMSTOResultParser(), + new GeoResultParser(), + new WifiResultParser(), + new URLTOResultParser(), + new URIResultParser(), + new ISBNResultParser(), + new ProductResultParser(), + new ExpandedProductResultParser(), + new VINResultParser(), + }; + + private static final Pattern DIGITS = Pattern.compile("\\d+"); + private static final Pattern AMPERSAND = Pattern.compile("&"); + private static final Pattern EQUALS = Pattern.compile("="); + private static final String BYTE_ORDER_MARK = "\ufeff"; + + /** + * Attempts to parse the raw {@link Result}'s contents as a particular type + * of information (email, URL, etc.) and return a {@link ParsedResult} encapsulating + * the result of parsing. + * + * @param theResult the raw {@link Result} to parse + * @return {@link ParsedResult} encapsulating the parsing result + */ + public abstract ParsedResult parse(Result theResult); + + protected static String getMassagedText(Result result) { + String text = result.getText(); + if (text.startsWith(BYTE_ORDER_MARK)) { + text = text.substring(1); + } + return text; + } + + public static ParsedResult parseResult(Result theResult) { + for (ResultParser parser : PARSERS) { + ParsedResult result = parser.parse(theResult); + if (result != null) { + return result; + } + } + return new TextParsedResult(theResult.getText(), null); + } + + protected static void maybeAppend(String value, StringBuilder result) { + if (value != null) { + result.append('\n'); + result.append(value); + } + } + + protected static void maybeAppend(String[] value, StringBuilder result) { + if (value != null) { + for (String s : value) { + result.append('\n'); + result.append(s); + } + } + } + + protected static String[] maybeWrap(String value) { + return value == null ? null : new String[] { value }; + } + + protected static String unescapeBackslash(String escaped) { + int backslash = escaped.indexOf('\\'); + if (backslash < 0) { + return escaped; + } + int max = escaped.length(); + StringBuilder unescaped = new StringBuilder(max - 1); + unescaped.append(escaped.toCharArray(), 0, backslash); + boolean nextIsEscaped = false; + for (int i = backslash; i < max; i++) { + char c = escaped.charAt(i); + if (nextIsEscaped || c != '\\') { + unescaped.append(c); + nextIsEscaped = false; + } else { + nextIsEscaped = true; + } + } + return unescaped.toString(); + } + + protected static int parseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return 10 + (c - 'a'); + } + if (c >= 'A' && c <= 'F') { + return 10 + (c - 'A'); + } + return -1; + } + + protected static boolean isStringOfDigits(CharSequence value, int length) { + return value != null && length > 0 && length == value.length() && DIGITS.matcher(value).matches(); + } + + protected static boolean isSubstringOfDigits(CharSequence value, int offset, int length) { + if (value == null || length <= 0) { + return false; + } + int max = offset + length; + return value.length() >= max && DIGITS.matcher(value.subSequence(offset, max)).matches(); + } + + static Map parseNameValuePairs(String uri) { + int paramStart = uri.indexOf('?'); + if (paramStart < 0) { + return null; + } + Map result = new HashMap<>(3); + for (String keyValue : AMPERSAND.split(uri.substring(paramStart + 1))) { + appendKeyValue(keyValue, result); + } + return result; + } + + private static void appendKeyValue(CharSequence keyValue, Map result) { + String[] keyValueTokens = EQUALS.split(keyValue, 2); + if (keyValueTokens.length == 2) { + String key = keyValueTokens[0]; + String value = keyValueTokens[1]; + try { + value = urlDecode(value); + result.put(key, value); + } catch (IllegalArgumentException iae) { + // continue; invalid data such as an escape like %0t + } + } + } + + static String urlDecode(String encoded) { + try { + return URLDecoder.decode(encoded, "UTF-8"); + } catch (UnsupportedEncodingException uee) { + throw new IllegalStateException(uee); // can't happen + } + } + + static String[] matchPrefixedField(String prefix, String rawText, char endChar, boolean trim) { + List matches = null; + int i = 0; + int max = rawText.length(); + while (i < max) { + i = rawText.indexOf(prefix, i); + if (i < 0) { + break; + } + i += prefix.length(); // Skip past this prefix we found to start + int start = i; // Found the start of a match here + boolean more = true; + while (more) { + i = rawText.indexOf(endChar, i); + if (i < 0) { + // No terminating end character? uh, done. Set i such that loop terminates and break + i = rawText.length(); + more = false; + } else if (countPrecedingBackslashes(rawText, i) % 2 != 0) { + // semicolon was escaped (odd count of preceding backslashes) so continue + i++; + } else { + // found a match + if (matches == null) { + matches = new ArrayList<>(3); // lazy init + } + String element = unescapeBackslash(rawText.substring(start, i)); + if (trim) { + element = element.trim(); + } + if (!element.isEmpty()) { + matches.add(element); + } + i++; + more = false; + } + } + } + if (matches == null || matches.isEmpty()) { + return null; + } + return matches.toArray(new String[matches.size()]); + } + + private static int countPrecedingBackslashes(CharSequence s, int pos) { + int count = 0; + for (int i = pos - 1; i >= 0; i--) { + if (s.charAt(i) == '\\') { + count++; + } else { + break; + } + } + return count; + } + + static String matchSinglePrefixedField(String prefix, String rawText, char endChar, boolean trim) { + String[] matches = matchPrefixedField(prefix, rawText, endChar, trim); + return matches == null ? null : matches[0]; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/SMSMMSResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/SMSMMSResultParser.java new file mode 100644 index 0000000..a8d65d3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/SMSMMSResultParser.java @@ -0,0 +1,109 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

Parses an "sms:" URI result, which specifies a number to SMS. + * See RFC 5724 on this.

+ * + *

This class supports "via" syntax for numbers, which is not part of the spec. + * For example "+12125551212;via=+12124440101" may appear as a number. + * It also supports a "subject" query parameter, which is not mentioned in the spec. + * These are included since they were mentioned in earlier IETF drafts and might be + * used.

+ * + *

This actually also parses URIs starting with "mms:" and treats them all the same way, + * and effectively converts them to an "sms:" URI for purposes of forwarding to the platform.

+ * + * @author Sean Owen + */ +public final class SMSMMSResultParser extends ResultParser { + + @Override + public SMSParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!(rawText.startsWith("sms:") || rawText.startsWith("SMS:") || + rawText.startsWith("mms:") || rawText.startsWith("MMS:"))) { + return null; + } + + // Check up front if this is a URI syntax string with query arguments + Map nameValuePairs = parseNameValuePairs(rawText); + String subject = null; + String body = null; + boolean querySyntax = false; + if (nameValuePairs != null && !nameValuePairs.isEmpty()) { + subject = nameValuePairs.get("subject"); + body = nameValuePairs.get("body"); + querySyntax = true; + } + + // Drop sms, query portion + int queryStart = rawText.indexOf('?', 4); + String smsURIWithoutQuery; + // If it's not query syntax, the question mark is part of the subject or message + if (queryStart < 0 || !querySyntax) { + smsURIWithoutQuery = rawText.substring(4); + } else { + smsURIWithoutQuery = rawText.substring(4, queryStart); + } + + int lastComma = -1; + int comma; + List numbers = new ArrayList<>(1); + List vias = new ArrayList<>(1); + while ((comma = smsURIWithoutQuery.indexOf(',', lastComma + 1)) > lastComma) { + String numberPart = smsURIWithoutQuery.substring(lastComma + 1, comma); + addNumberVia(numbers, vias, numberPart); + lastComma = comma; + } + addNumberVia(numbers, vias, smsURIWithoutQuery.substring(lastComma + 1)); + + return new SMSParsedResult(numbers.toArray(new String[numbers.size()]), + vias.toArray(new String[vias.size()]), + subject, + body); + } + + private static void addNumberVia(Collection numbers, + Collection vias, + String numberPart) { + int numberEnd = numberPart.indexOf(';'); + if (numberEnd < 0) { + numbers.add(numberPart); + vias.add(null); + } else { + numbers.add(numberPart.substring(0, numberEnd)); + String maybeVia = numberPart.substring(numberEnd + 1); + String via; + if (maybeVia.startsWith("via=")) { + via = maybeVia.substring(4); + } else { + via = null; + } + vias.add(via); + } + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/SMSParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/SMSParsedResult.java new file mode 100644 index 0000000..176078e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/SMSParsedResult.java @@ -0,0 +1,114 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes an SMS message, including recipients, subject + * and body text. + * + * @author Sean Owen + */ +public final class SMSParsedResult extends ParsedResult { + + private final String[] numbers; + private final String[] vias; + private final String subject; + private final String body; + + public SMSParsedResult(String number, + String via, + String subject, + String body) { + super(ParsedResultType.SMS); + this.numbers = new String[] {number}; + this.vias = new String[] {via}; + this.subject = subject; + this.body = body; + } + + public SMSParsedResult(String[] numbers, + String[] vias, + String subject, + String body) { + super(ParsedResultType.SMS); + this.numbers = numbers; + this.vias = vias; + this.subject = subject; + this.body = body; + } + + public String getSMSURI() { + StringBuilder result = new StringBuilder(); + result.append("sms:"); + boolean first = true; + for (int i = 0; i < numbers.length; i++) { + if (first) { + first = false; + } else { + result.append(','); + } + result.append(numbers[i]); + if (vias != null && vias[i] != null) { + result.append(";via="); + result.append(vias[i]); + } + } + boolean hasBody = body != null; + boolean hasSubject = subject != null; + if (hasBody || hasSubject) { + result.append('?'); + if (hasBody) { + result.append("body="); + result.append(body); + } + if (hasSubject) { + if (hasBody) { + result.append('&'); + } + result.append("subject="); + result.append(subject); + } + } + return result.toString(); + } + + public String[] getNumbers() { + return numbers; + } + + public String[] getVias() { + return vias; + } + + public String getSubject() { + return subject; + } + + public String getBody() { + return body; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(100); + maybeAppend(numbers, result); + maybeAppend(subject, result); + maybeAppend(body, result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/SMSTOMMSTOResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/SMSTOMMSTOResultParser.java new file mode 100644 index 0000000..0760565 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/SMSTOMMSTOResultParser.java @@ -0,0 +1,52 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + *

Parses an "smsto:" URI result, whose format is not standardized but appears to be like: + * {@code smsto:number(:body)}.

+ * + *

This actually also parses URIs starting with "smsto:", "mmsto:", "SMSTO:", and + * "MMSTO:", and treats them all the same way, and effectively converts them to an "sms:" URI + * for purposes of forwarding to the platform.

+ * + * @author Sean Owen + */ +public final class SMSTOMMSTOResultParser extends ResultParser { + + @Override + public SMSParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!(rawText.startsWith("smsto:") || rawText.startsWith("SMSTO:") || + rawText.startsWith("mmsto:") || rawText.startsWith("MMSTO:"))) { + return null; + } + // Thanks to dominik.wild for suggesting this enhancement to support + // smsto:number:body URIs + String number = rawText.substring(6); + String body = null; + int bodyStart = number.indexOf(':'); + if (bodyStart >= 0) { + body = number.substring(bodyStart + 1); + number = number.substring(0, bodyStart); + } + return new SMSParsedResult(number, null, null, body); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/SMTPResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/SMTPResultParser.java new file mode 100644 index 0000000..3279ceb --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/SMTPResultParser.java @@ -0,0 +1,54 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + *

Parses an "smtp:" URI result, whose format is not standardized but appears to be like: + * {@code smtp[:subject[:body]]}.

+ * + * @author Sean Owen + */ +public final class SMTPResultParser extends ResultParser { + + @Override + public EmailAddressParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!(rawText.startsWith("smtp:") || rawText.startsWith("SMTP:"))) { + return null; + } + String emailAddress = rawText.substring(5); + String subject = null; + String body = null; + int colon = emailAddress.indexOf(':'); + if (colon >= 0) { + subject = emailAddress.substring(colon + 1); + emailAddress = emailAddress.substring(0, colon); + colon = subject.indexOf(':'); + if (colon >= 0) { + body = subject.substring(colon + 1); + subject = subject.substring(0, colon); + } + } + return new EmailAddressParsedResult(new String[] {emailAddress}, + null, + null, + subject, + body); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/TelParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/TelParsedResult.java new file mode 100644 index 0000000..fb606f2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/TelParsedResult.java @@ -0,0 +1,57 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes a telephone number. + * + * @author Sean Owen + */ +public final class TelParsedResult extends ParsedResult { + + private final String number; + private final String telURI; + private final String title; + + public TelParsedResult(String number, String telURI, String title) { + super(ParsedResultType.TEL); + this.number = number; + this.telURI = telURI; + this.title = title; + } + + public String getNumber() { + return number; + } + + public String getTelURI() { + return telURI; + } + + public String getTitle() { + return title; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(20); + maybeAppend(number, result); + maybeAppend(title, result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/TelResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/TelResultParser.java new file mode 100644 index 0000000..e4bca1f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/TelResultParser.java @@ -0,0 +1,42 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + * Parses a "tel:" URI result, which specifies a phone number. + * + * @author Sean Owen + */ +public final class TelResultParser extends ResultParser { + + @Override + public TelParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("tel:") && !rawText.startsWith("TEL:")) { + return null; + } + // Normalize "TEL:" to "tel:" + String telURI = rawText.startsWith("TEL:") ? "tel:" + rawText.substring(4) : rawText; + // Drop tel, query portion + int queryStart = rawText.indexOf('?', 4); + String number = queryStart < 0 ? rawText.substring(4) : rawText.substring(4, queryStart); + return new TelParsedResult(number, telURI, null); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/TextParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/TextParsedResult.java new file mode 100644 index 0000000..9cc408e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/TextParsedResult.java @@ -0,0 +1,49 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * A simple result type encapsulating a string that has no further + * interpretation. + * + * @author Sean Owen + */ +public final class TextParsedResult extends ParsedResult { + + private final String text; + private final String language; + + public TextParsedResult(String text, String language) { + super(ParsedResultType.TEXT); + this.text = text; + this.language = language; + } + + public String getText() { + return text; + } + + public String getLanguage() { + return language; + } + + @Override + public String getDisplayResult() { + return text; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/URIParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/URIParsedResult.java new file mode 100644 index 0000000..e26e361 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/URIParsedResult.java @@ -0,0 +1,92 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import java.util.regex.Pattern; + +/** + * A simple result type encapsulating a URI that has no further interpretation. + * + * @author Sean Owen + */ +public final class URIParsedResult extends ParsedResult { + + private static final Pattern USER_IN_HOST = Pattern.compile(":/*([^/@]+)@[^/]+"); + + private final String uri; + private final String title; + + public URIParsedResult(String uri, String title) { + super(ParsedResultType.URI); + this.uri = massageURI(uri); + this.title = title; + } + + public String getURI() { + return uri; + } + + public String getTitle() { + return title; + } + + /** + * @return true if the URI contains suspicious patterns that may suggest it intends to + * mislead the user about its true nature. At the moment this looks for the presence + * of user/password syntax in the host/authority portion of a URI which may be used + * in attempts to make the URI's host appear to be other than it is. Example: + * http://yourbank.com@phisher.com This URI connects to phisher.com but may appear + * to connect to yourbank.com at first glance. + */ + public boolean isPossiblyMaliciousURI() { + return USER_IN_HOST.matcher(uri).find(); + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(30); + maybeAppend(title, result); + maybeAppend(uri, result); + return result.toString(); + } + + /** + * Transforms a string that represents a URI into something more proper, by adding or canonicalizing + * the protocol. + */ + private static String massageURI(String uri) { + uri = uri.trim(); + int protocolEnd = uri.indexOf(':'); + if (protocolEnd < 0 || isColonFollowedByPortNumber(uri, protocolEnd)) { + // No protocol, or found a colon, but it looks like it is after the host, so the protocol is still missing, + // so assume http + uri = "http://" + uri; + } + return uri; + } + + private static boolean isColonFollowedByPortNumber(String uri, int protocolEnd) { + int start = protocolEnd + 1; + int nextSlash = uri.indexOf('/', start); + if (nextSlash < 0) { + nextSlash = uri.length(); + } + return ResultParser.isSubstringOfDigits(uri, start, nextSlash - start); + } + + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/URIResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/URIResultParser.java new file mode 100644 index 0000000..852f39c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/URIResultParser.java @@ -0,0 +1,63 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Tries to parse results that are a URI of some kind. + * + * @author Sean Owen + */ +public final class URIResultParser extends ResultParser { + + // See http://www.ietf.org/rfc/rfc2396.txt + private static final Pattern URL_WITH_PROTOCOL_PATTERN = Pattern.compile("[a-zA-Z][a-zA-Z0-9+-.]+:"); + private static final Pattern URL_WITHOUT_PROTOCOL_PATTERN = Pattern.compile( + "([a-zA-Z0-9\\-]+\\.){1,6}[a-zA-Z]{2,}" + // host name elements; allow up to say 6 domain elements + "(:\\d{1,5})?" + // maybe port + "(/|\\?|$)"); // query, path or nothing + + @Override + public URIParsedResult parse(Result result) { + String rawText = getMassagedText(result); + // We specifically handle the odd "URL" scheme here for simplicity and add "URI" for fun + // Assume anything starting this way really means to be a URI + if (rawText.startsWith("URL:") || rawText.startsWith("URI:")) { + return new URIParsedResult(rawText.substring(4).trim(), null); + } + rawText = rawText.trim(); + return isBasicallyValidURI(rawText) ? new URIParsedResult(rawText, null) : null; + } + + static boolean isBasicallyValidURI(String uri) { + if (uri.contains(" ")) { + // Quick hack check for a common case + return false; + } + Matcher m = URL_WITH_PROTOCOL_PATTERN.matcher(uri); + if (m.find() && m.start() == 0) { // match at start only + return true; + } + m = URL_WITHOUT_PROTOCOL_PATTERN.matcher(uri); + return m.find() && m.start() == 0; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/URLTOResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/URLTOResultParser.java new file mode 100644 index 0000000..fd25900 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/URLTOResultParser.java @@ -0,0 +1,45 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + * Parses the "URLTO" result format, which is of the form "URLTO:[title]:[url]". + * This seems to be used sometimes, but I am not able to find documentation + * on its origin or official format? + * + * @author Sean Owen + */ +public final class URLTOResultParser extends ResultParser { + + @Override + public URIParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("urlto:") && !rawText.startsWith("URLTO:")) { + return null; + } + int titleEnd = rawText.indexOf(':', 6); + if (titleEnd < 0) { + return null; + } + String title = titleEnd <= 6 ? null : rawText.substring(6, titleEnd); + String uri = rawText.substring(titleEnd + 1); + return new URIParsedResult(uri, title); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/VCardResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/VCardResultParser.java new file mode 100644 index 0000000..e2c47ab --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/VCardResultParser.java @@ -0,0 +1,360 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.io.ByteArrayOutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Parses contact information formatted according to the VCard (2.1) format. This is not a complete + * implementation but should parse information as commonly encoded in 2D barcodes. + * + * @author Sean Owen + */ +public final class VCardResultParser extends ResultParser { + + private static final Pattern BEGIN_VCARD = Pattern.compile("BEGIN:VCARD", Pattern.CASE_INSENSITIVE); + private static final Pattern VCARD_LIKE_DATE = Pattern.compile("\\d{4}-?\\d{2}-?\\d{2}"); + private static final Pattern CR_LF_SPACE_TAB = Pattern.compile("\r\n[ \t]"); + private static final Pattern NEWLINE_ESCAPE = Pattern.compile("\\\\[nN]"); + private static final Pattern VCARD_ESCAPES = Pattern.compile("\\\\([,;\\\\])"); + private static final Pattern EQUALS = Pattern.compile("="); + private static final Pattern SEMICOLON = Pattern.compile(";"); + private static final Pattern UNESCAPED_SEMICOLONS = Pattern.compile("(?> names = matchVCardPrefixedField("FN", rawText, true, false); + if (names == null) { + // If no display names found, look for regular name fields and format them + names = matchVCardPrefixedField("N", rawText, true, false); + formatNames(names); + } + List nicknameString = matchSingleVCardPrefixedField("NICKNAME", rawText, true, false); + String[] nicknames = nicknameString == null ? null : COMMA.split(nicknameString.get(0)); + List> phoneNumbers = matchVCardPrefixedField("TEL", rawText, true, false); + List> emails = matchVCardPrefixedField("EMAIL", rawText, true, false); + List note = matchSingleVCardPrefixedField("NOTE", rawText, false, false); + List> addresses = matchVCardPrefixedField("ADR", rawText, true, true); + List org = matchSingleVCardPrefixedField("ORG", rawText, true, true); + List birthday = matchSingleVCardPrefixedField("BDAY", rawText, true, false); + if (birthday != null && !isLikeVCardDate(birthday.get(0))) { + birthday = null; + } + List title = matchSingleVCardPrefixedField("TITLE", rawText, true, false); + List> urls = matchVCardPrefixedField("URL", rawText, true, false); + List instantMessenger = matchSingleVCardPrefixedField("IMPP", rawText, true, false); + List geoString = matchSingleVCardPrefixedField("GEO", rawText, true, false); + String[] geo = geoString == null ? null : SEMICOLON_OR_COMMA.split(geoString.get(0)); + if (geo != null && geo.length != 2) { + geo = null; + } + return new AddressBookParsedResult(toPrimaryValues(names), + nicknames, + null, + toPrimaryValues(phoneNumbers), + toTypes(phoneNumbers), + toPrimaryValues(emails), + toTypes(emails), + toPrimaryValue(instantMessenger), + toPrimaryValue(note), + toPrimaryValues(addresses), + toTypes(addresses), + toPrimaryValue(org), + toPrimaryValue(birthday), + toPrimaryValue(title), + toPrimaryValues(urls), + geo); + } + + static List> matchVCardPrefixedField(CharSequence prefix, + String rawText, + boolean trim, + boolean parseFieldDivider) { + List> matches = null; + int i = 0; + int max = rawText.length(); + + while (i < max) { + + // At start or after newline, match prefix, followed by optional metadata + // (led by ;) ultimately ending in colon + Matcher matcher = Pattern.compile("(?:^|\n)" + prefix + "(?:;([^:]*))?:", + Pattern.CASE_INSENSITIVE).matcher(rawText); + if (i > 0) { + i--; // Find from i-1 not i since looking at the preceding character + } + if (!matcher.find(i)) { + break; + } + i = matcher.end(0); // group 0 = whole pattern; end(0) is past final colon + + String metadataString = matcher.group(1); // group 1 = metadata substring + List metadata = null; + boolean quotedPrintable = false; + String quotedPrintableCharset = null; + if (metadataString != null) { + for (String metadatum : SEMICOLON.split(metadataString)) { + if (metadata == null) { + metadata = new ArrayList<>(1); + } + metadata.add(metadatum); + String[] metadatumTokens = EQUALS.split(metadatum, 2); + if (metadatumTokens.length > 1) { + String key = metadatumTokens[0]; + String value = metadatumTokens[1]; + if ("ENCODING".equalsIgnoreCase(key) && "QUOTED-PRINTABLE".equalsIgnoreCase(value)) { + quotedPrintable = true; + } else if ("CHARSET".equalsIgnoreCase(key)) { + quotedPrintableCharset = value; + } + } + } + } + + int matchStart = i; // Found the start of a match here + + while ((i = rawText.indexOf('\n', i)) >= 0) { // Really, end in \r\n + if (i < rawText.length() - 1 && // But if followed by tab or space, + (rawText.charAt(i + 1) == ' ' || // this is only a continuation + rawText.charAt(i + 1) == '\t')) { + i += 2; // Skip \n and continutation whitespace + } else if (quotedPrintable && // If preceded by = in quoted printable + ((i >= 1 && rawText.charAt(i - 1) == '=') || // this is a continuation + (i >= 2 && rawText.charAt(i - 2) == '='))) { + i++; // Skip \n + } else { + break; + } + } + + if (i < 0) { + // No terminating end character? uh, done. Set i such that loop terminates and break + i = max; + } else if (i > matchStart) { + // found a match + if (matches == null) { + matches = new ArrayList<>(1); // lazy init + } + if (i >= 1 && rawText.charAt(i - 1) == '\r') { + i--; // Back up over \r, which really should be there + } + String element = rawText.substring(matchStart, i); + if (trim) { + element = element.trim(); + } + if (quotedPrintable) { + element = decodeQuotedPrintable(element, quotedPrintableCharset); + if (parseFieldDivider) { + element = UNESCAPED_SEMICOLONS.matcher(element).replaceAll("\n").trim(); + } + } else { + if (parseFieldDivider) { + element = UNESCAPED_SEMICOLONS.matcher(element).replaceAll("\n").trim(); + } + element = CR_LF_SPACE_TAB.matcher(element).replaceAll(""); + element = NEWLINE_ESCAPE.matcher(element).replaceAll("\n"); + element = VCARD_ESCAPES.matcher(element).replaceAll("$1"); + } + if (metadata == null) { + List match = new ArrayList<>(1); + match.add(element); + matches.add(match); + } else { + metadata.add(0, element); + matches.add(metadata); + } + i++; + } else { + i++; + } + + } + + return matches; + } + + private static String decodeQuotedPrintable(CharSequence value, String charset) { + int length = value.length(); + StringBuilder result = new StringBuilder(length); + ByteArrayOutputStream fragmentBuffer = new ByteArrayOutputStream(); + for (int i = 0; i < length; i++) { + char c = value.charAt(i); + switch (c) { + case '\r': + case '\n': + break; + case '=': + if (i < length - 2) { + char nextChar = value.charAt(i + 1); + if (nextChar != '\r' && nextChar != '\n') { + char nextNextChar = value.charAt(i + 2); + int firstDigit = parseHexDigit(nextChar); + int secondDigit = parseHexDigit(nextNextChar); + if (firstDigit >= 0 && secondDigit >= 0) { + fragmentBuffer.write((firstDigit << 4) + secondDigit); + } // else ignore it, assume it was incorrectly encoded + i += 2; + } + } + break; + default: + maybeAppendFragment(fragmentBuffer, charset, result); + result.append(c); + } + } + maybeAppendFragment(fragmentBuffer, charset, result); + return result.toString(); + } + + private static void maybeAppendFragment(ByteArrayOutputStream fragmentBuffer, + String charset, + StringBuilder result) { + if (fragmentBuffer.size() > 0) { + byte[] fragmentBytes = fragmentBuffer.toByteArray(); + String fragment; + if (charset == null) { + fragment = new String(fragmentBytes, Charset.forName("UTF-8")); + } else { + try { + fragment = new String(fragmentBytes, charset); + } catch (UnsupportedEncodingException e) { + fragment = new String(fragmentBytes, Charset.forName("UTF-8")); + } + } + fragmentBuffer.reset(); + result.append(fragment); + } + } + + static List matchSingleVCardPrefixedField(CharSequence prefix, + String rawText, + boolean trim, + boolean parseFieldDivider) { + List> values = matchVCardPrefixedField(prefix, rawText, trim, parseFieldDivider); + return values == null || values.isEmpty() ? null : values.get(0); + } + + private static String toPrimaryValue(List list) { + return list == null || list.isEmpty() ? null : list.get(0); + } + + private static String[] toPrimaryValues(Collection> lists) { + if (lists == null || lists.isEmpty()) { + return null; + } + List result = new ArrayList<>(lists.size()); + for (List list : lists) { + String value = list.get(0); + if (value != null && !value.isEmpty()) { + result.add(value); + } + } + return result.toArray(new String[result.size()]); + } + + private static String[] toTypes(Collection> lists) { + if (lists == null || lists.isEmpty()) { + return null; + } + List result = new ArrayList<>(lists.size()); + for (List list : lists) { + String value = list.get(0); + if (value != null && !value.isEmpty()) { + String type = null; + for (int i = 1; i < list.size(); i++) { + String metadatum = list.get(i); + int equals = metadatum.indexOf('='); + if (equals < 0) { + // take the whole thing as a usable label + type = metadatum; + break; + } + if ("TYPE".equalsIgnoreCase(metadatum.substring(0, equals))) { + type = metadatum.substring(equals + 1); + break; + } + } + result.add(type); + } + } + return result.toArray(new String[result.size()]); + } + + private static boolean isLikeVCardDate(CharSequence value) { + return value == null || VCARD_LIKE_DATE.matcher(value).matches(); + } + + /** + * Formats name fields of the form "Public;John;Q.;Reverend;III" into a form like + * "Reverend John Q. Public III". + * + * @param names name values to format, in place + */ + private static void formatNames(Iterable> names) { + if (names != null) { + for (List list : names) { + String name = list.get(0); + String[] components = new String[5]; + int start = 0; + int end; + int componentIndex = 0; + while (componentIndex < components.length - 1 && (end = name.indexOf(';', start)) >= 0) { + components[componentIndex] = name.substring(start, end); + componentIndex++; + start = end + 1; + } + components[componentIndex] = name.substring(start); + StringBuilder newName = new StringBuilder(100); + maybeAppendComponent(components, 3, newName); + maybeAppendComponent(components, 1, newName); + maybeAppendComponent(components, 2, newName); + maybeAppendComponent(components, 0, newName); + maybeAppendComponent(components, 4, newName); + list.set(0, newName.toString().trim()); + } + } + } + + private static void maybeAppendComponent(String[] components, int i, StringBuilder newName) { + if (components[i] != null && !components[i].isEmpty()) { + if (newName.length() > 0) { + newName.append(' '); + } + newName.append(components[i]); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/VEventResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/VEventResultParser.java new file mode 100644 index 0000000..f6b2f39 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/VEventResultParser.java @@ -0,0 +1,119 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +import java.util.List; + +/** + * Partially implements the iCalendar format's "VEVENT" format for specifying a + * calendar event. See RFC 2445. This supports SUMMARY, LOCATION, GEO, DTSTART and DTEND fields. + * + * @author Sean Owen + */ +public final class VEventResultParser extends ResultParser { + + @Override + public CalendarParsedResult parse(Result result) { + String rawText = getMassagedText(result); + int vEventStart = rawText.indexOf("BEGIN:VEVENT"); + if (vEventStart < 0) { + return null; + } + + String summary = matchSingleVCardPrefixedField("SUMMARY", rawText, true); + String start = matchSingleVCardPrefixedField("DTSTART", rawText, true); + if (start == null) { + return null; + } + String end = matchSingleVCardPrefixedField("DTEND", rawText, true); + String duration = matchSingleVCardPrefixedField("DURATION", rawText, true); + String location = matchSingleVCardPrefixedField("LOCATION", rawText, true); + String organizer = stripMailto(matchSingleVCardPrefixedField("ORGANIZER", rawText, true)); + + String[] attendees = matchVCardPrefixedField("ATTENDEE", rawText, true); + if (attendees != null) { + for (int i = 0; i < attendees.length; i++) { + attendees[i] = stripMailto(attendees[i]); + } + } + String description = matchSingleVCardPrefixedField("DESCRIPTION", rawText, true); + + String geoString = matchSingleVCardPrefixedField("GEO", rawText, true); + double latitude; + double longitude; + if (geoString == null) { + latitude = Double.NaN; + longitude = Double.NaN; + } else { + int semicolon = geoString.indexOf(';'); + if (semicolon < 0) { + return null; + } + try { + latitude = Double.parseDouble(geoString.substring(0, semicolon)); + longitude = Double.parseDouble(geoString.substring(semicolon + 1)); + } catch (NumberFormatException ignored) { + return null; + } + } + + try { + return new CalendarParsedResult(summary, + start, + end, + duration, + location, + organizer, + attendees, + description, + latitude, + longitude); + } catch (IllegalArgumentException ignored) { + return null; + } + } + + private static String matchSingleVCardPrefixedField(CharSequence prefix, + String rawText, + boolean trim) { + List values = VCardResultParser.matchSingleVCardPrefixedField(prefix, rawText, trim, false); + return values == null || values.isEmpty() ? null : values.get(0); + } + + private static String[] matchVCardPrefixedField(CharSequence prefix, String rawText, boolean trim) { + List> values = VCardResultParser.matchVCardPrefixedField(prefix, rawText, trim, false); + if (values == null || values.isEmpty()) { + return null; + } + int size = values.size(); + String[] result = new String[size]; + for (int i = 0; i < size; i++) { + result[i] = values.get(i).get(0); + } + return result; + } + + private static String stripMailto(String s) { + if (s != null && (s.startsWith("mailto:") || s.startsWith("MAILTO:"))) { + s = s.substring(7); + } + return s; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/VINParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/VINParsedResult.java new file mode 100644 index 0000000..79f722d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/VINParsedResult.java @@ -0,0 +1,106 @@ +/* + * Copyright 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes a Vehicle Identification Number (VIN). + */ +public final class VINParsedResult extends ParsedResult { + + private final String vin; + private final String worldManufacturerID; + private final String vehicleDescriptorSection; + private final String vehicleIdentifierSection; + private final String countryCode; + private final String vehicleAttributes; + private final int modelYear; + private final char plantCode; + private final String sequentialNumber; + + public VINParsedResult(String vin, + String worldManufacturerID, + String vehicleDescriptorSection, + String vehicleIdentifierSection, + String countryCode, + String vehicleAttributes, + int modelYear, + char plantCode, + String sequentialNumber) { + super(ParsedResultType.VIN); + this.vin = vin; + this.worldManufacturerID = worldManufacturerID; + this.vehicleDescriptorSection = vehicleDescriptorSection; + this.vehicleIdentifierSection = vehicleIdentifierSection; + this.countryCode = countryCode; + this.vehicleAttributes = vehicleAttributes; + this.modelYear = modelYear; + this.plantCode = plantCode; + this.sequentialNumber = sequentialNumber; + } + + public String getVIN() { + return vin; + } + + public String getWorldManufacturerID() { + return worldManufacturerID; + } + + public String getVehicleDescriptorSection() { + return vehicleDescriptorSection; + } + + public String getVehicleIdentifierSection() { + return vehicleIdentifierSection; + } + + public String getCountryCode() { + return countryCode; + } + + public String getVehicleAttributes() { + return vehicleAttributes; + } + + public int getModelYear() { + return modelYear; + } + + public char getPlantCode() { + return plantCode; + } + + public String getSequentialNumber() { + return sequentialNumber; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(50); + result.append(worldManufacturerID).append(' '); + result.append(vehicleDescriptorSection).append(' '); + result.append(vehicleIdentifierSection).append('\n'); + if (countryCode != null) { + result.append(countryCode).append(' '); + } + result.append(modelYear).append(' '); + result.append(plantCode).append(' '); + result.append(sequentialNumber).append('\n'); + return result.toString(); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/VINResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/VINResultParser.java new file mode 100644 index 0000000..0d1314c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/VINResultParser.java @@ -0,0 +1,209 @@ +/* + * Copyright 2014 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.Result; + +import java.util.regex.Pattern; + +/** + * Detects a result that is likely a vehicle identification number. + * + * @author Sean Owen + */ +public final class VINResultParser extends ResultParser { + + private static final Pattern IOQ = Pattern.compile("[IOQ]"); + private static final Pattern AZ09 = Pattern.compile("[A-Z0-9]{17}"); + + @Override + public VINParsedResult parse(Result result) { + if (result.getBarcodeFormat() != BarcodeFormat.CODE_39) { + return null; + } + String rawText = result.getText(); + rawText = IOQ.matcher(rawText).replaceAll("").trim(); + if (!AZ09.matcher(rawText).matches()) { + return null; + } + try { + if (!checkChecksum(rawText)) { + return null; + } + String wmi = rawText.substring(0, 3); + return new VINParsedResult(rawText, + wmi, + rawText.substring(3, 9), + rawText.substring(9, 17), + countryCode(wmi), + rawText.substring(3, 8), + modelYear(rawText.charAt(9)), + rawText.charAt(10), + rawText.substring(11)); + } catch (IllegalArgumentException iae) { + return null; + } + } + + private static boolean checkChecksum(CharSequence vin) { + int sum = 0; + for (int i = 0; i < vin.length(); i++) { + sum += vinPositionWeight(i + 1) * vinCharValue(vin.charAt(i)); + } + char checkChar = vin.charAt(8); + char expectedCheckChar = checkChar(sum % 11); + return checkChar == expectedCheckChar; + } + + private static int vinCharValue(char c) { + if (c >= 'A' && c <= 'I') { + return (c - 'A') + 1; + } + if (c >= 'J' && c <= 'R') { + return (c - 'J') + 1; + } + if (c >= 'S' && c <= 'Z') { + return (c - 'S') + 2; + } + if (c >= '0' && c <= '9') { + return c - '0'; + } + throw new IllegalArgumentException(); + } + + private static int vinPositionWeight(int position) { + if (position >= 1 && position <= 7) { + return 9 - position; + } + if (position == 8) { + return 10; + } + if (position == 9) { + return 0; + } + if (position >= 10 && position <= 17) { + return 19 - position; + } + throw new IllegalArgumentException(); + } + + private static char checkChar(int remainder) { + if (remainder < 10) { + return (char) ('0' + remainder); + } + if (remainder == 10) { + return 'X'; + } + throw new IllegalArgumentException(); + } + + private static int modelYear(char c) { + if (c >= 'E' && c <= 'H') { + return (c - 'E') + 1984; + } + if (c >= 'J' && c <= 'N') { + return (c - 'J') + 1988; + } + if (c == 'P') { + return 1993; + } + if (c >= 'R' && c <= 'T') { + return (c - 'R') + 1994; + } + if (c >= 'V' && c <= 'Y') { + return (c - 'V') + 1997; + } + if (c >= '1' && c <= '9') { + return (c - '1') + 2001; + } + if (c >= 'A' && c <= 'D') { + return (c - 'A') + 2010; + } + throw new IllegalArgumentException(); + } + + private static String countryCode(CharSequence wmi) { + char c1 = wmi.charAt(0); + char c2 = wmi.charAt(1); + switch (c1) { + case '1': + case '4': + case '5': + return "US"; + case '2': + return "CA"; + case '3': + if (c2 >= 'A' && c2 <= 'W') { + return "MX"; + } + break; + case '9': + if ((c2 >= 'A' && c2 <= 'E') || (c2 >= '3' && c2 <= '9')) { + return "BR"; + } + break; + case 'J': + if (c2 >= 'A' && c2 <= 'T') { + return "JP"; + } + break; + case 'K': + if (c2 >= 'L' && c2 <= 'R') { + return "KO"; + } + break; + case 'L': + return "CN"; + case 'M': + if (c2 >= 'A' && c2 <= 'E') { + return "IN"; + } + break; + case 'S': + if (c2 >= 'A' && c2 <= 'M') { + return "UK"; + } + if (c2 >= 'N' && c2 <= 'T') { + return "DE"; + } + break; + case 'V': + if (c2 >= 'F' && c2 <= 'R') { + return "FR"; + } + if (c2 >= 'S' && c2 <= 'W') { + return "ES"; + } + break; + case 'W': + return "DE"; + case 'X': + if (c2 == '0' || (c2 >= '3' && c2 <= '9')) { + return "RU"; + } + break; + case 'Z': + if (c2 >= 'A' && c2 <= 'R') { + return "IT"; + } + break; + } + return null; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/client/result/WifiParsedResult.java b/rubylib/src/main/java/com/google/zxing/client/result/WifiParsedResult.java new file mode 100644 index 0000000..44c937a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/WifiParsedResult.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +/** + * Represents a parsed result that encodes wifi network information, like SSID and password. + * + * @author Vikram Aggarwal + */ +public final class WifiParsedResult extends ParsedResult { + + private final String ssid; + private final String networkEncryption; + private final String password; + private final boolean hidden; + + public WifiParsedResult(String networkEncryption, String ssid, String password) { + this(networkEncryption, ssid, password, false); + } + + public WifiParsedResult(String networkEncryption, String ssid, String password, boolean hidden) { + super(ParsedResultType.WIFI); + this.ssid = ssid; + this.networkEncryption = networkEncryption; + this.password = password; + this.hidden = hidden; + } + + public String getSsid() { + return ssid; + } + + public String getNetworkEncryption() { + return networkEncryption; + } + + public String getPassword() { + return password; + } + + public boolean isHidden() { + return hidden; + } + + @Override + public String getDisplayResult() { + StringBuilder result = new StringBuilder(80); + maybeAppend(ssid, result); + maybeAppend(networkEncryption, result); + maybeAppend(password, result); + maybeAppend(Boolean.toString(hidden), result); + return result.toString(); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/client/result/WifiResultParser.java b/rubylib/src/main/java/com/google/zxing/client/result/WifiResultParser.java new file mode 100644 index 0000000..b62151c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/client/result/WifiResultParser.java @@ -0,0 +1,51 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.client.result; + +import com.google.zxing.Result; + +/** + *

Parses a WIFI configuration string. Strings will be of the form:

+ * + *

{@code WIFI:T:[network type];S:[network SSID];P:[network password];H:[hidden?];;}

+ * + *

The fields can appear in any order. Only "S:" is required.

+ * + * @author Vikram Aggarwal + * @author Sean Owen + */ +public final class WifiResultParser extends ResultParser { + + @Override + public WifiParsedResult parse(Result result) { + String rawText = getMassagedText(result); + if (!rawText.startsWith("WIFI:")) { + return null; + } + String ssid = matchSinglePrefixedField("S:", rawText, ';', false); + if (ssid == null || ssid.isEmpty()) { + return null; + } + String pass = matchSinglePrefixedField("P:", rawText, ';', false); + String type = matchSinglePrefixedField("T:", rawText, ';', false); + if (type == null) { + type = "nopass"; + } + boolean hidden = Boolean.parseBoolean(matchSinglePrefixedField("H:", rawText, ';', false)); + return new WifiParsedResult(type, ssid, pass, hidden); + } +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/common/BitArray.java b/rubylib/src/main/java/com/google/zxing/common/BitArray.java new file mode 100644 index 0000000..421f2df --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/BitArray.java @@ -0,0 +1,357 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import java.util.Arrays; + +/** + *

A simple, fast array of bits, represented compactly by an array of ints internally.

+ * + * @author Sean Owen + */ +public final class BitArray implements Cloneable { + + private int[] bits; + private int size; + + public BitArray() { + this.size = 0; + this.bits = new int[1]; + } + + public BitArray(int size) { + this.size = size; + this.bits = makeArray(size); + } + + // For testing only + BitArray(int[] bits, int size) { + this.bits = bits; + this.size = size; + } + + public int getSize() { + return size; + } + + public int getSizeInBytes() { + return (size + 7) / 8; + } + + private void ensureCapacity(int size) { + if (size > bits.length * 32) { + int[] newBits = makeArray(size); + System.arraycopy(bits, 0, newBits, 0, bits.length); + this.bits = newBits; + } + } + + /** + * @param i bit to get + * @return true iff bit i is set + */ + public boolean get(int i) { + return (bits[i / 32] & (1 << (i & 0x1F))) != 0; + } + + /** + * Sets bit i. + * + * @param i bit to set + */ + public void set(int i) { + bits[i / 32] |= 1 << (i & 0x1F); + } + + /** + * Flips bit i. + * + * @param i bit to set + */ + public void flip(int i) { + bits[i / 32] ^= 1 << (i & 0x1F); + } + + /** + * @param from first bit to check + * @return index of first bit that is set, starting from the given index, or size if none are set + * at or beyond this given index + * @see #getNextUnset(int) + */ + public int getNextSet(int from) { + if (from >= size) { + return size; + } + int bitsOffset = from / 32; + int currentBits = bits[bitsOffset]; + // mask off lesser bits first + currentBits &= ~((1 << (from & 0x1F)) - 1); + while (currentBits == 0) { + if (++bitsOffset == bits.length) { + return size; + } + currentBits = bits[bitsOffset]; + } + int result = (bitsOffset * 32) + Integer.numberOfTrailingZeros(currentBits); + return result > size ? size : result; + } + + /** + * @param from index to start looking for unset bit + * @return index of next unset bit, or {@code size} if none are unset until the end + * @see #getNextSet(int) + */ + public int getNextUnset(int from) { + if (from >= size) { + return size; + } + int bitsOffset = from / 32; + int currentBits = ~bits[bitsOffset]; + // mask off lesser bits first + currentBits &= ~((1 << (from & 0x1F)) - 1); + while (currentBits == 0) { + if (++bitsOffset == bits.length) { + return size; + } + currentBits = ~bits[bitsOffset]; + } + int result = (bitsOffset * 32) + Integer.numberOfTrailingZeros(currentBits); + return result > size ? size : result; + } + + /** + * Sets a block of 32 bits, starting at bit i. + * + * @param i first bit to set + * @param newBits the new value of the next 32 bits. Note again that the least-significant bit + * corresponds to bit i, the next-least-significant to i+1, and so on. + */ + public void setBulk(int i, int newBits) { + bits[i / 32] = newBits; + } + + /** + * Sets a range of bits. + * + * @param start start of range, inclusive. + * @param end end of range, exclusive + */ + public void setRange(int start, int end) { + if (end < start || start < 0 || end > size) { + throw new IllegalArgumentException(); + } + if (end == start) { + return; + } + end--; // will be easier to treat this as the last actually set bit -- inclusive + int firstInt = start / 32; + int lastInt = end / 32; + for (int i = firstInt; i <= lastInt; i++) { + int firstBit = i > firstInt ? 0 : start & 0x1F; + int lastBit = i < lastInt ? 31 : end & 0x1F; + // Ones from firstBit to lastBit, inclusive + int mask = (2 << lastBit) - (1 << firstBit); + bits[i] |= mask; + } + } + + /** + * Clears all bits (sets to false). + */ + public void clear() { + int max = bits.length; + for (int i = 0; i < max; i++) { + bits[i] = 0; + } + } + + /** + * Efficient method to check if a range of bits is set, or not set. + * + * @param start start of range, inclusive. + * @param end end of range, exclusive + * @param value if true, checks that bits in range are set, otherwise checks that they are not set + * @return true iff all bits are set or not set in range, according to value argument + * @throws IllegalArgumentException if end is less than start or the range is not contained in the array + */ + public boolean isRange(int start, int end, boolean value) { + if (end < start || start < 0 || end > size) { + throw new IllegalArgumentException(); + } + if (end == start) { + return true; // empty range matches + } + end--; // will be easier to treat this as the last actually set bit -- inclusive + int firstInt = start / 32; + int lastInt = end / 32; + for (int i = firstInt; i <= lastInt; i++) { + int firstBit = i > firstInt ? 0 : start & 0x1F; + int lastBit = i < lastInt ? 31 : end & 0x1F; + // Ones from firstBit to lastBit, inclusive + int mask = (2 << lastBit) - (1 << firstBit); + + // Return false if we're looking for 1s and the masked bits[i] isn't all 1s (that is, + // equals the mask, or we're looking for 0s and the masked portion is not all 0s + if ((bits[i] & mask) != (value ? mask : 0)) { + return false; + } + } + return true; + } + + public void appendBit(boolean bit) { + ensureCapacity(size + 1); + if (bit) { + bits[size / 32] |= 1 << (size & 0x1F); + } + size++; + } + + /** + * Appends the least-significant bits, from value, in order from most-significant to + * least-significant. For example, appending 6 bits from 0x000001E will append the bits + * 0, 1, 1, 1, 1, 0 in that order. + * + * @param value {@code int} containing bits to append + * @param numBits bits from value to append + */ + public void appendBits(int value, int numBits) { + if (numBits < 0 || numBits > 32) { + throw new IllegalArgumentException("Num bits must be between 0 and 32"); + } + ensureCapacity(size + numBits); + for (int numBitsLeft = numBits; numBitsLeft > 0; numBitsLeft--) { + appendBit(((value >> (numBitsLeft - 1)) & 0x01) == 1); + } + } + + public void appendBitArray(BitArray other) { + int otherSize = other.size; + ensureCapacity(size + otherSize); + for (int i = 0; i < otherSize; i++) { + appendBit(other.get(i)); + } + } + + public void xor(BitArray other) { + if (size != other.size) { + throw new IllegalArgumentException("Sizes don't match"); + } + for (int i = 0; i < bits.length; i++) { + // The last int could be incomplete (i.e. not have 32 bits in + // it) but there is no problem since 0 XOR 0 == 0. + bits[i] ^= other.bits[i]; + } + } + + /** + * + * @param bitOffset first bit to start writing + * @param array array to write into. Bytes are written most-significant byte first. This is the opposite + * of the internal representation, which is exposed by {@link #getBitArray()} + * @param offset position in array to start writing + * @param numBytes how many bytes to write + */ + public void toBytes(int bitOffset, byte[] array, int offset, int numBytes) { + for (int i = 0; i < numBytes; i++) { + int theByte = 0; + for (int j = 0; j < 8; j++) { + if (get(bitOffset)) { + theByte |= 1 << (7 - j); + } + bitOffset++; + } + array[offset + i] = (byte) theByte; + } + } + + /** + * @return underlying array of ints. The first element holds the first 32 bits, and the least + * significant bit is bit 0. + */ + public int[] getBitArray() { + return bits; + } + + /** + * Reverses all bits in the array. + */ + public void reverse() { + int[] newBits = new int[bits.length]; + // reverse all int's first + int len = (size - 1) / 32; + int oldBitsLen = len + 1; + for (int i = 0; i < oldBitsLen; i++) { + long x = bits[i]; + x = ((x >> 1) & 0x55555555L) | ((x & 0x55555555L) << 1); + x = ((x >> 2) & 0x33333333L) | ((x & 0x33333333L) << 2); + x = ((x >> 4) & 0x0f0f0f0fL) | ((x & 0x0f0f0f0fL) << 4); + x = ((x >> 8) & 0x00ff00ffL) | ((x & 0x00ff00ffL) << 8); + x = ((x >> 16) & 0x0000ffffL) | ((x & 0x0000ffffL) << 16); + newBits[len - i] = (int) x; + } + // now correct the int's if the bit size isn't a multiple of 32 + if (size != oldBitsLen * 32) { + int leftOffset = oldBitsLen * 32 - size; + int currentInt = newBits[0] >>> leftOffset; + for (int i = 1; i < oldBitsLen; i++) { + int nextInt = newBits[i]; + currentInt |= nextInt << (32 - leftOffset); + newBits[i - 1] = currentInt; + currentInt = nextInt >>> leftOffset; + } + newBits[oldBitsLen - 1] = currentInt; + } + bits = newBits; + } + + private static int[] makeArray(int size) { + return new int[(size + 31) / 32]; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BitArray)) { + return false; + } + BitArray other = (BitArray) o; + return size == other.size && Arrays.equals(bits, other.bits); + } + + @Override + public int hashCode() { + return 31 * size + Arrays.hashCode(bits); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(size); + for (int i = 0; i < size; i++) { + if ((i & 0x07) == 0) { + result.append(' '); + } + result.append(get(i) ? 'X' : '.'); + } + return result.toString(); + } + + @Override + public BitArray clone() { + return new BitArray(bits.clone(), size); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/BitMatrix.java b/rubylib/src/main/java/com/google/zxing/common/BitMatrix.java new file mode 100644 index 0000000..bd08efc --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/BitMatrix.java @@ -0,0 +1,481 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import java.util.Arrays; + +/** + *

Represents a 2D matrix of bits. In function arguments below, and throughout the common + * module, x is the column position, and y is the row position. The ordering is always x, y. + * The origin is at the top-left.

+ * + *

Internally the bits are represented in a 1-D array of 32-bit ints. However, each row begins + * with a new int. This is done intentionally so that we can copy out a row into a BitArray very + * efficiently.

+ * + *

The ordering of bits is row-major. Within each int, the least significant bits are used first, + * meaning they represent lower x values. This is compatible with BitArray's implementation.

+ * + * @author Sean Owen + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class BitMatrix implements Cloneable { + + private final int width; + private final int height; + private final int rowSize; + private final int[] bits; + + /** + * Creates an empty square {@link BitMatrix}. + * + * @param dimension height and width + */ + public BitMatrix(int dimension) { + this(dimension, dimension); + } + + /** + * Creates an empty {@link BitMatrix}. + * + * @param width bit matrix width + * @param height bit matrix height + */ + public BitMatrix(int width, int height) { + if (width < 1 || height < 1) { + throw new IllegalArgumentException("Both dimensions must be greater than 0"); + } + this.width = width; + this.height = height; + this.rowSize = (width + 31) / 32; + bits = new int[rowSize * height]; + } + + private BitMatrix(int width, int height, int rowSize, int[] bits) { + this.width = width; + this.height = height; + this.rowSize = rowSize; + this.bits = bits; + } + + /** + * Interprets a 2D array of booleans as a {@link BitMatrix}, where "true" means an "on" bit. + * + * @param image bits of the image, as a row-major 2D array. Elements are arrays representing rows + * @return {@link BitMatrix} representation of image + */ + public static BitMatrix parse(boolean[][] image) { + int height = image.length; + int width = image[0].length; + BitMatrix bits = new BitMatrix(width, height); + for (int i = 0; i < height; i++) { + boolean[] imageI = image[i]; + for (int j = 0; j < width; j++) { + if (imageI[j]) { + bits.set(j, i); + } + } + } + return bits; + } + + public static BitMatrix parse(String stringRepresentation, String setString, String unsetString) { + if (stringRepresentation == null) { + throw new IllegalArgumentException(); + } + + boolean[] bits = new boolean[stringRepresentation.length()]; + int bitsPos = 0; + int rowStartPos = 0; + int rowLength = -1; + int nRows = 0; + int pos = 0; + while (pos < stringRepresentation.length()) { + if (stringRepresentation.charAt(pos) == '\n' || + stringRepresentation.charAt(pos) == '\r') { + if (bitsPos > rowStartPos) { + if (rowLength == -1) { + rowLength = bitsPos - rowStartPos; + } else if (bitsPos - rowStartPos != rowLength) { + throw new IllegalArgumentException("row lengths do not match"); + } + rowStartPos = bitsPos; + nRows++; + } + pos++; + } else if (stringRepresentation.substring(pos, pos + setString.length()).equals(setString)) { + pos += setString.length(); + bits[bitsPos] = true; + bitsPos++; + } else if (stringRepresentation.substring(pos, pos + unsetString.length()).equals(unsetString)) { + pos += unsetString.length(); + bits[bitsPos] = false; + bitsPos++; + } else { + throw new IllegalArgumentException( + "illegal character encountered: " + stringRepresentation.substring(pos)); + } + } + + // no EOL at end? + if (bitsPos > rowStartPos) { + if (rowLength == -1) { + rowLength = bitsPos - rowStartPos; + } else if (bitsPos - rowStartPos != rowLength) { + throw new IllegalArgumentException("row lengths do not match"); + } + nRows++; + } + + BitMatrix matrix = new BitMatrix(rowLength, nRows); + for (int i = 0; i < bitsPos; i++) { + if (bits[i]) { + matrix.set(i % rowLength, i / rowLength); + } + } + return matrix; + } + + /** + *

Gets the requested bit, where true means black.

+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + * @return value of given bit in matrix + */ + public boolean get(int x, int y) { + int offset = y * rowSize + (x / 32); + return ((bits[offset] >>> (x & 0x1f)) & 1) != 0; + } + + /** + *

Sets the given bit to true.

+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + */ + public void set(int x, int y) { + int offset = y * rowSize + (x / 32); + bits[offset] |= 1 << (x & 0x1f); + } + + public void unset(int x, int y) { + int offset = y * rowSize + (x / 32); + bits[offset] &= ~(1 << (x & 0x1f)); + } + + /** + *

Flips the given bit.

+ * + * @param x The horizontal component (i.e. which column) + * @param y The vertical component (i.e. which row) + */ + public void flip(int x, int y) { + int offset = y * rowSize + (x / 32); + bits[offset] ^= 1 << (x & 0x1f); + } + + /** + * Exclusive-or (XOR): Flip the bit in this {@code BitMatrix} if the corresponding + * mask bit is set. + * + * @param mask XOR mask + */ + public void xor(BitMatrix mask) { + if (width != mask.getWidth() || height != mask.getHeight() + || rowSize != mask.getRowSize()) { + throw new IllegalArgumentException("input matrix dimensions do not match"); + } + BitArray rowArray = new BitArray(width / 32 + 1); + for (int y = 0; y < height; y++) { + int offset = y * rowSize; + int[] row = mask.getRow(y, rowArray).getBitArray(); + for (int x = 0; x < rowSize; x++) { + bits[offset + x] ^= row[x]; + } + } + } + + /** + * Clears all bits (sets to false). + */ + public void clear() { + int max = bits.length; + for (int i = 0; i < max; i++) { + bits[i] = 0; + } + } + + /** + *

Sets a square region of the bit matrix to true.

+ * + * @param left The horizontal position to begin at (inclusive) + * @param top The vertical position to begin at (inclusive) + * @param width The width of the region + * @param height The height of the region + */ + public void setRegion(int left, int top, int width, int height) { + if (top < 0 || left < 0) { + throw new IllegalArgumentException("Left and top must be nonnegative"); + } + if (height < 1 || width < 1) { + throw new IllegalArgumentException("Height and width must be at least 1"); + } + int right = left + width; + int bottom = top + height; + if (bottom > this.height || right > this.width) { + throw new IllegalArgumentException("The region must fit inside the matrix"); + } + for (int y = top; y < bottom; y++) { + int offset = y * rowSize; + for (int x = left; x < right; x++) { + bits[offset + (x / 32)] |= 1 << (x & 0x1f); + } + } + } + + /** + * A fast method to retrieve one row of data from the matrix as a BitArray. + * + * @param y The row to retrieve + * @param row An optional caller-allocated BitArray, will be allocated if null or too small + * @return The resulting BitArray - this reference should always be used even when passing + * your own row + */ + public BitArray getRow(int y, BitArray row) { + if (row == null || row.getSize() < width) { + row = new BitArray(width); + } else { + row.clear(); + } + int offset = y * rowSize; + for (int x = 0; x < rowSize; x++) { + row.setBulk(x * 32, bits[offset + x]); + } + return row; + } + + /** + * @param y row to set + * @param row {@link BitArray} to copy from + */ + public void setRow(int y, BitArray row) { + System.arraycopy(row.getBitArray(), 0, bits, y * rowSize, rowSize); + } + + /** + * Modifies this {@code BitMatrix} to represent the same but rotated 180 degrees + */ + public void rotate180() { + int width = getWidth(); + int height = getHeight(); + BitArray topRow = new BitArray(width); + BitArray bottomRow = new BitArray(width); + for (int i = 0; i < (height + 1) / 2; i++) { + topRow = getRow(i, topRow); + bottomRow = getRow(height - 1 - i, bottomRow); + topRow.reverse(); + bottomRow.reverse(); + setRow(i, bottomRow); + setRow(height - 1 - i, topRow); + } + } + + /** + * This is useful in detecting the enclosing rectangle of a 'pure' barcode. + * + * @return {@code left,top,width,height} enclosing rectangle of all 1 bits, or null if it is all white + */ + public int[] getEnclosingRectangle() { + int left = width; + int top = height; + int right = -1; + int bottom = -1; + + for (int y = 0; y < height; y++) { + for (int x32 = 0; x32 < rowSize; x32++) { + int theBits = bits[y * rowSize + x32]; + if (theBits != 0) { + if (y < top) { + top = y; + } + if (y > bottom) { + bottom = y; + } + if (x32 * 32 < left) { + int bit = 0; + while ((theBits << (31 - bit)) == 0) { + bit++; + } + if ((x32 * 32 + bit) < left) { + left = x32 * 32 + bit; + } + } + if (x32 * 32 + 31 > right) { + int bit = 31; + while ((theBits >>> bit) == 0) { + bit--; + } + if ((x32 * 32 + bit) > right) { + right = x32 * 32 + bit; + } + } + } + } + } + + if (right < left || bottom < top) { + return null; + } + + return new int[] {left, top, right - left + 1, bottom - top + 1}; + } + + /** + * This is useful in detecting a corner of a 'pure' barcode. + * + * @return {@code x,y} coordinate of top-left-most 1 bit, or null if it is all white + */ + public int[] getTopLeftOnBit() { + int bitsOffset = 0; + while (bitsOffset < bits.length && bits[bitsOffset] == 0) { + bitsOffset++; + } + if (bitsOffset == bits.length) { + return null; + } + int y = bitsOffset / rowSize; + int x = (bitsOffset % rowSize) * 32; + + int theBits = bits[bitsOffset]; + int bit = 0; + while ((theBits << (31 - bit)) == 0) { + bit++; + } + x += bit; + return new int[] {x, y}; + } + + public int[] getBottomRightOnBit() { + int bitsOffset = bits.length - 1; + while (bitsOffset >= 0 && bits[bitsOffset] == 0) { + bitsOffset--; + } + if (bitsOffset < 0) { + return null; + } + + int y = bitsOffset / rowSize; + int x = (bitsOffset % rowSize) * 32; + + int theBits = bits[bitsOffset]; + int bit = 31; + while ((theBits >>> bit) == 0) { + bit--; + } + x += bit; + + return new int[] {x, y}; + } + + /** + * @return The width of the matrix + */ + public int getWidth() { + return width; + } + + /** + * @return The height of the matrix + */ + public int getHeight() { + return height; + } + + /** + * @return The row size of the matrix + */ + public int getRowSize() { + return rowSize; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BitMatrix)) { + return false; + } + BitMatrix other = (BitMatrix) o; + return width == other.width && height == other.height && rowSize == other.rowSize && + Arrays.equals(bits, other.bits); + } + + @Override + public int hashCode() { + int hash = width; + hash = 31 * hash + width; + hash = 31 * hash + height; + hash = 31 * hash + rowSize; + hash = 31 * hash + Arrays.hashCode(bits); + return hash; + } + + /** + * @return string representation using "X" for set and " " for unset bits + */ + @Override + public String toString() { + return toString("X ", " "); + } + + /** + * @param setString representation of a set bit + * @param unsetString representation of an unset bit + * @return string representation of entire matrix utilizing given strings + */ + public String toString(String setString, String unsetString) { + return buildToString(setString, unsetString, "\n"); + } + + /** + * @param setString representation of a set bit + * @param unsetString representation of an unset bit + * @param lineSeparator newline character in string representation + * @return string representation of entire matrix utilizing given strings and line separator + * @deprecated call {@link #toString(String,String)} only, which uses \n line separator always + */ + @Deprecated + public String toString(String setString, String unsetString, String lineSeparator) { + return buildToString(setString, unsetString, lineSeparator); + } + + private String buildToString(String setString, String unsetString, String lineSeparator) { + StringBuilder result = new StringBuilder(height * (width + 1)); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + result.append(get(x, y) ? setString : unsetString); + } + result.append(lineSeparator); + } + return result.toString(); + } + + @Override + public BitMatrix clone() { + return new BitMatrix(width, height, rowSize, bits.clone()); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/BitSource.java b/rubylib/src/main/java/com/google/zxing/common/BitSource.java new file mode 100644 index 0000000..45a3858 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/BitSource.java @@ -0,0 +1,111 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +/** + *

This provides an easy abstraction to read bits at a time from a sequence of bytes, where the + * number of bits read is not often a multiple of 8.

+ * + *

This class is thread-safe but not reentrant -- unless the caller modifies the bytes array + * it passed in, in which case all bets are off.

+ * + * @author Sean Owen + */ +public final class BitSource { + + private final byte[] bytes; + private int byteOffset; + private int bitOffset; + + /** + * @param bytes bytes from which this will read bits. Bits will be read from the first byte first. + * Bits are read within a byte from most-significant to least-significant bit. + */ + public BitSource(byte[] bytes) { + this.bytes = bytes; + } + + /** + * @return index of next bit in current byte which would be read by the next call to {@link #readBits(int)}. + */ + public int getBitOffset() { + return bitOffset; + } + + /** + * @return index of next byte in input byte array which would be read by the next call to {@link #readBits(int)}. + */ + public int getByteOffset() { + return byteOffset; + } + + /** + * @param numBits number of bits to read + * @return int representing the bits read. The bits will appear as the least-significant + * bits of the int + * @throws IllegalArgumentException if numBits isn't in [1,32] or more than is available + */ + public int readBits(int numBits) { + if (numBits < 1 || numBits > 32 || numBits > available()) { + throw new IllegalArgumentException(String.valueOf(numBits)); + } + + int result = 0; + + // First, read remainder from current byte + if (bitOffset > 0) { + int bitsLeft = 8 - bitOffset; + int toRead = numBits < bitsLeft ? numBits : bitsLeft; + int bitsToNotRead = bitsLeft - toRead; + int mask = (0xFF >> (8 - toRead)) << bitsToNotRead; + result = (bytes[byteOffset] & mask) >> bitsToNotRead; + numBits -= toRead; + bitOffset += toRead; + if (bitOffset == 8) { + bitOffset = 0; + byteOffset++; + } + } + + // Next read whole bytes + if (numBits > 0) { + while (numBits >= 8) { + result = (result << 8) | (bytes[byteOffset] & 0xFF); + byteOffset++; + numBits -= 8; + } + + // Finally read a partial byte + if (numBits > 0) { + int bitsToNotRead = 8 - numBits; + int mask = (0xFF >> bitsToNotRead) << bitsToNotRead; + result = (result << numBits) | ((bytes[byteOffset] & mask) >> bitsToNotRead); + bitOffset += numBits; + } + } + + return result; + } + + /** + * @return number of bits that can be read successfully + */ + public int available() { + return 8 * (bytes.length - byteOffset) - bitOffset; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/CharacterSetECI.java b/rubylib/src/main/java/com/google/zxing/common/CharacterSetECI.java new file mode 100644 index 0000000..4be90c2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/CharacterSetECI.java @@ -0,0 +1,118 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.FormatException; + +import java.util.HashMap; +import java.util.Map; + +/** + * Encapsulates a Character Set ECI, according to "Extended Channel Interpretations" 5.3.1.1 + * of ISO 18004. + * + * @author Sean Owen + */ +public enum CharacterSetECI { + + // Enum name is a Java encoding valid for java.lang and java.io + Cp437(new int[]{0,2}), + ISO8859_1(new int[]{1,3}, "ISO-8859-1"), + ISO8859_2(4, "ISO-8859-2"), + ISO8859_3(5, "ISO-8859-3"), + ISO8859_4(6, "ISO-8859-4"), + ISO8859_5(7, "ISO-8859-5"), + ISO8859_6(8, "ISO-8859-6"), + ISO8859_7(9, "ISO-8859-7"), + ISO8859_8(10, "ISO-8859-8"), + ISO8859_9(11, "ISO-8859-9"), + ISO8859_10(12, "ISO-8859-10"), + ISO8859_11(13, "ISO-8859-11"), + ISO8859_13(15, "ISO-8859-13"), + ISO8859_14(16, "ISO-8859-14"), + ISO8859_15(17, "ISO-8859-15"), + ISO8859_16(18, "ISO-8859-16"), + SJIS(20, "Shift_JIS"), + Cp1250(21, "windows-1250"), + Cp1251(22, "windows-1251"), + Cp1252(23, "windows-1252"), + Cp1256(24, "windows-1256"), + UnicodeBigUnmarked(25, "UTF-16BE", "UnicodeBig"), + UTF8(26, "UTF-8"), + ASCII(new int[] {27, 170}, "US-ASCII"), + Big5(28), + GB18030(29, "GB2312", "EUC_CN", "GBK"), + EUC_KR(30, "EUC-KR"); + + private static final Map VALUE_TO_ECI = new HashMap<>(); + private static final Map NAME_TO_ECI = new HashMap<>(); + static { + for (CharacterSetECI eci : values()) { + for (int value : eci.values) { + VALUE_TO_ECI.put(value, eci); + } + NAME_TO_ECI.put(eci.name(), eci); + for (String name : eci.otherEncodingNames) { + NAME_TO_ECI.put(name, eci); + } + } + } + + private final int[] values; + private final String[] otherEncodingNames; + + CharacterSetECI(int value) { + this(new int[] {value}); + } + + CharacterSetECI(int value, String... otherEncodingNames) { + this.values = new int[] {value}; + this.otherEncodingNames = otherEncodingNames; + } + + CharacterSetECI(int[] values, String... otherEncodingNames) { + this.values = values; + this.otherEncodingNames = otherEncodingNames; + } + + public int getValue() { + return values[0]; + } + + /** + * @param value character set ECI value + * @return {@code CharacterSetECI} representing ECI of given value, or null if it is legal but + * unsupported + * @throws FormatException if ECI value is invalid + */ + public static CharacterSetECI getCharacterSetECIByValue(int value) throws FormatException { + if (value < 0 || value >= 900) { + throw FormatException.getFormatInstance(); + } + return VALUE_TO_ECI.get(value); + } + + /** + * @param name character set ECI encoding name + * @return CharacterSetECI representing ECI for character encoding, or null if it is legal + * but unsupported + */ + public static CharacterSetECI getCharacterSetECIByName(String name) { + return NAME_TO_ECI.get(name); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/DecoderResult.java b/rubylib/src/main/java/com/google/zxing/common/DecoderResult.java new file mode 100644 index 0000000..9a0d1b1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/DecoderResult.java @@ -0,0 +1,152 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import java.util.List; + +/** + *

Encapsulates the result of decoding a matrix of bits. This typically + * applies to 2D barcode formats. For now it contains the raw bytes obtained, + * as well as a String interpretation of those bytes, if applicable.

+ * + * @author Sean Owen + */ +public final class DecoderResult { + + private final byte[] rawBytes; + private int numBits; + private final String text; + private final List byteSegments; + private final String ecLevel; + private Integer errorsCorrected; + private Integer erasures; + private Object other; + private final int structuredAppendParity; + private final int structuredAppendSequenceNumber; + + public DecoderResult(byte[] rawBytes, + String text, + List byteSegments, + String ecLevel) { + this(rawBytes, text, byteSegments, ecLevel, -1, -1); + } + + public DecoderResult(byte[] rawBytes, + String text, + List byteSegments, + String ecLevel, + int saSequence, + int saParity) { + this.rawBytes = rawBytes; + this.numBits = rawBytes == null ? 0 : 8 * rawBytes.length; + this.text = text; + this.byteSegments = byteSegments; + this.ecLevel = ecLevel; + this.structuredAppendParity = saParity; + this.structuredAppendSequenceNumber = saSequence; + } + + /** + * @return raw bytes representing the result, or {@code null} if not applicable + */ + public byte[] getRawBytes() { + return rawBytes; + } + + /** + * @return how many bits of {@link #getRawBytes()} are valid; typically 8 times its length + * @since 3.3.0 + */ + public int getNumBits() { + return numBits; + } + + /** + * @param numBits overrides the number of bits that are valid in {@link #getRawBytes()} + * @since 3.3.0 + */ + public void setNumBits(int numBits) { + this.numBits = numBits; + } + + /** + * @return text representation of the result + */ + public String getText() { + return text; + } + + /** + * @return list of byte segments in the result, or {@code null} if not applicable + */ + public List getByteSegments() { + return byteSegments; + } + + /** + * @return name of error correction level used, or {@code null} if not applicable + */ + public String getECLevel() { + return ecLevel; + } + + /** + * @return number of errors corrected, or {@code null} if not applicable + */ + public Integer getErrorsCorrected() { + return errorsCorrected; + } + + public void setErrorsCorrected(Integer errorsCorrected) { + this.errorsCorrected = errorsCorrected; + } + + /** + * @return number of erasures corrected, or {@code null} if not applicable + */ + public Integer getErasures() { + return erasures; + } + + public void setErasures(Integer erasures) { + this.erasures = erasures; + } + + /** + * @return arbitrary additional metadata + */ + public Object getOther() { + return other; + } + + public void setOther(Object other) { + this.other = other; + } + + public boolean hasStructuredAppend() { + return structuredAppendParity >= 0 && structuredAppendSequenceNumber >= 0; + } + + public int getStructuredAppendParity() { + return structuredAppendParity; + } + + public int getStructuredAppendSequenceNumber() { + return structuredAppendSequenceNumber; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/DefaultGridSampler.java b/rubylib/src/main/java/com/google/zxing/common/DefaultGridSampler.java new file mode 100644 index 0000000..1248de0 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/DefaultGridSampler.java @@ -0,0 +1,88 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.NotFoundException; + +/** + * @author Sean Owen + */ +public final class DefaultGridSampler extends GridSampler { + + @Override + public BitMatrix sampleGrid(BitMatrix image, + int dimensionX, + int dimensionY, + float p1ToX, float p1ToY, + float p2ToX, float p2ToY, + float p3ToX, float p3ToY, + float p4ToX, float p4ToY, + float p1FromX, float p1FromY, + float p2FromX, float p2FromY, + float p3FromX, float p3FromY, + float p4FromX, float p4FromY) throws NotFoundException { + + PerspectiveTransform transform = PerspectiveTransform.quadrilateralToQuadrilateral( + p1ToX, p1ToY, p2ToX, p2ToY, p3ToX, p3ToY, p4ToX, p4ToY, + p1FromX, p1FromY, p2FromX, p2FromY, p3FromX, p3FromY, p4FromX, p4FromY); + + return sampleGrid(image, dimensionX, dimensionY, transform); + } + + @Override + public BitMatrix sampleGrid(BitMatrix image, + int dimensionX, + int dimensionY, + PerspectiveTransform transform) throws NotFoundException { + if (dimensionX <= 0 || dimensionY <= 0) { + throw NotFoundException.getNotFoundInstance(); + } + BitMatrix bits = new BitMatrix(dimensionX, dimensionY); + float[] points = new float[2 * dimensionX]; + for (int y = 0; y < dimensionY; y++) { + int max = points.length; + float iValue = y + 0.5f; + for (int x = 0; x < max; x += 2) { + points[x] = (float) (x / 2) + 0.5f; + points[x + 1] = iValue; + } + transform.transformPoints(points); + // Quick check to see if points transformed to something inside the image; + // sufficient to check the endpoints + checkAndNudgePoints(image, points); + try { + for (int x = 0; x < max; x += 2) { + if (image.get((int) points[x], (int) points[x + 1])) { + // Black(-ish) pixel + bits.set(x / 2, y); + } + } + } catch (ArrayIndexOutOfBoundsException aioobe) { + // This feels wrong, but, sometimes if the finder patterns are misidentified, the resulting + // transform gets "twisted" such that it maps a straight line of points to a set of points + // whose endpoints are in bounds, but others are not. There is probably some mathematical + // way to detect this about the transformation that I don't know yet. + // This results in an ugly runtime exception despite our clever checks above -- can't have + // that. We could check each point's coordinates but that feels duplicative. We settle for + // catching and wrapping ArrayIndexOutOfBoundsException. + throw NotFoundException.getNotFoundInstance(); + } + } + return bits; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/DetectorResult.java b/rubylib/src/main/java/com/google/zxing/common/DetectorResult.java new file mode 100644 index 0000000..0f3cf15 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/DetectorResult.java @@ -0,0 +1,46 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.ResultPoint; + +/** + *

Encapsulates the result of detecting a barcode in an image. This includes the raw + * matrix of black/white pixels corresponding to the barcode, and possibly points of interest + * in the image, like the location of finder patterns or corners of the barcode in the image.

+ * + * @author Sean Owen + */ +public class DetectorResult { + + private final BitMatrix bits; + private final ResultPoint[] points; + + public DetectorResult(BitMatrix bits, ResultPoint[] points) { + this.bits = bits; + this.points = points; + } + + public final BitMatrix getBits() { + return bits; + } + + public final ResultPoint[] getPoints() { + return points; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/common/GlobalHistogramBinarizer.java b/rubylib/src/main/java/com/google/zxing/common/GlobalHistogramBinarizer.java new file mode 100644 index 0000000..1856744 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/GlobalHistogramBinarizer.java @@ -0,0 +1,203 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.Binarizer; +import com.google.zxing.LuminanceSource; +import com.google.zxing.NotFoundException; + +/** + * This Binarizer implementation uses the old ZXing global histogram approach. It is suitable + * for low-end mobile devices which don't have enough CPU or memory to use a local thresholding + * algorithm. However, because it picks a global black point, it cannot handle difficult shadows + * and gradients. + * + * Faster mobile devices and all desktop applications should probably use HybridBinarizer instead. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public class GlobalHistogramBinarizer extends Binarizer { + + private static final int LUMINANCE_BITS = 5; + private static final int LUMINANCE_SHIFT = 8 - LUMINANCE_BITS; + private static final int LUMINANCE_BUCKETS = 1 << LUMINANCE_BITS; + private static final byte[] EMPTY = new byte[0]; + + private byte[] luminances; + private final int[] buckets; + + public GlobalHistogramBinarizer(LuminanceSource source) { + super(source); + luminances = EMPTY; + buckets = new int[LUMINANCE_BUCKETS]; + } + + // Applies simple sharpening to the row data to improve performance of the 1D Readers. + @Override + public BitArray getBlackRow(int y, BitArray row) throws NotFoundException { + LuminanceSource source = getLuminanceSource(); + int width = source.getWidth(); + if (row == null || row.getSize() < width) { + row = new BitArray(width); + } else { + row.clear(); + } + + initArrays(width); + byte[] localLuminances = source.getRow(y, luminances); + int[] localBuckets = buckets; + for (int x = 0; x < width; x++) { + localBuckets[(localLuminances[x] & 0xff) >> LUMINANCE_SHIFT]++; + } + int blackPoint = estimateBlackPoint(localBuckets); + + if (width < 3) { + // Special case for very small images + for (int x = 0; x < width; x++) { + if ((localLuminances[x] & 0xff) < blackPoint) { + row.set(x); + } + } + } else { + int left = localLuminances[0] & 0xff; + int center = localLuminances[1] & 0xff; + for (int x = 1; x < width - 1; x++) { + int right = localLuminances[x + 1] & 0xff; + // A simple -1 4 -1 box filter with a weight of 2. + if (((center * 4) - left - right) / 2 < blackPoint) { + row.set(x); + } + left = center; + center = right; + } + } + return row; + } + + // Does not sharpen the data, as this call is intended to only be used by 2D Readers. + @Override + public BitMatrix getBlackMatrix() throws NotFoundException { + LuminanceSource source = getLuminanceSource(); + int width = source.getWidth(); + int height = source.getHeight(); + BitMatrix matrix = new BitMatrix(width, height); + + // Quickly calculates the histogram by sampling four rows from the image. This proved to be + // more robust on the blackbox tests than sampling a diagonal as we used to do. + initArrays(width); + int[] localBuckets = buckets; + for (int y = 1; y < 5; y++) { + int row = height * y / 5; + byte[] localLuminances = source.getRow(row, luminances); + int right = (width * 4) / 5; + for (int x = width / 5; x < right; x++) { + int pixel = localLuminances[x] & 0xff; + localBuckets[pixel >> LUMINANCE_SHIFT]++; + } + } + int blackPoint = estimateBlackPoint(localBuckets); + + // We delay reading the entire image luminance until the black point estimation succeeds. + // Although we end up reading four rows twice, it is consistent with our motto of + // "fail quickly" which is necessary for continuous scanning. + byte[] localLuminances = source.getMatrix(); + for (int y = 0; y < height; y++) { + int offset = y * width; + for (int x = 0; x < width; x++) { + int pixel = localLuminances[offset + x] & 0xff; + if (pixel < blackPoint) { + matrix.set(x, y); + } + } + } + + return matrix; + } + + @Override + public Binarizer createBinarizer(LuminanceSource source) { + return new GlobalHistogramBinarizer(source); + } + + private void initArrays(int luminanceSize) { + if (luminances.length < luminanceSize) { + luminances = new byte[luminanceSize]; + } + for (int x = 0; x < LUMINANCE_BUCKETS; x++) { + buckets[x] = 0; + } + } + + private static int estimateBlackPoint(int[] buckets) throws NotFoundException { + // Find the tallest peak in the histogram. + int numBuckets = buckets.length; + int maxBucketCount = 0; + int firstPeak = 0; + int firstPeakSize = 0; + for (int x = 0; x < numBuckets; x++) { + if (buckets[x] > firstPeakSize) { + firstPeak = x; + firstPeakSize = buckets[x]; + } + if (buckets[x] > maxBucketCount) { + maxBucketCount = buckets[x]; + } + } + + // Find the second-tallest peak which is somewhat far from the tallest peak. + int secondPeak = 0; + int secondPeakScore = 0; + for (int x = 0; x < numBuckets; x++) { + int distanceToBiggest = x - firstPeak; + // Encourage more distant second peaks by multiplying by square of distance. + int score = buckets[x] * distanceToBiggest * distanceToBiggest; + if (score > secondPeakScore) { + secondPeak = x; + secondPeakScore = score; + } + } + + // Make sure firstPeak corresponds to the black peak. + if (firstPeak > secondPeak) { + int temp = firstPeak; + firstPeak = secondPeak; + secondPeak = temp; + } + + // If there is too little contrast in the image to pick a meaningful black point, throw rather + // than waste time trying to decode the image, and risk false positives. + if (secondPeak - firstPeak <= numBuckets / 16) { + throw NotFoundException.getNotFoundInstance(); + } + + // Find a valley between them that is low and closer to the white peak. + int bestValley = secondPeak - 1; + int bestValleyScore = -1; + for (int x = secondPeak - 1; x > firstPeak; x--) { + int fromFirst = x - firstPeak; + int score = fromFirst * fromFirst * (secondPeak - x) * (maxBucketCount - buckets[x]); + if (score > bestValleyScore) { + bestValley = x; + bestValleyScore = score; + } + } + + return bestValley << LUMINANCE_SHIFT; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/GridSampler.java b/rubylib/src/main/java/com/google/zxing/common/GridSampler.java new file mode 100644 index 0000000..c17f121 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/GridSampler.java @@ -0,0 +1,173 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.NotFoundException; + +/** + * Implementations of this class can, given locations of finder patterns for a QR code in an + * image, sample the right points in the image to reconstruct the QR code, accounting for + * perspective distortion. It is abstracted since it is relatively expensive and should be allowed + * to take advantage of platform-specific optimized implementations, like Sun's Java Advanced + * Imaging library, but which may not be available in other environments such as J2ME, and vice + * versa. + * + * The implementation used can be controlled by calling {@link #setGridSampler(GridSampler)} + * with an instance of a class which implements this interface. + * + * @author Sean Owen + */ +public abstract class GridSampler { + + private static GridSampler gridSampler = new DefaultGridSampler(); + + /** + * Sets the implementation of GridSampler used by the library. One global + * instance is stored, which may sound problematic. But, the implementation provided + * ought to be appropriate for the entire platform, and all uses of this library + * in the whole lifetime of the JVM. For instance, an Android activity can swap in + * an implementation that takes advantage of native platform libraries. + * + * @param newGridSampler The platform-specific object to install. + */ + public static void setGridSampler(GridSampler newGridSampler) { + gridSampler = newGridSampler; + } + + /** + * @return the current implementation of GridSampler + */ + public static GridSampler getInstance() { + return gridSampler; + } + + /** + * Samples an image for a rectangular matrix of bits of the given dimension. The sampling + * transformation is determined by the coordinates of 4 points, in the original and transformed + * image space. + * + * @param image image to sample + * @param dimensionX width of {@link BitMatrix} to sample from image + * @param dimensionY height of {@link BitMatrix} to sample from image + * @param p1ToX point 1 preimage X + * @param p1ToY point 1 preimage Y + * @param p2ToX point 2 preimage X + * @param p2ToY point 2 preimage Y + * @param p3ToX point 3 preimage X + * @param p3ToY point 3 preimage Y + * @param p4ToX point 4 preimage X + * @param p4ToY point 4 preimage Y + * @param p1FromX point 1 image X + * @param p1FromY point 1 image Y + * @param p2FromX point 2 image X + * @param p2FromY point 2 image Y + * @param p3FromX point 3 image X + * @param p3FromY point 3 image Y + * @param p4FromX point 4 image X + * @param p4FromY point 4 image Y + * @return {@link BitMatrix} representing a grid of points sampled from the image within a region + * defined by the "from" parameters + * @throws NotFoundException if image can't be sampled, for example, if the transformation defined + * by the given points is invalid or results in sampling outside the image boundaries + */ + public abstract BitMatrix sampleGrid(BitMatrix image, + int dimensionX, + int dimensionY, + float p1ToX, float p1ToY, + float p2ToX, float p2ToY, + float p3ToX, float p3ToY, + float p4ToX, float p4ToY, + float p1FromX, float p1FromY, + float p2FromX, float p2FromY, + float p3FromX, float p3FromY, + float p4FromX, float p4FromY) throws NotFoundException; + + public abstract BitMatrix sampleGrid(BitMatrix image, + int dimensionX, + int dimensionY, + PerspectiveTransform transform) throws NotFoundException; + + /** + *

Checks a set of points that have been transformed to sample points on an image against + * the image's dimensions to see if the point are even within the image.

+ * + *

This method will actually "nudge" the endpoints back onto the image if they are found to be + * barely (less than 1 pixel) off the image. This accounts for imperfect detection of finder + * patterns in an image where the QR Code runs all the way to the image border.

+ * + *

For efficiency, the method will check points from either end of the line until one is found + * to be within the image. Because the set of points are assumed to be linear, this is valid.

+ * + * @param image image into which the points should map + * @param points actual points in x1,y1,...,xn,yn form + * @throws NotFoundException if an endpoint is lies outside the image boundaries + */ + protected static void checkAndNudgePoints(BitMatrix image, + float[] points) throws NotFoundException { + int width = image.getWidth(); + int height = image.getHeight(); + // Check and nudge points from start until we see some that are OK: + boolean nudged = true; + for (int offset = 0; offset < points.length && nudged; offset += 2) { + int x = (int) points[offset]; + int y = (int) points[offset + 1]; + if (x < -1 || x > width || y < -1 || y > height) { + throw NotFoundException.getNotFoundInstance(); + } + nudged = false; + if (x == -1) { + points[offset] = 0.0f; + nudged = true; + } else if (x == width) { + points[offset] = width - 1; + nudged = true; + } + if (y == -1) { + points[offset + 1] = 0.0f; + nudged = true; + } else if (y == height) { + points[offset + 1] = height - 1; + nudged = true; + } + } + // Check and nudge points from end: + nudged = true; + for (int offset = points.length - 2; offset >= 0 && nudged; offset -= 2) { + int x = (int) points[offset]; + int y = (int) points[offset + 1]; + if (x < -1 || x > width || y < -1 || y > height) { + throw NotFoundException.getNotFoundInstance(); + } + nudged = false; + if (x == -1) { + points[offset] = 0.0f; + nudged = true; + } else if (x == width) { + points[offset] = width - 1; + nudged = true; + } + if (y == -1) { + points[offset + 1] = 0.0f; + nudged = true; + } else if (y == height) { + points[offset + 1] = height - 1; + nudged = true; + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/HybridBinarizer.java b/rubylib/src/main/java/com/google/zxing/common/HybridBinarizer.java new file mode 100644 index 0000000..107e61c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/HybridBinarizer.java @@ -0,0 +1,237 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import com.google.zxing.Binarizer; +import com.google.zxing.LuminanceSource; +import com.google.zxing.NotFoundException; + +/** + * This class implements a local thresholding algorithm, which while slower than the + * GlobalHistogramBinarizer, is fairly efficient for what it does. It is designed for + * high frequency images of barcodes with black data on white backgrounds. For this application, + * it does a much better job than a global blackpoint with severe shadows and gradients. + * However it tends to produce artifacts on lower frequency images and is therefore not + * a good general purpose binarizer for uses outside ZXing. + * + * This class extends GlobalHistogramBinarizer, using the older histogram approach for 1D readers, + * and the newer local approach for 2D readers. 1D decoding using a per-row histogram is already + * inherently local, and only fails for horizontal gradients. We can revisit that problem later, + * but for now it was not a win to use local blocks for 1D. + * + * This Binarizer is the default for the unit tests and the recommended class for library users. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class HybridBinarizer extends GlobalHistogramBinarizer { + + // This class uses 5x5 blocks to compute local luminance, where each block is 8x8 pixels. + // So this is the smallest dimension in each axis we can accept. + private static final int BLOCK_SIZE_POWER = 3; + private static final int BLOCK_SIZE = 1 << BLOCK_SIZE_POWER; // ...0100...00 + private static final int BLOCK_SIZE_MASK = BLOCK_SIZE - 1; // ...0011...11 + private static final int MINIMUM_DIMENSION = BLOCK_SIZE * 5; + private static final int MIN_DYNAMIC_RANGE = 24; + + private BitMatrix matrix; + + public HybridBinarizer(LuminanceSource source) { + super(source); + } + + /** + * Calculates the final BitMatrix once for all requests. This could be called once from the + * constructor instead, but there are some advantages to doing it lazily, such as making + * profiling easier, and not doing heavy lifting when callers don't expect it. + */ + @Override + public BitMatrix getBlackMatrix() throws NotFoundException { + if (matrix != null) { + return matrix; + } + LuminanceSource source = getLuminanceSource(); + int width = source.getWidth(); + int height = source.getHeight(); + if (width >= MINIMUM_DIMENSION && height >= MINIMUM_DIMENSION) { + byte[] luminances = source.getMatrix(); + int subWidth = width >> BLOCK_SIZE_POWER; + if ((width & BLOCK_SIZE_MASK) != 0) { + subWidth++; + } + int subHeight = height >> BLOCK_SIZE_POWER; + if ((height & BLOCK_SIZE_MASK) != 0) { + subHeight++; + } + int[][] blackPoints = calculateBlackPoints(luminances, subWidth, subHeight, width, height); + + BitMatrix newMatrix = new BitMatrix(width, height); + calculateThresholdForBlock(luminances, subWidth, subHeight, width, height, blackPoints, newMatrix); + matrix = newMatrix; + } else { + // If the image is too small, fall back to the global histogram approach. + matrix = super.getBlackMatrix(); + } + return matrix; + } + + @Override + public Binarizer createBinarizer(LuminanceSource source) { + return new HybridBinarizer(source); + } + + /** + * For each block in the image, calculate the average black point using a 5x5 grid + * of the blocks around it. Also handles the corner cases (fractional blocks are computed based + * on the last pixels in the row/column which are also used in the previous block). + */ + private static void calculateThresholdForBlock(byte[] luminances, + int subWidth, + int subHeight, + int width, + int height, + int[][] blackPoints, + BitMatrix matrix) { + int maxYOffset = height - BLOCK_SIZE; + int maxXOffset = width - BLOCK_SIZE; + for (int y = 0; y < subHeight; y++) { + int yoffset = y << BLOCK_SIZE_POWER; + if (yoffset > maxYOffset) { + yoffset = maxYOffset; + } + int top = cap(y, 2, subHeight - 3); + for (int x = 0; x < subWidth; x++) { + int xoffset = x << BLOCK_SIZE_POWER; + if (xoffset > maxXOffset) { + xoffset = maxXOffset; + } + int left = cap(x, 2, subWidth - 3); + int sum = 0; + for (int z = -2; z <= 2; z++) { + int[] blackRow = blackPoints[top + z]; + sum += blackRow[left - 2] + blackRow[left - 1] + blackRow[left] + blackRow[left + 1] + blackRow[left + 2]; + } + int average = sum / 25; + thresholdBlock(luminances, xoffset, yoffset, average, width, matrix); + } + } + } + + private static int cap(int value, int min, int max) { + return value < min ? min : value > max ? max : value; + } + + /** + * Applies a single threshold to a block of pixels. + */ + private static void thresholdBlock(byte[] luminances, + int xoffset, + int yoffset, + int threshold, + int stride, + BitMatrix matrix) { + for (int y = 0, offset = yoffset * stride + xoffset; y < BLOCK_SIZE; y++, offset += stride) { + for (int x = 0; x < BLOCK_SIZE; x++) { + // Comparison needs to be <= so that black == 0 pixels are black even if the threshold is 0. + if ((luminances[offset + x] & 0xFF) <= threshold) { + matrix.set(xoffset + x, yoffset + y); + } + } + } + } + + /** + * Calculates a single black point for each block of pixels and saves it away. + * See the following thread for a discussion of this algorithm: + * http://groups.google.com/group/zxing/browse_thread/thread/d06efa2c35a7ddc0 + */ + private static int[][] calculateBlackPoints(byte[] luminances, + int subWidth, + int subHeight, + int width, + int height) { + int maxYOffset = height - BLOCK_SIZE; + int maxXOffset = width - BLOCK_SIZE; + int[][] blackPoints = new int[subHeight][subWidth]; + for (int y = 0; y < subHeight; y++) { + int yoffset = y << BLOCK_SIZE_POWER; + if (yoffset > maxYOffset) { + yoffset = maxYOffset; + } + for (int x = 0; x < subWidth; x++) { + int xoffset = x << BLOCK_SIZE_POWER; + if (xoffset > maxXOffset) { + xoffset = maxXOffset; + } + int sum = 0; + int min = 0xFF; + int max = 0; + for (int yy = 0, offset = yoffset * width + xoffset; yy < BLOCK_SIZE; yy++, offset += width) { + for (int xx = 0; xx < BLOCK_SIZE; xx++) { + int pixel = luminances[offset + xx] & 0xFF; + sum += pixel; + // still looking for good contrast + if (pixel < min) { + min = pixel; + } + if (pixel > max) { + max = pixel; + } + } + // short-circuit min/max tests once dynamic range is met + if (max - min > MIN_DYNAMIC_RANGE) { + // finish the rest of the rows quickly + for (yy++, offset += width; yy < BLOCK_SIZE; yy++, offset += width) { + for (int xx = 0; xx < BLOCK_SIZE; xx++) { + sum += luminances[offset + xx] & 0xFF; + } + } + } + } + + // The default estimate is the average of the values in the block. + int average = sum >> (BLOCK_SIZE_POWER * 2); + if (max - min <= MIN_DYNAMIC_RANGE) { + // If variation within the block is low, assume this is a block with only light or only + // dark pixels. In that case we do not want to use the average, as it would divide this + // low contrast area into black and white pixels, essentially creating data out of noise. + // + // The default assumption is that the block is light/background. Since no estimate for + // the level of dark pixels exists locally, use half the min for the block. + average = min / 2; + + if (y > 0 && x > 0) { + // Correct the "white background" assumption for blocks that have neighbors by comparing + // the pixels in this block to the previously calculated black points. This is based on + // the fact that dark barcode symbology is always surrounded by some amount of light + // background for which reasonable black point estimates were made. The bp estimated at + // the boundaries is used for the interior. + + // The (min < bp) is arbitrary but works better than other heuristics that were tried. + int averageNeighborBlackPoint = + (blackPoints[y - 1][x] + (2 * blackPoints[y][x - 1]) + blackPoints[y - 1][x - 1]) / 4; + if (min < averageNeighborBlackPoint) { + average = averageNeighborBlackPoint; + } + } + } + blackPoints[y][x] = average; + } + } + return blackPoints; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/PerspectiveTransform.java b/rubylib/src/main/java/com/google/zxing/common/PerspectiveTransform.java new file mode 100644 index 0000000..18b6091 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/PerspectiveTransform.java @@ -0,0 +1,156 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +/** + *

This class implements a perspective transform in two dimensions. Given four source and four + * destination points, it will compute the transformation implied between them. The code is based + * directly upon section 3.4.2 of George Wolberg's "Digital Image Warping"; see pages 54-56.

+ * + * @author Sean Owen + */ +public final class PerspectiveTransform { + + private final float a11; + private final float a12; + private final float a13; + private final float a21; + private final float a22; + private final float a23; + private final float a31; + private final float a32; + private final float a33; + + private PerspectiveTransform(float a11, float a21, float a31, + float a12, float a22, float a32, + float a13, float a23, float a33) { + this.a11 = a11; + this.a12 = a12; + this.a13 = a13; + this.a21 = a21; + this.a22 = a22; + this.a23 = a23; + this.a31 = a31; + this.a32 = a32; + this.a33 = a33; + } + + public static PerspectiveTransform quadrilateralToQuadrilateral(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3, + float x0p, float y0p, + float x1p, float y1p, + float x2p, float y2p, + float x3p, float y3p) { + + PerspectiveTransform qToS = quadrilateralToSquare(x0, y0, x1, y1, x2, y2, x3, y3); + PerspectiveTransform sToQ = squareToQuadrilateral(x0p, y0p, x1p, y1p, x2p, y2p, x3p, y3p); + return sToQ.times(qToS); + } + + public void transformPoints(float[] points) { + int max = points.length; + float a11 = this.a11; + float a12 = this.a12; + float a13 = this.a13; + float a21 = this.a21; + float a22 = this.a22; + float a23 = this.a23; + float a31 = this.a31; + float a32 = this.a32; + float a33 = this.a33; + for (int i = 0; i < max; i += 2) { + float x = points[i]; + float y = points[i + 1]; + float denominator = a13 * x + a23 * y + a33; + points[i] = (a11 * x + a21 * y + a31) / denominator; + points[i + 1] = (a12 * x + a22 * y + a32) / denominator; + } + } + + public void transformPoints(float[] xValues, float[] yValues) { + int n = xValues.length; + for (int i = 0; i < n; i++) { + float x = xValues[i]; + float y = yValues[i]; + float denominator = a13 * x + a23 * y + a33; + xValues[i] = (a11 * x + a21 * y + a31) / denominator; + yValues[i] = (a12 * x + a22 * y + a32) / denominator; + } + } + + public static PerspectiveTransform squareToQuadrilateral(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3) { + float dx3 = x0 - x1 + x2 - x3; + float dy3 = y0 - y1 + y2 - y3; + if (dx3 == 0.0f && dy3 == 0.0f) { + // Affine + return new PerspectiveTransform(x1 - x0, x2 - x1, x0, + y1 - y0, y2 - y1, y0, + 0.0f, 0.0f, 1.0f); + } else { + float dx1 = x1 - x2; + float dx2 = x3 - x2; + float dy1 = y1 - y2; + float dy2 = y3 - y2; + float denominator = dx1 * dy2 - dx2 * dy1; + float a13 = (dx3 * dy2 - dx2 * dy3) / denominator; + float a23 = (dx1 * dy3 - dx3 * dy1) / denominator; + return new PerspectiveTransform(x1 - x0 + a13 * x1, x3 - x0 + a23 * x3, x0, + y1 - y0 + a13 * y1, y3 - y0 + a23 * y3, y0, + a13, a23, 1.0f); + } + } + + public static PerspectiveTransform quadrilateralToSquare(float x0, float y0, + float x1, float y1, + float x2, float y2, + float x3, float y3) { + // Here, the adjoint serves as the inverse: + return squareToQuadrilateral(x0, y0, x1, y1, x2, y2, x3, y3).buildAdjoint(); + } + + PerspectiveTransform buildAdjoint() { + // Adjoint is the transpose of the cofactor matrix: + return new PerspectiveTransform(a22 * a33 - a23 * a32, + a23 * a31 - a21 * a33, + a21 * a32 - a22 * a31, + a13 * a32 - a12 * a33, + a11 * a33 - a13 * a31, + a12 * a31 - a11 * a32, + a12 * a23 - a13 * a22, + a13 * a21 - a11 * a23, + a11 * a22 - a12 * a21); + } + + PerspectiveTransform times(PerspectiveTransform other) { + return new PerspectiveTransform(a11 * other.a11 + a21 * other.a12 + a31 * other.a13, + a11 * other.a21 + a21 * other.a22 + a31 * other.a23, + a11 * other.a31 + a21 * other.a32 + a31 * other.a33, + a12 * other.a11 + a22 * other.a12 + a32 * other.a13, + a12 * other.a21 + a22 * other.a22 + a32 * other.a23, + a12 * other.a31 + a22 * other.a32 + a32 * other.a33, + a13 * other.a11 + a23 * other.a12 + a33 * other.a13, + a13 * other.a21 + a23 * other.a22 + a33 * other.a23, + a13 * other.a31 + a23 * other.a32 + a33 * other.a33); + + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/StringUtils.java b/rubylib/src/main/java/com/google/zxing/common/StringUtils.java new file mode 100644 index 0000000..db3dc9b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/StringUtils.java @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common; + +import java.nio.charset.Charset; +import java.util.Map; + +import com.google.zxing.DecodeHintType; + +/** + * Common string-related functions. + * + * @author Sean Owen + * @author Alex Dupre + */ +public final class StringUtils { + + private static final String PLATFORM_DEFAULT_ENCODING = Charset.defaultCharset().name(); + public static final String SHIFT_JIS = "SJIS"; + public static final String GB2312 = "GB2312"; + private static final String EUC_JP = "EUC_JP"; + private static final String UTF8 = "UTF8"; + private static final String ISO88591 = "ISO8859_1"; + private static final boolean ASSUME_SHIFT_JIS = + SHIFT_JIS.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING) || + EUC_JP.equalsIgnoreCase(PLATFORM_DEFAULT_ENCODING); + + private StringUtils() { } + + /** + * @param bytes bytes encoding a string, whose encoding should be guessed + * @param hints decode hints if applicable + * @return name of guessed encoding; at the moment will only guess one of: + * {@link #SHIFT_JIS}, {@link #UTF8}, {@link #ISO88591}, or the platform + * default encoding if none of these can possibly be correct + */ + public static String guessEncoding(byte[] bytes, Map hints) { + if (hints != null && hints.containsKey(DecodeHintType.CHARACTER_SET)) { + return hints.get(DecodeHintType.CHARACTER_SET).toString(); + } + // For now, merely tries to distinguish ISO-8859-1, UTF-8 and Shift_JIS, + // which should be by far the most common encodings. + int length = bytes.length; + boolean canBeISO88591 = true; + boolean canBeShiftJIS = true; + boolean canBeUTF8 = true; + int utf8BytesLeft = 0; + int utf2BytesChars = 0; + int utf3BytesChars = 0; + int utf4BytesChars = 0; + int sjisBytesLeft = 0; + int sjisKatakanaChars = 0; + int sjisCurKatakanaWordLength = 0; + int sjisCurDoubleBytesWordLength = 0; + int sjisMaxKatakanaWordLength = 0; + int sjisMaxDoubleBytesWordLength = 0; + int isoHighOther = 0; + + boolean utf8bom = bytes.length > 3 && + bytes[0] == (byte) 0xEF && + bytes[1] == (byte) 0xBB && + bytes[2] == (byte) 0xBF; + + for (int i = 0; + i < length && (canBeISO88591 || canBeShiftJIS || canBeUTF8); + i++) { + + int value = bytes[i] & 0xFF; + + // UTF-8 stuff + if (canBeUTF8) { + if (utf8BytesLeft > 0) { + if ((value & 0x80) == 0) { + canBeUTF8 = false; + } else { + utf8BytesLeft--; + } + } else if ((value & 0x80) != 0) { + if ((value & 0x40) == 0) { + canBeUTF8 = false; + } else { + utf8BytesLeft++; + if ((value & 0x20) == 0) { + utf2BytesChars++; + } else { + utf8BytesLeft++; + if ((value & 0x10) == 0) { + utf3BytesChars++; + } else { + utf8BytesLeft++; + if ((value & 0x08) == 0) { + utf4BytesChars++; + } else { + canBeUTF8 = false; + } + } + } + } + } + } + + // ISO-8859-1 stuff + if (canBeISO88591) { + if (value > 0x7F && value < 0xA0) { + canBeISO88591 = false; + } else if (value > 0x9F && (value < 0xC0 || value == 0xD7 || value == 0xF7)) { + isoHighOther++; + } + } + + // Shift_JIS stuff + if (canBeShiftJIS) { + if (sjisBytesLeft > 0) { + if (value < 0x40 || value == 0x7F || value > 0xFC) { + canBeShiftJIS = false; + } else { + sjisBytesLeft--; + } + } else if (value == 0x80 || value == 0xA0 || value > 0xEF) { + canBeShiftJIS = false; + } else if (value > 0xA0 && value < 0xE0) { + sjisKatakanaChars++; + sjisCurDoubleBytesWordLength = 0; + sjisCurKatakanaWordLength++; + if (sjisCurKatakanaWordLength > sjisMaxKatakanaWordLength) { + sjisMaxKatakanaWordLength = sjisCurKatakanaWordLength; + } + } else if (value > 0x7F) { + sjisBytesLeft++; + //sjisDoubleBytesChars++; + sjisCurKatakanaWordLength = 0; + sjisCurDoubleBytesWordLength++; + if (sjisCurDoubleBytesWordLength > sjisMaxDoubleBytesWordLength) { + sjisMaxDoubleBytesWordLength = sjisCurDoubleBytesWordLength; + } + } else { + //sjisLowChars++; + sjisCurKatakanaWordLength = 0; + sjisCurDoubleBytesWordLength = 0; + } + } + } + + if (canBeUTF8 && utf8BytesLeft > 0) { + canBeUTF8 = false; + } + if (canBeShiftJIS && sjisBytesLeft > 0) { + canBeShiftJIS = false; + } + + // Easy -- if there is BOM or at least 1 valid not-single byte character (and no evidence it can't be UTF-8), done + if (canBeUTF8 && (utf8bom || utf2BytesChars + utf3BytesChars + utf4BytesChars > 0)) { + return UTF8; + } + // Easy -- if assuming Shift_JIS or at least 3 valid consecutive not-ascii characters (and no evidence it can't be), done + if (canBeShiftJIS && (ASSUME_SHIFT_JIS || sjisMaxKatakanaWordLength >= 3 || sjisMaxDoubleBytesWordLength >= 3)) { + return SHIFT_JIS; + } + // Distinguishing Shift_JIS and ISO-8859-1 can be a little tough for short words. The crude heuristic is: + // - If we saw + // - only two consecutive katakana chars in the whole text, or + // - at least 10% of bytes that could be "upper" not-alphanumeric Latin1, + // - then we conclude Shift_JIS, else ISO-8859-1 + if (canBeISO88591 && canBeShiftJIS) { + return (sjisMaxKatakanaWordLength == 2 && sjisKatakanaChars == 2) || isoHighOther * 10 >= length + ? SHIFT_JIS : ISO88591; + } + + // Otherwise, try in order ISO-8859-1, Shift JIS, UTF-8 and fall back to default platform encoding + if (canBeISO88591) { + return ISO88591; + } + if (canBeShiftJIS) { + return SHIFT_JIS; + } + if (canBeUTF8) { + return UTF8; + } + // Otherwise, we take a wild guess with platform encoding + return PLATFORM_DEFAULT_ENCODING; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/detector/MathUtils.java b/rubylib/src/main/java/com/google/zxing/common/detector/MathUtils.java new file mode 100644 index 0000000..ec7080d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/detector/MathUtils.java @@ -0,0 +1,78 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.detector; + +/** + * General math-related and numeric utility functions. + */ +public final class MathUtils { + + private MathUtils() { + } + + /** + * Ends up being a bit faster than {@link Math#round(float)}. This merely rounds its + * argument to the nearest int, where x.5 rounds up to x+1. Semantics of this shortcut + * differ slightly from {@link Math#round(float)} in that half rounds down for negative + * values. -2.5 rounds to -3, not -2. For purposes here it makes no difference. + * + * @param d real value to round + * @return nearest {@code int} + */ + public static int round(float d) { + return (int) (d + (d < 0.0f ? -0.5f : 0.5f)); + } + + /** + * @param aX point A x coordinate + * @param aY point A y coordinate + * @param bX point B x coordinate + * @param bY point B y coordinate + * @return Euclidean distance between points A and B + */ + public static float distance(float aX, float aY, float bX, float bY) { + float xDiff = aX - bX; + float yDiff = aY - bY; + return (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff); + } + + /** + * @param aX point A x coordinate + * @param aY point A y coordinate + * @param bX point B x coordinate + * @param bY point B y coordinate + * @return Euclidean distance between points A and B + */ + public static float distance(int aX, int aY, int bX, int bY) { + int xDiff = aX - bX; + int yDiff = aY - bY; + return (float) Math.sqrt(xDiff * xDiff + yDiff * yDiff); + } + + /** + * @param array values to sum + * @return sum of values in array + */ + public static int sum(int[] array) { + int count = 0; + for (int a : array) { + count += a; + } + return count; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/detector/MonochromeRectangleDetector.java b/rubylib/src/main/java/com/google/zxing/common/detector/MonochromeRectangleDetector.java new file mode 100644 index 0000000..e87fa55 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/detector/MonochromeRectangleDetector.java @@ -0,0 +1,217 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +/** + *

A somewhat generic detector that looks for a barcode-like rectangular region within an image. + * It looks within a mostly white region of an image for a region of black and white, but mostly + * black. It returns the four corners of the region, as best it can determine.

+ * + * @author Sean Owen + * @deprecated without replacement since 3.3.0 + */ +@Deprecated +public final class MonochromeRectangleDetector { + + private static final int MAX_MODULES = 32; + + private final BitMatrix image; + + public MonochromeRectangleDetector(BitMatrix image) { + this.image = image; + } + + /** + *

Detects a rectangular region of black and white -- mostly black -- with a region of mostly + * white, in an image.

+ * + * @return {@link ResultPoint}[] describing the corners of the rectangular region. The first and + * last points are opposed on the diagonal, as are the second and third. The first point will be + * the topmost point and the last, the bottommost. The second point will be leftmost and the + * third, the rightmost + * @throws NotFoundException if no Data Matrix Code can be found + */ + public ResultPoint[] detect() throws NotFoundException { + int height = image.getHeight(); + int width = image.getWidth(); + int halfHeight = height / 2; + int halfWidth = width / 2; + int deltaY = Math.max(1, height / (MAX_MODULES * 8)); + int deltaX = Math.max(1, width / (MAX_MODULES * 8)); + + int top = 0; + int bottom = height; + int left = 0; + int right = width; + ResultPoint pointA = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, -deltaY, top, bottom, halfWidth / 2); + top = (int) pointA.getY() - 1; + ResultPoint pointB = findCornerFromCenter(halfWidth, -deltaX, left, right, + halfHeight, 0, top, bottom, halfHeight / 2); + left = (int) pointB.getX() - 1; + ResultPoint pointC = findCornerFromCenter(halfWidth, deltaX, left, right, + halfHeight, 0, top, bottom, halfHeight / 2); + right = (int) pointC.getX() + 1; + ResultPoint pointD = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, deltaY, top, bottom, halfWidth / 2); + bottom = (int) pointD.getY() + 1; + + // Go try to find point A again with better information -- might have been off at first. + pointA = findCornerFromCenter(halfWidth, 0, left, right, + halfHeight, -deltaY, top, bottom, halfWidth / 4); + + return new ResultPoint[] { pointA, pointB, pointC, pointD }; + } + + /** + * Attempts to locate a corner of the barcode by scanning up, down, left or right from a center + * point which should be within the barcode. + * + * @param centerX center's x component (horizontal) + * @param deltaX same as deltaY but change in x per step instead + * @param left minimum value of x + * @param right maximum value of x + * @param centerY center's y component (vertical) + * @param deltaY change in y per step. If scanning up this is negative; down, positive; + * left or right, 0 + * @param top minimum value of y to search through (meaningless when di == 0) + * @param bottom maximum value of y + * @param maxWhiteRun maximum run of white pixels that can still be considered to be within + * the barcode + * @return a {@link ResultPoint} encapsulating the corner that was found + * @throws NotFoundException if such a point cannot be found + */ + private ResultPoint findCornerFromCenter(int centerX, + int deltaX, + int left, + int right, + int centerY, + int deltaY, + int top, + int bottom, + int maxWhiteRun) throws NotFoundException { + int[] lastRange = null; + for (int y = centerY, x = centerX; + y < bottom && y >= top && x < right && x >= left; + y += deltaY, x += deltaX) { + int[] range; + if (deltaX == 0) { + // horizontal slices, up and down + range = blackWhiteRange(y, maxWhiteRun, left, right, true); + } else { + // vertical slices, left and right + range = blackWhiteRange(x, maxWhiteRun, top, bottom, false); + } + if (range == null) { + if (lastRange == null) { + throw NotFoundException.getNotFoundInstance(); + } + // lastRange was found + if (deltaX == 0) { + int lastY = y - deltaY; + if (lastRange[0] < centerX) { + if (lastRange[1] > centerX) { + // straddle, choose one or the other based on direction + return new ResultPoint(lastRange[deltaY > 0 ? 0 : 1], lastY); + } + return new ResultPoint(lastRange[0], lastY); + } else { + return new ResultPoint(lastRange[1], lastY); + } + } else { + int lastX = x - deltaX; + if (lastRange[0] < centerY) { + if (lastRange[1] > centerY) { + return new ResultPoint(lastX, lastRange[deltaX < 0 ? 0 : 1]); + } + return new ResultPoint(lastX, lastRange[0]); + } else { + return new ResultPoint(lastX, lastRange[1]); + } + } + } + lastRange = range; + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Computes the start and end of a region of pixels, either horizontally or vertically, that could + * be part of a Data Matrix barcode. + * + * @param fixedDimension if scanning horizontally, this is the row (the fixed vertical location) + * where we are scanning. If scanning vertically it's the column, the fixed horizontal location + * @param maxWhiteRun largest run of white pixels that can still be considered part of the + * barcode region + * @param minDim minimum pixel location, horizontally or vertically, to consider + * @param maxDim maximum pixel location, horizontally or vertically, to consider + * @param horizontal if true, we're scanning left-right, instead of up-down + * @return int[] with start and end of found range, or null if no such range is found + * (e.g. only white was found) + */ + private int[] blackWhiteRange(int fixedDimension, int maxWhiteRun, int minDim, int maxDim, boolean horizontal) { + + int center = (minDim + maxDim) / 2; + + // Scan left/up first + int start = center; + while (start >= minDim) { + if (horizontal ? image.get(start, fixedDimension) : image.get(fixedDimension, start)) { + start--; + } else { + int whiteRunStart = start; + do { + start--; + } while (start >= minDim && !(horizontal ? image.get(start, fixedDimension) : + image.get(fixedDimension, start))); + int whiteRunSize = whiteRunStart - start; + if (start < minDim || whiteRunSize > maxWhiteRun) { + start = whiteRunStart; + break; + } + } + } + start++; + + // Then try right/down + int end = center; + while (end < maxDim) { + if (horizontal ? image.get(end, fixedDimension) : image.get(fixedDimension, end)) { + end++; + } else { + int whiteRunStart = end; + do { + end++; + } while (end < maxDim && !(horizontal ? image.get(end, fixedDimension) : + image.get(fixedDimension, end))); + int whiteRunSize = end - whiteRunStart; + if (end >= maxDim || whiteRunSize > maxWhiteRun) { + end = whiteRunStart; + break; + } + } + } + end--; + + return end > start ? new int[]{start, end} : null; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/common/detector/WhiteRectangleDetector.java b/rubylib/src/main/java/com/google/zxing/common/detector/WhiteRectangleDetector.java new file mode 100644 index 0000000..0ac0267 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/detector/WhiteRectangleDetector.java @@ -0,0 +1,330 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +/** + *

+ * Detects a candidate barcode-like rectangular region within an image. It + * starts around the center of the image, increases the size of the candidate + * region until it finds a white rectangular region. By keeping track of the + * last black points it encountered, it determines the corners of the barcode. + *

+ * + * @author David Olivier + */ +public final class WhiteRectangleDetector { + + private static final int INIT_SIZE = 10; + private static final int CORR = 1; + + private final BitMatrix image; + private final int height; + private final int width; + private final int leftInit; + private final int rightInit; + private final int downInit; + private final int upInit; + + public WhiteRectangleDetector(BitMatrix image) throws NotFoundException { + this(image, INIT_SIZE, image.getWidth() / 2, image.getHeight() / 2); + } + + /** + * @param image barcode image to find a rectangle in + * @param initSize initial size of search area around center + * @param x x position of search center + * @param y y position of search center + * @throws NotFoundException if image is too small to accommodate {@code initSize} + */ + public WhiteRectangleDetector(BitMatrix image, int initSize, int x, int y) throws NotFoundException { + this.image = image; + height = image.getHeight(); + width = image.getWidth(); + int halfsize = initSize / 2; + leftInit = x - halfsize; + rightInit = x + halfsize; + upInit = y - halfsize; + downInit = y + halfsize; + if (upInit < 0 || leftInit < 0 || downInit >= height || rightInit >= width) { + throw NotFoundException.getNotFoundInstance(); + } + } + + /** + *

+ * Detects a candidate barcode-like rectangular region within an image. It + * starts around the center of the image, increases the size of the candidate + * region until it finds a white rectangular region. + *

+ * + * @return {@link ResultPoint}[] describing the corners of the rectangular + * region. The first and last points are opposed on the diagonal, as + * are the second and third. The first point will be the topmost + * point and the last, the bottommost. The second point will be + * leftmost and the third, the rightmost + * @throws NotFoundException if no Data Matrix Code can be found + */ + public ResultPoint[] detect() throws NotFoundException { + + int left = leftInit; + int right = rightInit; + int up = upInit; + int down = downInit; + boolean sizeExceeded = false; + boolean aBlackPointFoundOnBorder = true; + boolean atLeastOneBlackPointFoundOnBorder = false; + + boolean atLeastOneBlackPointFoundOnRight = false; + boolean atLeastOneBlackPointFoundOnBottom = false; + boolean atLeastOneBlackPointFoundOnLeft = false; + boolean atLeastOneBlackPointFoundOnTop = false; + + while (aBlackPointFoundOnBorder) { + + aBlackPointFoundOnBorder = false; + + // ..... + // . | + // ..... + boolean rightBorderNotWhite = true; + while ((rightBorderNotWhite || !atLeastOneBlackPointFoundOnRight) && right < width) { + rightBorderNotWhite = containsBlackPoint(up, down, right, false); + if (rightBorderNotWhite) { + right++; + aBlackPointFoundOnBorder = true; + atLeastOneBlackPointFoundOnRight = true; + } else if (!atLeastOneBlackPointFoundOnRight) { + right++; + } + } + + if (right >= width) { + sizeExceeded = true; + break; + } + + // ..... + // . . + // .___. + boolean bottomBorderNotWhite = true; + while ((bottomBorderNotWhite || !atLeastOneBlackPointFoundOnBottom) && down < height) { + bottomBorderNotWhite = containsBlackPoint(left, right, down, true); + if (bottomBorderNotWhite) { + down++; + aBlackPointFoundOnBorder = true; + atLeastOneBlackPointFoundOnBottom = true; + } else if (!atLeastOneBlackPointFoundOnBottom) { + down++; + } + } + + if (down >= height) { + sizeExceeded = true; + break; + } + + // ..... + // | . + // ..... + boolean leftBorderNotWhite = true; + while ((leftBorderNotWhite || !atLeastOneBlackPointFoundOnLeft) && left >= 0) { + leftBorderNotWhite = containsBlackPoint(up, down, left, false); + if (leftBorderNotWhite) { + left--; + aBlackPointFoundOnBorder = true; + atLeastOneBlackPointFoundOnLeft = true; + } else if (!atLeastOneBlackPointFoundOnLeft) { + left--; + } + } + + if (left < 0) { + sizeExceeded = true; + break; + } + + // .___. + // . . + // ..... + boolean topBorderNotWhite = true; + while ((topBorderNotWhite || !atLeastOneBlackPointFoundOnTop) && up >= 0) { + topBorderNotWhite = containsBlackPoint(left, right, up, true); + if (topBorderNotWhite) { + up--; + aBlackPointFoundOnBorder = true; + atLeastOneBlackPointFoundOnTop = true; + } else if (!atLeastOneBlackPointFoundOnTop) { + up--; + } + } + + if (up < 0) { + sizeExceeded = true; + break; + } + + if (aBlackPointFoundOnBorder) { + atLeastOneBlackPointFoundOnBorder = true; + } + + } + + if (!sizeExceeded && atLeastOneBlackPointFoundOnBorder) { + + int maxSize = right - left; + + ResultPoint z = null; + for (int i = 1; z == null && i < maxSize; i++) { + z = getBlackPointOnSegment(left, down - i, left + i, down); + } + + if (z == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint t = null; + //go down right + for (int i = 1; t == null && i < maxSize; i++) { + t = getBlackPointOnSegment(left, up + i, left + i, up); + } + + if (t == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint x = null; + //go down left + for (int i = 1; x == null && i < maxSize; i++) { + x = getBlackPointOnSegment(right, up + i, right - i, up); + } + + if (x == null) { + throw NotFoundException.getNotFoundInstance(); + } + + ResultPoint y = null; + //go up left + for (int i = 1; y == null && i < maxSize; i++) { + y = getBlackPointOnSegment(right, down - i, right - i, down); + } + + if (y == null) { + throw NotFoundException.getNotFoundInstance(); + } + + return centerEdges(y, z, x, t); + + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + + private ResultPoint getBlackPointOnSegment(float aX, float aY, float bX, float bY) { + int dist = MathUtils.round(MathUtils.distance(aX, aY, bX, bY)); + float xStep = (bX - aX) / dist; + float yStep = (bY - aY) / dist; + + for (int i = 0; i < dist; i++) { + int x = MathUtils.round(aX + i * xStep); + int y = MathUtils.round(aY + i * yStep); + if (image.get(x, y)) { + return new ResultPoint(x, y); + } + } + return null; + } + + /** + * recenters the points of a constant distance towards the center + * + * @param y bottom most point + * @param z left most point + * @param x right most point + * @param t top most point + * @return {@link ResultPoint}[] describing the corners of the rectangular + * region. The first and last points are opposed on the diagonal, as + * are the second and third. The first point will be the topmost + * point and the last, the bottommost. The second point will be + * leftmost and the third, the rightmost + */ + private ResultPoint[] centerEdges(ResultPoint y, ResultPoint z, + ResultPoint x, ResultPoint t) { + + // + // t t + // z x + // x OR z + // y y + // + + float yi = y.getX(); + float yj = y.getY(); + float zi = z.getX(); + float zj = z.getY(); + float xi = x.getX(); + float xj = x.getY(); + float ti = t.getX(); + float tj = t.getY(); + + if (yi < width / 2.0f) { + return new ResultPoint[]{ + new ResultPoint(ti - CORR, tj + CORR), + new ResultPoint(zi + CORR, zj + CORR), + new ResultPoint(xi - CORR, xj - CORR), + new ResultPoint(yi + CORR, yj - CORR)}; + } else { + return new ResultPoint[]{ + new ResultPoint(ti + CORR, tj + CORR), + new ResultPoint(zi + CORR, zj - CORR), + new ResultPoint(xi - CORR, xj + CORR), + new ResultPoint(yi - CORR, yj - CORR)}; + } + } + + /** + * Determines whether a segment contains a black point + * + * @param a min value of the scanned coordinate + * @param b max value of the scanned coordinate + * @param fixed value of fixed coordinate + * @param horizontal set to true if scan must be horizontal, false if vertical + * @return true if a black point has been found, else false. + */ + private boolean containsBlackPoint(int a, int b, int fixed, boolean horizontal) { + + if (horizontal) { + for (int x = a; x <= b; x++) { + if (image.get(x, fixed)) { + return true; + } + } + } else { + for (int y = a; y <= b; y++) { + if (image.get(fixed, y)) { + return true; + } + } + } + + return false; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGF.java b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGF.java new file mode 100644 index 0000000..2b32566 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGF.java @@ -0,0 +1,166 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.reedsolomon; + +/** + *

This class contains utility methods for performing mathematical operations over + * the Galois Fields. Operations use a given primitive polynomial in calculations.

+ * + *

Throughout this package, elements of the GF are represented as an {@code int} + * for convenience and speed (but at the cost of memory). + *

+ * + * @author Sean Owen + * @author David Olivier + */ +public final class GenericGF { + + public static final GenericGF AZTEC_DATA_12 = new GenericGF(0x1069, 4096, 1); // x^12 + x^6 + x^5 + x^3 + 1 + public static final GenericGF AZTEC_DATA_10 = new GenericGF(0x409, 1024, 1); // x^10 + x^3 + 1 + public static final GenericGF AZTEC_DATA_6 = new GenericGF(0x43, 64, 1); // x^6 + x + 1 + public static final GenericGF AZTEC_PARAM = new GenericGF(0x13, 16, 1); // x^4 + x + 1 + public static final GenericGF QR_CODE_FIELD_256 = new GenericGF(0x011D, 256, 0); // x^8 + x^4 + x^3 + x^2 + 1 + public static final GenericGF DATA_MATRIX_FIELD_256 = new GenericGF(0x012D, 256, 1); // x^8 + x^5 + x^3 + x^2 + 1 + public static final GenericGF AZTEC_DATA_8 = DATA_MATRIX_FIELD_256; + public static final GenericGF MAXICODE_FIELD_64 = AZTEC_DATA_6; + + private final int[] expTable; + private final int[] logTable; + private final GenericGFPoly zero; + private final GenericGFPoly one; + private final int size; + private final int primitive; + private final int generatorBase; + + /** + * Create a representation of GF(size) using the given primitive polynomial. + * + * @param primitive irreducible polynomial whose coefficients are represented by + * the bits of an int, where the least-significant bit represents the constant + * coefficient + * @param size the size of the field + * @param b the factor b in the generator polynomial can be 0- or 1-based + * (g(x) = (x+a^b)(x+a^(b+1))...(x+a^(b+2t-1))). + * In most cases it should be 1, but for QR code it is 0. + */ + public GenericGF(int primitive, int size, int b) { + this.primitive = primitive; + this.size = size; + this.generatorBase = b; + + expTable = new int[size]; + logTable = new int[size]; + int x = 1; + for (int i = 0; i < size; i++) { + expTable[i] = x; + x *= 2; // we're assuming the generator alpha is 2 + if (x >= size) { + x ^= primitive; + x &= size - 1; + } + } + for (int i = 0; i < size - 1; i++) { + logTable[expTable[i]] = i; + } + // logTable[0] == 0 but this should never be used + zero = new GenericGFPoly(this, new int[]{0}); + one = new GenericGFPoly(this, new int[]{1}); + } + + GenericGFPoly getZero() { + return zero; + } + + GenericGFPoly getOne() { + return one; + } + + /** + * @return the monomial representing coefficient * x^degree + */ + GenericGFPoly buildMonomial(int degree, int coefficient) { + if (degree < 0) { + throw new IllegalArgumentException(); + } + if (coefficient == 0) { + return zero; + } + int[] coefficients = new int[degree + 1]; + coefficients[0] = coefficient; + return new GenericGFPoly(this, coefficients); + } + + /** + * Implements both addition and subtraction -- they are the same in GF(size). + * + * @return sum/difference of a and b + */ + static int addOrSubtract(int a, int b) { + return a ^ b; + } + + /** + * @return 2 to the power of a in GF(size) + */ + int exp(int a) { + return expTable[a]; + } + + /** + * @return base 2 log of a in GF(size) + */ + int log(int a) { + if (a == 0) { + throw new IllegalArgumentException(); + } + return logTable[a]; + } + + /** + * @return multiplicative inverse of a + */ + int inverse(int a) { + if (a == 0) { + throw new ArithmeticException(); + } + return expTable[size - logTable[a] - 1]; + } + + /** + * @return product of a and b in GF(size) + */ + int multiply(int a, int b) { + if (a == 0 || b == 0) { + return 0; + } + return expTable[(logTable[a] + logTable[b]) % (size - 1)]; + } + + public int getSize() { + return size; + } + + public int getGeneratorBase() { + return generatorBase; + } + + @Override + public String toString() { + return "GF(0x" + Integer.toHexString(primitive) + ',' + size + ')'; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGFPoly.java b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGFPoly.java new file mode 100644 index 0000000..ddad982 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/GenericGFPoly.java @@ -0,0 +1,264 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.reedsolomon; + +/** + *

Represents a polynomial whose coefficients are elements of a GF. + * Instances of this class are immutable.

+ * + *

Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

+ * + * @author Sean Owen + */ +final class GenericGFPoly { + + private final GenericGF field; + private final int[] coefficients; + + /** + * @param field the {@link GenericGF} instance representing the field to use + * to perform computations + * @param coefficients coefficients as ints representing elements of GF(size), arranged + * from most significant (highest-power term) coefficient to least significant + * @throws IllegalArgumentException if argument is null or empty, + * or if leading coefficient is 0 and this is not a + * constant polynomial (that is, it is not the monomial "0") + */ + GenericGFPoly(GenericGF field, int[] coefficients) { + if (coefficients.length == 0) { + throw new IllegalArgumentException(); + } + this.field = field; + int coefficientsLength = coefficients.length; + if (coefficientsLength > 1 && coefficients[0] == 0) { + // Leading term must be non-zero for anything except the constant polynomial "0" + int firstNonZero = 1; + while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) { + firstNonZero++; + } + if (firstNonZero == coefficientsLength) { + this.coefficients = new int[]{0}; + } else { + this.coefficients = new int[coefficientsLength - firstNonZero]; + System.arraycopy(coefficients, + firstNonZero, + this.coefficients, + 0, + this.coefficients.length); + } + } else { + this.coefficients = coefficients; + } + } + + int[] getCoefficients() { + return coefficients; + } + + /** + * @return degree of this polynomial + */ + int getDegree() { + return coefficients.length - 1; + } + + /** + * @return true iff this polynomial is the monomial "0" + */ + boolean isZero() { + return coefficients[0] == 0; + } + + /** + * @return coefficient of x^degree term in this polynomial + */ + int getCoefficient(int degree) { + return coefficients[coefficients.length - 1 - degree]; + } + + /** + * @return evaluation of this polynomial at a given point + */ + int evaluateAt(int a) { + if (a == 0) { + // Just return the x^0 coefficient + return getCoefficient(0); + } + if (a == 1) { + // Just the sum of the coefficients + int result = 0; + for (int coefficient : coefficients) { + result = GenericGF.addOrSubtract(result, coefficient); + } + return result; + } + int result = coefficients[0]; + int size = coefficients.length; + for (int i = 1; i < size; i++) { + result = GenericGF.addOrSubtract(field.multiply(a, result), coefficients[i]); + } + return result; + } + + GenericGFPoly addOrSubtract(GenericGFPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if (isZero()) { + return other; + } + if (other.isZero()) { + return this; + } + + int[] smallerCoefficients = this.coefficients; + int[] largerCoefficients = other.coefficients; + if (smallerCoefficients.length > largerCoefficients.length) { + int[] temp = smallerCoefficients; + smallerCoefficients = largerCoefficients; + largerCoefficients = temp; + } + int[] sumDiff = new int[largerCoefficients.length]; + int lengthDiff = largerCoefficients.length - smallerCoefficients.length; + // Copy high-order terms only found in higher-degree polynomial's coefficients + System.arraycopy(largerCoefficients, 0, sumDiff, 0, lengthDiff); + + for (int i = lengthDiff; i < largerCoefficients.length; i++) { + sumDiff[i] = GenericGF.addOrSubtract(smallerCoefficients[i - lengthDiff], largerCoefficients[i]); + } + + return new GenericGFPoly(field, sumDiff); + } + + GenericGFPoly multiply(GenericGFPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if (isZero() || other.isZero()) { + return field.getZero(); + } + int[] aCoefficients = this.coefficients; + int aLength = aCoefficients.length; + int[] bCoefficients = other.coefficients; + int bLength = bCoefficients.length; + int[] product = new int[aLength + bLength - 1]; + for (int i = 0; i < aLength; i++) { + int aCoeff = aCoefficients[i]; + for (int j = 0; j < bLength; j++) { + product[i + j] = GenericGF.addOrSubtract(product[i + j], + field.multiply(aCoeff, bCoefficients[j])); + } + } + return new GenericGFPoly(field, product); + } + + GenericGFPoly multiply(int scalar) { + if (scalar == 0) { + return field.getZero(); + } + if (scalar == 1) { + return this; + } + int size = coefficients.length; + int[] product = new int[size]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], scalar); + } + return new GenericGFPoly(field, product); + } + + GenericGFPoly multiplyByMonomial(int degree, int coefficient) { + if (degree < 0) { + throw new IllegalArgumentException(); + } + if (coefficient == 0) { + return field.getZero(); + } + int size = coefficients.length; + int[] product = new int[size + degree]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], coefficient); + } + return new GenericGFPoly(field, product); + } + + GenericGFPoly[] divide(GenericGFPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("GenericGFPolys do not have same GenericGF field"); + } + if (other.isZero()) { + throw new IllegalArgumentException("Divide by 0"); + } + + GenericGFPoly quotient = field.getZero(); + GenericGFPoly remainder = this; + + int denominatorLeadingTerm = other.getCoefficient(other.getDegree()); + int inverseDenominatorLeadingTerm = field.inverse(denominatorLeadingTerm); + + while (remainder.getDegree() >= other.getDegree() && !remainder.isZero()) { + int degreeDifference = remainder.getDegree() - other.getDegree(); + int scale = field.multiply(remainder.getCoefficient(remainder.getDegree()), inverseDenominatorLeadingTerm); + GenericGFPoly term = other.multiplyByMonomial(degreeDifference, scale); + GenericGFPoly iterationQuotient = field.buildMonomial(degreeDifference, scale); + quotient = quotient.addOrSubtract(iterationQuotient); + remainder = remainder.addOrSubtract(term); + } + + return new GenericGFPoly[] { quotient, remainder }; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(8 * getDegree()); + for (int degree = getDegree(); degree >= 0; degree--) { + int coefficient = getCoefficient(degree); + if (coefficient != 0) { + if (coefficient < 0) { + result.append(" - "); + coefficient = -coefficient; + } else { + if (result.length() > 0) { + result.append(" + "); + } + } + if (degree == 0 || coefficient != 1) { + int alphaPower = field.log(coefficient); + if (alphaPower == 0) { + result.append('1'); + } else if (alphaPower == 1) { + result.append('a'); + } else { + result.append("a^"); + result.append(alphaPower); + } + } + if (degree != 0) { + if (degree == 1) { + result.append('x'); + } else { + result.append("x^"); + result.append(degree); + } + } + } + } + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java new file mode 100644 index 0000000..e406e2f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonDecoder.java @@ -0,0 +1,190 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.reedsolomon; + +/** + *

Implements Reed-Solomon decoding, as the name implies.

+ * + *

The algorithm will not be explained here, but the following references were helpful + * in creating this implementation:

+ * + * + * + *

Much credit is due to William Rucklidge since portions of this code are an indirect + * port of his C++ Reed-Solomon implementation.

+ * + * @author Sean Owen + * @author William Rucklidge + * @author sanfordsquires + */ +public final class ReedSolomonDecoder { + + private final GenericGF field; + + public ReedSolomonDecoder(GenericGF field) { + this.field = field; + } + + /** + *

Decodes given set of received codewords, which include both data and error-correction + * codewords. Really, this means it uses Reed-Solomon to detect and correct errors, in-place, + * in the input.

+ * + * @param received data and error-correction codewords + * @param twoS number of error-correction codewords available + * @throws ReedSolomonException if decoding fails for any reason + */ + public void decode(int[] received, int twoS) throws ReedSolomonException { + GenericGFPoly poly = new GenericGFPoly(field, received); + int[] syndromeCoefficients = new int[twoS]; + boolean noError = true; + for (int i = 0; i < twoS; i++) { + int eval = poly.evaluateAt(field.exp(i + field.getGeneratorBase())); + syndromeCoefficients[syndromeCoefficients.length - 1 - i] = eval; + if (eval != 0) { + noError = false; + } + } + if (noError) { + return; + } + GenericGFPoly syndrome = new GenericGFPoly(field, syndromeCoefficients); + GenericGFPoly[] sigmaOmega = + runEuclideanAlgorithm(field.buildMonomial(twoS, 1), syndrome, twoS); + GenericGFPoly sigma = sigmaOmega[0]; + GenericGFPoly omega = sigmaOmega[1]; + int[] errorLocations = findErrorLocations(sigma); + int[] errorMagnitudes = findErrorMagnitudes(omega, errorLocations); + for (int i = 0; i < errorLocations.length; i++) { + int position = received.length - 1 - field.log(errorLocations[i]); + if (position < 0) { + throw new ReedSolomonException("Bad error location"); + } + received[position] = GenericGF.addOrSubtract(received[position], errorMagnitudes[i]); + } + } + + private GenericGFPoly[] runEuclideanAlgorithm(GenericGFPoly a, GenericGFPoly b, int R) + throws ReedSolomonException { + // Assume a's degree is >= b's + if (a.getDegree() < b.getDegree()) { + GenericGFPoly temp = a; + a = b; + b = temp; + } + + GenericGFPoly rLast = a; + GenericGFPoly r = b; + GenericGFPoly tLast = field.getZero(); + GenericGFPoly t = field.getOne(); + + // Run Euclidean algorithm until r's degree is less than R/2 + while (r.getDegree() >= R / 2) { + GenericGFPoly rLastLast = rLast; + GenericGFPoly tLastLast = tLast; + rLast = r; + tLast = t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + if (rLast.isZero()) { + // Oops, Euclidean algorithm already terminated? + throw new ReedSolomonException("r_{i-1} was zero"); + } + r = rLastLast; + GenericGFPoly q = field.getZero(); + int denominatorLeadingTerm = rLast.getCoefficient(rLast.getDegree()); + int dltInverse = field.inverse(denominatorLeadingTerm); + while (r.getDegree() >= rLast.getDegree() && !r.isZero()) { + int degreeDiff = r.getDegree() - rLast.getDegree(); + int scale = field.multiply(r.getCoefficient(r.getDegree()), dltInverse); + q = q.addOrSubtract(field.buildMonomial(degreeDiff, scale)); + r = r.addOrSubtract(rLast.multiplyByMonomial(degreeDiff, scale)); + } + + t = q.multiply(tLast).addOrSubtract(tLastLast); + + if (r.getDegree() >= rLast.getDegree()) { + throw new IllegalStateException("Division algorithm failed to reduce polynomial?"); + } + } + + int sigmaTildeAtZero = t.getCoefficient(0); + if (sigmaTildeAtZero == 0) { + throw new ReedSolomonException("sigmaTilde(0) was zero"); + } + + int inverse = field.inverse(sigmaTildeAtZero); + GenericGFPoly sigma = t.multiply(inverse); + GenericGFPoly omega = r.multiply(inverse); + return new GenericGFPoly[]{sigma, omega}; + } + + private int[] findErrorLocations(GenericGFPoly errorLocator) throws ReedSolomonException { + // This is a direct application of Chien's search + int numErrors = errorLocator.getDegree(); + if (numErrors == 1) { // shortcut + return new int[] { errorLocator.getCoefficient(1) }; + } + int[] result = new int[numErrors]; + int e = 0; + for (int i = 1; i < field.getSize() && e < numErrors; i++) { + if (errorLocator.evaluateAt(i) == 0) { + result[e] = field.inverse(i); + e++; + } + } + if (e != numErrors) { + throw new ReedSolomonException("Error locator degree does not match number of roots"); + } + return result; + } + + private int[] findErrorMagnitudes(GenericGFPoly errorEvaluator, int[] errorLocations) { + // This is directly applying Forney's Formula + int s = errorLocations.length; + int[] result = new int[s]; + for (int i = 0; i < s; i++) { + int xiInverse = field.inverse(errorLocations[i]); + int denominator = 1; + for (int j = 0; j < s; j++) { + if (i != j) { + //denominator = field.multiply(denominator, + // GenericGF.addOrSubtract(1, field.multiply(errorLocations[j], xiInverse))); + // Above should work but fails on some Apple and Linux JDKs due to a Hotspot bug. + // Below is a funny-looking workaround from Steven Parkes + int term = field.multiply(errorLocations[j], xiInverse); + int termPlus1 = (term & 0x1) == 0 ? term | 1 : term & ~1; + denominator = field.multiply(denominator, termPlus1); + } + } + result[i] = field.multiply(errorEvaluator.evaluateAt(xiInverse), + field.inverse(denominator)); + if (field.getGeneratorBase() != 0) { + result[i] = field.multiply(result[i], xiInverse); + } + } + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java new file mode 100644 index 0000000..2e2b2f0 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonEncoder.java @@ -0,0 +1,74 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.reedsolomon; + +import java.util.ArrayList; +import java.util.List; + +/** + *

Implements Reed-Solomon encoding, as the name implies.

+ * + * @author Sean Owen + * @author William Rucklidge + */ +public final class ReedSolomonEncoder { + + private final GenericGF field; + private final List cachedGenerators; + + public ReedSolomonEncoder(GenericGF field) { + this.field = field; + this.cachedGenerators = new ArrayList<>(); + cachedGenerators.add(new GenericGFPoly(field, new int[]{1})); + } + + private GenericGFPoly buildGenerator(int degree) { + if (degree >= cachedGenerators.size()) { + GenericGFPoly lastGenerator = cachedGenerators.get(cachedGenerators.size() - 1); + for (int d = cachedGenerators.size(); d <= degree; d++) { + GenericGFPoly nextGenerator = lastGenerator.multiply( + new GenericGFPoly(field, new int[] { 1, field.exp(d - 1 + field.getGeneratorBase()) })); + cachedGenerators.add(nextGenerator); + lastGenerator = nextGenerator; + } + } + return cachedGenerators.get(degree); + } + + public void encode(int[] toEncode, int ecBytes) { + if (ecBytes == 0) { + throw new IllegalArgumentException("No error correction bytes"); + } + int dataBytes = toEncode.length - ecBytes; + if (dataBytes <= 0) { + throw new IllegalArgumentException("No data bytes provided"); + } + GenericGFPoly generator = buildGenerator(ecBytes); + int[] infoCoefficients = new int[dataBytes]; + System.arraycopy(toEncode, 0, infoCoefficients, 0, dataBytes); + GenericGFPoly info = new GenericGFPoly(field, infoCoefficients); + info = info.multiplyByMonomial(ecBytes, 1); + GenericGFPoly remainder = info.divide(generator)[1]; + int[] coefficients = remainder.getCoefficients(); + int numZeroCoefficients = ecBytes - coefficients.length; + for (int i = 0; i < numZeroCoefficients; i++) { + toEncode[dataBytes + i] = 0; + } + System.arraycopy(coefficients, 0, toEncode, dataBytes + numZeroCoefficients, coefficients.length); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonException.java b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonException.java new file mode 100644 index 0000000..d5b45a6 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/common/reedsolomon/ReedSolomonException.java @@ -0,0 +1,31 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.common.reedsolomon; + +/** + *

Thrown when an exception occurs during Reed-Solomon decoding, such as when + * there are too many errors to correct.

+ * + * @author Sean Owen + */ +public final class ReedSolomonException extends Exception { + + public ReedSolomonException(String message) { + super(message); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixReader.java b/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixReader.java new file mode 100644 index 0000000..e02d766 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixReader.java @@ -0,0 +1,161 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.datamatrix.decoder.Decoder; +import com.google.zxing.datamatrix.detector.Detector; + +import java.util.List; +import java.util.Map; + +/** + * This implementation can detect and decode Data Matrix codes in an image. + * + * @author bbrown@google.com (Brian Brown) + */ +public final class DataMatrixReader implements Reader { + + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + + private final Decoder decoder = new Decoder(); + + /** + * Locates and decodes a Data Matrix code in an image. + * + * @return a String representing the content encoded by the Data Matrix code + * @throws NotFoundException if a Data Matrix code cannot be found + * @throws FormatException if a Data Matrix code cannot be decoded + * @throws ChecksumException if error correction fails + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, ChecksumException, FormatException { + DecoderResult decoderResult; + ResultPoint[] points; + if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) { + BitMatrix bits = extractPureBits(image.getBlackMatrix()); + decoderResult = decoder.decode(bits); + points = NO_POINTS; + } else { + DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(); + decoderResult = decoder.decode(detectorResult.getBits()); + points = detectorResult.getPoints(); + } + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, + BarcodeFormat.DATA_MATRIX); + List byteSegments = decoderResult.getByteSegments(); + if (byteSegments != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments); + } + String ecLevel = decoderResult.getECLevel(); + if (ecLevel != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); + } + return result; + } + + @Override + public void reset() { + // do nothing + } + + /** + * This method detects a code in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a code, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + * + * @see com.google.zxing.qrcode.QRCodeReader#extractPureBits(BitMatrix) + */ + private static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException { + + int[] leftTopBlack = image.getTopLeftOnBit(); + int[] rightBottomBlack = image.getBottomRightOnBit(); + if (leftTopBlack == null || rightBottomBlack == null) { + throw NotFoundException.getNotFoundInstance(); + } + + int moduleSize = moduleSize(leftTopBlack, image); + + int top = leftTopBlack[1]; + int bottom = rightBottomBlack[1]; + int left = leftTopBlack[0]; + int right = rightBottomBlack[0]; + + int matrixWidth = (right - left + 1) / moduleSize; + int matrixHeight = (bottom - top + 1) / moduleSize; + if (matrixWidth <= 0 || matrixHeight <= 0) { + throw NotFoundException.getNotFoundInstance(); + } + + // Push in the "border" by half the module width so that we start + // sampling in the middle of the module. Just in case the image is a + // little off, this will help recover. + int nudge = moduleSize / 2; + top += nudge; + left += nudge; + + // Now just read off the bits + BitMatrix bits = new BitMatrix(matrixWidth, matrixHeight); + for (int y = 0; y < matrixHeight; y++) { + int iOffset = top + y * moduleSize; + for (int x = 0; x < matrixWidth; x++) { + if (image.get(left + x * moduleSize, iOffset)) { + bits.set(x, y); + } + } + } + return bits; + } + + private static int moduleSize(int[] leftTopBlack, BitMatrix image) throws NotFoundException { + int width = image.getWidth(); + int x = leftTopBlack[0]; + int y = leftTopBlack[1]; + while (x < width && image.get(x, y)) { + x++; + } + if (x == width) { + throw NotFoundException.getNotFoundInstance(); + } + + int moduleSize = x - leftTopBlack[0]; + if (moduleSize == 0) { + throw NotFoundException.getNotFoundInstance(); + } + return moduleSize; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixWriter.java b/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixWriter.java new file mode 100644 index 0000000..52ec549 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/DataMatrixWriter.java @@ -0,0 +1,180 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.datamatrix.encoder.DefaultPlacement; +import com.google.zxing.Dimension; +import com.google.zxing.datamatrix.encoder.ErrorCorrection; +import com.google.zxing.datamatrix.encoder.HighLevelEncoder; +import com.google.zxing.datamatrix.encoder.SymbolInfo; +import com.google.zxing.datamatrix.encoder.SymbolShapeHint; +import com.google.zxing.qrcode.encoder.ByteMatrix; + +import java.util.Map; + +/** + * This object renders a Data Matrix code as a BitMatrix 2D array of greyscale values. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Guillaume Le Biller Added to zxing lib. + */ +public final class DataMatrixWriter implements Writer { + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) { + return encode(contents, format, width, height, null); + } + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height, Map hints) { + + if (contents.isEmpty()) { + throw new IllegalArgumentException("Found empty contents"); + } + + if (format != BarcodeFormat.DATA_MATRIX) { + throw new IllegalArgumentException("Can only encode DATA_MATRIX, but got " + format); + } + + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' + height); + } + + // Try to get force shape & min / max size + SymbolShapeHint shape = SymbolShapeHint.FORCE_NONE; + Dimension minSize = null; + Dimension maxSize = null; + if (hints != null) { + SymbolShapeHint requestedShape = (SymbolShapeHint) hints.get(EncodeHintType.DATA_MATRIX_SHAPE); + if (requestedShape != null) { + shape = requestedShape; + } + @SuppressWarnings("deprecation") + Dimension requestedMinSize = (Dimension) hints.get(EncodeHintType.MIN_SIZE); + if (requestedMinSize != null) { + minSize = requestedMinSize; + } + @SuppressWarnings("deprecation") + Dimension requestedMaxSize = (Dimension) hints.get(EncodeHintType.MAX_SIZE); + if (requestedMaxSize != null) { + maxSize = requestedMaxSize; + } + } + + + //1. step: Data encodation + String encoded = HighLevelEncoder.encodeHighLevel(contents, shape, minSize, maxSize); + + SymbolInfo symbolInfo = SymbolInfo.lookup(encoded.length(), shape, minSize, maxSize, true); + + //2. step: ECC generation + String codewords = ErrorCorrection.encodeECC200(encoded, symbolInfo); + + //3. step: Module placement in Matrix + DefaultPlacement placement = + new DefaultPlacement(codewords, symbolInfo.getSymbolDataWidth(), symbolInfo.getSymbolDataHeight()); + placement.place(); + + //4. step: low-level encoding + return encodeLowLevel(placement, symbolInfo); + } + + /** + * Encode the given symbol info to a bit matrix. + * + * @param placement The DataMatrix placement. + * @param symbolInfo The symbol info to encode. + * @return The bit matrix generated. + */ + private static BitMatrix encodeLowLevel(DefaultPlacement placement, SymbolInfo symbolInfo) { + int symbolWidth = symbolInfo.getSymbolDataWidth(); + int symbolHeight = symbolInfo.getSymbolDataHeight(); + + ByteMatrix matrix = new ByteMatrix(symbolInfo.getSymbolWidth(), symbolInfo.getSymbolHeight()); + + int matrixY = 0; + + for (int y = 0; y < symbolHeight; y++) { + // Fill the top edge with alternate 0 / 1 + int matrixX; + if ((y % symbolInfo.matrixHeight) == 0) { + matrixX = 0; + for (int x = 0; x < symbolInfo.getSymbolWidth(); x++) { + matrix.set(matrixX, matrixY, (x % 2) == 0); + matrixX++; + } + matrixY++; + } + matrixX = 0; + for (int x = 0; x < symbolWidth; x++) { + // Fill the right edge with full 1 + if ((x % symbolInfo.matrixWidth) == 0) { + matrix.set(matrixX, matrixY, true); + matrixX++; + } + matrix.set(matrixX, matrixY, placement.getBit(x, y)); + matrixX++; + // Fill the right edge with alternate 0 / 1 + if ((x % symbolInfo.matrixWidth) == symbolInfo.matrixWidth - 1) { + matrix.set(matrixX, matrixY, (y % 2) == 0); + matrixX++; + } + } + matrixY++; + // Fill the bottom edge with full 1 + if ((y % symbolInfo.matrixHeight) == symbolInfo.matrixHeight - 1) { + matrixX = 0; + for (int x = 0; x < symbolInfo.getSymbolWidth(); x++) { + matrix.set(matrixX, matrixY, true); + matrixX++; + } + matrixY++; + } + } + + return convertByteMatrixToBitMatrix(matrix); + } + + /** + * Convert the ByteMatrix to BitMatrix. + * + * @param matrix The input matrix. + * @return The output matrix. + */ + private static BitMatrix convertByteMatrixToBitMatrix(ByteMatrix matrix) { + int matrixWidgth = matrix.getWidth(); + int matrixHeight = matrix.getHeight(); + + BitMatrix output = new BitMatrix(matrixWidgth, matrixHeight); + output.clear(); + for (int i = 0; i < matrixWidgth; i++) { + for (int j = 0; j < matrixHeight; j++) { + // Zero is white in the bytematrix + if (matrix.get(i, j) == 1) { + output.set(i, j); + } + } + } + + return output; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/BitMatrixParser.java b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/BitMatrixParser.java new file mode 100644 index 0000000..151267e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/BitMatrixParser.java @@ -0,0 +1,440 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * @author bbrown@google.com (Brian Brown) + */ +final class BitMatrixParser { + + private final BitMatrix mappingBitMatrix; + private final BitMatrix readMappingMatrix; + private final Version version; + + /** + * @param bitMatrix {@link BitMatrix} to parse + * @throws FormatException if dimension is < 8 or > 144 or not 0 mod 2 + */ + BitMatrixParser(BitMatrix bitMatrix) throws FormatException { + int dimension = bitMatrix.getHeight(); + if (dimension < 8 || dimension > 144 || (dimension & 0x01) != 0) { + throw FormatException.getFormatInstance(); + } + + version = readVersion(bitMatrix); + this.mappingBitMatrix = extractDataRegion(bitMatrix); + this.readMappingMatrix = new BitMatrix(this.mappingBitMatrix.getWidth(), this.mappingBitMatrix.getHeight()); + } + + Version getVersion() { + return version; + } + + /** + *

Creates the version object based on the dimension of the original bit matrix from + * the datamatrix code.

+ * + *

See ISO 16022:2006 Table 7 - ECC 200 symbol attributes

+ * + * @param bitMatrix Original {@link BitMatrix} including alignment patterns + * @return {@link Version} encapsulating the Data Matrix Code's "version" + * @throws FormatException if the dimensions of the mapping matrix are not valid + * Data Matrix dimensions. + */ + private static Version readVersion(BitMatrix bitMatrix) throws FormatException { + int numRows = bitMatrix.getHeight(); + int numColumns = bitMatrix.getWidth(); + return Version.getVersionForDimensions(numRows, numColumns); + } + + /** + *

Reads the bits in the {@link BitMatrix} representing the mapping matrix (No alignment patterns) + * in the correct order in order to reconstitute the codewords bytes contained within the + * Data Matrix Code.

+ * + * @return bytes encoded within the Data Matrix Code + * @throws FormatException if the exact number of bytes expected is not read + */ + byte[] readCodewords() throws FormatException { + + byte[] result = new byte[version.getTotalCodewords()]; + int resultOffset = 0; + + int row = 4; + int column = 0; + + int numRows = mappingBitMatrix.getHeight(); + int numColumns = mappingBitMatrix.getWidth(); + + boolean corner1Read = false; + boolean corner2Read = false; + boolean corner3Read = false; + boolean corner4Read = false; + + // Read all of the codewords + do { + // Check the four corner cases + if ((row == numRows) && (column == 0) && !corner1Read) { + result[resultOffset++] = (byte) readCorner1(numRows, numColumns); + row -= 2; + column += 2; + corner1Read = true; + } else if ((row == numRows - 2) && (column == 0) && ((numColumns & 0x03) != 0) && !corner2Read) { + result[resultOffset++] = (byte) readCorner2(numRows, numColumns); + row -= 2; + column += 2; + corner2Read = true; + } else if ((row == numRows + 4) && (column == 2) && ((numColumns & 0x07) == 0) && !corner3Read) { + result[resultOffset++] = (byte) readCorner3(numRows, numColumns); + row -= 2; + column += 2; + corner3Read = true; + } else if ((row == numRows - 2) && (column == 0) && ((numColumns & 0x07) == 4) && !corner4Read) { + result[resultOffset++] = (byte) readCorner4(numRows, numColumns); + row -= 2; + column += 2; + corner4Read = true; + } else { + // Sweep upward diagonally to the right + do { + if ((row < numRows) && (column >= 0) && !readMappingMatrix.get(column, row)) { + result[resultOffset++] = (byte) readUtah(row, column, numRows, numColumns); + } + row -= 2; + column += 2; + } while ((row >= 0) && (column < numColumns)); + row += 1; + column += 3; + + // Sweep downward diagonally to the left + do { + if ((row >= 0) && (column < numColumns) && !readMappingMatrix.get(column, row)) { + result[resultOffset++] = (byte) readUtah(row, column, numRows, numColumns); + } + row += 2; + column -= 2; + } while ((row < numRows) && (column >= 0)); + row += 3; + column += 1; + } + } while ((row < numRows) || (column < numColumns)); + + if (resultOffset != version.getTotalCodewords()) { + throw FormatException.getFormatInstance(); + } + return result; + } + + /** + *

Reads a bit of the mapping matrix accounting for boundary wrapping.

+ * + * @param row Row to read in the mapping matrix + * @param column Column to read in the mapping matrix + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return value of the given bit in the mapping matrix + */ + private boolean readModule(int row, int column, int numRows, int numColumns) { + // Adjust the row and column indices based on boundary wrapping + if (row < 0) { + row += numRows; + column += 4 - ((numRows + 4) & 0x07); + } + if (column < 0) { + column += numColumns; + row += 4 - ((numColumns + 4) & 0x07); + } + readMappingMatrix.set(column, row); + return mappingBitMatrix.get(column, row); + } + + /** + *

Reads the 8 bits of the standard Utah-shaped pattern.

+ * + *

See ISO 16022:2006, 5.8.1 Figure 6

+ * + * @param row Current row in the mapping matrix, anchored at the 8th bit (LSB) of the pattern + * @param column Current column in the mapping matrix, anchored at the 8th bit (LSB) of the pattern + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the utah shape + */ + private int readUtah(int row, int column, int numRows, int numColumns) { + int currentByte = 0; + if (readModule(row - 2, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 2, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row - 1, column, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(row, column, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *

Reads the 8 bits of the special corner condition 1.

+ * + *

See ISO 16022:2006, Figure F.3

+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 1 + */ + private int readCorner1(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(2, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(3, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *

Reads the 8 bits of the special corner condition 2.

+ * + *

See ISO 16022:2006, Figure F.4

+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 2 + */ + private int readCorner2(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 3, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 2, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 4, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *

Reads the 8 bits of the special corner condition 3.

+ * + *

See ISO 16022:2006, Figure F.5

+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 3 + */ + private int readCorner3(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 3, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *

Reads the 8 bits of the special corner condition 4.

+ * + *

See ISO 16022:2006, Figure F.6

+ * + * @param numRows Number of rows in the mapping matrix + * @param numColumns Number of columns in the mapping matrix + * @return byte from the Corner condition 4 + */ + private int readCorner4(int numRows, int numColumns) { + int currentByte = 0; + if (readModule(numRows - 3, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 2, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(numRows - 1, 0, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 2, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(0, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(1, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(2, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + currentByte <<= 1; + if (readModule(3, numColumns - 1, numRows, numColumns)) { + currentByte |= 1; + } + return currentByte; + } + + /** + *

Extracts the data region from a {@link BitMatrix} that contains + * alignment patterns.

+ * + * @param bitMatrix Original {@link BitMatrix} with alignment patterns + * @return BitMatrix that has the alignment patterns removed + */ + private BitMatrix extractDataRegion(BitMatrix bitMatrix) { + int symbolSizeRows = version.getSymbolSizeRows(); + int symbolSizeColumns = version.getSymbolSizeColumns(); + + if (bitMatrix.getHeight() != symbolSizeRows) { + throw new IllegalArgumentException("Dimension of bitMatrix must match the version size"); + } + + int dataRegionSizeRows = version.getDataRegionSizeRows(); + int dataRegionSizeColumns = version.getDataRegionSizeColumns(); + + int numDataRegionsRow = symbolSizeRows / dataRegionSizeRows; + int numDataRegionsColumn = symbolSizeColumns / dataRegionSizeColumns; + + int sizeDataRegionRow = numDataRegionsRow * dataRegionSizeRows; + int sizeDataRegionColumn = numDataRegionsColumn * dataRegionSizeColumns; + + BitMatrix bitMatrixWithoutAlignment = new BitMatrix(sizeDataRegionColumn, sizeDataRegionRow); + for (int dataRegionRow = 0; dataRegionRow < numDataRegionsRow; ++dataRegionRow) { + int dataRegionRowOffset = dataRegionRow * dataRegionSizeRows; + for (int dataRegionColumn = 0; dataRegionColumn < numDataRegionsColumn; ++dataRegionColumn) { + int dataRegionColumnOffset = dataRegionColumn * dataRegionSizeColumns; + for (int i = 0; i < dataRegionSizeRows; ++i) { + int readRowOffset = dataRegionRow * (dataRegionSizeRows + 2) + 1 + i; + int writeRowOffset = dataRegionRowOffset + i; + for (int j = 0; j < dataRegionSizeColumns; ++j) { + int readColumnOffset = dataRegionColumn * (dataRegionSizeColumns + 2) + 1 + j; + if (bitMatrix.get(readColumnOffset, readRowOffset)) { + int writeColumnOffset = dataRegionColumnOffset + j; + bitMatrixWithoutAlignment.set(writeColumnOffset, writeRowOffset); + } + } + } + } + } + return bitMatrixWithoutAlignment; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DataBlock.java b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DataBlock.java new file mode 100644 index 0000000..e764c08 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DataBlock.java @@ -0,0 +1,118 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.decoder; + +/** + *

Encapsulates a block of data within a Data Matrix Code. Data Matrix Codes may split their data into + * multiple blocks, each of which is a unit of data and error-correction codewords. Each + * is represented by an instance of this class.

+ * + * @author bbrown@google.com (Brian Brown) + */ +final class DataBlock { + + private final int numDataCodewords; + private final byte[] codewords; + + private DataBlock(int numDataCodewords, byte[] codewords) { + this.numDataCodewords = numDataCodewords; + this.codewords = codewords; + } + + /** + *

When Data Matrix Codes use multiple data blocks, they actually interleave the bytes of each of them. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.

+ * + * @param rawCodewords bytes as read directly from the Data Matrix Code + * @param version version of the Data Matrix Code + * @return DataBlocks containing original bytes, "de-interleaved" from representation in the + * Data Matrix Code + */ + static DataBlock[] getDataBlocks(byte[] rawCodewords, + Version version) { + // Figure out the number and size of data blocks used by this version + Version.ECBlocks ecBlocks = version.getECBlocks(); + + // First count the total number of data blocks + int totalBlocks = 0; + Version.ECB[] ecBlockArray = ecBlocks.getECBlocks(); + for (Version.ECB ecBlock : ecBlockArray) { + totalBlocks += ecBlock.getCount(); + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + DataBlock[] result = new DataBlock[totalBlocks]; + int numResultBlocks = 0; + for (Version.ECB ecBlock : ecBlockArray) { + for (int i = 0; i < ecBlock.getCount(); i++) { + int numDataCodewords = ecBlock.getDataCodewords(); + int numBlockCodewords = ecBlocks.getECCodewords() + numDataCodewords; + result[numResultBlocks++] = new DataBlock(numDataCodewords, new byte[numBlockCodewords]); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 less byte. Figure out where these start. + // TODO(bbrown): There is only one case where there is a difference for Data Matrix for size 144 + int longerBlocksTotalCodewords = result[0].codewords.length; + //int shorterBlocksTotalCodewords = longerBlocksTotalCodewords - 1; + + int longerBlocksNumDataCodewords = longerBlocksTotalCodewords - ecBlocks.getECCodewords(); + int shorterBlocksNumDataCodewords = longerBlocksNumDataCodewords - 1; + // The last elements of result may be 1 element shorter for 144 matrix + // first fill out as many elements as all of them have minus 1 + int rawCodewordsOffset = 0; + for (int i = 0; i < shorterBlocksNumDataCodewords; i++) { + for (int j = 0; j < numResultBlocks; j++) { + result[j].codewords[i] = rawCodewords[rawCodewordsOffset++]; + } + } + + // Fill out the last data block in the longer ones + boolean specialVersion = version.getVersionNumber() == 24; + int numLongerBlocks = specialVersion ? 8 : numResultBlocks; + for (int j = 0; j < numLongerBlocks; j++) { + result[j].codewords[longerBlocksNumDataCodewords - 1] = rawCodewords[rawCodewordsOffset++]; + } + + // Now add in error correction blocks + int max = result[0].codewords.length; + for (int i = longerBlocksNumDataCodewords; i < max; i++) { + for (int j = 0; j < numResultBlocks; j++) { + int jOffset = specialVersion ? (j + 8) % numResultBlocks : j; + int iOffset = specialVersion && jOffset > 7 ? i - 1 : i; + result[jOffset].codewords[iOffset] = rawCodewords[rawCodewordsOffset++]; + } + } + + if (rawCodewordsOffset != rawCodewords.length) { + throw new IllegalArgumentException(); + } + + return result; + } + + int getNumDataCodewords() { + return numDataCodewords; + } + + byte[] getCodewords() { + return codewords; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..cdbaf31 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/DecodedBitStreamParser.java @@ -0,0 +1,525 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitSource; +import com.google.zxing.common.DecoderResult; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + *

Data Matrix Codes can encode text as bits in one of several modes, and can use multiple modes + * in one Data Matrix Code. This class decodes the bits back into text.

+ * + *

See ISO 16022:2006, 5.2.1 - 5.2.9.2

+ * + * @author bbrown@google.com (Brian Brown) + * @author Sean Owen + */ +final class DecodedBitStreamParser { + + private enum Mode { + PAD_ENCODE, // Not really a mode + ASCII_ENCODE, + C40_ENCODE, + TEXT_ENCODE, + ANSIX12_ENCODE, + EDIFACT_ENCODE, + BASE256_ENCODE + } + + /** + * See ISO 16022:2006, Annex C Table C.1 + * The C40 Basic Character Set (*'s used for placeholders for the shift values) + */ + private static final char[] C40_BASIC_SET_CHARS = { + '*', '*', '*', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' + }; + + private static final char[] C40_SHIFT2_SET_CHARS = { + '!', '"', '#', '$', '%', '&', '\'', '(', ')', '*', '+', ',', '-', '.', + '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_' + }; + + /** + * See ISO 16022:2006, Annex C Table C.2 + * The Text Basic Character Set (*'s used for placeholders for the shift values) + */ + private static final char[] TEXT_BASIC_SET_CHARS = { + '*', '*', '*', ' ', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', + 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z' + }; + + // Shift 2 for Text is the same encoding as C40 + private static final char[] TEXT_SHIFT2_SET_CHARS = C40_SHIFT2_SET_CHARS; + + private static final char[] TEXT_SHIFT3_SET_CHARS = { + '`', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', + 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '{', '|', '}', '~', (char) 127 + }; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(byte[] bytes) throws FormatException { + BitSource bits = new BitSource(bytes); + StringBuilder result = new StringBuilder(100); + StringBuilder resultTrailer = new StringBuilder(0); + List byteSegments = new ArrayList<>(1); + Mode mode = Mode.ASCII_ENCODE; + do { + if (mode == Mode.ASCII_ENCODE) { + mode = decodeAsciiSegment(bits, result, resultTrailer); + } else { + switch (mode) { + case C40_ENCODE: + decodeC40Segment(bits, result); + break; + case TEXT_ENCODE: + decodeTextSegment(bits, result); + break; + case ANSIX12_ENCODE: + decodeAnsiX12Segment(bits, result); + break; + case EDIFACT_ENCODE: + decodeEdifactSegment(bits, result); + break; + case BASE256_ENCODE: + decodeBase256Segment(bits, result, byteSegments); + break; + default: + throw FormatException.getFormatInstance(); + } + mode = Mode.ASCII_ENCODE; + } + } while (mode != Mode.PAD_ENCODE && bits.available() > 0); + if (resultTrailer.length() > 0) { + result.append(resultTrailer); + } + return new DecoderResult(bytes, result.toString(), byteSegments.isEmpty() ? null : byteSegments, null); + } + + /** + * See ISO 16022:2006, 5.2.3 and Annex C, Table C.2 + */ + private static Mode decodeAsciiSegment(BitSource bits, + StringBuilder result, + StringBuilder resultTrailer) throws FormatException { + boolean upperShift = false; + do { + int oneByte = bits.readBits(8); + if (oneByte == 0) { + throw FormatException.getFormatInstance(); + } else if (oneByte <= 128) { // ASCII data (ASCII value + 1) + if (upperShift) { + oneByte += 128; + //upperShift = false; + } + result.append((char) (oneByte - 1)); + return Mode.ASCII_ENCODE; + } else if (oneByte == 129) { // Pad + return Mode.PAD_ENCODE; + } else if (oneByte <= 229) { // 2-digit data 00-99 (Numeric Value + 130) + int value = oneByte - 130; + if (value < 10) { // pad with '0' for single digit values + result.append('0'); + } + result.append(value); + } else { + switch (oneByte) { + case 230: // Latch to C40 encodation + return Mode.C40_ENCODE; + case 231: // Latch to Base 256 encodation + return Mode.BASE256_ENCODE; + case 232: // FNC1 + result.append((char) 29); // translate as ASCII 29 + break; + case 233: // Structured Append + case 234: // Reader Programming + // Ignore these symbols for now + //throw ReaderException.getInstance(); + break; + case 235: // Upper Shift (shift to Extended ASCII) + upperShift = true; + break; + case 236: // 05 Macro + result.append("[)>\u001E05\u001D"); + resultTrailer.insert(0, "\u001E\u0004"); + break; + case 237: // 06 Macro + result.append("[)>\u001E06\u001D"); + resultTrailer.insert(0, "\u001E\u0004"); + break; + case 238: // Latch to ANSI X12 encodation + return Mode.ANSIX12_ENCODE; + case 239: // Latch to Text encodation + return Mode.TEXT_ENCODE; + case 240: // Latch to EDIFACT encodation + return Mode.EDIFACT_ENCODE; + case 241: // ECI Character + // TODO(bbrown): I think we need to support ECI + //throw ReaderException.getInstance(); + // Ignore this symbol for now + break; + default: + // Not to be used in ASCII encodation + // but work around encoders that end with 254, latch back to ASCII + if (oneByte >= 242 && (oneByte != 254 || bits.available() != 0)) { + throw FormatException.getFormatInstance(); + } + break; + } + } + } while (bits.available() > 0); + return Mode.ASCII_ENCODE; + } + + /** + * See ISO 16022:2006, 5.2.5 and Annex C, Table C.1 + */ + private static void decodeC40Segment(BitSource bits, StringBuilder result) throws FormatException { + // Three C40 values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + // TODO(bbrown): The Upper Shift with C40 doesn't work in the 4 value scenario all the time + boolean upperShift = false; + + int[] cValues = new int[3]; + int shift = 0; + + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + switch (shift) { + case 0: + if (cValue < 3) { + shift = cValue + 1; + } else if (cValue < C40_BASIC_SET_CHARS.length) { + char c40char = C40_BASIC_SET_CHARS[cValue]; + if (upperShift) { + result.append((char) (c40char + 128)); + upperShift = false; + } else { + result.append(c40char); + } + } else { + throw FormatException.getFormatInstance(); + } + break; + case 1: + if (upperShift) { + result.append((char) (cValue + 128)); + upperShift = false; + } else { + result.append((char) cValue); + } + shift = 0; + break; + case 2: + if (cValue < C40_SHIFT2_SET_CHARS.length) { + char c40char = C40_SHIFT2_SET_CHARS[cValue]; + if (upperShift) { + result.append((char) (c40char + 128)); + upperShift = false; + } else { + result.append(c40char); + } + } else { + switch (cValue) { + case 27: // FNC1 + result.append((char) 29); // translate as ASCII 29 + break; + case 30: // Upper Shift + upperShift = true; + break; + default: + throw FormatException.getFormatInstance(); + } + } + shift = 0; + break; + case 3: + if (upperShift) { + result.append((char) (cValue + 224)); + upperShift = false; + } else { + result.append((char) (cValue + 96)); + } + shift = 0; + break; + default: + throw FormatException.getFormatInstance(); + } + } + } while (bits.available() > 0); + } + + /** + * See ISO 16022:2006, 5.2.6 and Annex C, Table C.2 + */ + private static void decodeTextSegment(BitSource bits, StringBuilder result) throws FormatException { + // Three Text values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + // TODO(bbrown): The Upper Shift with Text doesn't work in the 4 value scenario all the time + boolean upperShift = false; + + int[] cValues = new int[3]; + int shift = 0; + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + switch (shift) { + case 0: + if (cValue < 3) { + shift = cValue + 1; + } else if (cValue < TEXT_BASIC_SET_CHARS.length) { + char textChar = TEXT_BASIC_SET_CHARS[cValue]; + if (upperShift) { + result.append((char) (textChar + 128)); + upperShift = false; + } else { + result.append(textChar); + } + } else { + throw FormatException.getFormatInstance(); + } + break; + case 1: + if (upperShift) { + result.append((char) (cValue + 128)); + upperShift = false; + } else { + result.append((char) cValue); + } + shift = 0; + break; + case 2: + // Shift 2 for Text is the same encoding as C40 + if (cValue < TEXT_SHIFT2_SET_CHARS.length) { + char textChar = TEXT_SHIFT2_SET_CHARS[cValue]; + if (upperShift) { + result.append((char) (textChar + 128)); + upperShift = false; + } else { + result.append(textChar); + } + } else { + switch (cValue) { + case 27: // FNC1 + result.append((char) 29); // translate as ASCII 29 + break; + case 30: // Upper Shift + upperShift = true; + break; + default: + throw FormatException.getFormatInstance(); + } + } + shift = 0; + break; + case 3: + if (cValue < TEXT_SHIFT3_SET_CHARS.length) { + char textChar = TEXT_SHIFT3_SET_CHARS[cValue]; + if (upperShift) { + result.append((char) (textChar + 128)); + upperShift = false; + } else { + result.append(textChar); + } + shift = 0; + } else { + throw FormatException.getFormatInstance(); + } + break; + default: + throw FormatException.getFormatInstance(); + } + } + } while (bits.available() > 0); + } + + /** + * See ISO 16022:2006, 5.2.7 + */ + private static void decodeAnsiX12Segment(BitSource bits, + StringBuilder result) throws FormatException { + // Three ANSI X12 values are encoded in a 16-bit value as + // (1600 * C1) + (40 * C2) + C3 + 1 + + int[] cValues = new int[3]; + do { + // If there is only one byte left then it will be encoded as ASCII + if (bits.available() == 8) { + return; + } + int firstByte = bits.readBits(8); + if (firstByte == 254) { // Unlatch codeword + return; + } + + parseTwoBytes(firstByte, bits.readBits(8), cValues); + + for (int i = 0; i < 3; i++) { + int cValue = cValues[i]; + switch (cValue) { + case 0: // X12 segment terminator + result.append('\r'); + break; + case 1: // X12 segment separator * + result.append('*'); + break; + case 2: // X12 sub-element separator > + result.append('>'); + break; + case 3: // space + result.append(' '); + break; + default: + if (cValue < 14) { // 0 - 9 + result.append((char) (cValue + 44)); + } else if (cValue < 40) { // A - Z + result.append((char) (cValue + 51)); + } else { + throw FormatException.getFormatInstance(); + } + break; + } + } + } while (bits.available() > 0); + } + + private static void parseTwoBytes(int firstByte, int secondByte, int[] result) { + int fullBitValue = (firstByte << 8) + secondByte - 1; + int temp = fullBitValue / 1600; + result[0] = temp; + fullBitValue -= temp * 1600; + temp = fullBitValue / 40; + result[1] = temp; + result[2] = fullBitValue - temp * 40; + } + + /** + * See ISO 16022:2006, 5.2.8 and Annex C Table C.3 + */ + private static void decodeEdifactSegment(BitSource bits, StringBuilder result) { + do { + // If there is only two or less bytes left then it will be encoded as ASCII + if (bits.available() <= 16) { + return; + } + + for (int i = 0; i < 4; i++) { + int edifactValue = bits.readBits(6); + + // Check for the unlatch character + if (edifactValue == 0x1F) { // 011111 + // Read rest of byte, which should be 0, and stop + int bitsLeft = 8 - bits.getBitOffset(); + if (bitsLeft != 8) { + bits.readBits(bitsLeft); + } + return; + } + + if ((edifactValue & 0x20) == 0) { // no 1 in the leading (6th) bit + edifactValue |= 0x40; // Add a leading 01 to the 6 bit binary value + } + result.append((char) edifactValue); + } + } while (bits.available() > 0); + } + + /** + * See ISO 16022:2006, 5.2.9 and Annex B, B.2 + */ + private static void decodeBase256Segment(BitSource bits, + StringBuilder result, + Collection byteSegments) + throws FormatException { + // Figure out how long the Base 256 Segment is. + int codewordPosition = 1 + bits.getByteOffset(); // position is 1-indexed + int d1 = unrandomize255State(bits.readBits(8), codewordPosition++); + int count; + if (d1 == 0) { // Read the remainder of the symbol + count = bits.available() / 8; + } else if (d1 < 250) { + count = d1; + } else { + count = 250 * (d1 - 249) + unrandomize255State(bits.readBits(8), codewordPosition++); + } + + // We're seeing NegativeArraySizeException errors from users. + if (count < 0) { + throw FormatException.getFormatInstance(); + } + + byte[] bytes = new byte[count]; + for (int i = 0; i < count; i++) { + // Have seen this particular error in the wild, such as at + // http://www.bcgen.com/demo/IDAutomationStreamingDataMatrix.aspx?MODE=3&D=Fred&PFMT=3&PT=F&X=0.3&O=0&LM=0.2 + if (bits.available() < 8) { + throw FormatException.getFormatInstance(); + } + bytes[i] = (byte) unrandomize255State(bits.readBits(8), codewordPosition++); + } + byteSegments.add(bytes); + try { + result.append(new String(bytes, "ISO8859_1")); + } catch (UnsupportedEncodingException uee) { + throw new IllegalStateException("Platform does not support required encoding: " + uee); + } + } + + /** + * See ISO 16022:2006, Annex B, B.2 + */ + private static int unrandomize255State(int randomizedBase256Codeword, + int base256CodewordPosition) { + int pseudoRandomNumber = ((149 * base256CodewordPosition) % 255) + 1; + int tempVariable = randomizedBase256Codeword - pseudoRandomNumber; + return tempVariable >= 0 ? tempVariable : tempVariable + 256; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Decoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Decoder.java new file mode 100644 index 0000000..8711e70 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Decoder.java @@ -0,0 +1,125 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +/** + *

The main class which implements Data Matrix Code decoding -- as opposed to locating and extracting + * the Data Matrix Code from an image.

+ * + * @author bbrown@google.com (Brian Brown) + */ +public final class Decoder { + + private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + rsDecoder = new ReedSolomonDecoder(GenericGF.DATA_MATRIX_FIELD_256); + } + + /** + *

Convenience method that can decode a Data Matrix Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.

+ * + * @param image booleans representing white/black Data Matrix Code modules + * @return text and bytes encoded within the Data Matrix Code + * @throws FormatException if the Data Matrix Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(boolean[][] image) throws FormatException, ChecksumException { + return decode(BitMatrix.parse(image)); + } + + /** + *

Decodes a Data Matrix Code represented as a {@link BitMatrix}. A 1 or "true" is taken + * to mean a black module.

+ * + * @param bits booleans representing white/black Data Matrix Code modules + * @return text and bytes encoded within the Data Matrix Code + * @throws FormatException if the Data Matrix Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(BitMatrix bits) throws FormatException, ChecksumException { + + // Construct a parser and read version, error-correction level + BitMatrixParser parser = new BitMatrixParser(bits); + Version version = parser.getVersion(); + + // Read codewords + byte[] codewords = parser.readCodewords(); + // Separate into data blocks + DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version); + + // Count total number of data bytes + int totalBytes = 0; + for (DataBlock db : dataBlocks) { + totalBytes += db.getNumDataCodewords(); + } + byte[] resultBytes = new byte[totalBytes]; + + int dataBlocksCount = dataBlocks.length; + // Error-correct and copy data blocks together into a stream of bytes + for (int j = 0; j < dataBlocksCount; j++) { + DataBlock dataBlock = dataBlocks[j]; + byte[] codewordBytes = dataBlock.getCodewords(); + int numDataCodewords = dataBlock.getNumDataCodewords(); + correctErrors(codewordBytes, numDataCodewords); + for (int i = 0; i < numDataCodewords; i++) { + // De-interlace data blocks. + resultBytes[i * dataBlocksCount + j] = codewordBytes[i]; + } + } + + // Decode the contents of that stream of bytes + return DecodedBitStreamParser.decode(resultBytes); + } + + /** + *

Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.

+ * + * @param codewordBytes data and error correction codewords + * @param numDataCodewords number of codewords that are data bytes + * @throws ChecksumException if error correction fails + */ + private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException { + int numCodewords = codewordBytes.length; + // First read into an array of ints + int[] codewordsInts = new int[numCodewords]; + for (int i = 0; i < numCodewords; i++) { + codewordsInts[i] = codewordBytes[i] & 0xFF; + } + try { + rsDecoder.decode(codewordsInts, codewordBytes.length - numDataCodewords); + } catch (ReedSolomonException ignored) { + throw ChecksumException.getChecksumInstance(); + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (int i = 0; i < numDataCodewords; i++) { + codewordBytes[i] = (byte) codewordsInts[i]; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Version.java b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Version.java new file mode 100644 index 0000000..5341d23 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/decoder/Version.java @@ -0,0 +1,237 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.decoder; + +import com.google.zxing.FormatException; + +/** + * The Version object encapsulates attributes about a particular + * size Data Matrix Code. + * + * @author bbrown@google.com (Brian Brown) + */ +public final class Version { + + private static final Version[] VERSIONS = buildVersions(); + + private final int versionNumber; + private final int symbolSizeRows; + private final int symbolSizeColumns; + private final int dataRegionSizeRows; + private final int dataRegionSizeColumns; + private final ECBlocks ecBlocks; + private final int totalCodewords; + + private Version(int versionNumber, + int symbolSizeRows, + int symbolSizeColumns, + int dataRegionSizeRows, + int dataRegionSizeColumns, + ECBlocks ecBlocks) { + this.versionNumber = versionNumber; + this.symbolSizeRows = symbolSizeRows; + this.symbolSizeColumns = symbolSizeColumns; + this.dataRegionSizeRows = dataRegionSizeRows; + this.dataRegionSizeColumns = dataRegionSizeColumns; + this.ecBlocks = ecBlocks; + + // Calculate the total number of codewords + int total = 0; + int ecCodewords = ecBlocks.getECCodewords(); + ECB[] ecbArray = ecBlocks.getECBlocks(); + for (ECB ecBlock : ecbArray) { + total += ecBlock.getCount() * (ecBlock.getDataCodewords() + ecCodewords); + } + this.totalCodewords = total; + } + + public int getVersionNumber() { + return versionNumber; + } + + public int getSymbolSizeRows() { + return symbolSizeRows; + } + + public int getSymbolSizeColumns() { + return symbolSizeColumns; + } + + public int getDataRegionSizeRows() { + return dataRegionSizeRows; + } + + public int getDataRegionSizeColumns() { + return dataRegionSizeColumns; + } + + public int getTotalCodewords() { + return totalCodewords; + } + + ECBlocks getECBlocks() { + return ecBlocks; + } + + /** + *

Deduces version information from Data Matrix dimensions.

+ * + * @param numRows Number of rows in modules + * @param numColumns Number of columns in modules + * @return Version for a Data Matrix Code of those dimensions + * @throws FormatException if dimensions do correspond to a valid Data Matrix size + */ + public static Version getVersionForDimensions(int numRows, int numColumns) throws FormatException { + if ((numRows & 0x01) != 0 || (numColumns & 0x01) != 0) { + throw FormatException.getFormatInstance(); + } + + for (Version version : VERSIONS) { + if (version.symbolSizeRows == numRows && version.symbolSizeColumns == numColumns) { + return version; + } + } + + throw FormatException.getFormatInstance(); + } + + /** + *

Encapsulates a set of error-correction blocks in one symbol version. Most versions will + * use blocks of differing sizes within one version, so, this encapsulates the parameters for + * each set of blocks. It also holds the number of error-correction codewords per block since it + * will be the same across all blocks within one version.

+ */ + static final class ECBlocks { + private final int ecCodewords; + private final ECB[] ecBlocks; + + private ECBlocks(int ecCodewords, ECB ecBlocks) { + this.ecCodewords = ecCodewords; + this.ecBlocks = new ECB[] { ecBlocks }; + } + + private ECBlocks(int ecCodewords, ECB ecBlocks1, ECB ecBlocks2) { + this.ecCodewords = ecCodewords; + this.ecBlocks = new ECB[] { ecBlocks1, ecBlocks2 }; + } + + int getECCodewords() { + return ecCodewords; + } + + ECB[] getECBlocks() { + return ecBlocks; + } + } + + /** + *

Encapsulates the parameters for one error-correction block in one symbol version. + * This includes the number of data codewords, and the number of times a block with these + * parameters is used consecutively in the Data Matrix code version's format.

+ */ + static final class ECB { + private final int count; + private final int dataCodewords; + + private ECB(int count, int dataCodewords) { + this.count = count; + this.dataCodewords = dataCodewords; + } + + int getCount() { + return count; + } + + int getDataCodewords() { + return dataCodewords; + } + } + + @Override + public String toString() { + return String.valueOf(versionNumber); + } + + /** + * See ISO 16022:2006 5.5.1 Table 7 + */ + private static Version[] buildVersions() { + return new Version[]{ + new Version(1, 10, 10, 8, 8, + new ECBlocks(5, new ECB(1, 3))), + new Version(2, 12, 12, 10, 10, + new ECBlocks(7, new ECB(1, 5))), + new Version(3, 14, 14, 12, 12, + new ECBlocks(10, new ECB(1, 8))), + new Version(4, 16, 16, 14, 14, + new ECBlocks(12, new ECB(1, 12))), + new Version(5, 18, 18, 16, 16, + new ECBlocks(14, new ECB(1, 18))), + new Version(6, 20, 20, 18, 18, + new ECBlocks(18, new ECB(1, 22))), + new Version(7, 22, 22, 20, 20, + new ECBlocks(20, new ECB(1, 30))), + new Version(8, 24, 24, 22, 22, + new ECBlocks(24, new ECB(1, 36))), + new Version(9, 26, 26, 24, 24, + new ECBlocks(28, new ECB(1, 44))), + new Version(10, 32, 32, 14, 14, + new ECBlocks(36, new ECB(1, 62))), + new Version(11, 36, 36, 16, 16, + new ECBlocks(42, new ECB(1, 86))), + new Version(12, 40, 40, 18, 18, + new ECBlocks(48, new ECB(1, 114))), + new Version(13, 44, 44, 20, 20, + new ECBlocks(56, new ECB(1, 144))), + new Version(14, 48, 48, 22, 22, + new ECBlocks(68, new ECB(1, 174))), + new Version(15, 52, 52, 24, 24, + new ECBlocks(42, new ECB(2, 102))), + new Version(16, 64, 64, 14, 14, + new ECBlocks(56, new ECB(2, 140))), + new Version(17, 72, 72, 16, 16, + new ECBlocks(36, new ECB(4, 92))), + new Version(18, 80, 80, 18, 18, + new ECBlocks(48, new ECB(4, 114))), + new Version(19, 88, 88, 20, 20, + new ECBlocks(56, new ECB(4, 144))), + new Version(20, 96, 96, 22, 22, + new ECBlocks(68, new ECB(4, 174))), + new Version(21, 104, 104, 24, 24, + new ECBlocks(56, new ECB(6, 136))), + new Version(22, 120, 120, 18, 18, + new ECBlocks(68, new ECB(6, 175))), + new Version(23, 132, 132, 20, 20, + new ECBlocks(62, new ECB(8, 163))), + new Version(24, 144, 144, 22, 22, + new ECBlocks(62, new ECB(8, 156), new ECB(2, 155))), + new Version(25, 8, 18, 6, 16, + new ECBlocks(7, new ECB(1, 5))), + new Version(26, 8, 32, 6, 14, + new ECBlocks(11, new ECB(1, 10))), + new Version(27, 12, 26, 10, 24, + new ECBlocks(14, new ECB(1, 16))), + new Version(28, 12, 36, 10, 16, + new ECBlocks(18, new ECB(1, 22))), + new Version(29, 16, 36, 14, 16, + new ECBlocks(24, new ECB(1, 32))), + new Version(30, 16, 48, 14, 22, + new ECBlocks(28, new ECB(1, 49))) + }; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/detector/Detector.java b/rubylib/src/main/java/com/google/zxing/datamatrix/detector/Detector.java new file mode 100644 index 0000000..f39330b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/detector/Detector.java @@ -0,0 +1,440 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.common.GridSampler; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.common.detector.WhiteRectangleDetector; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + *

Encapsulates logic that can detect a Data Matrix Code in an image, even if the Data Matrix Code + * is rotated or skewed, or partially obscured.

+ * + * @author Sean Owen + */ +public final class Detector { + + private final BitMatrix image; + private final WhiteRectangleDetector rectangleDetector; + + public Detector(BitMatrix image) throws NotFoundException { + this.image = image; + rectangleDetector = new WhiteRectangleDetector(image); + } + + /** + *

Detects a Data Matrix Code in an image.

+ * + * @return {@link DetectorResult} encapsulating results of detecting a Data Matrix Code + * @throws NotFoundException if no Data Matrix Code can be found + */ + public DetectorResult detect() throws NotFoundException { + + ResultPoint[] cornerPoints = rectangleDetector.detect(); + ResultPoint pointA = cornerPoints[0]; + ResultPoint pointB = cornerPoints[1]; + ResultPoint pointC = cornerPoints[2]; + ResultPoint pointD = cornerPoints[3]; + + // Point A and D are across the diagonal from one another, + // as are B and C. Figure out which are the solid black lines + // by counting transitions + List transitions = new ArrayList<>(4); + transitions.add(transitionsBetween(pointA, pointB)); + transitions.add(transitionsBetween(pointA, pointC)); + transitions.add(transitionsBetween(pointB, pointD)); + transitions.add(transitionsBetween(pointC, pointD)); + Collections.sort(transitions, new ResultPointsAndTransitionsComparator()); + + // Sort by number of transitions. First two will be the two solid sides; last two + // will be the two alternating black/white sides + ResultPointsAndTransitions lSideOne = transitions.get(0); + ResultPointsAndTransitions lSideTwo = transitions.get(1); + + // Figure out which point is their intersection by tallying up the number of times we see the + // endpoints in the four endpoints. One will show up twice. + Map pointCount = new HashMap<>(); + increment(pointCount, lSideOne.getFrom()); + increment(pointCount, lSideOne.getTo()); + increment(pointCount, lSideTwo.getFrom()); + increment(pointCount, lSideTwo.getTo()); + + ResultPoint maybeTopLeft = null; + ResultPoint bottomLeft = null; + ResultPoint maybeBottomRight = null; + for (Map.Entry entry : pointCount.entrySet()) { + ResultPoint point = entry.getKey(); + Integer value = entry.getValue(); + if (value == 2) { + bottomLeft = point; // this is definitely the bottom left, then -- end of two L sides + } else { + // Otherwise it's either top left or bottom right -- just assign the two arbitrarily now + if (maybeTopLeft == null) { + maybeTopLeft = point; + } else { + maybeBottomRight = point; + } + } + } + + if (maybeTopLeft == null || bottomLeft == null || maybeBottomRight == null) { + throw NotFoundException.getNotFoundInstance(); + } + + // Bottom left is correct but top left and bottom right might be switched + ResultPoint[] corners = { maybeTopLeft, bottomLeft, maybeBottomRight }; + // Use the dot product trick to sort them out + ResultPoint.orderBestPatterns(corners); + + // Now we know which is which: + ResultPoint bottomRight = corners[0]; + bottomLeft = corners[1]; + ResultPoint topLeft = corners[2]; + + // Which point didn't we find in relation to the "L" sides? that's the top right corner + ResultPoint topRight; + if (!pointCount.containsKey(pointA)) { + topRight = pointA; + } else if (!pointCount.containsKey(pointB)) { + topRight = pointB; + } else if (!pointCount.containsKey(pointC)) { + topRight = pointC; + } else { + topRight = pointD; + } + + // Next determine the dimension by tracing along the top or right side and counting black/white + // transitions. Since we start inside a black module, we should see a number of transitions + // equal to 1 less than the code dimension. Well, actually 2 less, because we are going to + // end on a black module: + + // The top right point is actually the corner of a module, which is one of the two black modules + // adjacent to the white module at the top right. Tracing to that corner from either the top left + // or bottom right should work here. + + int dimensionTop = transitionsBetween(topLeft, topRight).getTransitions(); + int dimensionRight = transitionsBetween(bottomRight, topRight).getTransitions(); + + if ((dimensionTop & 0x01) == 1) { + // it can't be odd, so, round... up? + dimensionTop++; + } + dimensionTop += 2; + + if ((dimensionRight & 0x01) == 1) { + // it can't be odd, so, round... up? + dimensionRight++; + } + dimensionRight += 2; + + BitMatrix bits; + ResultPoint correctedTopRight; + + // Rectangular symbols are 6x16, 6x28, 10x24, 10x32, 14x32, or 14x44. If one dimension is more + // than twice the other, it's certainly rectangular, but to cut a bit more slack we accept it as + // rectangular if the bigger side is at least 7/4 times the other: + if (4 * dimensionTop >= 7 * dimensionRight || 4 * dimensionRight >= 7 * dimensionTop) { + // The matrix is rectangular + + correctedTopRight = + correctTopRightRectangular(bottomLeft, bottomRight, topLeft, topRight, dimensionTop, dimensionRight); + if (correctedTopRight == null) { + correctedTopRight = topRight; + } + + dimensionTop = transitionsBetween(topLeft, correctedTopRight).getTransitions(); + dimensionRight = transitionsBetween(bottomRight, correctedTopRight).getTransitions(); + + if ((dimensionTop & 0x01) == 1) { + // it can't be odd, so, round... up? + dimensionTop++; + } + + if ((dimensionRight & 0x01) == 1) { + // it can't be odd, so, round... up? + dimensionRight++; + } + + bits = sampleGrid(image, topLeft, bottomLeft, bottomRight, correctedTopRight, dimensionTop, dimensionRight); + + } else { + // The matrix is square + + int dimension = Math.min(dimensionRight, dimensionTop); + // correct top right point to match the white module + correctedTopRight = correctTopRight(bottomLeft, bottomRight, topLeft, topRight, dimension); + if (correctedTopRight == null) { + correctedTopRight = topRight; + } + + // Redetermine the dimension using the corrected top right point + int dimensionCorrected = Math.max(transitionsBetween(topLeft, correctedTopRight).getTransitions(), + transitionsBetween(bottomRight, correctedTopRight).getTransitions()); + dimensionCorrected++; + if ((dimensionCorrected & 0x01) == 1) { + dimensionCorrected++; + } + + bits = sampleGrid(image, + topLeft, + bottomLeft, + bottomRight, + correctedTopRight, + dimensionCorrected, + dimensionCorrected); + } + + return new DetectorResult(bits, new ResultPoint[]{topLeft, bottomLeft, bottomRight, correctedTopRight}); + } + + /** + * Calculates the position of the white top right module using the output of the rectangle detector + * for a rectangular matrix + */ + private ResultPoint correctTopRightRectangular(ResultPoint bottomLeft, + ResultPoint bottomRight, + ResultPoint topLeft, + ResultPoint topRight, + int dimensionTop, + int dimensionRight) { + + float corr = distance(bottomLeft, bottomRight) / (float) dimensionTop; + int norm = distance(topLeft, topRight); + float cos = (topRight.getX() - topLeft.getX()) / norm; + float sin = (topRight.getY() - topLeft.getY()) / norm; + + ResultPoint c1 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + + corr = distance(bottomLeft, topLeft) / (float) dimensionRight; + norm = distance(bottomRight, topRight); + cos = (topRight.getX() - bottomRight.getX()) / norm; + sin = (topRight.getY() - bottomRight.getY()) / norm; + + ResultPoint c2 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + + if (!isValid(c1)) { + if (isValid(c2)) { + return c2; + } + return null; + } + if (!isValid(c2)) { + return c1; + } + + int l1 = Math.abs(dimensionTop - transitionsBetween(topLeft, c1).getTransitions()) + + Math.abs(dimensionRight - transitionsBetween(bottomRight, c1).getTransitions()); + int l2 = Math.abs(dimensionTop - transitionsBetween(topLeft, c2).getTransitions()) + + Math.abs(dimensionRight - transitionsBetween(bottomRight, c2).getTransitions()); + + if (l1 <= l2) { + return c1; + } + + return c2; + } + + /** + * Calculates the position of the white top right module using the output of the rectangle detector + * for a square matrix + */ + private ResultPoint correctTopRight(ResultPoint bottomLeft, + ResultPoint bottomRight, + ResultPoint topLeft, + ResultPoint topRight, + int dimension) { + + float corr = distance(bottomLeft, bottomRight) / (float) dimension; + int norm = distance(topLeft, topRight); + float cos = (topRight.getX() - topLeft.getX()) / norm; + float sin = (topRight.getY() - topLeft.getY()) / norm; + + ResultPoint c1 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + + corr = distance(bottomLeft, topLeft) / (float) dimension; + norm = distance(bottomRight, topRight); + cos = (topRight.getX() - bottomRight.getX()) / norm; + sin = (topRight.getY() - bottomRight.getY()) / norm; + + ResultPoint c2 = new ResultPoint(topRight.getX() + corr * cos, topRight.getY() + corr * sin); + + if (!isValid(c1)) { + if (isValid(c2)) { + return c2; + } + return null; + } + if (!isValid(c2)) { + return c1; + } + + int l1 = Math.abs(transitionsBetween(topLeft, c1).getTransitions() - + transitionsBetween(bottomRight, c1).getTransitions()); + int l2 = Math.abs(transitionsBetween(topLeft, c2).getTransitions() - + transitionsBetween(bottomRight, c2).getTransitions()); + + return l1 <= l2 ? c1 : c2; + } + + private boolean isValid(ResultPoint p) { + return p.getX() >= 0 && p.getX() < image.getWidth() && p.getY() > 0 && p.getY() < image.getHeight(); + } + + private static int distance(ResultPoint a, ResultPoint b) { + return MathUtils.round(ResultPoint.distance(a, b)); + } + + /** + * Increments the Integer associated with a key by one. + */ + private static void increment(Map table, ResultPoint key) { + Integer value = table.get(key); + table.put(key, value == null ? 1 : value + 1); + } + + private static BitMatrix sampleGrid(BitMatrix image, + ResultPoint topLeft, + ResultPoint bottomLeft, + ResultPoint bottomRight, + ResultPoint topRight, + int dimensionX, + int dimensionY) throws NotFoundException { + + GridSampler sampler = GridSampler.getInstance(); + + return sampler.sampleGrid(image, + dimensionX, + dimensionY, + 0.5f, + 0.5f, + dimensionX - 0.5f, + 0.5f, + dimensionX - 0.5f, + dimensionY - 0.5f, + 0.5f, + dimensionY - 0.5f, + topLeft.getX(), + topLeft.getY(), + topRight.getX(), + topRight.getY(), + bottomRight.getX(), + bottomRight.getY(), + bottomLeft.getX(), + bottomLeft.getY()); + } + + /** + * Counts the number of black/white transitions between two points, using something like Bresenham's algorithm. + */ + private ResultPointsAndTransitions transitionsBetween(ResultPoint from, ResultPoint to) { + // See QR Code Detector, sizeOfBlackWhiteBlackRun() + int fromX = (int) from.getX(); + int fromY = (int) from.getY(); + int toX = (int) to.getX(); + int toY = (int) to.getY(); + boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX); + if (steep) { + int temp = fromX; + fromX = fromY; + fromY = temp; + temp = toX; + toX = toY; + toY = temp; + } + + int dx = Math.abs(toX - fromX); + int dy = Math.abs(toY - fromY); + int error = -dx / 2; + int ystep = fromY < toY ? 1 : -1; + int xstep = fromX < toX ? 1 : -1; + int transitions = 0; + boolean inBlack = image.get(steep ? fromY : fromX, steep ? fromX : fromY); + for (int x = fromX, y = fromY; x != toX; x += xstep) { + boolean isBlack = image.get(steep ? y : x, steep ? x : y); + if (isBlack != inBlack) { + transitions++; + inBlack = isBlack; + } + error += dy; + if (error > 0) { + if (y == toY) { + break; + } + y += ystep; + error -= dx; + } + } + return new ResultPointsAndTransitions(from, to, transitions); + } + + /** + * Simply encapsulates two points and a number of transitions between them. + */ + private static final class ResultPointsAndTransitions { + + private final ResultPoint from; + private final ResultPoint to; + private final int transitions; + + private ResultPointsAndTransitions(ResultPoint from, ResultPoint to, int transitions) { + this.from = from; + this.to = to; + this.transitions = transitions; + } + + ResultPoint getFrom() { + return from; + } + + ResultPoint getTo() { + return to; + } + + int getTransitions() { + return transitions; + } + + @Override + public String toString() { + return from + "/" + to + '/' + transitions; + } + } + + /** + * Orders ResultPointsAndTransitions by number of transitions, ascending. + */ + private static final class ResultPointsAndTransitionsComparator + implements Comparator, Serializable { + @Override + public int compare(ResultPointsAndTransitions o1, ResultPointsAndTransitions o2) { + return o1.getTransitions() - o2.getTransitions(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ASCIIEncoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ASCIIEncoder.java new file mode 100644 index 0000000..866c3fe --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ASCIIEncoder.java @@ -0,0 +1,82 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class ASCIIEncoder implements Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.ASCII_ENCODATION; + } + + @Override + public void encode(EncoderContext context) { + //step B + int n = HighLevelEncoder.determineConsecutiveDigitCount(context.getMessage(), context.pos); + if (n >= 2) { + context.writeCodeword(encodeASCIIDigits(context.getMessage().charAt(context.pos), + context.getMessage().charAt(context.pos + 1))); + context.pos += 2; + } else { + char c = context.getCurrentChar(); + int newMode = HighLevelEncoder.lookAheadTest(context.getMessage(), context.pos, getEncodingMode()); + if (newMode != getEncodingMode()) { + switch (newMode) { + case HighLevelEncoder.BASE256_ENCODATION: + context.writeCodeword(HighLevelEncoder.LATCH_TO_BASE256); + context.signalEncoderChange(HighLevelEncoder.BASE256_ENCODATION); + return; + case HighLevelEncoder.C40_ENCODATION: + context.writeCodeword(HighLevelEncoder.LATCH_TO_C40); + context.signalEncoderChange(HighLevelEncoder.C40_ENCODATION); + return; + case HighLevelEncoder.X12_ENCODATION: + context.writeCodeword(HighLevelEncoder.LATCH_TO_ANSIX12); + context.signalEncoderChange(HighLevelEncoder.X12_ENCODATION); + break; + case HighLevelEncoder.TEXT_ENCODATION: + context.writeCodeword(HighLevelEncoder.LATCH_TO_TEXT); + context.signalEncoderChange(HighLevelEncoder.TEXT_ENCODATION); + break; + case HighLevelEncoder.EDIFACT_ENCODATION: + context.writeCodeword(HighLevelEncoder.LATCH_TO_EDIFACT); + context.signalEncoderChange(HighLevelEncoder.EDIFACT_ENCODATION); + break; + default: + throw new IllegalStateException("Illegal mode: " + newMode); + } + } else if (HighLevelEncoder.isExtendedASCII(c)) { + context.writeCodeword(HighLevelEncoder.UPPER_SHIFT); + context.writeCodeword((char) (c - 128 + 1)); + context.pos++; + } else { + context.writeCodeword((char) (c + 1)); + context.pos++; + } + + } + } + + private static char encodeASCIIDigits(char digit1, char digit2) { + if (HighLevelEncoder.isDigit(digit1) && HighLevelEncoder.isDigit(digit2)) { + int num = (digit1 - 48) * 10 + (digit2 - 48); + return (char) (num + 130); + } + throw new IllegalArgumentException("not digits: " + digit1 + digit2); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Base256Encoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Base256Encoder.java new file mode 100644 index 0000000..a0f91de --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Base256Encoder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class Base256Encoder implements Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.BASE256_ENCODATION; + } + + @Override + public void encode(EncoderContext context) { + StringBuilder buffer = new StringBuilder(); + buffer.append('\0'); //Initialize length field + while (context.hasMoreCharacters()) { + char c = context.getCurrentChar(); + buffer.append(c); + + context.pos++; + + int newMode = HighLevelEncoder.lookAheadTest(context.getMessage(), context.pos, getEncodingMode()); + if (newMode != getEncodingMode()) { + // Return to ASCII encodation, which will actually handle latch to new mode + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + break; + } + } + int dataCount = buffer.length() - 1; + int lengthFieldSize = 1; + int currentSize = context.getCodewordCount() + dataCount + lengthFieldSize; + context.updateSymbolInfo(currentSize); + boolean mustPad = (context.getSymbolInfo().getDataCapacity() - currentSize) > 0; + if (context.hasMoreCharacters() || mustPad) { + if (dataCount <= 249) { + buffer.setCharAt(0, (char) dataCount); + } else if (dataCount <= 1555) { + buffer.setCharAt(0, (char) ((dataCount / 250) + 249)); + buffer.insert(1, (char) (dataCount % 250)); + } else { + throw new IllegalStateException( + "Message length not in valid ranges: " + dataCount); + } + } + for (int i = 0, c = buffer.length(); i < c; i++) { + context.writeCodeword(randomize255State( + buffer.charAt(i), context.getCodewordCount() + 1)); + } + } + + private static char randomize255State(char ch, int codewordPosition) { + int pseudoRandom = ((149 * codewordPosition) % 255) + 1; + int tempVariable = ch + pseudoRandom; + if (tempVariable <= 255) { + return (char) tempVariable; + } else { + return (char) (tempVariable - 256); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/C40Encoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/C40Encoder.java new file mode 100644 index 0000000..abc7486 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/C40Encoder.java @@ -0,0 +1,178 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +class C40Encoder implements Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.C40_ENCODATION; + } + + @Override + public void encode(EncoderContext context) { + //step C + StringBuilder buffer = new StringBuilder(); + while (context.hasMoreCharacters()) { + char c = context.getCurrentChar(); + context.pos++; + + int lastCharSize = encodeChar(c, buffer); + + int unwritten = (buffer.length() / 3) * 2; + + int curCodewordCount = context.getCodewordCount() + unwritten; + context.updateSymbolInfo(curCodewordCount); + int available = context.getSymbolInfo().getDataCapacity() - curCodewordCount; + + if (!context.hasMoreCharacters()) { + //Avoid having a single C40 value in the last triplet + StringBuilder removed = new StringBuilder(); + if ((buffer.length() % 3) == 2 && (available < 2 || available > 2)) { + lastCharSize = backtrackOneCharacter(context, buffer, removed, lastCharSize); + } + while ((buffer.length() % 3) == 1 + && ((lastCharSize <= 3 && available != 1) || lastCharSize > 3)) { + lastCharSize = backtrackOneCharacter(context, buffer, removed, lastCharSize); + } + break; + } + + int count = buffer.length(); + if ((count % 3) == 0) { + int newMode = HighLevelEncoder.lookAheadTest(context.getMessage(), context.pos, getEncodingMode()); + if (newMode != getEncodingMode()) { + // Return to ASCII encodation, which will actually handle latch to new mode + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + break; + } + } + } + handleEOD(context, buffer); + } + + private int backtrackOneCharacter(EncoderContext context, + StringBuilder buffer, StringBuilder removed, int lastCharSize) { + int count = buffer.length(); + buffer.delete(count - lastCharSize, count); + context.pos--; + char c = context.getCurrentChar(); + lastCharSize = encodeChar(c, removed); + context.resetSymbolInfo(); //Deal with possible reduction in symbol size + return lastCharSize; + } + + static void writeNextTriplet(EncoderContext context, StringBuilder buffer) { + context.writeCodewords(encodeToCodewords(buffer, 0)); + buffer.delete(0, 3); + } + + /** + * Handle "end of data" situations + * + * @param context the encoder context + * @param buffer the buffer with the remaining encoded characters + */ + void handleEOD(EncoderContext context, StringBuilder buffer) { + int unwritten = (buffer.length() / 3) * 2; + int rest = buffer.length() % 3; + + int curCodewordCount = context.getCodewordCount() + unwritten; + context.updateSymbolInfo(curCodewordCount); + int available = context.getSymbolInfo().getDataCapacity() - curCodewordCount; + + if (rest == 2) { + buffer.append('\0'); //Shift 1 + while (buffer.length() >= 3) { + writeNextTriplet(context, buffer); + } + if (context.hasMoreCharacters()) { + context.writeCodeword(HighLevelEncoder.C40_UNLATCH); + } + } else if (available == 1 && rest == 1) { + while (buffer.length() >= 3) { + writeNextTriplet(context, buffer); + } + if (context.hasMoreCharacters()) { + context.writeCodeword(HighLevelEncoder.C40_UNLATCH); + } + // else no unlatch + context.pos--; + } else if (rest == 0) { + while (buffer.length() >= 3) { + writeNextTriplet(context, buffer); + } + if (available > 0 || context.hasMoreCharacters()) { + context.writeCodeword(HighLevelEncoder.C40_UNLATCH); + } + } else { + throw new IllegalStateException("Unexpected case. Please report!"); + } + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + } + + int encodeChar(char c, StringBuilder sb) { + if (c == ' ') { + sb.append('\3'); + return 1; + } else if (c >= '0' && c <= '9') { + sb.append((char) (c - 48 + 4)); + return 1; + } else if (c >= 'A' && c <= 'Z') { + sb.append((char) (c - 65 + 14)); + return 1; + } else if (c >= '\0' && c <= '\u001f') { + sb.append('\0'); //Shift 1 Set + sb.append(c); + return 2; + } else if (c >= '!' && c <= '/') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 33)); + return 2; + } else if (c >= ':' && c <= '@') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 58 + 15)); + return 2; + } else if (c >= '[' && c <= '_') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 91 + 22)); + return 2; + } else if (c >= '\u0060' && c <= '\u007f') { + sb.append('\2'); //Shift 3 Set + sb.append((char) (c - 96)); + return 2; + } else if (c >= '\u0080') { + sb.append("\1\u001e"); //Shift 2, Upper Shift + int len = 2; + len += encodeChar((char) (c - 128), sb); + return len; + } else { + throw new IllegalArgumentException("Illegal character: " + c); + } + } + + private static String encodeToCodewords(CharSequence sb, int startPos) { + char c1 = sb.charAt(startPos); + char c2 = sb.charAt(startPos + 1); + char c3 = sb.charAt(startPos + 2); + int v = (1600 * c1) + (40 * c2) + c3 + 1; + char cw1 = (char) (v / 256); + char cw2 = (char) (v % 256); + return new String(new char[] {cw1, cw2}); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DataMatrixSymbolInfo144.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DataMatrixSymbolInfo144.java new file mode 100644 index 0000000..3146353 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DataMatrixSymbolInfo144.java @@ -0,0 +1,35 @@ +/* + * Copyright 2006 Jeremias Maerki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class DataMatrixSymbolInfo144 extends SymbolInfo { + + DataMatrixSymbolInfo144() { + super(false, 1558, 620, 22, 22, 36, -1, 62); + } + + @Override + public int getInterleavedBlockCount() { + return 10; + } + + @Override + public int getDataLengthForInterleavedBlock(int index) { + return (index <= 8) ? 156 : 155; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DefaultPlacement.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DefaultPlacement.java new file mode 100644 index 0000000..1463ef1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/DefaultPlacement.java @@ -0,0 +1,198 @@ +/* + * Copyright 2006 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +import java.util.Arrays; + +/** + * Symbol Character Placement Program. Adapted from Annex M.1 in ISO/IEC 16022:2000(E). + */ +public class DefaultPlacement { + + private final CharSequence codewords; + private final int numrows; + private final int numcols; + private final byte[] bits; + + /** + * Main constructor + * + * @param codewords the codewords to place + * @param numcols the number of columns + * @param numrows the number of rows + */ + public DefaultPlacement(CharSequence codewords, int numcols, int numrows) { + this.codewords = codewords; + this.numcols = numcols; + this.numrows = numrows; + this.bits = new byte[numcols * numrows]; + Arrays.fill(this.bits, (byte) -1); //Initialize with "not set" value + } + + final int getNumrows() { + return numrows; + } + + final int getNumcols() { + return numcols; + } + + final byte[] getBits() { + return bits; + } + + public final boolean getBit(int col, int row) { + return bits[row * numcols + col] == 1; + } + + private void setBit(int col, int row, boolean bit) { + bits[row * numcols + col] = (byte) (bit ? 1 : 0); + } + + private boolean hasBit(int col, int row) { + return bits[row * numcols + col] >= 0; + } + + public final void place() { + int pos = 0; + int row = 4; + int col = 0; + + do { + /* repeatedly first check for one of the special corner cases, then... */ + if ((row == numrows) && (col == 0)) { + corner1(pos++); + } + if ((row == numrows - 2) && (col == 0) && ((numcols % 4) != 0)) { + corner2(pos++); + } + if ((row == numrows - 2) && (col == 0) && (numcols % 8 == 4)) { + corner3(pos++); + } + if ((row == numrows + 4) && (col == 2) && ((numcols % 8) == 0)) { + corner4(pos++); + } + /* sweep upward diagonally, inserting successive characters... */ + do { + if ((row < numrows) && (col >= 0) && !hasBit(col, row)) { + utah(row, col, pos++); + } + row -= 2; + col += 2; + } while (row >= 0 && (col < numcols)); + row++; + col += 3; + + /* and then sweep downward diagonally, inserting successive characters, ... */ + do { + if ((row >= 0) && (col < numcols) && !hasBit(col, row)) { + utah(row, col, pos++); + } + row += 2; + col -= 2; + } while ((row < numrows) && (col >= 0)); + row += 3; + col++; + + /* ...until the entire array is scanned */ + } while ((row < numrows) || (col < numcols)); + + /* Lastly, if the lower righthand corner is untouched, fill in fixed pattern */ + if (!hasBit(numcols - 1, numrows - 1)) { + setBit(numcols - 1, numrows - 1, true); + setBit(numcols - 2, numrows - 2, true); + } + } + + private void module(int row, int col, int pos, int bit) { + if (row < 0) { + row += numrows; + col += 4 - ((numrows + 4) % 8); + } + if (col < 0) { + col += numcols; + row += 4 - ((numcols + 4) % 8); + } + // Note the conversion: + int v = codewords.charAt(pos); + v &= 1 << (8 - bit); + setBit(col, row, v != 0); + } + + /** + * Places the 8 bits of a utah-shaped symbol character in ECC200. + * + * @param row the row + * @param col the column + * @param pos character position + */ + private void utah(int row, int col, int pos) { + module(row - 2, col - 2, pos, 1); + module(row - 2, col - 1, pos, 2); + module(row - 1, col - 2, pos, 3); + module(row - 1, col - 1, pos, 4); + module(row - 1, col, pos, 5); + module(row, col - 2, pos, 6); + module(row, col - 1, pos, 7); + module(row, col, pos, 8); + } + + private void corner1(int pos) { + module(numrows - 1, 0, pos, 1); + module(numrows - 1, 1, pos, 2); + module(numrows - 1, 2, pos, 3); + module(0, numcols - 2, pos, 4); + module(0, numcols - 1, pos, 5); + module(1, numcols - 1, pos, 6); + module(2, numcols - 1, pos, 7); + module(3, numcols - 1, pos, 8); + } + + private void corner2(int pos) { + module(numrows - 3, 0, pos, 1); + module(numrows - 2, 0, pos, 2); + module(numrows - 1, 0, pos, 3); + module(0, numcols - 4, pos, 4); + module(0, numcols - 3, pos, 5); + module(0, numcols - 2, pos, 6); + module(0, numcols - 1, pos, 7); + module(1, numcols - 1, pos, 8); + } + + private void corner3(int pos) { + module(numrows - 3, 0, pos, 1); + module(numrows - 2, 0, pos, 2); + module(numrows - 1, 0, pos, 3); + module(0, numcols - 2, pos, 4); + module(0, numcols - 1, pos, 5); + module(1, numcols - 1, pos, 6); + module(2, numcols - 1, pos, 7); + module(3, numcols - 1, pos, 8); + } + + private void corner4(int pos) { + module(numrows - 1, 0, pos, 1); + module(numrows - 1, numcols - 1, pos, 2); + module(0, numcols - 3, pos, 3); + module(0, numcols - 2, pos, 4); + module(0, numcols - 1, pos, 5); + module(1, numcols - 3, pos, 6); + module(1, numcols - 2, pos, 7); + module(1, numcols - 1, pos, 8); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EdifactEncoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EdifactEncoder.java new file mode 100644 index 0000000..3e739b4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EdifactEncoder.java @@ -0,0 +1,138 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class EdifactEncoder implements Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.EDIFACT_ENCODATION; + } + + @Override + public void encode(EncoderContext context) { + //step F + StringBuilder buffer = new StringBuilder(); + while (context.hasMoreCharacters()) { + char c = context.getCurrentChar(); + encodeChar(c, buffer); + context.pos++; + + int count = buffer.length(); + if (count >= 4) { + context.writeCodewords(encodeToCodewords(buffer, 0)); + buffer.delete(0, 4); + + int newMode = HighLevelEncoder.lookAheadTest(context.getMessage(), context.pos, getEncodingMode()); + if (newMode != getEncodingMode()) { + // Return to ASCII encodation, which will actually handle latch to new mode + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + break; + } + } + } + buffer.append((char) 31); //Unlatch + handleEOD(context, buffer); + } + + /** + * Handle "end of data" situations + * + * @param context the encoder context + * @param buffer the buffer with the remaining encoded characters + */ + private static void handleEOD(EncoderContext context, CharSequence buffer) { + try { + int count = buffer.length(); + if (count == 0) { + return; //Already finished + } + if (count == 1) { + //Only an unlatch at the end + context.updateSymbolInfo(); + int available = context.getSymbolInfo().getDataCapacity() - context.getCodewordCount(); + int remaining = context.getRemainingCharacters(); + if (remaining == 0 && available <= 2) { + return; //No unlatch + } + } + + if (count > 4) { + throw new IllegalStateException("Count must not exceed 4"); + } + int restChars = count - 1; + String encoded = encodeToCodewords(buffer, 0); + boolean endOfSymbolReached = !context.hasMoreCharacters(); + boolean restInAscii = endOfSymbolReached && restChars <= 2; + + if (restChars <= 2) { + context.updateSymbolInfo(context.getCodewordCount() + restChars); + int available = context.getSymbolInfo().getDataCapacity() - context.getCodewordCount(); + if (available >= 3) { + restInAscii = false; + context.updateSymbolInfo(context.getCodewordCount() + encoded.length()); + //available = context.symbolInfo.dataCapacity - context.getCodewordCount(); + } + } + + if (restInAscii) { + context.resetSymbolInfo(); + context.pos -= restChars; + } else { + context.writeCodewords(encoded); + } + } finally { + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + } + } + + private static void encodeChar(char c, StringBuilder sb) { + if (c >= ' ' && c <= '?') { + sb.append(c); + } else if (c >= '@' && c <= '^') { + sb.append((char) (c - 64)); + } else { + HighLevelEncoder.illegalCharacter(c); + } + } + + private static String encodeToCodewords(CharSequence sb, int startPos) { + int len = sb.length() - startPos; + if (len == 0) { + throw new IllegalStateException("StringBuilder must not be empty"); + } + char c1 = sb.charAt(startPos); + char c2 = len >= 2 ? sb.charAt(startPos + 1) : 0; + char c3 = len >= 3 ? sb.charAt(startPos + 2) : 0; + char c4 = len >= 4 ? sb.charAt(startPos + 3) : 0; + + int v = (c1 << 18) + (c2 << 12) + (c3 << 6) + c4; + char cw1 = (char) ((v >> 16) & 255); + char cw2 = (char) ((v >> 8) & 255); + char cw3 = (char) (v & 255); + StringBuilder res = new StringBuilder(3); + res.append(cw1); + if (len >= 2) { + res.append(cw2); + } + if (len >= 3) { + res.append(cw3); + } + return res.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Encoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Encoder.java new file mode 100644 index 0000000..7a91e1e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/Encoder.java @@ -0,0 +1,25 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +interface Encoder { + + int getEncodingMode(); + + void encode(EncoderContext context); + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EncoderContext.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EncoderContext.java new file mode 100644 index 0000000..25b5a4c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/EncoderContext.java @@ -0,0 +1,134 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +import com.google.zxing.Dimension; + +import java.nio.charset.Charset; + +final class EncoderContext { + + private final String msg; + private SymbolShapeHint shape; + private Dimension minSize; + private Dimension maxSize; + private final StringBuilder codewords; + int pos; + private int newEncoding; + private SymbolInfo symbolInfo; + private int skipAtEnd; + + EncoderContext(String msg) { + //From this point on Strings are not Unicode anymore! + byte[] msgBinary = msg.getBytes(Charset.forName("ISO-8859-1")); + StringBuilder sb = new StringBuilder(msgBinary.length); + for (int i = 0, c = msgBinary.length; i < c; i++) { + char ch = (char) (msgBinary[i] & 0xff); + if (ch == '?' && msg.charAt(i) != '?') { + throw new IllegalArgumentException("Message contains characters outside ISO-8859-1 encoding."); + } + sb.append(ch); + } + this.msg = sb.toString(); //Not Unicode here! + shape = SymbolShapeHint.FORCE_NONE; + this.codewords = new StringBuilder(msg.length()); + newEncoding = -1; + } + + public void setSymbolShape(SymbolShapeHint shape) { + this.shape = shape; + } + + public void setSizeConstraints(Dimension minSize, Dimension maxSize) { + this.minSize = minSize; + this.maxSize = maxSize; + } + + public String getMessage() { + return this.msg; + } + + public void setSkipAtEnd(int count) { + this.skipAtEnd = count; + } + + public char getCurrentChar() { + return msg.charAt(pos); + } + + public char getCurrent() { + return msg.charAt(pos); + } + + public StringBuilder getCodewords() { + return codewords; + } + + public void writeCodewords(String codewords) { + this.codewords.append(codewords); + } + + public void writeCodeword(char codeword) { + this.codewords.append(codeword); + } + + public int getCodewordCount() { + return this.codewords.length(); + } + + public int getNewEncoding() { + return newEncoding; + } + + public void signalEncoderChange(int encoding) { + this.newEncoding = encoding; + } + + public void resetEncoderSignal() { + this.newEncoding = -1; + } + + public boolean hasMoreCharacters() { + return pos < getTotalMessageCharCount(); + } + + private int getTotalMessageCharCount() { + return msg.length() - skipAtEnd; + } + + public int getRemainingCharacters() { + return getTotalMessageCharCount() - pos; + } + + public SymbolInfo getSymbolInfo() { + return symbolInfo; + } + + public void updateSymbolInfo() { + updateSymbolInfo(getCodewordCount()); + } + + public void updateSymbolInfo(int len) { + if (this.symbolInfo == null || len > this.symbolInfo.getDataCapacity()) { + this.symbolInfo = SymbolInfo.lookup(len, shape, minSize, maxSize, true); + } + } + + public void resetSymbolInfo() { + this.symbolInfo = null; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ErrorCorrection.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ErrorCorrection.java new file mode 100644 index 0000000..d389334 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/ErrorCorrection.java @@ -0,0 +1,184 @@ +/* + * Copyright 2006 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +/** + * Error Correction Code for ECC200. + */ +public final class ErrorCorrection { + + /** + * Lookup table which factors to use for which number of error correction codewords. + * See FACTORS. + */ + private static final int[] FACTOR_SETS + = {5, 7, 10, 11, 12, 14, 18, 20, 24, 28, 36, 42, 48, 56, 62, 68}; + + /** + * Precomputed polynomial factors for ECC 200. + */ + private static final int[][] FACTORS = { + {228, 48, 15, 111, 62}, + {23, 68, 144, 134, 240, 92, 254}, + {28, 24, 185, 166, 223, 248, 116, 255, 110, 61}, + {175, 138, 205, 12, 194, 168, 39, 245, 60, 97, 120}, + {41, 153, 158, 91, 61, 42, 142, 213, 97, 178, 100, 242}, + {156, 97, 192, 252, 95, 9, 157, 119, 138, 45, 18, 186, 83, 185}, + {83, 195, 100, 39, 188, 75, 66, 61, 241, 213, 109, 129, 94, 254, 225, 48, 90, 188}, + {15, 195, 244, 9, 233, 71, 168, 2, 188, 160, 153, 145, 253, 79, 108, 82, 27, 174, 186, 172}, + {52, 190, 88, 205, 109, 39, 176, 21, 155, 197, 251, 223, 155, 21, 5, 172, + 254, 124, 12, 181, 184, 96, 50, 193}, + {211, 231, 43, 97, 71, 96, 103, 174, 37, 151, 170, 53, 75, 34, 249, 121, + 17, 138, 110, 213, 141, 136, 120, 151, 233, 168, 93, 255}, + {245, 127, 242, 218, 130, 250, 162, 181, 102, 120, 84, 179, 220, 251, 80, 182, + 229, 18, 2, 4, 68, 33, 101, 137, 95, 119, 115, 44, 175, 184, 59, 25, + 225, 98, 81, 112}, + {77, 193, 137, 31, 19, 38, 22, 153, 247, 105, 122, 2, 245, 133, 242, 8, + 175, 95, 100, 9, 167, 105, 214, 111, 57, 121, 21, 1, 253, 57, 54, 101, + 248, 202, 69, 50, 150, 177, 226, 5, 9, 5}, + {245, 132, 172, 223, 96, 32, 117, 22, 238, 133, 238, 231, 205, 188, 237, 87, + 191, 106, 16, 147, 118, 23, 37, 90, 170, 205, 131, 88, 120, 100, 66, 138, + 186, 240, 82, 44, 176, 87, 187, 147, 160, 175, 69, 213, 92, 253, 225, 19}, + {175, 9, 223, 238, 12, 17, 220, 208, 100, 29, 175, 170, 230, 192, 215, 235, + 150, 159, 36, 223, 38, 200, 132, 54, 228, 146, 218, 234, 117, 203, 29, 232, + 144, 238, 22, 150, 201, 117, 62, 207, 164, 13, 137, 245, 127, 67, 247, 28, + 155, 43, 203, 107, 233, 53, 143, 46}, + {242, 93, 169, 50, 144, 210, 39, 118, 202, 188, 201, 189, 143, 108, 196, 37, + 185, 112, 134, 230, 245, 63, 197, 190, 250, 106, 185, 221, 175, 64, 114, 71, + 161, 44, 147, 6, 27, 218, 51, 63, 87, 10, 40, 130, 188, 17, 163, 31, + 176, 170, 4, 107, 232, 7, 94, 166, 224, 124, 86, 47, 11, 204}, + {220, 228, 173, 89, 251, 149, 159, 56, 89, 33, 147, 244, 154, 36, 73, 127, + 213, 136, 248, 180, 234, 197, 158, 177, 68, 122, 93, 213, 15, 160, 227, 236, + 66, 139, 153, 185, 202, 167, 179, 25, 220, 232, 96, 210, 231, 136, 223, 239, + 181, 241, 59, 52, 172, 25, 49, 232, 211, 189, 64, 54, 108, 153, 132, 63, + 96, 103, 82, 186}}; + + private static final int MODULO_VALUE = 0x12D; + + private static final int[] LOG; + private static final int[] ALOG; + + static { + //Create log and antilog table + LOG = new int[256]; + ALOG = new int[255]; + + int p = 1; + for (int i = 0; i < 255; i++) { + ALOG[i] = p; + LOG[p] = i; + p *= 2; + if (p >= 256) { + p ^= MODULO_VALUE; + } + } + } + + private ErrorCorrection() { + } + + /** + * Creates the ECC200 error correction for an encoded message. + * + * @param codewords the codewords + * @param symbolInfo information about the symbol to be encoded + * @return the codewords with interleaved error correction. + */ + public static String encodeECC200(String codewords, SymbolInfo symbolInfo) { + if (codewords.length() != symbolInfo.getDataCapacity()) { + throw new IllegalArgumentException( + "The number of codewords does not match the selected symbol"); + } + StringBuilder sb = new StringBuilder(symbolInfo.getDataCapacity() + symbolInfo.getErrorCodewords()); + sb.append(codewords); + int blockCount = symbolInfo.getInterleavedBlockCount(); + if (blockCount == 1) { + String ecc = createECCBlock(codewords, symbolInfo.getErrorCodewords()); + sb.append(ecc); + } else { + sb.setLength(sb.capacity()); + int[] dataSizes = new int[blockCount]; + int[] errorSizes = new int[blockCount]; + int[] startPos = new int[blockCount]; + for (int i = 0; i < blockCount; i++) { + dataSizes[i] = symbolInfo.getDataLengthForInterleavedBlock(i + 1); + errorSizes[i] = symbolInfo.getErrorLengthForInterleavedBlock(i + 1); + startPos[i] = 0; + if (i > 0) { + startPos[i] = startPos[i - 1] + dataSizes[i]; + } + } + for (int block = 0; block < blockCount; block++) { + StringBuilder temp = new StringBuilder(dataSizes[block]); + for (int d = block; d < symbolInfo.getDataCapacity(); d += blockCount) { + temp.append(codewords.charAt(d)); + } + String ecc = createECCBlock(temp.toString(), errorSizes[block]); + int pos = 0; + for (int e = block; e < errorSizes[block] * blockCount; e += blockCount) { + sb.setCharAt(symbolInfo.getDataCapacity() + e, ecc.charAt(pos++)); + } + } + } + return sb.toString(); + + } + + private static String createECCBlock(CharSequence codewords, int numECWords) { + return createECCBlock(codewords, 0, codewords.length(), numECWords); + } + + private static String createECCBlock(CharSequence codewords, int start, int len, int numECWords) { + int table = -1; + for (int i = 0; i < FACTOR_SETS.length; i++) { + if (FACTOR_SETS[i] == numECWords) { + table = i; + break; + } + } + if (table < 0) { + throw new IllegalArgumentException( + "Illegal number of error correction codewords specified: " + numECWords); + } + int[] poly = FACTORS[table]; + char[] ecc = new char[numECWords]; + for (int i = 0; i < numECWords; i++) { + ecc[i] = 0; + } + for (int i = start; i < start + len; i++) { + int m = ecc[numECWords - 1] ^ codewords.charAt(i); + for (int k = numECWords - 1; k > 0; k--) { + if (m != 0 && poly[k] != 0) { + ecc[k] = (char) (ecc[k - 1] ^ ALOG[(LOG[m] + LOG[poly[k]]) % 255]); + } else { + ecc[k] = ecc[k - 1]; + } + } + if (m != 0 && poly[0] != 0) { + ecc[0] = (char) ALOG[(LOG[m] + LOG[poly[0]]) % 255]; + } else { + ecc[0] = 0; + } + } + char[] eccReversed = new char[numECWords]; + for (int i = 0; i < numECWords; i++) { + eccReversed[i] = ecc[numECWords - i - 1]; + } + return String.valueOf(eccReversed); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/HighLevelEncoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/HighLevelEncoder.java new file mode 100644 index 0000000..76cc49b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/HighLevelEncoder.java @@ -0,0 +1,446 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +import com.google.zxing.Dimension; + +import java.util.Arrays; + +/** + * DataMatrix ECC 200 data encoder following the algorithm described in ISO/IEC 16022:200(E) in + * annex S. + */ +public final class HighLevelEncoder { + + /** + * Padding character + */ + private static final char PAD = 129; + /** + * mode latch to C40 encodation mode + */ + static final char LATCH_TO_C40 = 230; + /** + * mode latch to Base 256 encodation mode + */ + static final char LATCH_TO_BASE256 = 231; + /** + * FNC1 Codeword + */ + //private static final char FNC1 = 232; + /** + * Structured Append Codeword + */ + //private static final char STRUCTURED_APPEND = 233; + /** + * Reader Programming + */ + //private static final char READER_PROGRAMMING = 234; + /** + * Upper Shift + */ + static final char UPPER_SHIFT = 235; + /** + * 05 Macro + */ + private static final char MACRO_05 = 236; + /** + * 06 Macro + */ + private static final char MACRO_06 = 237; + /** + * mode latch to ANSI X.12 encodation mode + */ + static final char LATCH_TO_ANSIX12 = 238; + /** + * mode latch to Text encodation mode + */ + static final char LATCH_TO_TEXT = 239; + /** + * mode latch to EDIFACT encodation mode + */ + static final char LATCH_TO_EDIFACT = 240; + /** + * ECI character (Extended Channel Interpretation) + */ + //private static final char ECI = 241; + + /** + * Unlatch from C40 encodation + */ + static final char C40_UNLATCH = 254; + /** + * Unlatch from X12 encodation + */ + static final char X12_UNLATCH = 254; + + /** + * 05 Macro header + */ + private static final String MACRO_05_HEADER = "[)>\u001E05\u001D"; + /** + * 06 Macro header + */ + private static final String MACRO_06_HEADER = "[)>\u001E06\u001D"; + /** + * Macro trailer + */ + private static final String MACRO_TRAILER = "\u001E\u0004"; + + static final int ASCII_ENCODATION = 0; + static final int C40_ENCODATION = 1; + static final int TEXT_ENCODATION = 2; + static final int X12_ENCODATION = 3; + static final int EDIFACT_ENCODATION = 4; + static final int BASE256_ENCODATION = 5; + + private HighLevelEncoder() { + } + + /* + * Converts the message to a byte array using the default encoding (cp437) as defined by the + * specification + * + * @param msg the message + * @return the byte array of the message + */ + + /* + public static byte[] getBytesForMessage(String msg) { + return msg.getBytes(Charset.forName("cp437")); //See 4.4.3 and annex B of ISO/IEC 15438:2001(E) + } + */ + + private static char randomize253State(char ch, int codewordPosition) { + int pseudoRandom = ((149 * codewordPosition) % 253) + 1; + int tempVariable = ch + pseudoRandom; + return (char) (tempVariable <= 254 ? tempVariable : tempVariable - 254); + } + + /** + * Performs message encoding of a DataMatrix message using the algorithm described in annex P + * of ISO/IEC 16022:2000(E). + * + * @param msg the message + * @return the encoded message (the char values range from 0 to 255) + */ + public static String encodeHighLevel(String msg) { + return encodeHighLevel(msg, SymbolShapeHint.FORCE_NONE, null, null); + } + + /** + * Performs message encoding of a DataMatrix message using the algorithm described in annex P + * of ISO/IEC 16022:2000(E). + * + * @param msg the message + * @param shape requested shape. May be {@code SymbolShapeHint.FORCE_NONE}, + * {@code SymbolShapeHint.FORCE_SQUARE} or {@code SymbolShapeHint.FORCE_RECTANGLE}. + * @param minSize the minimum symbol size constraint or null for no constraint + * @param maxSize the maximum symbol size constraint or null for no constraint + * @return the encoded message (the char values range from 0 to 255) + */ + public static String encodeHighLevel(String msg, + SymbolShapeHint shape, + Dimension minSize, + Dimension maxSize) { + //the codewords 0..255 are encoded as Unicode characters + Encoder[] encoders = { + new ASCIIEncoder(), new C40Encoder(), new TextEncoder(), + new X12Encoder(), new EdifactEncoder(), new Base256Encoder() + }; + + EncoderContext context = new EncoderContext(msg); + context.setSymbolShape(shape); + context.setSizeConstraints(minSize, maxSize); + + if (msg.startsWith(MACRO_05_HEADER) && msg.endsWith(MACRO_TRAILER)) { + context.writeCodeword(MACRO_05); + context.setSkipAtEnd(2); + context.pos += MACRO_05_HEADER.length(); + } else if (msg.startsWith(MACRO_06_HEADER) && msg.endsWith(MACRO_TRAILER)) { + context.writeCodeword(MACRO_06); + context.setSkipAtEnd(2); + context.pos += MACRO_06_HEADER.length(); + } + + int encodingMode = ASCII_ENCODATION; //Default mode + while (context.hasMoreCharacters()) { + encoders[encodingMode].encode(context); + if (context.getNewEncoding() >= 0) { + encodingMode = context.getNewEncoding(); + context.resetEncoderSignal(); + } + } + int len = context.getCodewordCount(); + context.updateSymbolInfo(); + int capacity = context.getSymbolInfo().getDataCapacity(); + if (len < capacity && encodingMode != ASCII_ENCODATION && encodingMode != BASE256_ENCODATION) { + context.writeCodeword('\u00fe'); //Unlatch (254) + } + //Padding + StringBuilder codewords = context.getCodewords(); + if (codewords.length() < capacity) { + codewords.append(PAD); + } + while (codewords.length() < capacity) { + codewords.append(randomize253State(PAD, codewords.length() + 1)); + } + + return context.getCodewords().toString(); + } + + static int lookAheadTest(CharSequence msg, int startpos, int currentMode) { + if (startpos >= msg.length()) { + return currentMode; + } + float[] charCounts; + //step J + if (currentMode == ASCII_ENCODATION) { + charCounts = new float[]{0, 1, 1, 1, 1, 1.25f}; + } else { + charCounts = new float[]{1, 2, 2, 2, 2, 2.25f}; + charCounts[currentMode] = 0; + } + + int charsProcessed = 0; + while (true) { + //step K + if ((startpos + charsProcessed) == msg.length()) { + int min = Integer.MAX_VALUE; + byte[] mins = new byte[6]; + int[] intCharCounts = new int[6]; + min = findMinimums(charCounts, intCharCounts, min, mins); + int minCount = getMinimumCount(mins); + + if (intCharCounts[ASCII_ENCODATION] == min) { + return ASCII_ENCODATION; + } + if (minCount == 1 && mins[BASE256_ENCODATION] > 0) { + return BASE256_ENCODATION; + } + if (minCount == 1 && mins[EDIFACT_ENCODATION] > 0) { + return EDIFACT_ENCODATION; + } + if (minCount == 1 && mins[TEXT_ENCODATION] > 0) { + return TEXT_ENCODATION; + } + if (minCount == 1 && mins[X12_ENCODATION] > 0) { + return X12_ENCODATION; + } + return C40_ENCODATION; + } + + char c = msg.charAt(startpos + charsProcessed); + charsProcessed++; + + //step L + if (isDigit(c)) { + charCounts[ASCII_ENCODATION] += 0.5f; + } else if (isExtendedASCII(c)) { + charCounts[ASCII_ENCODATION] = (float) Math.ceil(charCounts[ASCII_ENCODATION]); + charCounts[ASCII_ENCODATION] += 2.0f; + } else { + charCounts[ASCII_ENCODATION] = (float) Math.ceil(charCounts[ASCII_ENCODATION]); + charCounts[ASCII_ENCODATION]++; + } + + //step M + if (isNativeC40(c)) { + charCounts[C40_ENCODATION] += 2.0f / 3.0f; + } else if (isExtendedASCII(c)) { + charCounts[C40_ENCODATION] += 8.0f / 3.0f; + } else { + charCounts[C40_ENCODATION] += 4.0f / 3.0f; + } + + //step N + if (isNativeText(c)) { + charCounts[TEXT_ENCODATION] += 2.0f / 3.0f; + } else if (isExtendedASCII(c)) { + charCounts[TEXT_ENCODATION] += 8.0f / 3.0f; + } else { + charCounts[TEXT_ENCODATION] += 4.0f / 3.0f; + } + + //step O + if (isNativeX12(c)) { + charCounts[X12_ENCODATION] += 2.0f / 3.0f; + } else if (isExtendedASCII(c)) { + charCounts[X12_ENCODATION] += 13.0f / 3.0f; + } else { + charCounts[X12_ENCODATION] += 10.0f / 3.0f; + } + + //step P + if (isNativeEDIFACT(c)) { + charCounts[EDIFACT_ENCODATION] += 3.0f / 4.0f; + } else if (isExtendedASCII(c)) { + charCounts[EDIFACT_ENCODATION] += 17.0f / 4.0f; + } else { + charCounts[EDIFACT_ENCODATION] += 13.0f / 4.0f; + } + + // step Q + if (isSpecialB256(c)) { + charCounts[BASE256_ENCODATION] += 4.0f; + } else { + charCounts[BASE256_ENCODATION]++; + } + + //step R + if (charsProcessed >= 4) { + int[] intCharCounts = new int[6]; + byte[] mins = new byte[6]; + findMinimums(charCounts, intCharCounts, Integer.MAX_VALUE, mins); + int minCount = getMinimumCount(mins); + + if (intCharCounts[ASCII_ENCODATION] < intCharCounts[BASE256_ENCODATION] + && intCharCounts[ASCII_ENCODATION] < intCharCounts[C40_ENCODATION] + && intCharCounts[ASCII_ENCODATION] < intCharCounts[TEXT_ENCODATION] + && intCharCounts[ASCII_ENCODATION] < intCharCounts[X12_ENCODATION] + && intCharCounts[ASCII_ENCODATION] < intCharCounts[EDIFACT_ENCODATION]) { + return ASCII_ENCODATION; + } + if (intCharCounts[BASE256_ENCODATION] < intCharCounts[ASCII_ENCODATION] + || (mins[C40_ENCODATION] + mins[TEXT_ENCODATION] + mins[X12_ENCODATION] + mins[EDIFACT_ENCODATION]) == 0) { + return BASE256_ENCODATION; + } + if (minCount == 1 && mins[EDIFACT_ENCODATION] > 0) { + return EDIFACT_ENCODATION; + } + if (minCount == 1 && mins[TEXT_ENCODATION] > 0) { + return TEXT_ENCODATION; + } + if (minCount == 1 && mins[X12_ENCODATION] > 0) { + return X12_ENCODATION; + } + if (intCharCounts[C40_ENCODATION] + 1 < intCharCounts[ASCII_ENCODATION] + && intCharCounts[C40_ENCODATION] + 1 < intCharCounts[BASE256_ENCODATION] + && intCharCounts[C40_ENCODATION] + 1 < intCharCounts[EDIFACT_ENCODATION] + && intCharCounts[C40_ENCODATION] + 1 < intCharCounts[TEXT_ENCODATION]) { + if (intCharCounts[C40_ENCODATION] < intCharCounts[X12_ENCODATION]) { + return C40_ENCODATION; + } + if (intCharCounts[C40_ENCODATION] == intCharCounts[X12_ENCODATION]) { + int p = startpos + charsProcessed + 1; + while (p < msg.length()) { + char tc = msg.charAt(p); + if (isX12TermSep(tc)) { + return X12_ENCODATION; + } + if (!isNativeX12(tc)) { + break; + } + p++; + } + return C40_ENCODATION; + } + } + } + } + } + + private static int findMinimums(float[] charCounts, int[] intCharCounts, int min, byte[] mins) { + Arrays.fill(mins, (byte) 0); + for (int i = 0; i < 6; i++) { + intCharCounts[i] = (int) Math.ceil(charCounts[i]); + int current = intCharCounts[i]; + if (min > current) { + min = current; + Arrays.fill(mins, (byte) 0); + } + if (min == current) { + mins[i]++; + + } + } + return min; + } + + private static int getMinimumCount(byte[] mins) { + int minCount = 0; + for (int i = 0; i < 6; i++) { + minCount += mins[i]; + } + return minCount; + } + + static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + + static boolean isExtendedASCII(char ch) { + return ch >= 128 && ch <= 255; + } + + private static boolean isNativeC40(char ch) { + return (ch == ' ') || (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z'); + } + + private static boolean isNativeText(char ch) { + return (ch == ' ') || (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'z'); + } + + private static boolean isNativeX12(char ch) { + return isX12TermSep(ch) || (ch == ' ') || (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z'); + } + + private static boolean isX12TermSep(char ch) { + return (ch == '\r') //CR + || (ch == '*') + || (ch == '>'); + } + + private static boolean isNativeEDIFACT(char ch) { + return ch >= ' ' && ch <= '^'; + } + + private static boolean isSpecialB256(char ch) { + return false; //TODO NOT IMPLEMENTED YET!!! + } + + /** + * Determines the number of consecutive characters that are encodable using numeric compaction. + * + * @param msg the message + * @param startpos the start position within the message + * @return the requested character count + */ + public static int determineConsecutiveDigitCount(CharSequence msg, int startpos) { + int count = 0; + int len = msg.length(); + int idx = startpos; + if (idx < len) { + char ch = msg.charAt(idx); + while (isDigit(ch) && idx < len) { + count++; + idx++; + if (idx < len) { + ch = msg.charAt(idx); + } + } + } + return count; + } + + static void illegalCharacter(char c) { + String hex = Integer.toHexString(c); + hex = "0000".substring(0, 4 - hex.length()) + hex; + throw new IllegalArgumentException("Illegal character: " + c + " (0x" + hex + ')'); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolInfo.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolInfo.java new file mode 100644 index 0000000..d57ffd0 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolInfo.java @@ -0,0 +1,236 @@ +/* + * Copyright 2006 Jeremias Maerki + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +import com.google.zxing.Dimension; + +/** + * Symbol info table for DataMatrix. + * + * @version $Id$ + */ +public class SymbolInfo { + + static final SymbolInfo[] PROD_SYMBOLS = { + new SymbolInfo(false, 3, 5, 8, 8, 1), + new SymbolInfo(false, 5, 7, 10, 10, 1), + /*rect*/new SymbolInfo(true, 5, 7, 16, 6, 1), + new SymbolInfo(false, 8, 10, 12, 12, 1), + /*rect*/new SymbolInfo(true, 10, 11, 14, 6, 2), + new SymbolInfo(false, 12, 12, 14, 14, 1), + /*rect*/new SymbolInfo(true, 16, 14, 24, 10, 1), + + new SymbolInfo(false, 18, 14, 16, 16, 1), + new SymbolInfo(false, 22, 18, 18, 18, 1), + /*rect*/new SymbolInfo(true, 22, 18, 16, 10, 2), + new SymbolInfo(false, 30, 20, 20, 20, 1), + /*rect*/new SymbolInfo(true, 32, 24, 16, 14, 2), + new SymbolInfo(false, 36, 24, 22, 22, 1), + new SymbolInfo(false, 44, 28, 24, 24, 1), + /*rect*/new SymbolInfo(true, 49, 28, 22, 14, 2), + + new SymbolInfo(false, 62, 36, 14, 14, 4), + new SymbolInfo(false, 86, 42, 16, 16, 4), + new SymbolInfo(false, 114, 48, 18, 18, 4), + new SymbolInfo(false, 144, 56, 20, 20, 4), + new SymbolInfo(false, 174, 68, 22, 22, 4), + + new SymbolInfo(false, 204, 84, 24, 24, 4, 102, 42), + new SymbolInfo(false, 280, 112, 14, 14, 16, 140, 56), + new SymbolInfo(false, 368, 144, 16, 16, 16, 92, 36), + new SymbolInfo(false, 456, 192, 18, 18, 16, 114, 48), + new SymbolInfo(false, 576, 224, 20, 20, 16, 144, 56), + new SymbolInfo(false, 696, 272, 22, 22, 16, 174, 68), + new SymbolInfo(false, 816, 336, 24, 24, 16, 136, 56), + new SymbolInfo(false, 1050, 408, 18, 18, 36, 175, 68), + new SymbolInfo(false, 1304, 496, 20, 20, 36, 163, 62), + new DataMatrixSymbolInfo144(), + }; + + private static SymbolInfo[] symbols = PROD_SYMBOLS; + + private final boolean rectangular; + private final int dataCapacity; + private final int errorCodewords; + public final int matrixWidth; + public final int matrixHeight; + private final int dataRegions; + private final int rsBlockData; + private final int rsBlockError; + + /** + * Overrides the symbol info set used by this class. Used for testing purposes. + * + * @param override the symbol info set to use + */ + public static void overrideSymbolSet(SymbolInfo[] override) { + symbols = override; + } + + public SymbolInfo(boolean rectangular, int dataCapacity, int errorCodewords, + int matrixWidth, int matrixHeight, int dataRegions) { + this(rectangular, dataCapacity, errorCodewords, matrixWidth, matrixHeight, dataRegions, + dataCapacity, errorCodewords); + } + + SymbolInfo(boolean rectangular, int dataCapacity, int errorCodewords, + int matrixWidth, int matrixHeight, int dataRegions, + int rsBlockData, int rsBlockError) { + this.rectangular = rectangular; + this.dataCapacity = dataCapacity; + this.errorCodewords = errorCodewords; + this.matrixWidth = matrixWidth; + this.matrixHeight = matrixHeight; + this.dataRegions = dataRegions; + this.rsBlockData = rsBlockData; + this.rsBlockError = rsBlockError; + } + + public static SymbolInfo lookup(int dataCodewords) { + return lookup(dataCodewords, SymbolShapeHint.FORCE_NONE, true); + } + + public static SymbolInfo lookup(int dataCodewords, SymbolShapeHint shape) { + return lookup(dataCodewords, shape, true); + } + + public static SymbolInfo lookup(int dataCodewords, boolean allowRectangular, boolean fail) { + SymbolShapeHint shape = allowRectangular + ? SymbolShapeHint.FORCE_NONE : SymbolShapeHint.FORCE_SQUARE; + return lookup(dataCodewords, shape, fail); + } + + private static SymbolInfo lookup(int dataCodewords, SymbolShapeHint shape, boolean fail) { + return lookup(dataCodewords, shape, null, null, fail); + } + + public static SymbolInfo lookup(int dataCodewords, + SymbolShapeHint shape, + Dimension minSize, + Dimension maxSize, + boolean fail) { + for (SymbolInfo symbol : symbols) { + if (shape == SymbolShapeHint.FORCE_SQUARE && symbol.rectangular) { + continue; + } + if (shape == SymbolShapeHint.FORCE_RECTANGLE && !symbol.rectangular) { + continue; + } + if (minSize != null + && (symbol.getSymbolWidth() < minSize.getWidth() + || symbol.getSymbolHeight() < minSize.getHeight())) { + continue; + } + if (maxSize != null + && (symbol.getSymbolWidth() > maxSize.getWidth() + || symbol.getSymbolHeight() > maxSize.getHeight())) { + continue; + } + if (dataCodewords <= symbol.dataCapacity) { + return symbol; + } + } + if (fail) { + throw new IllegalArgumentException( + "Can't find a symbol arrangement that matches the message. Data codewords: " + + dataCodewords); + } + return null; + } + + private int getHorizontalDataRegions() { + switch (dataRegions) { + case 1: + return 1; + case 2: + case 4: + return 2; + case 16: + return 4; + case 36: + return 6; + default: + throw new IllegalStateException("Cannot handle this number of data regions"); + } + } + + private int getVerticalDataRegions() { + switch (dataRegions) { + case 1: + case 2: + return 1; + case 4: + return 2; + case 16: + return 4; + case 36: + return 6; + default: + throw new IllegalStateException("Cannot handle this number of data regions"); + } + } + + public final int getSymbolDataWidth() { + return getHorizontalDataRegions() * matrixWidth; + } + + public final int getSymbolDataHeight() { + return getVerticalDataRegions() * matrixHeight; + } + + public final int getSymbolWidth() { + return getSymbolDataWidth() + (getHorizontalDataRegions() * 2); + } + + public final int getSymbolHeight() { + return getSymbolDataHeight() + (getVerticalDataRegions() * 2); + } + + public int getCodewordCount() { + return dataCapacity + errorCodewords; + } + + public int getInterleavedBlockCount() { + return dataCapacity / rsBlockData; + } + + public final int getDataCapacity() { + return dataCapacity; + } + + public final int getErrorCodewords() { + return errorCodewords; + } + + public int getDataLengthForInterleavedBlock(int index) { + return rsBlockData; + } + + public final int getErrorLengthForInterleavedBlock(int index) { + return rsBlockError; + } + + @Override + public final String toString() { + return (rectangular ? "Rectangular Symbol:" : "Square Symbol:") + + " data region " + matrixWidth + 'x' + matrixHeight + + ", symbol size " + getSymbolWidth() + 'x' + getSymbolHeight() + + ", symbol data size " + getSymbolDataWidth() + 'x' + getSymbolDataHeight() + + ", codewords " + dataCapacity + '+' + errorCodewords; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolShapeHint.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolShapeHint.java new file mode 100644 index 0000000..898727f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/SymbolShapeHint.java @@ -0,0 +1,29 @@ +/* + * Copyright 2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +/** + * Enumeration for DataMatrix symbol shape hint. It can be used to force square or rectangular + * symbols. + */ +public enum SymbolShapeHint { + + FORCE_NONE, + FORCE_SQUARE, + FORCE_RECTANGLE, + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/TextEncoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/TextEncoder.java new file mode 100644 index 0000000..19af016 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/TextEncoder.java @@ -0,0 +1,85 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class TextEncoder extends C40Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.TEXT_ENCODATION; + } + + @Override + int encodeChar(char c, StringBuilder sb) { + if (c == ' ') { + sb.append('\3'); + return 1; + } + if (c >= '0' && c <= '9') { + sb.append((char) (c - 48 + 4)); + return 1; + } + if (c >= 'a' && c <= 'z') { + sb.append((char) (c - 97 + 14)); + return 1; + } + if (c >= '\0' && c <= '\u001f') { + sb.append('\0'); //Shift 1 Set + sb.append(c); + return 2; + } + if (c >= '!' && c <= '/') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 33)); + return 2; + } + if (c >= ':' && c <= '@') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 58 + 15)); + return 2; + } + if (c >= '[' && c <= '_') { + sb.append('\1'); //Shift 2 Set + sb.append((char) (c - 91 + 22)); + return 2; + } + if (c == '\u0060') { + sb.append('\2'); //Shift 3 Set + sb.append((char) (c - 96)); + return 2; + } + if (c >= 'A' && c <= 'Z') { + sb.append('\2'); //Shift 3 Set + sb.append((char) (c - 65 + 1)); + return 2; + } + if (c >= '{' && c <= '\u007f') { + sb.append('\2'); //Shift 3 Set + sb.append((char) (c - 123 + 27)); + return 2; + } + if (c >= '\u0080') { + sb.append("\1\u001e"); //Shift 2, Upper Shift + int len = 2; + len += encodeChar((char) (c - 128), sb); + return len; + } + HighLevelEncoder.illegalCharacter(c); + return -1; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/X12Encoder.java b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/X12Encoder.java new file mode 100644 index 0000000..79daf49 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/datamatrix/encoder/X12Encoder.java @@ -0,0 +1,93 @@ +/* + * Copyright 2006-2007 Jeremias Maerki. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.datamatrix.encoder; + +final class X12Encoder extends C40Encoder { + + @Override + public int getEncodingMode() { + return HighLevelEncoder.X12_ENCODATION; + } + + @Override + public void encode(EncoderContext context) { + //step C + StringBuilder buffer = new StringBuilder(); + while (context.hasMoreCharacters()) { + char c = context.getCurrentChar(); + context.pos++; + + encodeChar(c, buffer); + + int count = buffer.length(); + if ((count % 3) == 0) { + writeNextTriplet(context, buffer); + + int newMode = HighLevelEncoder.lookAheadTest(context.getMessage(), context.pos, getEncodingMode()); + if (newMode != getEncodingMode()) { + // Return to ASCII encodation, which will actually handle latch to new mode + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + break; + } + } + } + handleEOD(context, buffer); + } + + @Override + int encodeChar(char c, StringBuilder sb) { + switch (c) { + case '\r': + sb.append('\0'); + break; + case '*': + sb.append('\1'); + break; + case '>': + sb.append('\2'); + break; + case ' ': + sb.append('\3'); + break; + default: + if (c >= '0' && c <= '9') { + sb.append((char) (c - 48 + 4)); + } else if (c >= 'A' && c <= 'Z') { + sb.append((char) (c - 65 + 14)); + } else { + HighLevelEncoder.illegalCharacter(c); + } + break; + } + return 1; + } + + @Override + void handleEOD(EncoderContext context, StringBuilder buffer) { + context.updateSymbolInfo(); + int available = context.getSymbolInfo().getDataCapacity() - context.getCodewordCount(); + int count = buffer.length(); + context.pos -= count; + if (context.getRemainingCharacters() > 1 || available > 1 || + context.getRemainingCharacters() != available) { + context.writeCodeword(HighLevelEncoder.X12_UNLATCH); + } + if (context.getNewEncoding() < 0) { + context.signalEncoderChange(HighLevelEncoder.ASCII_ENCODATION); + } + } +} diff --git a/rubylib/src/main/java/com/google/zxing/maxicode/MaxiCodeReader.java b/rubylib/src/main/java/com/google/zxing/maxicode/MaxiCodeReader.java new file mode 100644 index 0000000..a9134e3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/maxicode/MaxiCodeReader.java @@ -0,0 +1,125 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.maxicode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.maxicode.decoder.Decoder; + +import java.util.Map; + +/** + * This implementation can detect and decode a MaxiCode in an image. + */ +public final class MaxiCodeReader implements Reader { + + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + private static final int MATRIX_WIDTH = 30; + private static final int MATRIX_HEIGHT = 33; + + private final Decoder decoder = new Decoder(); + + /* + Decoder getDecoder() { + return decoder; + } + */ + + /** + * Locates and decodes a MaxiCode in an image. + * + * @return a String representing the content encoded by the MaxiCode + * @throws NotFoundException if a MaxiCode cannot be found + * @throws FormatException if a MaxiCode cannot be decoded + * @throws ChecksumException if error correction fails + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, ChecksumException, FormatException { + DecoderResult decoderResult; + if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) { + BitMatrix bits = extractPureBits(image.getBlackMatrix()); + decoderResult = decoder.decode(bits, hints); + } else { + throw NotFoundException.getNotFoundInstance(); + } + + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), NO_POINTS, BarcodeFormat.MAXICODE); + + String ecLevel = decoderResult.getECLevel(); + if (ecLevel != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); + } + return result; + } + + @Override + public void reset() { + // do nothing + } + + /** + * This method detects a code in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a code, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + * + * @see com.google.zxing.datamatrix.DataMatrixReader#extractPureBits(BitMatrix) + * @see com.google.zxing.qrcode.QRCodeReader#extractPureBits(BitMatrix) + */ + private static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException { + + int[] enclosingRectangle = image.getEnclosingRectangle(); + if (enclosingRectangle == null) { + throw NotFoundException.getNotFoundInstance(); + } + + int left = enclosingRectangle[0]; + int top = enclosingRectangle[1]; + int width = enclosingRectangle[2]; + int height = enclosingRectangle[3]; + + // Now just read off the bits + BitMatrix bits = new BitMatrix(MATRIX_WIDTH, MATRIX_HEIGHT); + for (int y = 0; y < MATRIX_HEIGHT; y++) { + int iy = top + (y * height + height / 2) / MATRIX_HEIGHT; + for (int x = 0; x < MATRIX_WIDTH; x++) { + int ix = left + (x * width + width / 2 + (y & 0x01) * width / 2) / MATRIX_WIDTH; + if (image.get(ix, iy)) { + bits.set(x, y); + } + } + } + return bits; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/maxicode/decoder/BitMatrixParser.java b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/BitMatrixParser.java new file mode 100644 index 0000000..be18a7e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/BitMatrixParser.java @@ -0,0 +1,88 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.maxicode.decoder; + +import com.google.zxing.common.BitMatrix; + +/** + * @author mike32767 + * @author Manuel Kasten + */ +final class BitMatrixParser { + + private static final int[][] BITNR = { + {121,120,127,126,133,132,139,138,145,144,151,150,157,156,163,162,169,168,175,174,181,180,187,186,193,192,199,198, -2, -2}, + {123,122,129,128,135,134,141,140,147,146,153,152,159,158,165,164,171,170,177,176,183,182,189,188,195,194,201,200,816, -3}, + {125,124,131,130,137,136,143,142,149,148,155,154,161,160,167,166,173,172,179,178,185,184,191,190,197,196,203,202,818,817}, + {283,282,277,276,271,270,265,264,259,258,253,252,247,246,241,240,235,234,229,228,223,222,217,216,211,210,205,204,819, -3}, + {285,284,279,278,273,272,267,266,261,260,255,254,249,248,243,242,237,236,231,230,225,224,219,218,213,212,207,206,821,820}, + {287,286,281,280,275,274,269,268,263,262,257,256,251,250,245,244,239,238,233,232,227,226,221,220,215,214,209,208,822, -3}, + {289,288,295,294,301,300,307,306,313,312,319,318,325,324,331,330,337,336,343,342,349,348,355,354,361,360,367,366,824,823}, + {291,290,297,296,303,302,309,308,315,314,321,320,327,326,333,332,339,338,345,344,351,350,357,356,363,362,369,368,825, -3}, + {293,292,299,298,305,304,311,310,317,316,323,322,329,328,335,334,341,340,347,346,353,352,359,358,365,364,371,370,827,826}, + {409,408,403,402,397,396,391,390, 79, 78, -2, -2, 13, 12, 37, 36, 2, -1, 44, 43,109,108,385,384,379,378,373,372,828, -3}, + {411,410,405,404,399,398,393,392, 81, 80, 40, -2, 15, 14, 39, 38, 3, -1, -1, 45,111,110,387,386,381,380,375,374,830,829}, + {413,412,407,406,401,400,395,394, 83, 82, 41, -3, -3, -3, -3, -3, 5, 4, 47, 46,113,112,389,388,383,382,377,376,831, -3}, + {415,414,421,420,427,426,103,102, 55, 54, 16, -3, -3, -3, -3, -3, -3, -3, 20, 19, 85, 84,433,432,439,438,445,444,833,832}, + {417,416,423,422,429,428,105,104, 57, 56, -3, -3, -3, -3, -3, -3, -3, -3, 22, 21, 87, 86,435,434,441,440,447,446,834, -3}, + {419,418,425,424,431,430,107,106, 59, 58, -3, -3, -3, -3, -3, -3, -3, -3, -3, 23, 89, 88,437,436,443,442,449,448,836,835}, + {481,480,475,474,469,468, 48, -2, 30, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, 0, 53, 52,463,462,457,456,451,450,837, -3}, + {483,482,477,476,471,470, 49, -1, -2, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, -2, -1,465,464,459,458,453,452,839,838}, + {485,484,479,478,473,472, 51, 50, 31, -3, -3, -3, -3, -3, -3, -3, -3, -3, -3, 1, -2, 42,467,466,461,460,455,454,840, -3}, + {487,486,493,492,499,498, 97, 96, 61, 60, -3, -3, -3, -3, -3, -3, -3, -3, -3, 26, 91, 90,505,504,511,510,517,516,842,841}, + {489,488,495,494,501,500, 99, 98, 63, 62, -3, -3, -3, -3, -3, -3, -3, -3, 28, 27, 93, 92,507,506,513,512,519,518,843, -3}, + {491,490,497,496,503,502,101,100, 65, 64, 17, -3, -3, -3, -3, -3, -3, -3, 18, 29, 95, 94,509,508,515,514,521,520,845,844}, + {559,558,553,552,547,546,541,540, 73, 72, 32, -3, -3, -3, -3, -3, -3, 10, 67, 66,115,114,535,534,529,528,523,522,846, -3}, + {561,560,555,554,549,548,543,542, 75, 74, -2, -1, 7, 6, 35, 34, 11, -2, 69, 68,117,116,537,536,531,530,525,524,848,847}, + {563,562,557,556,551,550,545,544, 77, 76, -2, 33, 9, 8, 25, 24, -1, -2, 71, 70,119,118,539,538,533,532,527,526,849, -3}, + {565,564,571,570,577,576,583,582,589,588,595,594,601,600,607,606,613,612,619,618,625,624,631,630,637,636,643,642,851,850}, + {567,566,573,572,579,578,585,584,591,590,597,596,603,602,609,608,615,614,621,620,627,626,633,632,639,638,645,644,852, -3}, + {569,568,575,574,581,580,587,586,593,592,599,598,605,604,611,610,617,616,623,622,629,628,635,634,641,640,647,646,854,853}, + {727,726,721,720,715,714,709,708,703,702,697,696,691,690,685,684,679,678,673,672,667,666,661,660,655,654,649,648,855, -3}, + {729,728,723,722,717,716,711,710,705,704,699,698,693,692,687,686,681,680,675,674,669,668,663,662,657,656,651,650,857,856}, + {731,730,725,724,719,718,713,712,707,706,701,700,695,694,689,688,683,682,677,676,671,670,665,664,659,658,653,652,858, -3}, + {733,732,739,738,745,744,751,750,757,756,763,762,769,768,775,774,781,780,787,786,793,792,799,798,805,804,811,810,860,859}, + {735,734,741,740,747,746,753,752,759,758,765,764,771,770,777,776,783,782,789,788,795,794,801,800,807,806,813,812,861, -3}, + {737,736,743,742,749,748,755,754,761,760,767,766,773,772,779,778,785,784,791,790,797,796,803,802,809,808,815,814,863,862} + }; + + private final BitMatrix bitMatrix; + + /** + * @param bitMatrix {@link BitMatrix} to parse + */ + BitMatrixParser(BitMatrix bitMatrix) { + this.bitMatrix = bitMatrix; + } + + byte[] readCodewords() { + byte[] result = new byte[144]; + int height = bitMatrix.getHeight(); + int width = bitMatrix.getWidth(); + for (int y = 0; y < height; y++) { + int[] bitnrRow = BITNR[y]; + for (int x = 0; x < width; x++) { + int bit = bitnrRow[x]; + if (bit >= 0 && bitMatrix.get(x, y)) { + result[bit / 6] |= (byte) (1 << (5 - (bit % 6))); + } + } + } + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/maxicode/decoder/DecodedBitStreamParser.java b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..0e59dbd --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/DecodedBitStreamParser.java @@ -0,0 +1,207 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.maxicode.decoder; + +import com.google.zxing.common.DecoderResult; +import java.text.DecimalFormat; +import java.text.NumberFormat; + +/** + *

MaxiCodes can encode text or structured information as bits in one of several modes, + * with multiple character sets in one code. This class decodes the bits back into text.

+ * + * @author mike32767 + * @author Manuel Kasten + */ +final class DecodedBitStreamParser { + + private static final char SHIFTA = '\uFFF0'; + private static final char SHIFTB = '\uFFF1'; + private static final char SHIFTC = '\uFFF2'; + private static final char SHIFTD = '\uFFF3'; + private static final char SHIFTE = '\uFFF4'; + private static final char TWOSHIFTA = '\uFFF5'; + private static final char THREESHIFTA = '\uFFF6'; + private static final char LATCHA = '\uFFF7'; + private static final char LATCHB = '\uFFF8'; + private static final char LOCK = '\uFFF9'; + private static final char ECI = '\uFFFA'; + private static final char NS = '\uFFFB'; + private static final char PAD = '\uFFFC'; + private static final char FS = '\u001C'; + private static final char GS = '\u001D'; + private static final char RS = '\u001E'; + + private static final String[] SETS = { + "\nABCDEFGHIJKLMNOPQRSTUVWXYZ" + ECI + FS + GS + RS + NS + ' ' + PAD + + "\"#$%&'()*+,-./0123456789:" + SHIFTB + SHIFTC + SHIFTD + SHIFTE + LATCHB, + "`abcdefghijklmnopqrstuvwxyz" + ECI + FS + GS + RS + NS + '{' + PAD + + "}~\u007F;<=>?[\\]^_ ,./:@!|" + PAD + TWOSHIFTA + THREESHIFTA + PAD + + SHIFTA + SHIFTC + SHIFTD + SHIFTE + LATCHA, + "\u00C0\u00C1\u00C2\u00C3\u00C4\u00C5\u00C6\u00C7\u00C8\u00C9\u00CA\u00CB\u00CC\u00CD\u00CE\u00CF\u00D0\u00D1\u00D2\u00D3\u00D4\u00D5\u00D6\u00D7\u00D8\u00D9\u00DA" + + ECI + FS + GS + RS + + "\u00DB\u00DC\u00DD\u00DE\u00DF\u00AA\u00AC\u00B1\u00B2\u00B3\u00B5\u00B9\u00BA\u00BC\u00BD\u00BE\u0080\u0081\u0082\u0083\u0084\u0085\u0086\u0087\u0088\u0089" + + LATCHA + ' ' + LOCK + SHIFTD + SHIFTE + LATCHB, + "\u00E0\u00E1\u00E2\u00E3\u00E4\u00E5\u00E6\u00E7\u00E8\u00E9\u00EA\u00EB\u00EC\u00ED\u00EE\u00EF\u00F0\u00F1\u00F2\u00F3\u00F4\u00F5\u00F6\u00F7\u00F8\u00F9\u00FA" + + ECI + FS + GS + RS + NS + + "\u00FB\u00FC\u00FD\u00FE\u00FF\u00A1\u00A8\u00AB\u00AF\u00B0\u00B4\u00B7\u00B8\u00BB\u00BF\u008A\u008B\u008C\u008D\u008E\u008F\u0090\u0091\u0092\u0093\u0094" + + LATCHA + ' ' + SHIFTC + LOCK + SHIFTE + LATCHB, + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\n\u000B\u000C\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A" + + ECI + PAD + PAD + '\u001B' + NS + FS + GS + RS + + "\u001F\u009F\u00A0\u00A2\u00A3\u00A4\u00A5\u00A6\u00A7\u00A9\u00AD\u00AE\u00B6\u0095\u0096\u0097\u0098\u0099\u009A\u009B\u009C\u009D\u009E" + + LATCHA + ' ' + SHIFTC + SHIFTD + LOCK + LATCHB, + "\u0000\u0001\u0002\u0003\u0004\u0005\u0006\u0007\u0008\u0009\n\u000B\u000C\r\u000E\u000F\u0010\u0011\u0012\u0013\u0014\u0015\u0016\u0017\u0018\u0019\u001A\u001B\u001C\u001D\u001E\u001F\u0020\u0021\"\u0023\u0024\u0025\u0026\u0027\u0028\u0029\u002A\u002B\u002C\u002D\u002E\u002F\u0030\u0031\u0032\u0033\u0034\u0035\u0036\u0037\u0038\u0039\u003A\u003B\u003C\u003D\u003E\u003F" + }; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(byte[] bytes, int mode) { + StringBuilder result = new StringBuilder(144); + switch (mode) { + case 2: + case 3: + String postcode; + if (mode == 2) { + int pc = getPostCode2(bytes); + NumberFormat df = new DecimalFormat("0000000000".substring(0, getPostCode2Length(bytes))); + postcode = df.format(pc); + } else { + postcode = getPostCode3(bytes); + } + NumberFormat threeDigits = new DecimalFormat("000"); + String country = threeDigits.format(getCountry(bytes)); + String service = threeDigits.format(getServiceClass(bytes)); + result.append(getMessage(bytes, 10, 84)); + if (result.toString().startsWith("[)>" + RS + "01" + GS)) { + result.insert(9, postcode + GS + country + GS + service + GS); + } else { + result.insert(0, postcode + GS + country + GS + service + GS); + } + break; + case 4: + result.append(getMessage(bytes, 1, 93)); + break; + case 5: + result.append(getMessage(bytes, 1, 77)); + break; + } + return new DecoderResult(bytes, result.toString(), null, String.valueOf(mode)); + } + + private static int getBit(int bit, byte[] bytes) { + bit--; + return (bytes[bit / 6] & (1 << (5 - (bit % 6)))) == 0 ? 0 : 1; + } + + private static int getInt(byte[] bytes, byte[] x) { + if (x.length == 0) { + throw new IllegalArgumentException(); + } + int val = 0; + for (int i = 0; i < x.length; i++) { + val += getBit(x[i], bytes) << (x.length - i - 1); + } + return val; + } + + private static int getCountry(byte[] bytes) { + return getInt(bytes, new byte[] {53, 54, 43, 44, 45, 46, 47, 48, 37, 38}); + } + + private static int getServiceClass(byte[] bytes) { + return getInt(bytes, new byte[] {55, 56, 57, 58, 59, 60, 49, 50, 51, 52}); + } + + private static int getPostCode2Length(byte[] bytes) { + return getInt(bytes, new byte[] {39, 40, 41, 42, 31, 32}); + } + + private static int getPostCode2(byte[] bytes) { + return getInt(bytes, new byte[] {33, 34, 35, 36, 25, 26, 27, 28, 29, 30, 19, + 20, 21, 22, 23, 24, 13, 14, 15, 16, 17, 18, 7, 8, 9, 10, 11, 12, 1, 2}); + } + + private static String getPostCode3(byte[] bytes) { + return String.valueOf( + new char[] { + SETS[0].charAt(getInt(bytes, new byte[] {39, 40, 41, 42, 31, 32})), + SETS[0].charAt(getInt(bytes, new byte[] {33, 34, 35, 36, 25, 26})), + SETS[0].charAt(getInt(bytes, new byte[] {27, 28, 29, 30, 19, 20})), + SETS[0].charAt(getInt(bytes, new byte[] {21, 22, 23, 24, 13, 14})), + SETS[0].charAt(getInt(bytes, new byte[] {15, 16, 17, 18, 7, 8})), + SETS[0].charAt(getInt(bytes, new byte[] { 9, 10, 11, 12, 1, 2})), + } + ); + } + + private static String getMessage(byte[] bytes, int start, int len) { + StringBuilder sb = new StringBuilder(); + int shift = -1; + int set = 0; + int lastset = 0; + for (int i = start; i < start + len; i++) { + char c = SETS[set].charAt(bytes[i]); + switch (c) { + case LATCHA: + set = 0; + shift = -1; + break; + case LATCHB: + set = 1; + shift = -1; + break; + case SHIFTA: + case SHIFTB: + case SHIFTC: + case SHIFTD: + case SHIFTE: + lastset = set; + set = c - SHIFTA; + shift = 1; + break; + case TWOSHIFTA: + lastset = set; + set = 0; + shift = 2; + break; + case THREESHIFTA: + lastset = set; + set = 0; + shift = 3; + break; + case NS: + int nsval = (bytes[++i] << 24) + (bytes[++i] << 18) + (bytes[++i] << 12) + (bytes[++i] << 6) + bytes[++i]; + sb.append(new DecimalFormat("000000000").format(nsval)); + break; + case LOCK: + shift = -1; + break; + default: + sb.append(c); + } + if (shift-- == 0) { + set = lastset; + } + } + while (sb.length() > 0 && sb.charAt(sb.length() - 1) == PAD) { + sb.setLength(sb.length() - 1); + } + return sb.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/maxicode/decoder/Decoder.java b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/Decoder.java new file mode 100644 index 0000000..f7836f4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/maxicode/decoder/Decoder.java @@ -0,0 +1,114 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.maxicode.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +import java.util.Map; + +/** + *

The main class which implements MaxiCode decoding -- as opposed to locating and extracting + * the MaxiCode from an image.

+ * + * @author Manuel Kasten + */ +public final class Decoder { + + private static final int ALL = 0; + private static final int EVEN = 1; + private static final int ODD = 2; + + private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + rsDecoder = new ReedSolomonDecoder(GenericGF.MAXICODE_FIELD_64); + } + + public DecoderResult decode(BitMatrix bits) throws ChecksumException, FormatException { + return decode(bits, null); + } + + public DecoderResult decode(BitMatrix bits, + Map hints) throws FormatException, ChecksumException { + BitMatrixParser parser = new BitMatrixParser(bits); + byte[] codewords = parser.readCodewords(); + + correctErrors(codewords, 0, 10, 10, ALL); + int mode = codewords[0] & 0x0F; + byte[] datawords; + switch (mode) { + case 2: + case 3: + case 4: + correctErrors(codewords, 20, 84, 40, EVEN); + correctErrors(codewords, 20, 84, 40, ODD); + datawords = new byte[94]; + break; + case 5: + correctErrors(codewords, 20, 68, 56, EVEN); + correctErrors(codewords, 20, 68, 56, ODD); + datawords = new byte[78]; + break; + default: + throw FormatException.getFormatInstance(); + } + + System.arraycopy(codewords, 0, datawords, 0, 10); + System.arraycopy(codewords, 20, datawords, 10, datawords.length - 10); + + return DecodedBitStreamParser.decode(datawords, mode); + } + + private void correctErrors(byte[] codewordBytes, + int start, + int dataCodewords, + int ecCodewords, + int mode) throws ChecksumException { + int codewords = dataCodewords + ecCodewords; + + // in EVEN or ODD mode only half the codewords + int divisor = mode == ALL ? 1 : 2; + + // First read into an array of ints + int[] codewordsInts = new int[codewords / divisor]; + for (int i = 0; i < codewords; i++) { + if ((mode == ALL) || (i % 2 == (mode - 1))) { + codewordsInts[i / divisor] = codewordBytes[i + start] & 0xFF; + } + } + try { + rsDecoder.decode(codewordsInts, ecCodewords / divisor); + } catch (ReedSolomonException ignored) { + throw ChecksumException.getChecksumInstance(); + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (int i = 0; i < dataCodewords; i++) { + if ((mode == ALL) || (i % 2 == (mode - 1))) { + codewordBytes[i + start] = (byte) codewordsInts[i / divisor]; + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/ByQuadrantReader.java b/rubylib/src/main/java/com/google/zxing/multi/ByQuadrantReader.java new file mode 100644 index 0000000..06e4b1a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/ByQuadrantReader.java @@ -0,0 +1,115 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; + +import java.util.Map; + +/** + * This class attempts to decode a barcode from an image, not by scanning the whole image, + * but by scanning subsets of the image. This is important when there may be multiple barcodes in + * an image, and detecting a barcode may find parts of multiple barcode and fail to decode + * (e.g. QR Codes). Instead this scans the four quadrants of the image -- and also the center + * 'quadrant' to cover the case where a barcode is found in the center. + * + * @see GenericMultipleBarcodeReader + */ +public final class ByQuadrantReader implements Reader { + + private final Reader delegate; + + public ByQuadrantReader(Reader delegate) { + this.delegate = delegate; + } + + @Override + public Result decode(BinaryBitmap image) + throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, ChecksumException, FormatException { + + int width = image.getWidth(); + int height = image.getHeight(); + int halfWidth = width / 2; + int halfHeight = height / 2; + + try { + // No need to call makeAbsolute as results will be relative to original top left here + return delegate.decode(image.crop(0, 0, halfWidth, halfHeight), hints); + } catch (NotFoundException re) { + // continue + } + + try { + Result result = delegate.decode(image.crop(halfWidth, 0, halfWidth, halfHeight), hints); + makeAbsolute(result.getResultPoints(), halfWidth, 0); + return result; + } catch (NotFoundException re) { + // continue + } + + try { + Result result = delegate.decode(image.crop(0, halfHeight, halfWidth, halfHeight), hints); + makeAbsolute(result.getResultPoints(), 0, halfHeight); + return result; + } catch (NotFoundException re) { + // continue + } + + try { + Result result = delegate.decode(image.crop(halfWidth, halfHeight, halfWidth, halfHeight), hints); + makeAbsolute(result.getResultPoints(), halfWidth, halfHeight); + return result; + } catch (NotFoundException re) { + // continue + } + + int quarterWidth = halfWidth / 2; + int quarterHeight = halfHeight / 2; + BinaryBitmap center = image.crop(quarterWidth, quarterHeight, halfWidth, halfHeight); + Result result = delegate.decode(center, hints); + makeAbsolute(result.getResultPoints(), quarterWidth, quarterHeight); + return result; + } + + @Override + public void reset() { + delegate.reset(); + } + + private static void makeAbsolute(ResultPoint[] points, int leftOffset, int topOffset) { + if (points != null) { + for (int i = 0; i < points.length; i++) { + ResultPoint relative = points[i]; + points[i] = new ResultPoint(relative.getX() + leftOffset, relative.getY() + topOffset); + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/GenericMultipleBarcodeReader.java b/rubylib/src/main/java/com/google/zxing/multi/GenericMultipleBarcodeReader.java new file mode 100644 index 0000000..101835d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/GenericMultipleBarcodeReader.java @@ -0,0 +1,180 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + *

Attempts to locate multiple barcodes in an image by repeatedly decoding portion of the image. + * After one barcode is found, the areas left, above, right and below the barcode's + * {@link ResultPoint}s are scanned, recursively.

+ * + *

A caller may want to also employ {@link ByQuadrantReader} when attempting to find multiple + * 2D barcodes, like QR Codes, in an image, where the presence of multiple barcodes might prevent + * detecting any one of them.

+ * + *

That is, instead of passing a {@link Reader} a caller might pass + * {@code new ByQuadrantReader(reader)}.

+ * + * @author Sean Owen + */ +public final class GenericMultipleBarcodeReader implements MultipleBarcodeReader { + + private static final int MIN_DIMENSION_TO_RECUR = 100; + private static final int MAX_DEPTH = 4; + + private final Reader delegate; + + public GenericMultipleBarcodeReader(Reader delegate) { + this.delegate = delegate; + } + + @Override + public Result[] decodeMultiple(BinaryBitmap image) throws NotFoundException { + return decodeMultiple(image, null); + } + + @Override + public Result[] decodeMultiple(BinaryBitmap image, Map hints) + throws NotFoundException { + List results = new ArrayList<>(); + doDecodeMultiple(image, hints, results, 0, 0, 0); + if (results.isEmpty()) { + throw NotFoundException.getNotFoundInstance(); + } + return results.toArray(new Result[results.size()]); + } + + private void doDecodeMultiple(BinaryBitmap image, + Map hints, + List results, + int xOffset, + int yOffset, + int currentDepth) { + if (currentDepth > MAX_DEPTH) { + return; + } + + Result result; + try { + result = delegate.decode(image, hints); + } catch (ReaderException ignored) { + return; + } + boolean alreadyFound = false; + for (Result existingResult : results) { + if (existingResult.getText().equals(result.getText())) { + alreadyFound = true; + break; + } + } + if (!alreadyFound) { + results.add(translateResultPoints(result, xOffset, yOffset)); + } + ResultPoint[] resultPoints = result.getResultPoints(); + if (resultPoints == null || resultPoints.length == 0) { + return; + } + int width = image.getWidth(); + int height = image.getHeight(); + float minX = width; + float minY = height; + float maxX = 0.0f; + float maxY = 0.0f; + for (ResultPoint point : resultPoints) { + if (point == null) { + continue; + } + float x = point.getX(); + float y = point.getY(); + if (x < minX) { + minX = x; + } + if (y < minY) { + minY = y; + } + if (x > maxX) { + maxX = x; + } + if (y > maxY) { + maxY = y; + } + } + + // Decode left of barcode + if (minX > MIN_DIMENSION_TO_RECUR) { + doDecodeMultiple(image.crop(0, 0, (int) minX, height), + hints, results, + xOffset, yOffset, + currentDepth + 1); + } + // Decode above barcode + if (minY > MIN_DIMENSION_TO_RECUR) { + doDecodeMultiple(image.crop(0, 0, width, (int) minY), + hints, results, + xOffset, yOffset, + currentDepth + 1); + } + // Decode right of barcode + if (maxX < width - MIN_DIMENSION_TO_RECUR) { + doDecodeMultiple(image.crop((int) maxX, 0, width - (int) maxX, height), + hints, results, + xOffset + (int) maxX, yOffset, + currentDepth + 1); + } + // Decode below barcode + if (maxY < height - MIN_DIMENSION_TO_RECUR) { + doDecodeMultiple(image.crop(0, (int) maxY, width, height - (int) maxY), + hints, results, + xOffset, yOffset + (int) maxY, + currentDepth + 1); + } + } + + private static Result translateResultPoints(Result result, int xOffset, int yOffset) { + ResultPoint[] oldResultPoints = result.getResultPoints(); + if (oldResultPoints == null) { + return result; + } + ResultPoint[] newResultPoints = new ResultPoint[oldResultPoints.length]; + for (int i = 0; i < oldResultPoints.length; i++) { + ResultPoint oldPoint = oldResultPoints[i]; + if (oldPoint != null) { + newResultPoints[i] = new ResultPoint(oldPoint.getX() + xOffset, oldPoint.getY() + yOffset); + } + } + Result newResult = new Result(result.getText(), + result.getRawBytes(), + result.getNumBits(), + newResultPoints, + result.getBarcodeFormat(), + result.getTimestamp()); + newResult.putAllMetadata(result.getResultMetadata()); + return newResult; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/MultipleBarcodeReader.java b/rubylib/src/main/java/com/google/zxing/multi/MultipleBarcodeReader.java new file mode 100644 index 0000000..a358727 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/MultipleBarcodeReader.java @@ -0,0 +1,39 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; + +import java.util.Map; + +/** + * Implementation of this interface attempt to read several barcodes from one image. + * + * @see com.google.zxing.Reader + * @author Sean Owen + */ +public interface MultipleBarcodeReader { + + Result[] decodeMultiple(BinaryBitmap image) throws NotFoundException; + + Result[] decodeMultiple(BinaryBitmap image, + Map hints) throws NotFoundException; + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/qrcode/QRCodeMultiReader.java b/rubylib/src/main/java/com/google/zxing/multi/qrcode/QRCodeMultiReader.java new file mode 100644 index 0000000..083fcf3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/qrcode/QRCodeMultiReader.java @@ -0,0 +1,181 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.multi.MultipleBarcodeReader; +import com.google.zxing.multi.qrcode.detector.MultiDetector; +import com.google.zxing.qrcode.QRCodeReader; +import com.google.zxing.qrcode.decoder.QRCodeDecoderMetaData; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Collections; +import java.util.Comparator; + +/** + * This implementation can detect and decode multiple QR Codes in an image. + * + * @author Sean Owen + * @author Hannes Erven + */ +public final class QRCodeMultiReader extends QRCodeReader implements MultipleBarcodeReader { + + private static final Result[] EMPTY_RESULT_ARRAY = new Result[0]; + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + + @Override + public Result[] decodeMultiple(BinaryBitmap image) throws NotFoundException { + return decodeMultiple(image, null); + } + + @Override + public Result[] decodeMultiple(BinaryBitmap image, Map hints) throws NotFoundException { + List results = new ArrayList<>(); + DetectorResult[] detectorResults = new MultiDetector(image.getBlackMatrix()).detectMulti(hints); + for (DetectorResult detectorResult : detectorResults) { + try { + DecoderResult decoderResult = getDecoder().decode(detectorResult.getBits(), hints); + ResultPoint[] points = detectorResult.getPoints(); + // If the code was mirrored: swap the bottom-left and the top-right points. + if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) { + ((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points); + } + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, + BarcodeFormat.QR_CODE); + List byteSegments = decoderResult.getByteSegments(); + if (byteSegments != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments); + } + String ecLevel = decoderResult.getECLevel(); + if (ecLevel != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); + } + if (decoderResult.hasStructuredAppend()) { + result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE, + decoderResult.getStructuredAppendSequenceNumber()); + result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY, + decoderResult.getStructuredAppendParity()); + } + results.add(result); + } catch (ReaderException re) { + // ignore and continue + } + } + if (results.isEmpty()) { + return EMPTY_RESULT_ARRAY; + } else { + results = processStructuredAppend(results); + return results.toArray(new Result[results.size()]); + } + } + + private static List processStructuredAppend(List results) { + boolean hasSA = false; + + // first, check, if there is at least on SA result in the list + for (Result result : results) { + if (result.getResultMetadata().containsKey(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE)) { + hasSA = true; + break; + } + } + if (!hasSA) { + return results; + } + + // it is, second, split the lists and built a new result list + List newResults = new ArrayList<>(); + List saResults = new ArrayList<>(); + for (Result result : results) { + newResults.add(result); + if (result.getResultMetadata().containsKey(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE)) { + saResults.add(result); + } + } + // sort and concatenate the SA list items + Collections.sort(saResults, new SAComparator()); + StringBuilder concatedText = new StringBuilder(); + int rawBytesLen = 0; + int byteSegmentLength = 0; + for (Result saResult : saResults) { + concatedText.append(saResult.getText()); + rawBytesLen += saResult.getRawBytes().length; + if (saResult.getResultMetadata().containsKey(ResultMetadataType.BYTE_SEGMENTS)) { + @SuppressWarnings("unchecked") + Iterable byteSegments = + (Iterable) saResult.getResultMetadata().get(ResultMetadataType.BYTE_SEGMENTS); + for (byte[] segment : byteSegments) { + byteSegmentLength += segment.length; + } + } + } + byte[] newRawBytes = new byte[rawBytesLen]; + byte[] newByteSegment = new byte[byteSegmentLength]; + int newRawBytesIndex = 0; + int byteSegmentIndex = 0; + for (Result saResult : saResults) { + System.arraycopy(saResult.getRawBytes(), 0, newRawBytes, newRawBytesIndex, saResult.getRawBytes().length); + newRawBytesIndex += saResult.getRawBytes().length; + if (saResult.getResultMetadata().containsKey(ResultMetadataType.BYTE_SEGMENTS)) { + @SuppressWarnings("unchecked") + Iterable byteSegments = + (Iterable) saResult.getResultMetadata().get(ResultMetadataType.BYTE_SEGMENTS); + for (byte[] segment : byteSegments) { + System.arraycopy(segment, 0, newByteSegment, byteSegmentIndex, segment.length); + byteSegmentIndex += segment.length; + } + } + } + Result newResult = new Result(concatedText.toString(), newRawBytes, NO_POINTS, BarcodeFormat.QR_CODE); + if (byteSegmentLength > 0) { + Collection byteSegmentList = new ArrayList<>(); + byteSegmentList.add(newByteSegment); + newResult.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegmentList); + } + newResults.add(newResult); + return newResults; + } + + private static final class SAComparator implements Comparator, Serializable { + @Override + public int compare(Result a, Result b) { + int aNumber = (int) a.getResultMetadata().get(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE); + int bNumber = (int) b.getResultMetadata().get(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE); + if (aNumber < bNumber) { + return -1; + } + if (aNumber > bNumber) { + return 1; + } + return 0; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiDetector.java b/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiDetector.java new file mode 100644 index 0000000..9b24210 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiDetector.java @@ -0,0 +1,73 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ReaderException; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.qrcode.detector.Detector; +import com.google.zxing.qrcode.detector.FinderPatternInfo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + *

Encapsulates logic that can detect one or more QR Codes in an image, even if the QR Code + * is rotated or skewed, or partially obscured.

+ * + * @author Sean Owen + * @author Hannes Erven + */ +public final class MultiDetector extends Detector { + + private static final DetectorResult[] EMPTY_DETECTOR_RESULTS = new DetectorResult[0]; + + public MultiDetector(BitMatrix image) { + super(image); + } + + public DetectorResult[] detectMulti(Map hints) throws NotFoundException { + BitMatrix image = getImage(); + ResultPointCallback resultPointCallback = + hints == null ? null : (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + MultiFinderPatternFinder finder = new MultiFinderPatternFinder(image, resultPointCallback); + FinderPatternInfo[] infos = finder.findMulti(hints); + + if (infos.length == 0) { + throw NotFoundException.getNotFoundInstance(); + } + + List result = new ArrayList<>(); + for (FinderPatternInfo info : infos) { + try { + result.add(processFinderPatternInfo(info)); + } catch (ReaderException e) { + // ignore + } + } + if (result.isEmpty()) { + return EMPTY_DETECTOR_RESULTS; + } else { + return result.toArray(new DetectorResult[result.size()]); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java b/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java new file mode 100644 index 0000000..6ec4bc4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/multi/qrcode/detector/MultiFinderPatternFinder.java @@ -0,0 +1,311 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.multi.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.detector.FinderPattern; +import com.google.zxing.qrcode.detector.FinderPatternFinder; +import com.google.zxing.qrcode.detector.FinderPatternInfo; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + *

This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.

+ * + *

This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + *

In contrast to {@link FinderPatternFinder}, this class will return an array of all possible + * QR code locations in the image.

+ * + *

Use the TRY_HARDER hint to ask for a more thorough detection.

+ * + * @author Sean Owen + * @author Hannes Erven + */ +final class MultiFinderPatternFinder extends FinderPatternFinder { + + private static final FinderPatternInfo[] EMPTY_RESULT_ARRAY = new FinderPatternInfo[0]; + + // TODO MIN_MODULE_COUNT and MAX_MODULE_COUNT would be great hints to ask the user for + // since it limits the number of regions to decode + + // max. legal count of modules per QR code edge (177) + private static final float MAX_MODULE_COUNT_PER_EDGE = 180; + // min. legal count per modules per QR code edge (11) + private static final float MIN_MODULE_COUNT_PER_EDGE = 9; + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF_PERCENT percent in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF_PERCENT = 0.05f; + + /** + * More or less arbitrary cutoff point for determining if two finder patterns might belong + * to the same code if they differ less than DIFF_MODSIZE_CUTOFF pixels/module in their + * estimated modules sizes. + */ + private static final float DIFF_MODSIZE_CUTOFF = 0.5f; + + + /** + * A comparator that orders FinderPatterns by their estimated module size. + */ + private static final class ModuleSizeComparator implements Comparator, Serializable { + @Override + public int compare(FinderPattern center1, FinderPattern center2) { + float value = center2.getEstimatedModuleSize() - center1.getEstimatedModuleSize(); + return value < 0.0 ? -1 : value > 0.0 ? 1 : 0; + } + } + + /** + *

Creates a finder that will search the image for three finder patterns.

+ * + * @param image image to search + */ + MultiFinderPatternFinder(BitMatrix image) { + super(image); + } + + MultiFinderPatternFinder(BitMatrix image, ResultPointCallback resultPointCallback) { + super(image, resultPointCallback); + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws NotFoundException if 3 such finder patterns do not exist + */ + private FinderPattern[][] selectMutipleBestPatterns() throws NotFoundException { + List possibleCenters = getPossibleCenters(); + int size = possibleCenters.size(); + + if (size < 3) { + // Couldn't find enough finder patterns + throw NotFoundException.getNotFoundInstance(); + } + + /* + * Begin HE modifications to safely detect multiple codes of equal size + */ + if (size == 3) { + return new FinderPattern[][]{ + new FinderPattern[]{ + possibleCenters.get(0), + possibleCenters.get(1), + possibleCenters.get(2) + } + }; + } + + // Sort by estimated module size to speed up the upcoming checks + Collections.sort(possibleCenters, new ModuleSizeComparator()); + + /* + * Now lets start: build a list of tuples of three finder locations that + * - feature similar module sizes + * - are placed in a distance so the estimated module count is within the QR specification + * - have similar distance between upper left/right and left top/bottom finder patterns + * - form a triangle with 90° angle (checked by comparing top right/bottom left distance + * with pythagoras) + * + * Note: we allow each point to be used for more than one code region: this might seem + * counterintuitive at first, but the performance penalty is not that big. At this point, + * we cannot make a good quality decision whether the three finders actually represent + * a QR code, or are just by chance layouted so it looks like there might be a QR code there. + * So, if the layout seems right, lets have the decoder try to decode. + */ + + List results = new ArrayList<>(); // holder for the results + + for (int i1 = 0; i1 < (size - 2); i1++) { + FinderPattern p1 = possibleCenters.get(i1); + if (p1 == null) { + continue; + } + + for (int i2 = i1 + 1; i2 < (size - 1); i2++) { + FinderPattern p2 = possibleCenters.get(i2); + if (p2 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize12 = (p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()) / + Math.min(p1.getEstimatedModuleSize(), p2.getEstimatedModuleSize()); + float vModSize12A = Math.abs(p1.getEstimatedModuleSize() - p2.getEstimatedModuleSize()); + if (vModSize12A > DIFF_MODSIZE_CUTOFF && vModSize12 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + for (int i3 = i2 + 1; i3 < size; i3++) { + FinderPattern p3 = possibleCenters.get(i3); + if (p3 == null) { + continue; + } + + // Compare the expected module sizes; if they are really off, skip + float vModSize23 = (p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()) / + Math.min(p2.getEstimatedModuleSize(), p3.getEstimatedModuleSize()); + float vModSize23A = Math.abs(p2.getEstimatedModuleSize() - p3.getEstimatedModuleSize()); + if (vModSize23A > DIFF_MODSIZE_CUTOFF && vModSize23 >= DIFF_MODSIZE_CUTOFF_PERCENT) { + // break, since elements are ordered by the module size deviation there cannot be + // any more interesting elements for the given p1. + break; + } + + FinderPattern[] test = {p1, p2, p3}; + ResultPoint.orderBestPatterns(test); + + // Calculate the distances: a = topleft-bottomleft, b=topleft-topright, c = diagonal + FinderPatternInfo info = new FinderPatternInfo(test); + float dA = ResultPoint.distance(info.getTopLeft(), info.getBottomLeft()); + float dC = ResultPoint.distance(info.getTopRight(), info.getBottomLeft()); + float dB = ResultPoint.distance(info.getTopLeft(), info.getTopRight()); + + // Check the sizes + float estimatedModuleCount = (dA + dB) / (p1.getEstimatedModuleSize() * 2.0f); + if (estimatedModuleCount > MAX_MODULE_COUNT_PER_EDGE || + estimatedModuleCount < MIN_MODULE_COUNT_PER_EDGE) { + continue; + } + + // Calculate the difference of the edge lengths in percent + float vABBC = Math.abs((dA - dB) / Math.min(dA, dB)); + if (vABBC >= 0.1f) { + continue; + } + + // Calculate the diagonal length by assuming a 90° angle at topleft + float dCpy = (float) Math.sqrt(dA * dA + dB * dB); + // Compare to the real distance in % + float vPyC = Math.abs((dC - dCpy) / Math.min(dC, dCpy)); + + if (vPyC >= 0.1f) { + continue; + } + + // All tests passed! + results.add(test); + } // end iterate p3 + } // end iterate p2 + } // end iterate p1 + + if (!results.isEmpty()) { + return results.toArray(new FinderPattern[results.size()][]); + } + + // Nothing found! + throw NotFoundException.getNotFoundInstance(); + } + + public FinderPatternInfo[] findMulti(Map hints) throws NotFoundException { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + boolean pureBarcode = hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE); + BitMatrix image = getImage(); + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + int iSkip = (int) (maxI / (MAX_MODULES * 4.0f) * 3); + if (iSkip < MIN_SKIP || tryHarder) { + iSkip = MIN_SKIP; + } + + int[] stateCount = new int[5]; + for (int i = iSkip - 1; i < maxI; i += iSkip) { + // Get a row of black/white values + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + int currentState = 0; + for (int j = 0; j < maxJ; j++) { + if (image.get(j, i)) { + // Black pixel + if ((currentState & 1) == 1) { // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } else { // White pixel + if ((currentState & 1) == 0) { // Counting black pixels + if (currentState == 4) { // A winner? + if (foundPatternCross(stateCount) && handlePossibleCenter(stateCount, i, j, pureBarcode)) { // Yes + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } else { // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } else { + stateCount[++currentState]++; + } + } else { // Counting white pixels + stateCount[currentState]++; + } + } + } // for j=... + + if (foundPatternCross(stateCount)) { + handlePossibleCenter(stateCount, i, maxJ, pureBarcode); + } // end if foundPatternCross + } // for i=iSkip-1 ... + FinderPattern[][] patternInfo = selectMutipleBestPatterns(); + List result = new ArrayList<>(); + for (FinderPattern[] pattern : patternInfo) { + ResultPoint.orderBestPatterns(pattern); + result.add(new FinderPatternInfo(pattern)); + } + + if (result.isEmpty()) { + return EMPTY_RESULT_ARRAY; + } else { + return result.toArray(new FinderPatternInfo[result.size()]); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/CodaBarReader.java b/rubylib/src/main/java/com/google/zxing/oned/CodaBarReader.java new file mode 100644 index 0000000..0ba9f1e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/CodaBarReader.java @@ -0,0 +1,345 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.Map; + +/** + *

Decodes Codabar barcodes.

+ * + * @author Bas Vijfwinkel + * @author David Walker + */ +public final class CodaBarReader extends OneDReader { + + // These values are critical for determining how permissive the decoding + // will be. All stripe sizes must be within the window these define, as + // compared to the average stripe size. + private static final float MAX_ACCEPTABLE = 2.0f; + private static final float PADDING = 1.5f; + + private static final String ALPHABET_STRING = "0123456789-$:/.+ABCD"; + static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. The 7 least-significant bits of + * each int correspond to the pattern of wide and narrow, with 1s representing "wide" and 0s representing narrow. + */ + static final int[] CHARACTER_ENCODINGS = { + 0x003, 0x006, 0x009, 0x060, 0x012, 0x042, 0x021, 0x024, 0x030, 0x048, // 0-9 + 0x00c, 0x018, 0x045, 0x051, 0x054, 0x015, 0x01A, 0x029, 0x00B, 0x00E, // -$:/.+ABCD + }; + + // minimal number of characters that should be present (including start and stop characters) + // under normal circumstances this should be set to 3, but can be set higher + // as a last-ditch attempt to reduce false positives. + private static final int MIN_CHARACTER_LENGTH = 3; + + // official start and end patterns + private static final char[] STARTEND_ENCODING = {'A', 'B', 'C', 'D'}; + // some Codabar generator allow the Codabar string to be closed by every + // character. This will cause lots of false positives! + + // some industries use a checksum standard but this is not part of the original Codabar standard + // for more information see : http://www.mecsw.com/specs/codabar.html + + // Keep some instance variables to avoid reallocations + private final StringBuilder decodeRowResult; + private int[] counters; + private int counterLength; + + public CodaBarReader() { + decodeRowResult = new StringBuilder(20); + counters = new int[80]; + counterLength = 0; + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) throws NotFoundException { + + Arrays.fill(counters, 0); + setCounters(row); + int startOffset = findStartPattern(); + int nextStart = startOffset; + + decodeRowResult.setLength(0); + do { + int charOffset = toNarrowWidePattern(nextStart); + if (charOffset == -1) { + throw NotFoundException.getNotFoundInstance(); + } + // Hack: We store the position in the alphabet table into a + // StringBuilder, so that we can access the decoded patterns in + // validatePattern. We'll translate to the actual characters later. + decodeRowResult.append((char) charOffset); + nextStart += 8; + // Stop as soon as we see the end character. + if (decodeRowResult.length() > 1 && + arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) { + break; + } + } while (nextStart < counterLength); // no fixed end pattern so keep on reading while data is available + + // Look for whitespace after pattern: + int trailingWhitespace = counters[nextStart - 1]; + int lastPatternSize = 0; + for (int i = -8; i < -1; i++) { + lastPatternSize += counters[nextStart + i]; + } + + // We need to see whitespace equal to 50% of the last pattern size, + // otherwise this is probably a false positive. The exception is if we are + // at the end of the row. (I.e. the barcode barely fits.) + if (nextStart < counterLength && trailingWhitespace < lastPatternSize / 2) { + throw NotFoundException.getNotFoundInstance(); + } + + validatePattern(startOffset); + + // Translate character table offsets to actual characters. + for (int i = 0; i < decodeRowResult.length(); i++) { + decodeRowResult.setCharAt(i, ALPHABET[decodeRowResult.charAt(i)]); + } + // Ensure a valid start and end character + char startchar = decodeRowResult.charAt(0); + if (!arrayContains(STARTEND_ENCODING, startchar)) { + throw NotFoundException.getNotFoundInstance(); + } + char endchar = decodeRowResult.charAt(decodeRowResult.length() - 1); + if (!arrayContains(STARTEND_ENCODING, endchar)) { + throw NotFoundException.getNotFoundInstance(); + } + + // remove stop/start characters character and check if a long enough string is contained + if (decodeRowResult.length() <= MIN_CHARACTER_LENGTH) { + // Almost surely a false positive ( start + stop + at least 1 character) + throw NotFoundException.getNotFoundInstance(); + } + + if (hints == null || !hints.containsKey(DecodeHintType.RETURN_CODABAR_START_END)) { + decodeRowResult.deleteCharAt(decodeRowResult.length() - 1); + decodeRowResult.deleteCharAt(0); + } + + int runningCount = 0; + for (int i = 0; i < startOffset; i++) { + runningCount += counters[i]; + } + float left = runningCount; + for (int i = startOffset; i < nextStart - 1; i++) { + runningCount += counters[i]; + } + float right = runningCount; + return new Result( + decodeRowResult.toString(), + null, + new ResultPoint[]{ + new ResultPoint(left, rowNumber), + new ResultPoint(right, rowNumber)}, + BarcodeFormat.CODABAR); + } + + private void validatePattern(int start) throws NotFoundException { + // First, sum up the total size of our four categories of stripe sizes; + int[] sizes = {0, 0, 0, 0}; + int[] counts = {0, 0, 0, 0}; + int end = decodeRowResult.length() - 1; + + // We break out of this loop in the middle, in order to handle + // inter-character spaces properly. + int pos = start; + for (int i = 0; true; i++) { + int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)]; + for (int j = 6; j >= 0; j--) { + // Even j = bars, while odd j = spaces. Categories 2 and 3 are for + // long stripes, while 0 and 1 are for short stripes. + int category = (j & 1) + (pattern & 1) * 2; + sizes[category] += counters[pos + j]; + counts[category]++; + pattern >>= 1; + } + if (i >= end) { + break; + } + // We ignore the inter-character space - it could be of any size. + pos += 8; + } + + // Calculate our allowable size thresholds using fixed-point math. + float[] maxes = new float[4]; + float[] mins = new float[4]; + // Define the threshold of acceptability to be the midpoint between the + // average small stripe and the average large stripe. No stripe lengths + // should be on the "wrong" side of that line. + for (int i = 0; i < 2; i++) { + mins[i] = 0.0f; // Accept arbitrarily small "short" stripes. + mins[i + 2] = ((float) sizes[i] / counts[i] + (float) sizes[i + 2] / counts[i + 2]) / 2.0f; + maxes[i] = mins[i + 2]; + maxes[i + 2] = (sizes[i + 2] * MAX_ACCEPTABLE + PADDING) / counts[i + 2]; + } + + // Now verify that all of the stripes are within the thresholds. + pos = start; + for (int i = 0; true; i++) { + int pattern = CHARACTER_ENCODINGS[decodeRowResult.charAt(i)]; + for (int j = 6; j >= 0; j--) { + // Even j = bars, while odd j = spaces. Categories 2 and 3 are for + // long stripes, while 0 and 1 are for short stripes. + int category = (j & 1) + (pattern & 1) * 2; + int size = counters[pos + j]; + if (size < mins[category] || size > maxes[category]) { + throw NotFoundException.getNotFoundInstance(); + } + pattern >>= 1; + } + if (i >= end) { + break; + } + pos += 8; + } + } + + /** + * Records the size of all runs of white and black pixels, starting with white. + * This is just like recordPattern, except it records all the counters, and + * uses our builtin "counters" member for storage. + * @param row row to count from + */ + private void setCounters(BitArray row) throws NotFoundException { + counterLength = 0; + // Start from the first white bit. + int i = row.getNextUnset(0); + int end = row.getSize(); + if (i >= end) { + throw NotFoundException.getNotFoundInstance(); + } + boolean isWhite = true; + int count = 0; + while (i < end) { + if (row.get(i) != isWhite) { + count++; + } else { + counterAppend(count); + count = 1; + isWhite = !isWhite; + } + i++; + } + counterAppend(count); + } + + private void counterAppend(int e) { + counters[counterLength] = e; + counterLength++; + if (counterLength >= counters.length) { + int[] temp = new int[counterLength * 2]; + System.arraycopy(counters, 0, temp, 0, counterLength); + counters = temp; + } + } + + private int findStartPattern() throws NotFoundException { + for (int i = 1; i < counterLength; i += 2) { + int charOffset = toNarrowWidePattern(i); + if (charOffset != -1 && arrayContains(STARTEND_ENCODING, ALPHABET[charOffset])) { + // Look for whitespace before start pattern, >= 50% of width of start pattern + // We make an exception if the whitespace is the first element. + int patternSize = 0; + for (int j = i; j < i + 7; j++) { + patternSize += counters[j]; + } + if (i == 1 || counters[i - 1] >= patternSize / 2) { + return i; + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + + static boolean arrayContains(char[] array, char key) { + if (array != null) { + for (char c : array) { + if (c == key) { + return true; + } + } + } + return false; + } + + // Assumes that counters[position] is a bar. + private int toNarrowWidePattern(int position) { + int end = position + 7; + if (end >= counterLength) { + return -1; + } + + int[] theCounters = counters; + + int maxBar = 0; + int minBar = Integer.MAX_VALUE; + for (int j = position; j < end; j += 2) { + int currentCounter = theCounters[j]; + if (currentCounter < minBar) { + minBar = currentCounter; + } + if (currentCounter > maxBar) { + maxBar = currentCounter; + } + } + int thresholdBar = (minBar + maxBar) / 2; + + int maxSpace = 0; + int minSpace = Integer.MAX_VALUE; + for (int j = position + 1; j < end; j += 2) { + int currentCounter = theCounters[j]; + if (currentCounter < minSpace) { + minSpace = currentCounter; + } + if (currentCounter > maxSpace) { + maxSpace = currentCounter; + } + } + int thresholdSpace = (minSpace + maxSpace) / 2; + + int bitmask = 1 << 7; + int pattern = 0; + for (int i = 0; i < 7; i++) { + int threshold = (i & 1) == 0 ? thresholdBar : thresholdSpace; + bitmask >>= 1; + if (theCounters[position + i] > threshold) { + pattern |= bitmask; + } + } + + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return i; + } + } + return -1; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/CodaBarWriter.java b/rubylib/src/main/java/com/google/zxing/oned/CodaBarWriter.java new file mode 100644 index 0000000..d27a166 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/CodaBarWriter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +/** + * This class renders CodaBar as {@code boolean[]}. + * + * @author dsbnatut@gmail.com (Kazuki Nishiura) + */ +public final class CodaBarWriter extends OneDimensionalCodeWriter { + + private static final char[] START_END_CHARS = {'A', 'B', 'C', 'D'}; + private static final char[] ALT_START_END_CHARS = {'T', 'N', '*', 'E'}; + private static final char[] CHARS_WHICH_ARE_TEN_LENGTH_EACH_AFTER_DECODED = {'/', ':', '+', '.'}; + private static final char DEFAULT_GUARD = START_END_CHARS[0]; + + @Override + public boolean[] encode(String contents) { + + if (contents.length() < 2) { + // Can't have a start/end guard, so tentatively add default guards + contents = DEFAULT_GUARD + contents + DEFAULT_GUARD; + } else { + // Verify input and calculate decoded length. + char firstChar = Character.toUpperCase(contents.charAt(0)); + char lastChar = Character.toUpperCase(contents.charAt(contents.length() - 1)); + boolean startsNormal = CodaBarReader.arrayContains(START_END_CHARS, firstChar); + boolean endsNormal = CodaBarReader.arrayContains(START_END_CHARS, lastChar); + boolean startsAlt = CodaBarReader.arrayContains(ALT_START_END_CHARS, firstChar); + boolean endsAlt = CodaBarReader.arrayContains(ALT_START_END_CHARS, lastChar); + if (startsNormal) { + if (!endsNormal) { + throw new IllegalArgumentException("Invalid start/end guards: " + contents); + } + // else already has valid start/end + } else if (startsAlt) { + if (!endsAlt) { + throw new IllegalArgumentException("Invalid start/end guards: " + contents); + } + // else already has valid start/end + } else { + // Doesn't start with a guard + if (endsNormal || endsAlt) { + throw new IllegalArgumentException("Invalid start/end guards: " + contents); + } + // else doesn't end with guard either, so add a default + contents = DEFAULT_GUARD + contents + DEFAULT_GUARD; + } + } + + // The start character and the end character are decoded to 10 length each. + int resultLength = 20; + for (int i = 1; i < contents.length() - 1; i++) { + if (Character.isDigit(contents.charAt(i)) || contents.charAt(i) == '-' || contents.charAt(i) == '$') { + resultLength += 9; + } else if (CodaBarReader.arrayContains(CHARS_WHICH_ARE_TEN_LENGTH_EACH_AFTER_DECODED, contents.charAt(i))) { + resultLength += 10; + } else { + throw new IllegalArgumentException("Cannot encode : '" + contents.charAt(i) + '\''); + } + } + // A blank is placed between each character. + resultLength += contents.length() - 1; + + boolean[] result = new boolean[resultLength]; + int position = 0; + for (int index = 0; index < contents.length(); index++) { + char c = Character.toUpperCase(contents.charAt(index)); + if (index == 0 || index == contents.length() - 1) { + // The start/end chars are not in the CodaBarReader.ALPHABET. + switch (c) { + case 'T': + c = 'A'; + break; + case 'N': + c = 'B'; + break; + case '*': + c = 'C'; + break; + case 'E': + c = 'D'; + break; + } + } + int code = 0; + for (int i = 0; i < CodaBarReader.ALPHABET.length; i++) { + // Found any, because I checked above. + if (c == CodaBarReader.ALPHABET[i]) { + code = CodaBarReader.CHARACTER_ENCODINGS[i]; + break; + } + } + boolean color = true; + int counter = 0; + int bit = 0; + while (bit < 7) { // A character consists of 7 digit. + result[position] = color; + position++; + if (((code >> (6 - bit)) & 1) == 0 || counter == 1) { + color = !color; // Flip the color. + bit++; + counter = 0; + } else { + counter++; + } + } + if (index < contents.length() - 1) { + result[position] = false; + position++; + } + } + return result; + } +} + diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code128Reader.java b/rubylib/src/main/java/com/google/zxing/oned/Code128Reader.java new file mode 100644 index 0000000..a9d2bc2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code128Reader.java @@ -0,0 +1,539 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + *

Decodes Code 128 barcodes.

+ * + * @author Sean Owen + */ +public final class Code128Reader extends OneDReader { + + static final int[][] CODE_PATTERNS = { + {2, 1, 2, 2, 2, 2}, // 0 + {2, 2, 2, 1, 2, 2}, + {2, 2, 2, 2, 2, 1}, + {1, 2, 1, 2, 2, 3}, + {1, 2, 1, 3, 2, 2}, + {1, 3, 1, 2, 2, 2}, // 5 + {1, 2, 2, 2, 1, 3}, + {1, 2, 2, 3, 1, 2}, + {1, 3, 2, 2, 1, 2}, + {2, 2, 1, 2, 1, 3}, + {2, 2, 1, 3, 1, 2}, // 10 + {2, 3, 1, 2, 1, 2}, + {1, 1, 2, 2, 3, 2}, + {1, 2, 2, 1, 3, 2}, + {1, 2, 2, 2, 3, 1}, + {1, 1, 3, 2, 2, 2}, // 15 + {1, 2, 3, 1, 2, 2}, + {1, 2, 3, 2, 2, 1}, + {2, 2, 3, 2, 1, 1}, + {2, 2, 1, 1, 3, 2}, + {2, 2, 1, 2, 3, 1}, // 20 + {2, 1, 3, 2, 1, 2}, + {2, 2, 3, 1, 1, 2}, + {3, 1, 2, 1, 3, 1}, + {3, 1, 1, 2, 2, 2}, + {3, 2, 1, 1, 2, 2}, // 25 + {3, 2, 1, 2, 2, 1}, + {3, 1, 2, 2, 1, 2}, + {3, 2, 2, 1, 1, 2}, + {3, 2, 2, 2, 1, 1}, + {2, 1, 2, 1, 2, 3}, // 30 + {2, 1, 2, 3, 2, 1}, + {2, 3, 2, 1, 2, 1}, + {1, 1, 1, 3, 2, 3}, + {1, 3, 1, 1, 2, 3}, + {1, 3, 1, 3, 2, 1}, // 35 + {1, 1, 2, 3, 1, 3}, + {1, 3, 2, 1, 1, 3}, + {1, 3, 2, 3, 1, 1}, + {2, 1, 1, 3, 1, 3}, + {2, 3, 1, 1, 1, 3}, // 40 + {2, 3, 1, 3, 1, 1}, + {1, 1, 2, 1, 3, 3}, + {1, 1, 2, 3, 3, 1}, + {1, 3, 2, 1, 3, 1}, + {1, 1, 3, 1, 2, 3}, // 45 + {1, 1, 3, 3, 2, 1}, + {1, 3, 3, 1, 2, 1}, + {3, 1, 3, 1, 2, 1}, + {2, 1, 1, 3, 3, 1}, + {2, 3, 1, 1, 3, 1}, // 50 + {2, 1, 3, 1, 1, 3}, + {2, 1, 3, 3, 1, 1}, + {2, 1, 3, 1, 3, 1}, + {3, 1, 1, 1, 2, 3}, + {3, 1, 1, 3, 2, 1}, // 55 + {3, 3, 1, 1, 2, 1}, + {3, 1, 2, 1, 1, 3}, + {3, 1, 2, 3, 1, 1}, + {3, 3, 2, 1, 1, 1}, + {3, 1, 4, 1, 1, 1}, // 60 + {2, 2, 1, 4, 1, 1}, + {4, 3, 1, 1, 1, 1}, + {1, 1, 1, 2, 2, 4}, + {1, 1, 1, 4, 2, 2}, + {1, 2, 1, 1, 2, 4}, // 65 + {1, 2, 1, 4, 2, 1}, + {1, 4, 1, 1, 2, 2}, + {1, 4, 1, 2, 2, 1}, + {1, 1, 2, 2, 1, 4}, + {1, 1, 2, 4, 1, 2}, // 70 + {1, 2, 2, 1, 1, 4}, + {1, 2, 2, 4, 1, 1}, + {1, 4, 2, 1, 1, 2}, + {1, 4, 2, 2, 1, 1}, + {2, 4, 1, 2, 1, 1}, // 75 + {2, 2, 1, 1, 1, 4}, + {4, 1, 3, 1, 1, 1}, + {2, 4, 1, 1, 1, 2}, + {1, 3, 4, 1, 1, 1}, + {1, 1, 1, 2, 4, 2}, // 80 + {1, 2, 1, 1, 4, 2}, + {1, 2, 1, 2, 4, 1}, + {1, 1, 4, 2, 1, 2}, + {1, 2, 4, 1, 1, 2}, + {1, 2, 4, 2, 1, 1}, // 85 + {4, 1, 1, 2, 1, 2}, + {4, 2, 1, 1, 1, 2}, + {4, 2, 1, 2, 1, 1}, + {2, 1, 2, 1, 4, 1}, + {2, 1, 4, 1, 2, 1}, // 90 + {4, 1, 2, 1, 2, 1}, + {1, 1, 1, 1, 4, 3}, + {1, 1, 1, 3, 4, 1}, + {1, 3, 1, 1, 4, 1}, + {1, 1, 4, 1, 1, 3}, // 95 + {1, 1, 4, 3, 1, 1}, + {4, 1, 1, 1, 1, 3}, + {4, 1, 1, 3, 1, 1}, + {1, 1, 3, 1, 4, 1}, + {1, 1, 4, 1, 3, 1}, // 100 + {3, 1, 1, 1, 4, 1}, + {4, 1, 1, 1, 3, 1}, + {2, 1, 1, 4, 1, 2}, + {2, 1, 1, 2, 1, 4}, + {2, 1, 1, 2, 3, 2}, // 105 + {2, 3, 3, 1, 1, 1, 2} + }; + + private static final float MAX_AVG_VARIANCE = 0.25f; + private static final float MAX_INDIVIDUAL_VARIANCE = 0.7f; + + private static final int CODE_SHIFT = 98; + + private static final int CODE_CODE_C = 99; + private static final int CODE_CODE_B = 100; + private static final int CODE_CODE_A = 101; + + private static final int CODE_FNC_1 = 102; + private static final int CODE_FNC_2 = 97; + private static final int CODE_FNC_3 = 96; + private static final int CODE_FNC_4_A = 101; + private static final int CODE_FNC_4_B = 100; + + private static final int CODE_START_A = 103; + private static final int CODE_START_B = 104; + private static final int CODE_START_C = 105; + private static final int CODE_STOP = 106; + + private static int[] findStartPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = row.getNextSet(0); + + int counterPosition = 0; + int[] counters = new int[6]; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + if (row.get(i) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + float bestVariance = MAX_AVG_VARIANCE; + int bestMatch = -1; + for (int startCode = CODE_START_A; startCode <= CODE_START_C; startCode++) { + float variance = patternMatchVariance(counters, CODE_PATTERNS[startCode], + MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = startCode; + } + } + // Look for whitespace before start pattern, >= 50% of width of start pattern + if (bestMatch >= 0 && + row.isRange(Math.max(0, patternStart - (i - patternStart) / 2), patternStart, false)) { + return new int[]{patternStart, i, bestMatch}; + } + patternStart += counters[0] + counters[1]; + System.arraycopy(counters, 2, counters, 0, counterPosition - 1); + counters[counterPosition - 1] = 0; + counters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static int decodeCode(BitArray row, int[] counters, int rowOffset) + throws NotFoundException { + recordPattern(row, rowOffset, counters); + float bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int bestMatch = -1; + for (int d = 0; d < CODE_PATTERNS.length; d++) { + int[] pattern = CODE_PATTERNS[d]; + float variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = d; + } + } + // TODO We're overlooking the fact that the STOP pattern has 7 values, not 6. + if (bestMatch >= 0) { + return bestMatch; + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, FormatException, ChecksumException { + + boolean convertFNC1 = hints != null && hints.containsKey(DecodeHintType.ASSUME_GS1); + + int[] startPatternInfo = findStartPattern(row); + int startCode = startPatternInfo[2]; + + List rawCodes = new ArrayList<>(20); + rawCodes.add((byte) startCode); + + int codeSet; + switch (startCode) { + case CODE_START_A: + codeSet = CODE_CODE_A; + break; + case CODE_START_B: + codeSet = CODE_CODE_B; + break; + case CODE_START_C: + codeSet = CODE_CODE_C; + break; + default: + throw FormatException.getFormatInstance(); + } + + boolean done = false; + boolean isNextShifted = false; + + StringBuilder result = new StringBuilder(20); + + int lastStart = startPatternInfo[0]; + int nextStart = startPatternInfo[1]; + int[] counters = new int[6]; + + int lastCode = 0; + int code = 0; + int checksumTotal = startCode; + int multiplier = 0; + boolean lastCharacterWasPrintable = true; + boolean upperMode = false; + boolean shiftUpperMode = false; + + while (!done) { + + boolean unshift = isNextShifted; + isNextShifted = false; + + // Save off last code + lastCode = code; + + // Decode another code from image + code = decodeCode(row, counters, nextStart); + + rawCodes.add((byte) code); + + // Remember whether the last code was printable or not (excluding CODE_STOP) + if (code != CODE_STOP) { + lastCharacterWasPrintable = true; + } + + // Add to checksum computation (if not CODE_STOP of course) + if (code != CODE_STOP) { + multiplier++; + checksumTotal += multiplier * code; + } + + // Advance to where the next code will to start + lastStart = nextStart; + for (int counter : counters) { + nextStart += counter; + } + + // Take care of illegal start codes + switch (code) { + case CODE_START_A: + case CODE_START_B: + case CODE_START_C: + throw FormatException.getFormatInstance(); + } + + switch (codeSet) { + + case CODE_CODE_A: + if (code < 64) { + if (shiftUpperMode == upperMode) { + result.append((char) (' ' + code)); + } else { + result.append((char) (' ' + code + 128)); + } + shiftUpperMode = false; + } else if (code < 96) { + if (shiftUpperMode == upperMode) { + result.append((char) (code - 64)); + } else { + result.append((char) (code + 64)); + } + shiftUpperMode = false; + } else { + // Don't let CODE_STOP, which always appears, affect whether whether we think the last + // code was printable or not. + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + if (convertFNC1) { + if (result.length() == 0) { + // GS1 specification 5.4.3.7. and 5.4.6.4. If the first char after the start code + // is FNC1 then this is GS1-128. We add the symbology identifier. + result.append("]C1"); + } else { + // GS1 specification 5.4.7.5. Every subsequent FNC1 is returned as ASCII 29 (GS) + result.append((char) 29); + } + } + break; + case CODE_FNC_2: + case CODE_FNC_3: + // do nothing? + break; + case CODE_FNC_4_A: + if (!upperMode && shiftUpperMode) { + upperMode = true; + shiftUpperMode = false; + } else if (upperMode && shiftUpperMode) { + upperMode = false; + shiftUpperMode = false; + } else { + shiftUpperMode = true; + } + break; + case CODE_SHIFT: + isNextShifted = true; + codeSet = CODE_CODE_B; + break; + case CODE_CODE_B: + codeSet = CODE_CODE_B; + break; + case CODE_CODE_C: + codeSet = CODE_CODE_C; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + case CODE_CODE_B: + if (code < 96) { + if (shiftUpperMode == upperMode) { + result.append((char) (' ' + code)); + } else { + result.append((char) (' ' + code + 128)); + } + shiftUpperMode = false; + } else { + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + if (convertFNC1) { + if (result.length() == 0) { + // GS1 specification 5.4.3.7. and 5.4.6.4. If the first char after the start code + // is FNC1 then this is GS1-128. We add the symbology identifier. + result.append("]C1"); + } else { + // GS1 specification 5.4.7.5. Every subsequent FNC1 is returned as ASCII 29 (GS) + result.append((char) 29); + } + } + break; + case CODE_FNC_2: + case CODE_FNC_3: + // do nothing? + break; + case CODE_FNC_4_B: + if (!upperMode && shiftUpperMode) { + upperMode = true; + shiftUpperMode = false; + } else if (upperMode && shiftUpperMode) { + upperMode = false; + shiftUpperMode = false; + } else { + shiftUpperMode = true; + } + break; + case CODE_SHIFT: + isNextShifted = true; + codeSet = CODE_CODE_A; + break; + case CODE_CODE_A: + codeSet = CODE_CODE_A; + break; + case CODE_CODE_C: + codeSet = CODE_CODE_C; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + case CODE_CODE_C: + if (code < 100) { + if (code < 10) { + result.append('0'); + } + result.append(code); + } else { + if (code != CODE_STOP) { + lastCharacterWasPrintable = false; + } + switch (code) { + case CODE_FNC_1: + if (convertFNC1) { + if (result.length() == 0) { + // GS1 specification 5.4.3.7. and 5.4.6.4. If the first char after the start code + // is FNC1 then this is GS1-128. We add the symbology identifier. + result.append("]C1"); + } else { + // GS1 specification 5.4.7.5. Every subsequent FNC1 is returned as ASCII 29 (GS) + result.append((char) 29); + } + } + break; + case CODE_CODE_A: + codeSet = CODE_CODE_A; + break; + case CODE_CODE_B: + codeSet = CODE_CODE_B; + break; + case CODE_STOP: + done = true; + break; + } + } + break; + } + + // Unshift back to another code set if we were shifted + if (unshift) { + codeSet = codeSet == CODE_CODE_A ? CODE_CODE_B : CODE_CODE_A; + } + + } + + int lastPatternSize = nextStart - lastStart; + + // Check for ample whitespace following pattern, but, to do this we first need to remember that + // we fudged decoding CODE_STOP since it actually has 7 bars, not 6. There is a black bar left + // to read off. Would be slightly better to properly read. Here we just skip it: + nextStart = row.getNextUnset(nextStart); + if (!row.isRange(nextStart, + Math.min(row.getSize(), nextStart + (nextStart - lastStart) / 2), + false)) { + throw NotFoundException.getNotFoundInstance(); + } + + // Pull out from sum the value of the penultimate check code + checksumTotal -= multiplier * lastCode; + // lastCode is the checksum then: + if (checksumTotal % 103 != lastCode) { + throw ChecksumException.getChecksumInstance(); + } + + // Need to pull out the check digits from string + int resultLength = result.length(); + if (resultLength == 0) { + // false positive + throw NotFoundException.getNotFoundInstance(); + } + + // Only bother if the result had at least one character, and if the checksum digit happened to + // be a printable character. If it was just interpreted as a control code, nothing to remove. + if (resultLength > 0 && lastCharacterWasPrintable) { + if (codeSet == CODE_CODE_C) { + result.delete(resultLength - 2, resultLength); + } else { + result.delete(resultLength - 1, resultLength); + } + } + + float left = (startPatternInfo[1] + startPatternInfo[0]) / 2.0f; + float right = lastStart + lastPatternSize / 2.0f; + + int rawCodesSize = rawCodes.size(); + byte[] rawBytes = new byte[rawCodesSize]; + for (int i = 0; i < rawCodesSize; i++) { + rawBytes[i] = rawCodes.get(i); + } + + return new Result( + result.toString(), + rawBytes, + new ResultPoint[]{ + new ResultPoint(left, rowNumber), + new ResultPoint(right, rowNumber)}, + BarcodeFormat.CODE_128); + + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code128Writer.java b/rubylib/src/main/java/com/google/zxing/oned/Code128Writer.java new file mode 100644 index 0000000..7ce9808 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code128Writer.java @@ -0,0 +1,255 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * This object renders a CODE128 code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class Code128Writer extends OneDimensionalCodeWriter { + + private static final int CODE_START_B = 104; + private static final int CODE_START_C = 105; + private static final int CODE_CODE_B = 100; + private static final int CODE_CODE_C = 99; + private static final int CODE_STOP = 106; + + // Dummy characters used to specify control characters in input + private static final char ESCAPE_FNC_1 = '\u00f1'; + private static final char ESCAPE_FNC_2 = '\u00f2'; + private static final char ESCAPE_FNC_3 = '\u00f3'; + private static final char ESCAPE_FNC_4 = '\u00f4'; + + private static final int CODE_FNC_1 = 102; // Code A, Code B, Code C + private static final int CODE_FNC_2 = 97; // Code A, Code B + private static final int CODE_FNC_3 = 96; // Code A, Code B + private static final int CODE_FNC_4_B = 100; // Code B + + // Results of minimal lookahead for code C + private enum CType { + UNCODABLE, + ONE_DIGIT, + TWO_DIGITS, + FNC_1 + } + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.CODE_128) { + throw new IllegalArgumentException("Can only encode CODE_128, but got " + format); + } + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + // Check length + if (length < 1 || length > 80) { + throw new IllegalArgumentException( + "Contents length should be between 1 and 80 characters, but got " + length); + } + // Check content + for (int i = 0; i < length; i++) { + char c = contents.charAt(i); + if (c < ' ' || c > '~') { + switch (c) { + case ESCAPE_FNC_1: + case ESCAPE_FNC_2: + case ESCAPE_FNC_3: + case ESCAPE_FNC_4: + break; + default: + throw new IllegalArgumentException("Bad character in input: " + c); + } + } + } + + Collection patterns = new ArrayList<>(); // temporary storage for patterns + int checkSum = 0; + int checkWeight = 1; + int codeSet = 0; // selected code (CODE_CODE_B or CODE_CODE_C) + int position = 0; // position in contents + + while (position < length) { + //Select code to use + int newCodeSet = chooseCode(contents, position, codeSet); + + //Get the pattern index + int patternIndex; + if (newCodeSet == codeSet) { + // Encode the current character + // First handle escapes + switch (contents.charAt(position)) { + case ESCAPE_FNC_1: + patternIndex = CODE_FNC_1; + break; + case ESCAPE_FNC_2: + patternIndex = CODE_FNC_2; + break; + case ESCAPE_FNC_3: + patternIndex = CODE_FNC_3; + break; + case ESCAPE_FNC_4: + patternIndex = CODE_FNC_4_B; // FIXME if this ever outputs Code A + break; + default: + // Then handle normal characters otherwise + if (codeSet == CODE_CODE_B) { + patternIndex = contents.charAt(position) - ' '; + } else { // CODE_CODE_C + patternIndex = Integer.parseInt(contents.substring(position, position + 2)); + position++; // Also incremented below + } + } + position++; + } else { + // Should we change the current code? + // Do we have a code set? + if (codeSet == 0) { + // No, we don't have a code set + if (newCodeSet == CODE_CODE_B) { + patternIndex = CODE_START_B; + } else { + // CODE_CODE_C + patternIndex = CODE_START_C; + } + } else { + // Yes, we have a code set + patternIndex = newCodeSet; + } + codeSet = newCodeSet; + } + + // Get the pattern + patterns.add(Code128Reader.CODE_PATTERNS[patternIndex]); + + // Compute checksum + checkSum += patternIndex * checkWeight; + if (position != 0) { + checkWeight++; + } + } + + // Compute and append checksum + checkSum %= 103; + patterns.add(Code128Reader.CODE_PATTERNS[checkSum]); + + // Append stop code + patterns.add(Code128Reader.CODE_PATTERNS[CODE_STOP]); + + // Compute code width + int codeWidth = 0; + for (int[] pattern : patterns) { + for (int width : pattern) { + codeWidth += width; + } + } + + // Compute result + boolean[] result = new boolean[codeWidth]; + int pos = 0; + for (int[] pattern : patterns) { + pos += appendPattern(result, pos, pattern, true); + } + + return result; + } + + private static CType findCType(CharSequence value, int start) { + int last = value.length(); + if (start >= last) { + return CType.UNCODABLE; + } + char c = value.charAt(start); + if (c == ESCAPE_FNC_1) { + return CType.FNC_1; + } + if (c < '0' || c > '9') { + return CType.UNCODABLE; + } + if (start + 1 >= last) { + return CType.ONE_DIGIT; + } + c = value.charAt(start + 1); + if (c < '0' || c > '9') { + return CType.ONE_DIGIT; + } + return CType.TWO_DIGITS; + } + + private static int chooseCode(CharSequence value, int start, int oldCode) { + CType lookahead = findCType(value, start); + if (lookahead == CType.UNCODABLE || lookahead == CType.ONE_DIGIT) { + return CODE_CODE_B; // no choice + } + if (oldCode == CODE_CODE_C) { // can continue in code C + return CODE_CODE_C; + } + if (oldCode == CODE_CODE_B) { + if (lookahead == CType.FNC_1) { + return CODE_CODE_B; // can continue in code B + } + // Seen two consecutive digits, see what follows + lookahead = findCType(value, start + 2); + if (lookahead == CType.UNCODABLE || lookahead == CType.ONE_DIGIT) { + return CODE_CODE_B; // not worth switching now + } + if (lookahead == CType.FNC_1) { // two digits, then FNC_1... + lookahead = findCType(value, start + 3); + if (lookahead == CType.TWO_DIGITS) { // then two more digits, switch + return CODE_CODE_C; + } else { + return CODE_CODE_B; // otherwise not worth switching + } + } + // At this point, there are at least 4 consecutive digits. + // Look ahead to choose whether to switch now or on the next round. + int index = start + 4; + while ((lookahead = findCType(value, index)) == CType.TWO_DIGITS) { + index += 2; + } + if (lookahead == CType.ONE_DIGIT) { // odd number of digits, switch later + return CODE_CODE_B; + } + return CODE_CODE_C; // even number of digits, switch now + } + // Here oldCode == 0, which means we are choosing the initial code + if (lookahead == CType.FNC_1) { // ignore FNC_1 + lookahead = findCType(value, start + 1); + } + if (lookahead == CType.TWO_DIGITS) { // at least two digits, start in code C + return CODE_CODE_C; + } + return CODE_CODE_B; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code39Reader.java b/rubylib/src/main/java/com/google/zxing/oned/Code39Reader.java new file mode 100644 index 0000000..3dbd251 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code39Reader.java @@ -0,0 +1,324 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.Map; + +/** + *

Decodes Code 39 barcodes. This does not support "Full ASCII Code 39" yet.

+ * + * @author Sean Owen + * @see Code93Reader + */ +public final class Code39Reader extends OneDReader { + + static final String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. *$/+%"; + // Note this lacks '*' compared to ALPHABET_STRING + private static final String CHECK_DIGIT_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%"; + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. + * The 9 least-significant bits of each int correspond to the pattern of wide and narrow, + * with 1s representing "wide" and 0s representing narrow. + */ + static final int[] CHARACTER_ENCODINGS = { + 0x034, 0x121, 0x061, 0x160, 0x031, 0x130, 0x070, 0x025, 0x124, 0x064, // 0-9 + 0x109, 0x049, 0x148, 0x019, 0x118, 0x058, 0x00D, 0x10C, 0x04C, 0x01C, // A-J + 0x103, 0x043, 0x142, 0x013, 0x112, 0x052, 0x007, 0x106, 0x046, 0x016, // K-T + 0x181, 0x0C1, 0x1C0, 0x091, 0x190, 0x0D0, 0x085, 0x184, 0x0C4, 0x094, // U-* + 0x0A8, 0x0A2, 0x08A, 0x02A // $-% + }; + + static final int ASTERISK_ENCODING = CHARACTER_ENCODINGS[39]; + + private final boolean usingCheckDigit; + private final boolean extendedMode; + private final StringBuilder decodeRowResult; + private final int[] counters; + + /** + * Creates a reader that assumes all encoded data is data, and does not treat the final + * character as a check digit. It will not decoded "extended Code 39" sequences. + */ + public Code39Reader() { + this(false); + } + + /** + * Creates a reader that can be configured to check the last character as a check digit. + * It will not decoded "extended Code 39" sequences. + * + * @param usingCheckDigit if true, treat the last data character as a check digit, not + * data, and verify that the checksum passes. + */ + public Code39Reader(boolean usingCheckDigit) { + this(usingCheckDigit, false); + } + + /** + * Creates a reader that can be configured to check the last character as a check digit, + * or optionally attempt to decode "extended Code 39" sequences that are used to encode + * the full ASCII character set. + * + * @param usingCheckDigit if true, treat the last data character as a check digit, not + * data, and verify that the checksum passes. + * @param extendedMode if true, will attempt to decode extended Code 39 sequences in the + * text. + */ + public Code39Reader(boolean usingCheckDigit, boolean extendedMode) { + this.usingCheckDigit = usingCheckDigit; + this.extendedMode = extendedMode; + decodeRowResult = new StringBuilder(20); + counters = new int[9]; + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, ChecksumException, FormatException { + + int[] theCounters = counters; + Arrays.fill(theCounters, 0); + StringBuilder result = decodeRowResult; + result.setLength(0); + + int[] start = findAsteriskPattern(row, theCounters); + // Read off white space + int nextStart = row.getNextSet(start[1]); + int end = row.getSize(); + + char decodedChar; + int lastStart; + do { + recordPattern(row, nextStart, theCounters); + int pattern = toNarrowWidePattern(theCounters); + if (pattern < 0) { + throw NotFoundException.getNotFoundInstance(); + } + decodedChar = patternToChar(pattern); + result.append(decodedChar); + lastStart = nextStart; + for (int counter : theCounters) { + nextStart += counter; + } + // Read off white space + nextStart = row.getNextSet(nextStart); + } while (decodedChar != '*'); + result.setLength(result.length() - 1); // remove asterisk + + // Look for whitespace after pattern: + int lastPatternSize = 0; + for (int counter : theCounters) { + lastPatternSize += counter; + } + int whiteSpaceAfterEnd = nextStart - lastStart - lastPatternSize; + // If 50% of last pattern size, following last pattern, is not whitespace, fail + // (but if it's whitespace to the very end of the image, that's OK) + if (nextStart != end && (whiteSpaceAfterEnd * 2) < lastPatternSize) { + throw NotFoundException.getNotFoundInstance(); + } + + if (usingCheckDigit) { + int max = result.length() - 1; + int total = 0; + for (int i = 0; i < max; i++) { + total += CHECK_DIGIT_STRING.indexOf(decodeRowResult.charAt(i)); + } + if (result.charAt(max) != CHECK_DIGIT_STRING.charAt(total % 43)) { + throw ChecksumException.getChecksumInstance(); + } + result.setLength(max); + } + + if (result.length() == 0) { + // false positive + throw NotFoundException.getNotFoundInstance(); + } + + String resultString; + if (extendedMode) { + resultString = decodeExtended(result); + } else { + resultString = result.toString(); + } + + float left = (start[1] + start[0]) / 2.0f; + float right = lastStart + lastPatternSize / 2.0f; + return new Result( + resultString, + null, + new ResultPoint[]{ + new ResultPoint(left, rowNumber), + new ResultPoint(right, rowNumber)}, + BarcodeFormat.CODE_39); + + } + + private static int[] findAsteriskPattern(BitArray row, int[] counters) throws NotFoundException { + int width = row.getSize(); + int rowOffset = row.getNextSet(0); + + int counterPosition = 0; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = counters.length; + + for (int i = rowOffset; i < width; i++) { + if (row.get(i) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + // Look for whitespace before start pattern, >= 50% of width of start pattern + if (toNarrowWidePattern(counters) == ASTERISK_ENCODING && + row.isRange(Math.max(0, patternStart - ((i - patternStart) / 2)), patternStart, false)) { + return new int[]{patternStart, i}; + } + patternStart += counters[0] + counters[1]; + System.arraycopy(counters, 2, counters, 0, counterPosition - 1); + counters[counterPosition - 1] = 0; + counters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + // For efficiency, returns -1 on failure. Not throwing here saved as many as 700 exceptions + // per image when using some of our blackbox images. + private static int toNarrowWidePattern(int[] counters) { + int numCounters = counters.length; + int maxNarrowCounter = 0; + int wideCounters; + do { + int minCounter = Integer.MAX_VALUE; + for (int counter : counters) { + if (counter < minCounter && counter > maxNarrowCounter) { + minCounter = counter; + } + } + maxNarrowCounter = minCounter; + wideCounters = 0; + int totalWideCountersWidth = 0; + int pattern = 0; + for (int i = 0; i < numCounters; i++) { + int counter = counters[i]; + if (counter > maxNarrowCounter) { + pattern |= 1 << (numCounters - 1 - i); + wideCounters++; + totalWideCountersWidth += counter; + } + } + if (wideCounters == 3) { + // Found 3 wide counters, but are they close enough in width? + // We can perform a cheap, conservative check to see if any individual + // counter is more than 1.5 times the average: + for (int i = 0; i < numCounters && wideCounters > 0; i++) { + int counter = counters[i]; + if (counter > maxNarrowCounter) { + wideCounters--; + // totalWideCountersWidth = 3 * average, so this checks if counter >= 3/2 * average + if ((counter * 2) >= totalWideCountersWidth) { + return -1; + } + } + } + return pattern; + } + } while (wideCounters > 3); + return -1; + } + + private static char patternToChar(int pattern) throws NotFoundException { + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return ALPHABET_STRING.charAt(i); + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static String decodeExtended(CharSequence encoded) throws FormatException { + int length = encoded.length(); + StringBuilder decoded = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = encoded.charAt(i); + if (c == '+' || c == '$' || c == '%' || c == '/') { + char next = encoded.charAt(i + 1); + char decodedChar = '\0'; + switch (c) { + case '+': + // +A to +Z map to a to z + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next + 32); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '$': + // $A to $Z map to control codes SH to SB + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next - 64); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '%': + // %A to %E map to control codes ESC to US + if (next >= 'A' && next <= 'E') { + decodedChar = (char) (next - 38); + } else if (next >= 'F' && next <= 'W') { + decodedChar = (char) (next - 11); + } else { + throw FormatException.getFormatInstance(); + } + break; + case '/': + // /A to /O map to ! to , and /Z maps to : + if (next >= 'A' && next <= 'O') { + decodedChar = (char) (next - 32); + } else if (next == 'Z') { + decodedChar = ':'; + } else { + throw FormatException.getFormatInstance(); + } + break; + } + decoded.append(decodedChar); + // bump up i again since we read two characters + i++; + } else { + decoded.append(c); + } + } + return decoded.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code39Writer.java b/rubylib/src/main/java/com/google/zxing/oned/Code39Writer.java new file mode 100644 index 0000000..d376632 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code39Writer.java @@ -0,0 +1,89 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders a CODE39 code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class Code39Writer extends OneDimensionalCodeWriter { + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.CODE_39) { + throw new IllegalArgumentException("Can only encode CODE_39, but got " + format); + } + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + + int[] widths = new int[9]; + int codeWidth = 24 + 1 + length; + for (int i = 0; i < length; i++) { + int indexInString = Code39Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + if (indexInString < 0) { + throw new IllegalArgumentException("Bad contents: " + contents); + } + toIntArray(Code39Reader.CHARACTER_ENCODINGS[indexInString], widths); + for (int width : widths) { + codeWidth += width; + } + } + boolean[] result = new boolean[codeWidth]; + toIntArray(Code39Reader.ASTERISK_ENCODING, widths); + int pos = appendPattern(result, 0, widths, true); + int[] narrowWhite = {1}; + pos += appendPattern(result, pos, narrowWhite, false); + //append next character to byte matrix + for (int i = 0; i < length; i++) { + int indexInString = Code39Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + toIntArray(Code39Reader.CHARACTER_ENCODINGS[indexInString], widths); + pos += appendPattern(result, pos, widths, true); + pos += appendPattern(result, pos, narrowWhite, false); + } + toIntArray(Code39Reader.ASTERISK_ENCODING, widths); + appendPattern(result, pos, widths, true); + return result; + } + + private static void toIntArray(int a, int[] toReturn) { + for (int i = 0; i < 9; i++) { + int temp = a & (1 << (8 - i)); + toReturn[i] = temp == 0 ? 1 : 2; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code93Reader.java b/rubylib/src/main/java/com/google/zxing/oned/Code93Reader.java new file mode 100644 index 0000000..a27c30e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code93Reader.java @@ -0,0 +1,287 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.Map; + +/** + *

Decodes Code 93 barcodes.

+ * + * @author Sean Owen + * @see Code39Reader + */ +public final class Code93Reader extends OneDReader { + + // Note that 'abcd' are dummy characters in place of control characters. + static final String ALPHABET_STRING = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-. $/+%abcd*"; + private static final char[] ALPHABET = ALPHABET_STRING.toCharArray(); + + /** + * These represent the encodings of characters, as patterns of wide and narrow bars. + * The 9 least-significant bits of each int correspond to the pattern of wide and narrow. + */ + static final int[] CHARACTER_ENCODINGS = { + 0x114, 0x148, 0x144, 0x142, 0x128, 0x124, 0x122, 0x150, 0x112, 0x10A, // 0-9 + 0x1A8, 0x1A4, 0x1A2, 0x194, 0x192, 0x18A, 0x168, 0x164, 0x162, 0x134, // A-J + 0x11A, 0x158, 0x14C, 0x146, 0x12C, 0x116, 0x1B4, 0x1B2, 0x1AC, 0x1A6, // K-T + 0x196, 0x19A, 0x16C, 0x166, 0x136, 0x13A, // U-Z + 0x12E, 0x1D4, 0x1D2, 0x1CA, 0x16E, 0x176, 0x1AE, // - - % + 0x126, 0x1DA, 0x1D6, 0x132, 0x15E, // Control chars? $-* + }; + private static final int ASTERISK_ENCODING = CHARACTER_ENCODINGS[47]; + + private final StringBuilder decodeRowResult; + private final int[] counters; + + public Code93Reader() { + decodeRowResult = new StringBuilder(20); + counters = new int[6]; + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, ChecksumException, FormatException { + + int[] start = findAsteriskPattern(row); + // Read off white space + int nextStart = row.getNextSet(start[1]); + int end = row.getSize(); + + int[] theCounters = counters; + Arrays.fill(theCounters, 0); + StringBuilder result = decodeRowResult; + result.setLength(0); + + char decodedChar; + int lastStart; + do { + recordPattern(row, nextStart, theCounters); + int pattern = toPattern(theCounters); + if (pattern < 0) { + throw NotFoundException.getNotFoundInstance(); + } + decodedChar = patternToChar(pattern); + result.append(decodedChar); + lastStart = nextStart; + for (int counter : theCounters) { + nextStart += counter; + } + // Read off white space + nextStart = row.getNextSet(nextStart); + } while (decodedChar != '*'); + result.deleteCharAt(result.length() - 1); // remove asterisk + + int lastPatternSize = 0; + for (int counter : theCounters) { + lastPatternSize += counter; + } + + // Should be at least one more black module + if (nextStart == end || !row.get(nextStart)) { + throw NotFoundException.getNotFoundInstance(); + } + + if (result.length() < 2) { + // false positive -- need at least 2 checksum digits + throw NotFoundException.getNotFoundInstance(); + } + + checkChecksums(result); + // Remove checksum digits + result.setLength(result.length() - 2); + + String resultString = decodeExtended(result); + + float left = (start[1] + start[0]) / 2.0f; + float right = lastStart + lastPatternSize / 2.0f; + return new Result( + resultString, + null, + new ResultPoint[]{ + new ResultPoint(left, rowNumber), + new ResultPoint(right, rowNumber)}, + BarcodeFormat.CODE_93); + + } + + private int[] findAsteriskPattern(BitArray row) throws NotFoundException { + int width = row.getSize(); + int rowOffset = row.getNextSet(0); + + Arrays.fill(counters, 0); + int[] theCounters = counters; + int patternStart = rowOffset; + boolean isWhite = false; + int patternLength = theCounters.length; + + int counterPosition = 0; + for (int i = rowOffset; i < width; i++) { + if (row.get(i) != isWhite) { + theCounters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (toPattern(theCounters) == ASTERISK_ENCODING) { + return new int[]{patternStart, i}; + } + patternStart += theCounters[0] + theCounters[1]; + System.arraycopy(theCounters, 2, theCounters, 0, counterPosition - 1); + theCounters[counterPosition - 1] = 0; + theCounters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + theCounters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static int toPattern(int[] counters) { + int sum = 0; + for (int counter : counters) { + sum += counter; + } + int pattern = 0; + int max = counters.length; + for (int i = 0; i < max; i++) { + int scaled = Math.round(counters[i] * 9.0f / sum); + if (scaled < 1 || scaled > 4) { + return -1; + } + if ((i & 0x01) == 0) { + for (int j = 0; j < scaled; j++) { + pattern = (pattern << 1) | 0x01; + } + } else { + pattern <<= scaled; + } + } + return pattern; + } + + private static char patternToChar(int pattern) throws NotFoundException { + for (int i = 0; i < CHARACTER_ENCODINGS.length; i++) { + if (CHARACTER_ENCODINGS[i] == pattern) { + return ALPHABET[i]; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static String decodeExtended(CharSequence encoded) throws FormatException { + int length = encoded.length(); + StringBuilder decoded = new StringBuilder(length); + for (int i = 0; i < length; i++) { + char c = encoded.charAt(i); + if (c >= 'a' && c <= 'd') { + if (i >= length - 1) { + throw FormatException.getFormatInstance(); + } + char next = encoded.charAt(i + 1); + char decodedChar = '\0'; + switch (c) { + case 'd': + // +A to +Z map to a to z + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next + 32); + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'a': + // $A to $Z map to control codes SH to SB + if (next >= 'A' && next <= 'Z') { + decodedChar = (char) (next - 64); + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'b': + if (next >= 'A' && next <= 'E') { + // %A to %E map to control codes ESC to USep + decodedChar = (char) (next - 38); + } else if (next >= 'F' && next <= 'J') { + // %F to %J map to ; < = > ? + decodedChar = (char) (next - 11); + } else if (next >= 'K' && next <= 'O') { + // %K to %O map to [ \ ] ^ _ + decodedChar = (char) (next + 16); + } else if (next >= 'P' && next <= 'S') { + // %P to %S map to { | } ~ + decodedChar = (char) (next + 43); + } else if (next >= 'T' && next <= 'Z') { + // %T to %Z all map to DEL (127) + decodedChar = 127; + } else { + throw FormatException.getFormatInstance(); + } + break; + case 'c': + // /A to /O map to ! to , and /Z maps to : + if (next >= 'A' && next <= 'O') { + decodedChar = (char) (next - 32); + } else if (next == 'Z') { + decodedChar = ':'; + } else { + throw FormatException.getFormatInstance(); + } + break; + } + decoded.append(decodedChar); + // bump up i again since we read two characters + i++; + } else { + decoded.append(c); + } + } + return decoded.toString(); + } + + private static void checkChecksums(CharSequence result) throws ChecksumException { + int length = result.length(); + checkOneChecksum(result, length - 2, 20); + checkOneChecksum(result, length - 1, 15); + } + + private static void checkOneChecksum(CharSequence result, int checkPosition, int weightMax) + throws ChecksumException { + int weight = 1; + int total = 0; + for (int i = checkPosition - 1; i >= 0; i--) { + total += weight * ALPHABET_STRING.indexOf(result.charAt(i)); + if (++weight > weightMax) { + weight = 1; + } + } + if (result.charAt(checkPosition) != ALPHABET[total % 47]) { + throw ChecksumException.getChecksumInstance(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/Code93Writer.java b/rubylib/src/main/java/com/google/zxing/oned/Code93Writer.java new file mode 100644 index 0000000..a52eabb --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/Code93Writer.java @@ -0,0 +1,128 @@ +/* + * Copyright 2015 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders a CODE93 code as a BitMatrix + */ +public class Code93Writer extends OneDimensionalCodeWriter { + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.CODE_93) { + throw new IllegalArgumentException("Can only encode CODE_93, but got " + format); + } + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + //each character is encoded by 9 of 0/1's + int[] widths = new int[9]; + + //length of code + 2 start/stop characters + 2 checksums, each of 9 bits, plus a termination bar + int codeWidth = (contents.length() + 2 + 2) * 9 + 1; + + //start character (*) + toIntArray(Code93Reader.CHARACTER_ENCODINGS[47], widths); + + boolean[] result = new boolean[codeWidth]; + int pos = appendPattern(result, 0, widths); + + for (int i = 0; i < length; i++) { + int indexInString = Code93Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + toIntArray(Code93Reader.CHARACTER_ENCODINGS[indexInString], widths); + pos += appendPattern(result, pos, widths); + } + + //add two checksums + int check1 = computeChecksumIndex(contents, 20); + toIntArray(Code93Reader.CHARACTER_ENCODINGS[check1], widths); + pos += appendPattern(result, pos, widths); + + //append the contents to reflect the first checksum added + contents += Code93Reader.ALPHABET_STRING.charAt(check1); + + int check2 = computeChecksumIndex(contents, 15); + toIntArray(Code93Reader.CHARACTER_ENCODINGS[check2], widths); + pos += appendPattern(result, pos, widths); + + //end character (*) + toIntArray(Code93Reader.CHARACTER_ENCODINGS[47], widths); + pos += appendPattern(result, pos, widths); + + //termination bar (single black bar) + result[pos] = true; + + return result; + } + + private static void toIntArray(int a, int[] toReturn) { + for (int i = 0; i < 9; i++) { + int temp = a & (1 << (8 - i)); + toReturn[i] = temp == 0 ? 0 : 1; + } + } + + /** + * @param target output to append to + * @param pos start position + * @param pattern pattern to append + * @param startColor unused + * @return 9 + * @deprecated without replacement; intended as an internal-only method + */ + @Deprecated + protected static int appendPattern(boolean[] target, int pos, int[] pattern, boolean startColor) { + return appendPattern(target, pos, pattern); + } + + private static int appendPattern(boolean[] target, int pos, int[] pattern) { + for (int bit : pattern) { + target[pos++] = bit != 0; + } + return 9; + } + + private static int computeChecksumIndex(String contents, int maxWeight) { + int weight = 1; + int total = 0; + + for (int i = contents.length() - 1; i >= 0; i--) { + int indexInString = Code93Reader.ALPHABET_STRING.indexOf(contents.charAt(i)); + total += indexInString * weight; + if (++weight > maxWeight) { + weight = 1; + } + } + return total % 47; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/EAN13Reader.java b/rubylib/src/main/java/com/google/zxing/oned/EAN13Reader.java new file mode 100644 index 0000000..c537eac --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/EAN13Reader.java @@ -0,0 +1,138 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *

Implements decoding of the EAN-13 format.

+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + * @author alasdair@google.com (Alasdair Mackintosh) + */ +public final class EAN13Reader extends UPCEANReader { + + // For an EAN-13 barcode, the first digit is represented by the parities used + // to encode the next six digits, according to the table below. For example, + // if the barcode is 5 123456 789012 then the value of the first digit is + // signified by using odd for '1', even for '2', even for '3', odd for '4', + // odd for '5', and even for '6'. See http://en.wikipedia.org/wiki/EAN-13 + // + // Parity of next 6 digits + // Digit 0 1 2 3 4 5 + // 0 Odd Odd Odd Odd Odd Odd + // 1 Odd Odd Even Odd Even Even + // 2 Odd Odd Even Even Odd Even + // 3 Odd Odd Even Even Even Odd + // 4 Odd Even Odd Odd Even Even + // 5 Odd Even Even Odd Odd Even + // 6 Odd Even Even Even Odd Odd + // 7 Odd Even Odd Even Odd Even + // 8 Odd Even Odd Even Even Odd + // 9 Odd Even Even Odd Even Odd + // + // Note that the encoding for '0' uses the same parity as a UPC barcode. Hence + // a UPC barcode can be converted to an EAN-13 barcode by prepending a 0. + // + // The encoding is represented by the following array, which is a bit pattern + // using Odd = 0 and Even = 1. For example, 5 is represented by: + // + // Odd Even Even Odd Odd Even + // in binary: + // 0 1 1 0 0 1 == 0x19 + // + static final int[] FIRST_DIGIT_ENCODINGS = { + 0x00, 0x0B, 0x0D, 0xE, 0x13, 0x19, 0x1C, 0x15, 0x16, 0x1A + }; + + private final int[] decodeMiddleCounters; + + public EAN13Reader() { + decodeMiddleCounters = new int[4]; + } + + @Override + protected int decodeMiddle(BitArray row, + int[] startRange, + StringBuilder resultString) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int counter : counters) { + rowOffset += counter; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (5 - x); + } + } + + determineFirstDigit(resultString, lgPatternFound); + + int[] middleRange = findGuardPattern(row, rowOffset, true, MIDDLE_PATTERN); + rowOffset = middleRange[1]; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + resultString.append((char) ('0' + bestMatch)); + for (int counter : counters) { + rowOffset += counter; + } + } + + return rowOffset; + } + + @Override + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.EAN_13; + } + + /** + * Based on pattern of odd-even ('L' and 'G') patterns used to encoded the explicitly-encoded + * digits in a barcode, determines the implicitly encoded first digit and adds it to the + * result string. + * + * @param resultString string to insert decoded first digit into + * @param lgPatternFound int whose bits indicates the pattern of odd/even L/G patterns used to + * encode digits + * @throws NotFoundException if first digit cannot be determined + */ + private static void determineFirstDigit(StringBuilder resultString, int lgPatternFound) + throws NotFoundException { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == FIRST_DIGIT_ENCODINGS[d]) { + resultString.insert(0, (char) ('0' + d)); + return; + } + } + throw NotFoundException.getNotFoundInstance(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/EAN13Writer.java b/rubylib/src/main/java/com/google/zxing/oned/EAN13Writer.java new file mode 100644 index 0000000..467be30 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/EAN13Writer.java @@ -0,0 +1,109 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders an EAN13 code as a {@link BitMatrix}. + * + * @author aripollak@gmail.com (Ari Pollak) + */ +public final class EAN13Writer extends UPCEANWriter { + + private static final int CODE_WIDTH = 3 + // start guard + (7 * 6) + // left bars + 5 + // middle guard + (7 * 6) + // right bars + 3; // end guard + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.EAN_13) { + throw new IllegalArgumentException("Can only encode EAN_13, but got " + format); + } + + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + switch (length) { + case 12: + // No check digit present, calculate it and add it + int check; + try { + check = UPCEANReader.getStandardUPCEANChecksum(contents); + } catch (FormatException fe) { + throw new IllegalArgumentException(fe); + } + contents += check; + break; + case 13: + try { + if (!UPCEANReader.checkStandardUPCEANChecksum(contents)) { + throw new IllegalArgumentException("Contents do not pass checksum"); + } + } catch (FormatException ignored) { + throw new IllegalArgumentException("Illegal contents"); + } + break; + default: + throw new IllegalArgumentException( + "Requested contents should be 12 or 13 digits long, but got " + length); + } + + + int firstDigit = Character.digit(contents.charAt(0), 10); + int parities = EAN13Reader.FIRST_DIGIT_ENCODINGS[firstDigit]; + boolean[] result = new boolean[CODE_WIDTH]; + int pos = 0; + + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, true); + + // See EAN13Reader for a description of how the first digit & left bars are encoded + for (int i = 1; i <= 6; i++) { + int digit = Character.digit(contents.charAt(i), 10); + if ((parities >> (6 - i) & 1) == 1) { + digit += 10; + } + pos += appendPattern(result, pos, UPCEANReader.L_AND_G_PATTERNS[digit], false); + } + + pos += appendPattern(result, pos, UPCEANReader.MIDDLE_PATTERN, false); + + for (int i = 7; i <= 12; i++) { + int digit = Character.digit(contents.charAt(i), 10); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], true); + } + appendPattern(result, pos, UPCEANReader.START_END_PATTERN, true); + + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/EAN8Reader.java b/rubylib/src/main/java/com/google/zxing/oned/EAN8Reader.java new file mode 100644 index 0000000..8d0b7e2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/EAN8Reader.java @@ -0,0 +1,75 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *

Implements decoding of the EAN-8 format.

+ * + * @author Sean Owen + */ +public final class EAN8Reader extends UPCEANReader { + + private final int[] decodeMiddleCounters; + + public EAN8Reader() { + decodeMiddleCounters = new int[4]; + } + + @Override + protected int decodeMiddle(BitArray row, + int[] startRange, + StringBuilder result) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + for (int x = 0; x < 4 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + result.append((char) ('0' + bestMatch)); + for (int counter : counters) { + rowOffset += counter; + } + } + + int[] middleRange = findGuardPattern(row, rowOffset, true, MIDDLE_PATTERN); + rowOffset = middleRange[1]; + + for (int x = 0; x < 4 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_PATTERNS); + result.append((char) ('0' + bestMatch)); + for (int counter : counters) { + rowOffset += counter; + } + } + + return rowOffset; + } + + @Override + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.EAN_8; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/EAN8Writer.java b/rubylib/src/main/java/com/google/zxing/oned/EAN8Writer.java new file mode 100644 index 0000000..b9e3dfc --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/EAN8Writer.java @@ -0,0 +1,106 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders an EAN8 code as a {@link BitMatrix}. + * + * @author aripollak@gmail.com (Ari Pollak) + */ +public final class EAN8Writer extends UPCEANWriter { + + private static final int CODE_WIDTH = 3 + // start guard + (7 * 4) + // left bars + 5 + // middle guard + (7 * 4) + // right bars + 3; // end guard + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.EAN_8) { + throw new IllegalArgumentException("Can only encode EAN_8, but got " + + format); + } + + return super.encode(contents, format, width, height, hints); + } + + /** + * @return a byte array of horizontal pixels (false = white, true = black) + */ + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + switch (length) { + case 7: + // No check digit present, calculate it and add it + int check; + try { + check = UPCEANReader.getStandardUPCEANChecksum(contents); + } catch (FormatException fe) { + throw new IllegalArgumentException(fe); + } + contents += check; + break; + case 8: + try { + if (!UPCEANReader.checkStandardUPCEANChecksum(contents)) { + throw new IllegalArgumentException("Contents do not pass checksum"); + } + } catch (FormatException ignored) { + throw new IllegalArgumentException("Illegal contents"); + } + break; + default: + throw new IllegalArgumentException( + "Requested contents should be 8 digits long, but got " + length); + } + + boolean[] result = new boolean[CODE_WIDTH]; + int pos = 0; + + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, true); + + for (int i = 0; i <= 3; i++) { + int digit = Character.digit(contents.charAt(i), 10); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], false); + } + + pos += appendPattern(result, pos, UPCEANReader.MIDDLE_PATTERN, false); + + for (int i = 4; i <= 7; i++) { + int digit = Character.digit(contents.charAt(i), 10); + pos += appendPattern(result, pos, UPCEANReader.L_PATTERNS[digit], true); + } + appendPattern(result, pos, UPCEANReader.START_END_PATTERN, true); + + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/EANManufacturerOrgSupport.java b/rubylib/src/main/java/com/google/zxing/oned/EANManufacturerOrgSupport.java new file mode 100644 index 0000000..805dbd8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/EANManufacturerOrgSupport.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import java.util.ArrayList; +import java.util.List; + +/** + * Records EAN prefix to GS1 Member Organization, where the member organization + * correlates strongly with a country. This is an imperfect means of identifying + * a country of origin by EAN-13 barcode value. See + * + * http://en.wikipedia.org/wiki/List_of_GS1_country_codes. + * + * @author Sean Owen + */ +final class EANManufacturerOrgSupport { + + private final List ranges = new ArrayList<>(); + private final List countryIdentifiers = new ArrayList<>(); + + String lookupCountryIdentifier(String productCode) { + initIfNeeded(); + int prefix = Integer.parseInt(productCode.substring(0, 3)); + int max = ranges.size(); + for (int i = 0; i < max; i++) { + int[] range = ranges.get(i); + int start = range[0]; + if (prefix < start) { + return null; + } + int end = range.length == 1 ? start : range[1]; + if (prefix <= end) { + return countryIdentifiers.get(i); + } + } + return null; + } + + private void add(int[] range, String id) { + ranges.add(range); + countryIdentifiers.add(id); + } + + private synchronized void initIfNeeded() { + if (!ranges.isEmpty()) { + return; + } + add(new int[] {0,19}, "US/CA"); + add(new int[] {30,39}, "US"); + add(new int[] {60,139}, "US/CA"); + add(new int[] {300,379}, "FR"); + add(new int[] {380}, "BG"); + add(new int[] {383}, "SI"); + add(new int[] {385}, "HR"); + add(new int[] {387}, "BA"); + add(new int[] {400,440}, "DE"); + add(new int[] {450,459}, "JP"); + add(new int[] {460,469}, "RU"); + add(new int[] {471}, "TW"); + add(new int[] {474}, "EE"); + add(new int[] {475}, "LV"); + add(new int[] {476}, "AZ"); + add(new int[] {477}, "LT"); + add(new int[] {478}, "UZ"); + add(new int[] {479}, "LK"); + add(new int[] {480}, "PH"); + add(new int[] {481}, "BY"); + add(new int[] {482}, "UA"); + add(new int[] {484}, "MD"); + add(new int[] {485}, "AM"); + add(new int[] {486}, "GE"); + add(new int[] {487}, "KZ"); + add(new int[] {489}, "HK"); + add(new int[] {490,499}, "JP"); + add(new int[] {500,509}, "GB"); + add(new int[] {520}, "GR"); + add(new int[] {528}, "LB"); + add(new int[] {529}, "CY"); + add(new int[] {531}, "MK"); + add(new int[] {535}, "MT"); + add(new int[] {539}, "IE"); + add(new int[] {540,549}, "BE/LU"); + add(new int[] {560}, "PT"); + add(new int[] {569}, "IS"); + add(new int[] {570,579}, "DK"); + add(new int[] {590}, "PL"); + add(new int[] {594}, "RO"); + add(new int[] {599}, "HU"); + add(new int[] {600,601}, "ZA"); + add(new int[] {603}, "GH"); + add(new int[] {608}, "BH"); + add(new int[] {609}, "MU"); + add(new int[] {611}, "MA"); + add(new int[] {613}, "DZ"); + add(new int[] {616}, "KE"); + add(new int[] {618}, "CI"); + add(new int[] {619}, "TN"); + add(new int[] {621}, "SY"); + add(new int[] {622}, "EG"); + add(new int[] {624}, "LY"); + add(new int[] {625}, "JO"); + add(new int[] {626}, "IR"); + add(new int[] {627}, "KW"); + add(new int[] {628}, "SA"); + add(new int[] {629}, "AE"); + add(new int[] {640,649}, "FI"); + add(new int[] {690,695}, "CN"); + add(new int[] {700,709}, "NO"); + add(new int[] {729}, "IL"); + add(new int[] {730,739}, "SE"); + add(new int[] {740}, "GT"); + add(new int[] {741}, "SV"); + add(new int[] {742}, "HN"); + add(new int[] {743}, "NI"); + add(new int[] {744}, "CR"); + add(new int[] {745}, "PA"); + add(new int[] {746}, "DO"); + add(new int[] {750}, "MX"); + add(new int[] {754,755}, "CA"); + add(new int[] {759}, "VE"); + add(new int[] {760,769}, "CH"); + add(new int[] {770}, "CO"); + add(new int[] {773}, "UY"); + add(new int[] {775}, "PE"); + add(new int[] {777}, "BO"); + add(new int[] {779}, "AR"); + add(new int[] {780}, "CL"); + add(new int[] {784}, "PY"); + add(new int[] {785}, "PE"); + add(new int[] {786}, "EC"); + add(new int[] {789,790}, "BR"); + add(new int[] {800,839}, "IT"); + add(new int[] {840,849}, "ES"); + add(new int[] {850}, "CU"); + add(new int[] {858}, "SK"); + add(new int[] {859}, "CZ"); + add(new int[] {860}, "YU"); + add(new int[] {865}, "MN"); + add(new int[] {867}, "KP"); + add(new int[] {868,869}, "TR"); + add(new int[] {870,879}, "NL"); + add(new int[] {880}, "KR"); + add(new int[] {885}, "TH"); + add(new int[] {888}, "SG"); + add(new int[] {890}, "IN"); + add(new int[] {893}, "VN"); + add(new int[] {896}, "PK"); + add(new int[] {899}, "ID"); + add(new int[] {900,919}, "AT"); + add(new int[] {930,939}, "AU"); + add(new int[] {940,949}, "AZ"); + add(new int[] {955}, "MY"); + add(new int[] {958}, "MO"); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/ITFReader.java b/rubylib/src/main/java/com/google/zxing/oned/ITFReader.java new file mode 100644 index 0000000..d346d9e --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/ITFReader.java @@ -0,0 +1,352 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Map; + +/** + *

Implements decoding of the ITF format, or Interleaved Two of Five.

+ * + *

This Reader will scan ITF barcodes of certain lengths only. + * At the moment it reads length 6, 8, 10, 12, 14, 16, 18, 20, 24, and 44 as these have appeared "in the wild". Not all + * lengths are scanned, especially shorter ones, to avoid false positives. This in turn is due to a lack of + * required checksum function.

+ * + *

The checksum is optional and is not applied by this Reader. The consumer of the decoded + * value will have to apply a checksum if required.

+ * + *

http://en.wikipedia.org/wiki/Interleaved_2_of_5 + * is a great reference for Interleaved 2 of 5 information.

+ * + * @author kevin.osullivan@sita.aero, SITA Lab. + */ +public final class ITFReader extends OneDReader { + + private static final float MAX_AVG_VARIANCE = 0.38f; + private static final float MAX_INDIVIDUAL_VARIANCE = 0.78f; + + private static final int W = 3; // Pixel width of a wide line + private static final int N = 1; // Pixed width of a narrow line + + /** Valid ITF lengths. Anything longer than the largest value is also allowed. */ + private static final int[] DEFAULT_ALLOWED_LENGTHS = {6, 8, 10, 12, 14}; + + // Stores the actual narrow line width of the image being decoded. + private int narrowLineWidth = -1; + + /** + * Start/end guard pattern. + * + * Note: The end pattern is reversed because the row is reversed before + * searching for the END_PATTERN + */ + private static final int[] START_PATTERN = {N, N, N, N}; + private static final int[] END_PATTERN_REVERSED = {N, N, W}; + + /** + * Patterns of Wide / Narrow lines to indicate each digit + */ + static final int[][] PATTERNS = { + {N, N, W, W, N}, // 0 + {W, N, N, N, W}, // 1 + {N, W, N, N, W}, // 2 + {W, W, N, N, N}, // 3 + {N, N, W, N, W}, // 4 + {W, N, W, N, N}, // 5 + {N, W, W, N, N}, // 6 + {N, N, N, W, W}, // 7 + {W, N, N, W, N}, // 8 + {N, W, N, W, N} // 9 + }; + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws FormatException, NotFoundException { + + // Find out where the Middle section (payload) starts & ends + int[] startRange = decodeStart(row); + int[] endRange = decodeEnd(row); + + StringBuilder result = new StringBuilder(20); + decodeMiddle(row, startRange[1], endRange[0], result); + String resultString = result.toString(); + + int[] allowedLengths = null; + if (hints != null) { + allowedLengths = (int[]) hints.get(DecodeHintType.ALLOWED_LENGTHS); + + } + if (allowedLengths == null) { + allowedLengths = DEFAULT_ALLOWED_LENGTHS; + } + + // To avoid false positives with 2D barcodes (and other patterns), make + // an assumption that the decoded string must be a 'standard' length if it's short + int length = resultString.length(); + boolean lengthOK = false; + int maxAllowedLength = 0; + for (int allowedLength : allowedLengths) { + if (length == allowedLength) { + lengthOK = true; + break; + } + if (allowedLength > maxAllowedLength) { + maxAllowedLength = allowedLength; + } + } + if (!lengthOK && length > maxAllowedLength) { + lengthOK = true; + } + if (!lengthOK) { + throw FormatException.getFormatInstance(); + } + + return new Result( + resultString, + null, // no natural byte representation for these barcodes + new ResultPoint[] {new ResultPoint(startRange[1], rowNumber), + new ResultPoint(endRange[0], rowNumber)}, + BarcodeFormat.ITF); + } + + /** + * @param row row of black/white values to search + * @param payloadStart offset of start pattern + * @param resultString {@link StringBuilder} to append decoded chars to + * @throws NotFoundException if decoding could not complete successfully + */ + private static void decodeMiddle(BitArray row, + int payloadStart, + int payloadEnd, + StringBuilder resultString) throws NotFoundException { + + // Digits are interleaved in pairs - 5 black lines for one digit, and the + // 5 + // interleaved white lines for the second digit. + // Therefore, need to scan 10 lines and then + // split these into two arrays + int[] counterDigitPair = new int[10]; + int[] counterBlack = new int[5]; + int[] counterWhite = new int[5]; + + while (payloadStart < payloadEnd) { + + // Get 10 runs of black/white. + recordPattern(row, payloadStart, counterDigitPair); + // Split them into each array + for (int k = 0; k < 5; k++) { + int twoK = 2 * k; + counterBlack[k] = counterDigitPair[twoK]; + counterWhite[k] = counterDigitPair[twoK + 1]; + } + + int bestMatch = decodeDigit(counterBlack); + resultString.append((char) ('0' + bestMatch)); + bestMatch = decodeDigit(counterWhite); + resultString.append((char) ('0' + bestMatch)); + + for (int counterDigit : counterDigitPair) { + payloadStart += counterDigit; + } + } + } + + /** + * Identify where the start of the middle / payload section starts. + * + * @param row row of black/white values to search + * @return Array, containing index of start of 'start block' and end of + * 'start block' + */ + private int[] decodeStart(BitArray row) throws NotFoundException { + int endStart = skipWhiteSpace(row); + int[] startPattern = findGuardPattern(row, endStart, START_PATTERN); + + // Determine the width of a narrow line in pixels. We can do this by + // getting the width of the start pattern and dividing by 4 because its + // made up of 4 narrow lines. + this.narrowLineWidth = (startPattern[1] - startPattern[0]) / 4; + + validateQuietZone(row, startPattern[0]); + + return startPattern; + } + + /** + * The start & end patterns must be pre/post fixed by a quiet zone. This + * zone must be at least 10 times the width of a narrow line. Scan back until + * we either get to the start of the barcode or match the necessary number of + * quiet zone pixels. + * + * Note: Its assumed the row is reversed when using this method to find + * quiet zone after the end pattern. + * + * ref: http://www.barcode-1.net/i25code.html + * + * @param row bit array representing the scanned barcode. + * @param startPattern index into row of the start or end pattern. + * @throws NotFoundException if the quiet zone cannot be found + */ + private void validateQuietZone(BitArray row, int startPattern) throws NotFoundException { + + int quietCount = this.narrowLineWidth * 10; // expect to find this many pixels of quiet zone + + // if there are not so many pixel at all let's try as many as possible + quietCount = quietCount < startPattern ? quietCount : startPattern; + + for (int i = startPattern - 1; quietCount > 0 && i >= 0; i--) { + if (row.get(i)) { + break; + } + quietCount--; + } + if (quietCount != 0) { + // Unable to find the necessary number of quiet zone pixels. + throw NotFoundException.getNotFoundInstance(); + } + } + + /** + * Skip all whitespace until we get to the first black line. + * + * @param row row of black/white values to search + * @return index of the first black line. + * @throws NotFoundException Throws exception if no black lines are found in the row + */ + private static int skipWhiteSpace(BitArray row) throws NotFoundException { + int width = row.getSize(); + int endStart = row.getNextSet(0); + if (endStart == width) { + throw NotFoundException.getNotFoundInstance(); + } + + return endStart; + } + + /** + * Identify where the end of the middle / payload section ends. + * + * @param row row of black/white values to search + * @return Array, containing index of start of 'end block' and end of 'end + * block' + */ + private int[] decodeEnd(BitArray row) throws NotFoundException { + + // For convenience, reverse the row and then + // search from 'the start' for the end block + row.reverse(); + try { + int endStart = skipWhiteSpace(row); + int[] endPattern = findGuardPattern(row, endStart, END_PATTERN_REVERSED); + + // The start & end patterns must be pre/post fixed by a quiet zone. This + // zone must be at least 10 times the width of a narrow line. + // ref: http://www.barcode-1.net/i25code.html + validateQuietZone(row, endPattern[0]); + + // Now recalculate the indices of where the 'endblock' starts & stops to + // accommodate + // the reversed nature of the search + int temp = endPattern[0]; + endPattern[0] = row.getSize() - endPattern[1]; + endPattern[1] = row.getSize() - temp; + + return endPattern; + } finally { + // Put the row back the right way. + row.reverse(); + } + } + + /** + * @param row row of black/white values to search + * @param rowOffset position to start search + * @param pattern pattern of counts of number of black and white pixels that are + * being searched for as a pattern + * @return start/end horizontal offset of guard pattern, as an array of two + * ints + * @throws NotFoundException if pattern is not found + */ + private static int[] findGuardPattern(BitArray row, + int rowOffset, + int[] pattern) throws NotFoundException { + int patternLength = pattern.length; + int[] counters = new int[patternLength]; + int width = row.getSize(); + boolean isWhite = false; + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + if (row.get(x) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { + return new int[]{patternStart, x}; + } + patternStart += counters[0] + counters[1]; + System.arraycopy(counters, 2, counters, 0, counterPosition - 1); + counters[counterPosition - 1] = 0; + counters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Attempts to decode a sequence of ITF black/white lines into single + * digit. + * + * @param counters the counts of runs of observed black/white/black/... values + * @return The decoded digit + * @throws NotFoundException if digit cannot be decoded + */ + private static int decodeDigit(int[] counters) throws NotFoundException { + float bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int bestMatch = -1; + int max = PATTERNS.length; + for (int i = 0; i < max; i++) { + int[] pattern = PATTERNS[i]; + float variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = i; + } + } + if (bestMatch >= 0) { + return bestMatch; + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/ITFWriter.java b/rubylib/src/main/java/com/google/zxing/oned/ITFWriter.java new file mode 100644 index 0000000..8cfa1e7 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/ITFWriter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders a ITF code as a {@link BitMatrix}. + * + * @author erik.barbara@gmail.com (Erik Barbara) + */ +public final class ITFWriter extends OneDimensionalCodeWriter { + + private static final int[] START_PATTERN = {1, 1, 1, 1}; + private static final int[] END_PATTERN = {3, 1, 1}; + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.ITF) { + throw new IllegalArgumentException("Can only encode ITF, but got " + format); + } + + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + if (length % 2 != 0) { + throw new IllegalArgumentException("The length of the input should be even"); + } + if (length > 80) { + throw new IllegalArgumentException( + "Requested contents should be less than 80 digits long, but got " + length); + } + boolean[] result = new boolean[9 + 9 * length]; + int pos = appendPattern(result, 0, START_PATTERN, true); + for (int i = 0; i < length; i += 2) { + int one = Character.digit(contents.charAt(i), 10); + int two = Character.digit(contents.charAt(i + 1), 10); + int[] encoding = new int[10]; + for (int j = 0; j < 5; j++) { + encoding[2 * j] = ITFReader.PATTERNS[one][j]; + encoding[2 * j + 1] = ITFReader.PATTERNS[two][j]; + } + pos += appendPattern(result, pos, encoding, true); + } + appendPattern(result, pos, END_PATTERN, true); + + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/MultiFormatOneDReader.java b/rubylib/src/main/java/com/google/zxing/oned/MultiFormatOneDReader.java new file mode 100644 index 0000000..b2fc6ae --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/MultiFormatOneDReader.java @@ -0,0 +1,112 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; +import com.google.zxing.oned.rss.RSS14Reader; +import com.google.zxing.oned.rss.expanded.RSSExpandedReader; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class MultiFormatOneDReader extends OneDReader { + + private final OneDReader[] readers; + + public MultiFormatOneDReader(Map hints) { + @SuppressWarnings("unchecked") + Collection possibleFormats = hints == null ? null : + (Collection) hints.get(DecodeHintType.POSSIBLE_FORMATS); + boolean useCode39CheckDigit = hints != null && + hints.get(DecodeHintType.ASSUME_CODE_39_CHECK_DIGIT) != null; + Collection readers = new ArrayList<>(); + if (possibleFormats != null) { + if (possibleFormats.contains(BarcodeFormat.EAN_13) || + possibleFormats.contains(BarcodeFormat.UPC_A) || + possibleFormats.contains(BarcodeFormat.EAN_8) || + possibleFormats.contains(BarcodeFormat.UPC_E)) { + readers.add(new MultiFormatUPCEANReader(hints)); + } + if (possibleFormats.contains(BarcodeFormat.CODE_39)) { + readers.add(new Code39Reader(useCode39CheckDigit)); + } + if (possibleFormats.contains(BarcodeFormat.CODE_93)) { + readers.add(new Code93Reader()); + } + if (possibleFormats.contains(BarcodeFormat.CODE_128)) { + readers.add(new Code128Reader()); + } + if (possibleFormats.contains(BarcodeFormat.ITF)) { + readers.add(new ITFReader()); + } + if (possibleFormats.contains(BarcodeFormat.CODABAR)) { + readers.add(new CodaBarReader()); + } + if (possibleFormats.contains(BarcodeFormat.RSS_14)) { + readers.add(new RSS14Reader()); + } + if (possibleFormats.contains(BarcodeFormat.RSS_EXPANDED)) { + readers.add(new RSSExpandedReader()); + } + } + if (readers.isEmpty()) { + readers.add(new MultiFormatUPCEANReader(hints)); + readers.add(new Code39Reader()); + readers.add(new CodaBarReader()); + readers.add(new Code93Reader()); + readers.add(new Code128Reader()); + readers.add(new ITFReader()); + readers.add(new RSS14Reader()); + readers.add(new RSSExpandedReader()); + } + this.readers = readers.toArray(new OneDReader[readers.size()]); + } + + @Override + public Result decodeRow(int rowNumber, + BitArray row, + Map hints) throws NotFoundException { + for (OneDReader reader : readers) { + try { + return reader.decodeRow(rowNumber, row, hints); + } catch (ReaderException re) { + // continue + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + @Override + public void reset() { + for (Reader reader : readers) { + reader.reset(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/MultiFormatUPCEANReader.java b/rubylib/src/main/java/com/google/zxing/oned/MultiFormatUPCEANReader.java new file mode 100644 index 0000000..0d1c248 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/MultiFormatUPCEANReader.java @@ -0,0 +1,123 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + *

A reader that can read all available UPC/EAN formats. If a caller wants to try to + * read all such formats, it is most efficient to use this implementation rather than invoke + * individual readers.

+ * + * @author Sean Owen + */ +public final class MultiFormatUPCEANReader extends OneDReader { + + private final UPCEANReader[] readers; + + public MultiFormatUPCEANReader(Map hints) { + @SuppressWarnings("unchecked") + Collection possibleFormats = hints == null ? null : + (Collection) hints.get(DecodeHintType.POSSIBLE_FORMATS); + Collection readers = new ArrayList<>(); + if (possibleFormats != null) { + if (possibleFormats.contains(BarcodeFormat.EAN_13)) { + readers.add(new EAN13Reader()); + } else if (possibleFormats.contains(BarcodeFormat.UPC_A)) { + readers.add(new UPCAReader()); + } + if (possibleFormats.contains(BarcodeFormat.EAN_8)) { + readers.add(new EAN8Reader()); + } + if (possibleFormats.contains(BarcodeFormat.UPC_E)) { + readers.add(new UPCEReader()); + } + } + if (readers.isEmpty()) { + readers.add(new EAN13Reader()); + // UPC-A is covered by EAN-13 + readers.add(new EAN8Reader()); + readers.add(new UPCEReader()); + } + this.readers = readers.toArray(new UPCEANReader[readers.size()]); + } + + @Override + public Result decodeRow(int rowNumber, + BitArray row, + Map hints) throws NotFoundException { + // Compute this location once and reuse it on multiple implementations + int[] startGuardPattern = UPCEANReader.findStartGuardPattern(row); + for (UPCEANReader reader : readers) { + try { + Result result = reader.decodeRow(rowNumber, row, startGuardPattern, hints); + // Special case: a 12-digit code encoded in UPC-A is identical to a "0" + // followed by those 12 digits encoded as EAN-13. Each will recognize such a code, + // UPC-A as a 12-digit string and EAN-13 as a 13-digit string starting with "0". + // Individually these are correct and their readers will both read such a code + // and correctly call it EAN-13, or UPC-A, respectively. + // + // In this case, if we've been looking for both types, we'd like to call it + // a UPC-A code. But for efficiency we only run the EAN-13 decoder to also read + // UPC-A. So we special case it here, and convert an EAN-13 result to a UPC-A + // result if appropriate. + // + // But, don't return UPC-A if UPC-A was not a requested format! + boolean ean13MayBeUPCA = + result.getBarcodeFormat() == BarcodeFormat.EAN_13 && + result.getText().charAt(0) == '0'; + @SuppressWarnings("unchecked") + Collection possibleFormats = + hints == null ? null : (Collection) hints.get(DecodeHintType.POSSIBLE_FORMATS); + boolean canReturnUPCA = possibleFormats == null || possibleFormats.contains(BarcodeFormat.UPC_A); + + if (ean13MayBeUPCA && canReturnUPCA) { + // Transfer the metdata across + Result resultUPCA = new Result(result.getText().substring(1), + result.getRawBytes(), + result.getResultPoints(), + BarcodeFormat.UPC_A); + resultUPCA.putAllMetadata(result.getResultMetadata()); + return resultUPCA; + } + return result; + } catch (ReaderException ignored) { + // continue + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + @Override + public void reset() { + for (Reader reader : readers) { + reader.reset(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/OneDReader.java b/rubylib/src/main/java/com/google/zxing/oned/OneDReader.java new file mode 100644 index 0000000..7941e6f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/OneDReader.java @@ -0,0 +1,296 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.EnumMap; +import java.util.Map; + +/** + * Encapsulates functionality and implementation that is common to all families + * of one-dimensional barcodes. + * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public abstract class OneDReader implements Reader { + + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException { + return decode(image, null); + } + + // Note that we don't try rotation without the try harder flag, even if rotation was supported. + @Override + public Result decode(BinaryBitmap image, + Map hints) throws NotFoundException, FormatException { + try { + return doDecode(image, hints); + } catch (NotFoundException nfe) { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + if (tryHarder && image.isRotateSupported()) { + BinaryBitmap rotatedImage = image.rotateCounterClockwise(); + Result result = doDecode(rotatedImage, hints); + // Record that we found it rotated 90 degrees CCW / 270 degrees CW + Map metadata = result.getResultMetadata(); + int orientation = 270; + if (metadata != null && metadata.containsKey(ResultMetadataType.ORIENTATION)) { + // But if we found it reversed in doDecode(), add in that result here: + orientation = (orientation + + (Integer) metadata.get(ResultMetadataType.ORIENTATION)) % 360; + } + result.putMetadata(ResultMetadataType.ORIENTATION, orientation); + // Update result points + ResultPoint[] points = result.getResultPoints(); + if (points != null) { + int height = rotatedImage.getHeight(); + for (int i = 0; i < points.length; i++) { + points[i] = new ResultPoint(height - points[i].getY() - 1, points[i].getX()); + } + } + return result; + } else { + throw nfe; + } + } + } + + @Override + public void reset() { + // do nothing + } + + /** + * We're going to examine rows from the middle outward, searching alternately above and below the + * middle, and farther out each time. rowStep is the number of rows between each successive + * attempt above and below the middle. So we'd scan row middle, then middle - rowStep, then + * middle + rowStep, then middle - (2 * rowStep), etc. + * rowStep is bigger as the image is taller, but is always at least 1. We've somewhat arbitrarily + * decided that moving up and down by about 1/16 of the image is pretty good; we try more of the + * image if "trying harder". + * + * @param image The image to decode + * @param hints Any hints that were requested + * @return The contents of the decoded barcode + * @throws NotFoundException Any spontaneous errors which occur + */ + private Result doDecode(BinaryBitmap image, + Map hints) throws NotFoundException { + int width = image.getWidth(); + int height = image.getHeight(); + BitArray row = new BitArray(width); + + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + int rowStep = Math.max(1, height >> (tryHarder ? 8 : 5)); + int maxLines; + if (tryHarder) { + maxLines = height; // Look at the whole image, not just the center + } else { + maxLines = 15; // 15 rows spaced 1/32 apart is roughly the middle half of the image + } + + int middle = height / 2; + for (int x = 0; x < maxLines; x++) { + + // Scanning from the middle out. Determine which row we're looking at next: + int rowStepsAboveOrBelow = (x + 1) / 2; + boolean isAbove = (x & 0x01) == 0; // i.e. is x even? + int rowNumber = middle + rowStep * (isAbove ? rowStepsAboveOrBelow : -rowStepsAboveOrBelow); + if (rowNumber < 0 || rowNumber >= height) { + // Oops, if we run off the top or bottom, stop + break; + } + + // Estimate black point for this row and load it: + try { + row = image.getBlackRow(rowNumber, row); + } catch (NotFoundException ignored) { + continue; + } + + // While we have the image data in a BitArray, it's fairly cheap to reverse it in place to + // handle decoding upside down barcodes. + for (int attempt = 0; attempt < 2; attempt++) { + if (attempt == 1) { // trying again? + row.reverse(); // reverse the row and continue + // This means we will only ever draw result points *once* in the life of this method + // since we want to avoid drawing the wrong points after flipping the row, and, + // don't want to clutter with noise from every single row scan -- just the scans + // that start on the center line. + if (hints != null && hints.containsKey(DecodeHintType.NEED_RESULT_POINT_CALLBACK)) { + Map newHints = new EnumMap<>(DecodeHintType.class); + newHints.putAll(hints); + newHints.remove(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + hints = newHints; + } + } + try { + // Look for a barcode + Result result = decodeRow(rowNumber, row, hints); + // We found our barcode + if (attempt == 1) { + // But it was upside down, so note that + result.putMetadata(ResultMetadataType.ORIENTATION, 180); + // And remember to flip the result points horizontally. + ResultPoint[] points = result.getResultPoints(); + if (points != null) { + points[0] = new ResultPoint(width - points[0].getX() - 1, points[0].getY()); + points[1] = new ResultPoint(width - points[1].getX() - 1, points[1].getY()); + } + } + return result; + } catch (ReaderException re) { + // continue -- just couldn't decode this row + } + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Records the size of successive runs of white and black pixels in a row, starting at a given point. + * The values are recorded in the given array, and the number of runs recorded is equal to the size + * of the array. If the row starts on a white pixel at the given start point, then the first count + * recorded is the run of white pixels starting from that point; likewise it is the count of a run + * of black pixels if the row begin on a black pixels at that point. + * + * @param row row to count from + * @param start offset into row to start at + * @param counters array into which to record counts + * @throws NotFoundException if counters cannot be filled entirely from row before running out + * of pixels + */ + protected static void recordPattern(BitArray row, + int start, + int[] counters) throws NotFoundException { + int numCounters = counters.length; + Arrays.fill(counters, 0, numCounters, 0); + int end = row.getSize(); + if (start >= end) { + throw NotFoundException.getNotFoundInstance(); + } + boolean isWhite = !row.get(start); + int counterPosition = 0; + int i = start; + while (i < end) { + if (row.get(i) != isWhite) { + counters[counterPosition]++; + } else { + if (++counterPosition == numCounters) { + break; + } else { + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + i++; + } + // If we read fully the last section of pixels and filled up our counters -- or filled + // the last counter but ran off the side of the image, OK. Otherwise, a problem. + if (!(counterPosition == numCounters || (counterPosition == numCounters - 1 && i == end))) { + throw NotFoundException.getNotFoundInstance(); + } + } + + protected static void recordPatternInReverse(BitArray row, int start, int[] counters) + throws NotFoundException { + // This could be more efficient I guess + int numTransitionsLeft = counters.length; + boolean last = row.get(start); + while (start > 0 && numTransitionsLeft >= 0) { + if (row.get(--start) != last) { + numTransitionsLeft--; + last = !last; + } + } + if (numTransitionsLeft >= 0) { + throw NotFoundException.getNotFoundInstance(); + } + recordPattern(row, start + 1, counters); + } + + /** + * Determines how closely a set of observed counts of runs of black/white values matches a given + * target pattern. This is reported as the ratio of the total variance from the expected pattern + * proportions across all pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to total pattern size + */ + protected static float patternMatchVariance(int[] counters, + int[] pattern, + float maxIndividualVariance) { + int numCounters = counters.length; + int total = 0; + int patternLength = 0; + for (int i = 0; i < numCounters; i++) { + total += counters[i]; + patternLength += pattern[i]; + } + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this is too small + // to reliably match, so fail: + return Float.POSITIVE_INFINITY; + } + + float unitBarWidth = (float) total / patternLength; + maxIndividualVariance *= unitBarWidth; + + float totalVariance = 0.0f; + for (int x = 0; x < numCounters; x++) { + int counter = counters[x]; + float scaledPattern = pattern[x] * unitBarWidth; + float variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter; + if (variance > maxIndividualVariance) { + return Float.POSITIVE_INFINITY; + } + totalVariance += variance; + } + return totalVariance / total; + } + + /** + *

Attempts to decode a one-dimensional barcode format given a single row of + * an image.

+ * + * @param rowNumber row number from top of the row + * @param row the black/white pixel data of the row + * @param hints decode hints + * @return {@link Result} containing encoded string and start/end of barcode + * @throws NotFoundException if no potential barcode is found + * @throws ChecksumException if a potential barcode is found but does not pass its checksum + * @throws FormatException if a potential barcode is found but format is invalid + */ + public abstract Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, ChecksumException, FormatException; + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java b/rubylib/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java new file mode 100644 index 0000000..190c654 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/OneDimensionalCodeWriter.java @@ -0,0 +1,129 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + *

Encapsulates functionality and implementation that is common to one-dimensional barcodes.

+ * + * @author dsbnatut@gmail.com (Kazuki Nishiura) + */ +public abstract class OneDimensionalCodeWriter implements Writer { + + @Override + public final BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException { + return encode(contents, format, width, height, null); + } + + /** + * Encode the contents following specified format. + * {@code width} and {@code height} are required size. This method may return bigger size + * {@code BitMatrix} when specified size is too small. The user can set both {@code width} and + * {@code height} to zero to get minimum size barcode. If negative value is set to {@code width} + * or {@code height}, {@code IllegalArgumentException} is thrown. + */ + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (contents.isEmpty()) { + throw new IllegalArgumentException("Found empty contents"); + } + + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Negative size is not allowed. Input: " + + width + 'x' + height); + } + + int sidesMargin = getDefaultMargin(); + if (hints != null && hints.containsKey(EncodeHintType.MARGIN)) { + sidesMargin = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString()); + } + + boolean[] code = encode(contents); + return renderResult(code, width, height, sidesMargin); + } + + /** + * @return a byte array of horizontal pixels (0 = white, 1 = black) + */ + private static BitMatrix renderResult(boolean[] code, int width, int height, int sidesMargin) { + int inputWidth = code.length; + // Add quiet zone on both sides. + int fullWidth = inputWidth + sidesMargin; + int outputWidth = Math.max(width, fullWidth); + int outputHeight = Math.max(1, height); + + int multiple = outputWidth / fullWidth; + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (code[inputX]) { + output.setRegion(outputX, 0, multiple, outputHeight); + } + } + return output; + } + + + /** + * @param target encode black/white pattern into this array + * @param pos position to start encoding at in {@code target} + * @param pattern lengths of black/white runs to encode + * @param startColor starting color - false for white, true for black + * @return the number of elements added to target. + */ + protected static int appendPattern(boolean[] target, int pos, int[] pattern, boolean startColor) { + boolean color = startColor; + int numAdded = 0; + for (int len : pattern) { + for (int j = 0; j < len; j++) { + target[pos++] = color; + } + numAdded += len; + color = !color; // flip color after each segment + } + return numAdded; + } + + public int getDefaultMargin() { + // CodaBar spec requires a side margin to be more than ten times wider than narrow space. + // This seems like a decent idea for a default for all formats. + return 10; + } + + /** + * Encode the contents to boolean array expression of one-dimensional barcode. + * Start code and end code should be included in result, and side margins should not be included. + * + * @param contents barcode contents to encode + * @return a {@code boolean[]} of horizontal pixels (false = white, true = black) + */ + public abstract boolean[] encode(String contents); +} + diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCAReader.java b/rubylib/src/main/java/com/google/zxing/oned/UPCAReader.java new file mode 100644 index 0000000..62f69cc --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCAReader.java @@ -0,0 +1,86 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; + +import java.util.Map; + +/** + *

Implements decoding of the UPC-A format.

+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + */ +public final class UPCAReader extends UPCEANReader { + + private final UPCEANReader ean13Reader = new EAN13Reader(); + + @Override + public Result decodeRow(int rowNumber, + BitArray row, + int[] startGuardRange, + Map hints) + throws NotFoundException, FormatException, ChecksumException { + return maybeReturnResult(ean13Reader.decodeRow(rowNumber, row, startGuardRange, hints)); + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, FormatException, ChecksumException { + return maybeReturnResult(ean13Reader.decodeRow(rowNumber, row, hints)); + } + + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException { + return maybeReturnResult(ean13Reader.decode(image)); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, FormatException { + return maybeReturnResult(ean13Reader.decode(image, hints)); + } + + @Override + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.UPC_A; + } + + @Override + protected int decodeMiddle(BitArray row, int[] startRange, StringBuilder resultString) + throws NotFoundException { + return ean13Reader.decodeMiddle(row, startRange, resultString); + } + + private static Result maybeReturnResult(Result result) throws FormatException { + String text = result.getText(); + if (text.charAt(0) == '0') { + return new Result(text.substring(1), null, result.getResultPoints(), BarcodeFormat.UPC_A); + } else { + throw FormatException.getFormatInstance(); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCAWriter.java b/rubylib/src/main/java/com/google/zxing/oned/UPCAWriter.java new file mode 100644 index 0000000..fb372e3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCAWriter.java @@ -0,0 +1,55 @@ +/* + * Copyright 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +import java.util.Map; + +/** + * This object renders a UPC-A code as a {@link BitMatrix}. + * + * @author qwandor@google.com (Andrew Walbran) + */ +public final class UPCAWriter implements Writer { + + private final EAN13Writer subWriter = new EAN13Writer(); + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException { + return encode(contents, format, width, height, null); + } + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.UPC_A) { + throw new IllegalArgumentException("Can only encode UPC-A, but got " + format); + } + // Transform a UPC-A code into the equivalent EAN-13 code and write it that way + return subWriter.encode('0' + contents, BarcodeFormat.EAN_13, width, height, hints); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension2Support.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension2Support.java new file mode 100644 index 0000000..8a1c627 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension2Support.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.EnumMap; +import java.util.Map; + +/** + * @see UPCEANExtension5Support + */ +final class UPCEANExtension2Support { + + private final int[] decodeMiddleCounters = new int[4]; + private final StringBuilder decodeRowStringBuffer = new StringBuilder(); + + Result decodeRow(int rowNumber, BitArray row, int[] extensionStartRange) throws NotFoundException { + + StringBuilder result = decodeRowStringBuffer; + result.setLength(0); + int end = decodeMiddle(row, extensionStartRange, result); + + String resultString = result.toString(); + Map extensionData = parseExtensionString(resultString); + + Result extensionResult = + new Result(resultString, + null, + new ResultPoint[] { + new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, rowNumber), + new ResultPoint(end, rowNumber), + }, + BarcodeFormat.UPC_EAN_EXTENSION); + if (extensionData != null) { + extensionResult.putAllMetadata(extensionData); + } + return extensionResult; + } + + private int decodeMiddle(BitArray row, int[] startRange, StringBuilder resultString) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int checkParity = 0; + + for (int x = 0; x < 2 && rowOffset < end; x++) { + int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int counter : counters) { + rowOffset += counter; + } + if (bestMatch >= 10) { + checkParity |= 1 << (1 - x); + } + if (x != 1) { + // Read off separator if not last + rowOffset = row.getNextSet(rowOffset); + rowOffset = row.getNextUnset(rowOffset); + } + } + + if (resultString.length() != 2) { + throw NotFoundException.getNotFoundInstance(); + } + + if (Integer.parseInt(resultString.toString()) % 4 != checkParity) { + throw NotFoundException.getNotFoundInstance(); + } + + return rowOffset; + } + + /** + * @param raw raw content of extension + * @return formatted interpretation of raw content as a {@link Map} mapping + * one {@link ResultMetadataType} to appropriate value, or {@code null} if not known + */ + private static Map parseExtensionString(String raw) { + if (raw.length() != 2) { + return null; + } + Map result = new EnumMap<>(ResultMetadataType.class); + result.put(ResultMetadataType.ISSUE_NUMBER, Integer.valueOf(raw)); + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension5Support.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension5Support.java new file mode 100644 index 0000000..bde090c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtension5Support.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; + +import java.util.EnumMap; +import java.util.Map; + +/** + * @see UPCEANExtension2Support + */ +final class UPCEANExtension5Support { + + private static final int[] CHECK_DIGIT_ENCODINGS = { + 0x18, 0x14, 0x12, 0x11, 0x0C, 0x06, 0x03, 0x0A, 0x09, 0x05 + }; + + private final int[] decodeMiddleCounters = new int[4]; + private final StringBuilder decodeRowStringBuffer = new StringBuilder(); + + Result decodeRow(int rowNumber, BitArray row, int[] extensionStartRange) throws NotFoundException { + + StringBuilder result = decodeRowStringBuffer; + result.setLength(0); + int end = decodeMiddle(row, extensionStartRange, result); + + String resultString = result.toString(); + Map extensionData = parseExtensionString(resultString); + + Result extensionResult = + new Result(resultString, + null, + new ResultPoint[] { + new ResultPoint((extensionStartRange[0] + extensionStartRange[1]) / 2.0f, rowNumber), + new ResultPoint(end, rowNumber), + }, + BarcodeFormat.UPC_EAN_EXTENSION); + if (extensionData != null) { + extensionResult.putAllMetadata(extensionData); + } + return extensionResult; + } + + private int decodeMiddle(BitArray row, int[] startRange, StringBuilder resultString) throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 5 && rowOffset < end; x++) { + int bestMatch = UPCEANReader.decodeDigit(row, counters, rowOffset, UPCEANReader.L_AND_G_PATTERNS); + resultString.append((char) ('0' + bestMatch % 10)); + for (int counter : counters) { + rowOffset += counter; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (4 - x); + } + if (x != 4) { + // Read off separator if not last + rowOffset = row.getNextSet(rowOffset); + rowOffset = row.getNextUnset(rowOffset); + } + } + + if (resultString.length() != 5) { + throw NotFoundException.getNotFoundInstance(); + } + + int checkDigit = determineCheckDigit(lgPatternFound); + if (extensionChecksum(resultString.toString()) != checkDigit) { + throw NotFoundException.getNotFoundInstance(); + } + + return rowOffset; + } + + private static int extensionChecksum(CharSequence s) { + int length = s.length(); + int sum = 0; + for (int i = length - 2; i >= 0; i -= 2) { + sum += s.charAt(i) - '0'; + } + sum *= 3; + for (int i = length - 1; i >= 0; i -= 2) { + sum += s.charAt(i) - '0'; + } + sum *= 3; + return sum % 10; + } + + private static int determineCheckDigit(int lgPatternFound) + throws NotFoundException { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == CHECK_DIGIT_ENCODINGS[d]) { + return d; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * @param raw raw content of extension + * @return formatted interpretation of raw content as a {@link Map} mapping + * one {@link ResultMetadataType} to appropriate value, or {@code null} if not known + */ + private static Map parseExtensionString(String raw) { + if (raw.length() != 5) { + return null; + } + Object value = parseExtension5String(raw); + if (value == null) { + return null; + } + Map result = new EnumMap<>(ResultMetadataType.class); + result.put(ResultMetadataType.SUGGESTED_PRICE, value); + return result; + } + + private static String parseExtension5String(String raw) { + String currency; + switch (raw.charAt(0)) { + case '0': + currency = "£"; + break; + case '5': + currency = "$"; + break; + case '9': + // Reference: http://www.jollytech.com + if ("90000".equals(raw)) { + // No suggested retail price + return null; + } + if ("99991".equals(raw)) { + // Complementary + return "0.00"; + } + if ("99990".equals(raw)) { + return "Used"; + } + // Otherwise... unknown currency? + currency = ""; + break; + default: + currency = ""; + break; + } + int rawAmount = Integer.parseInt(raw.substring(1)); + String unitsString = String.valueOf(rawAmount / 100); + int hundredths = rawAmount % 100; + String hundredthsString = hundredths < 10 ? "0" + hundredths : String.valueOf(hundredths); + return currency + unitsString + '.' + hundredthsString; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtensionSupport.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtensionSupport.java new file mode 100644 index 0000000..b36a581 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEANExtensionSupport.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.common.BitArray; + +final class UPCEANExtensionSupport { + + private static final int[] EXTENSION_START_PATTERN = {1,1,2}; + + private final UPCEANExtension2Support twoSupport = new UPCEANExtension2Support(); + private final UPCEANExtension5Support fiveSupport = new UPCEANExtension5Support(); + + Result decodeRow(int rowNumber, BitArray row, int rowOffset) throws NotFoundException { + int[] extensionStartRange = UPCEANReader.findGuardPattern(row, rowOffset, false, EXTENSION_START_PATTERN); + try { + return fiveSupport.decodeRow(rowNumber, row, extensionStartRange); + } catch (ReaderException ignored) { + return twoSupport.decodeRow(rowNumber, row, extensionStartRange); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEANReader.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEANReader.java new file mode 100644 index 0000000..8e271ee --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEANReader.java @@ -0,0 +1,403 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.ReaderException; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitArray; + +import java.util.Arrays; +import java.util.Map; + +/** + *

Encapsulates functionality and implementation that is common to UPC and EAN families + * of one-dimensional barcodes.

+ * + * @author dswitkin@google.com (Daniel Switkin) + * @author Sean Owen + * @author alasdair@google.com (Alasdair Mackintosh) + */ +public abstract class UPCEANReader extends OneDReader { + + // These two values are critical for determining how permissive the decoding will be. + // We've arrived at these values through a lot of trial and error. Setting them any higher + // lets false positives creep in quickly. + private static final float MAX_AVG_VARIANCE = 0.48f; + private static final float MAX_INDIVIDUAL_VARIANCE = 0.7f; + + /** + * Start/end guard pattern. + */ + static final int[] START_END_PATTERN = {1, 1, 1,}; + + /** + * Pattern marking the middle of a UPC/EAN pattern, separating the two halves. + */ + static final int[] MIDDLE_PATTERN = {1, 1, 1, 1, 1}; + /** + * end guard pattern. + */ + static final int[] END_PATTERN = {1, 1, 1, 1, 1, 1}; + /** + * "Odd", or "L" patterns used to encode UPC/EAN digits. + */ + static final int[][] L_PATTERNS = { + {3, 2, 1, 1}, // 0 + {2, 2, 2, 1}, // 1 + {2, 1, 2, 2}, // 2 + {1, 4, 1, 1}, // 3 + {1, 1, 3, 2}, // 4 + {1, 2, 3, 1}, // 5 + {1, 1, 1, 4}, // 6 + {1, 3, 1, 2}, // 7 + {1, 2, 1, 3}, // 8 + {3, 1, 1, 2} // 9 + }; + + /** + * As above but also including the "even", or "G" patterns used to encode UPC/EAN digits. + */ + static final int[][] L_AND_G_PATTERNS; + + static { + L_AND_G_PATTERNS = new int[20][]; + System.arraycopy(L_PATTERNS, 0, L_AND_G_PATTERNS, 0, 10); + for (int i = 10; i < 20; i++) { + int[] widths = L_PATTERNS[i - 10]; + int[] reversedWidths = new int[widths.length]; + for (int j = 0; j < widths.length; j++) { + reversedWidths[j] = widths[widths.length - j - 1]; + } + L_AND_G_PATTERNS[i] = reversedWidths; + } + } + + private final StringBuilder decodeRowStringBuffer; + private final UPCEANExtensionSupport extensionReader; + private final EANManufacturerOrgSupport eanManSupport; + + protected UPCEANReader() { + decodeRowStringBuffer = new StringBuilder(20); + extensionReader = new UPCEANExtensionSupport(); + eanManSupport = new EANManufacturerOrgSupport(); + } + + static int[] findStartGuardPattern(BitArray row) throws NotFoundException { + boolean foundStart = false; + int[] startRange = null; + int nextStart = 0; + int[] counters = new int[START_END_PATTERN.length]; + while (!foundStart) { + Arrays.fill(counters, 0, START_END_PATTERN.length, 0); + startRange = findGuardPattern(row, nextStart, false, START_END_PATTERN, counters); + int start = startRange[0]; + nextStart = startRange[1]; + // Make sure there is a quiet zone at least as big as the start pattern before the barcode. + // If this check would run off the left edge of the image, do not accept this barcode, + // as it is very likely to be a false positive. + int quietStart = start - (nextStart - start); + if (quietStart >= 0) { + foundStart = row.isRange(quietStart, start, false); + } + } + return startRange; + } + + @Override + public Result decodeRow(int rowNumber, BitArray row, Map hints) + throws NotFoundException, ChecksumException, FormatException { + return decodeRow(rowNumber, row, findStartGuardPattern(row), hints); + } + + /** + *

Like {@link #decodeRow(int, BitArray, Map)}, but + * allows caller to inform method about where the UPC/EAN start pattern is + * found. This allows this to be computed once and reused across many implementations.

+ * + * @param rowNumber row index into the image + * @param row encoding of the row of the barcode image + * @param startGuardRange start/end column where the opening start pattern was found + * @param hints optional hints that influence decoding + * @return {@link Result} encapsulating the result of decoding a barcode in the row + * @throws NotFoundException if no potential barcode is found + * @throws ChecksumException if a potential barcode is found but does not pass its checksum + * @throws FormatException if a potential barcode is found but format is invalid + */ + public Result decodeRow(int rowNumber, + BitArray row, + int[] startGuardRange, + Map hints) + throws NotFoundException, ChecksumException, FormatException { + + ResultPointCallback resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + (startGuardRange[0] + startGuardRange[1]) / 2.0f, rowNumber + )); + } + + StringBuilder result = decodeRowStringBuffer; + result.setLength(0); + int endStart = decodeMiddle(row, startGuardRange, result); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + endStart, rowNumber + )); + } + + int[] endRange = decodeEnd(row, endStart); + + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(new ResultPoint( + (endRange[0] + endRange[1]) / 2.0f, rowNumber + )); + } + + + // Make sure there is a quiet zone at least as big as the end pattern after the barcode. The + // spec might want more whitespace, but in practice this is the maximum we can count on. + int end = endRange[1]; + int quietEnd = end + (end - endRange[0]); + if (quietEnd >= row.getSize() || !row.isRange(end, quietEnd, false)) { + throw NotFoundException.getNotFoundInstance(); + } + + String resultString = result.toString(); + // UPC/EAN should never be less than 8 chars anyway + if (resultString.length() < 8) { + throw FormatException.getFormatInstance(); + } + if (!checkChecksum(resultString)) { + throw ChecksumException.getChecksumInstance(); + } + + float left = (startGuardRange[1] + startGuardRange[0]) / 2.0f; + float right = (endRange[1] + endRange[0]) / 2.0f; + BarcodeFormat format = getBarcodeFormat(); + Result decodeResult = new Result(resultString, + null, // no natural byte representation for these barcodes + new ResultPoint[]{ + new ResultPoint(left, rowNumber), + new ResultPoint(right, rowNumber)}, + format); + + int extensionLength = 0; + + try { + Result extensionResult = extensionReader.decodeRow(rowNumber, row, endRange[1]); + decodeResult.putMetadata(ResultMetadataType.UPC_EAN_EXTENSION, extensionResult.getText()); + decodeResult.putAllMetadata(extensionResult.getResultMetadata()); + decodeResult.addResultPoints(extensionResult.getResultPoints()); + extensionLength = extensionResult.getText().length(); + } catch (ReaderException re) { + // continue + } + + int[] allowedExtensions = + hints == null ? null : (int[]) hints.get(DecodeHintType.ALLOWED_EAN_EXTENSIONS); + if (allowedExtensions != null) { + boolean valid = false; + for (int length : allowedExtensions) { + if (extensionLength == length) { + valid = true; + break; + } + } + if (!valid) { + throw NotFoundException.getNotFoundInstance(); + } + } + + if (format == BarcodeFormat.EAN_13 || format == BarcodeFormat.UPC_A) { + String countryID = eanManSupport.lookupCountryIdentifier(resultString); + if (countryID != null) { + decodeResult.putMetadata(ResultMetadataType.POSSIBLE_COUNTRY, countryID); + } + } + + return decodeResult; + } + + /** + * @param s string of digits to check + * @return {@link #checkStandardUPCEANChecksum(CharSequence)} + * @throws FormatException if the string does not contain only digits + */ + boolean checkChecksum(String s) throws FormatException { + return checkStandardUPCEANChecksum(s); + } + + /** + * Computes the UPC/EAN checksum on a string of digits, and reports + * whether the checksum is correct or not. + * + * @param s string of digits to check + * @return true iff string of digits passes the UPC/EAN checksum algorithm + * @throws FormatException if the string does not contain only digits + */ + static boolean checkStandardUPCEANChecksum(CharSequence s) throws FormatException { + int length = s.length(); + if (length == 0) { + return false; + } + int check = Character.digit(s.charAt(length - 1), 10); + return getStandardUPCEANChecksum(s.subSequence(0, length - 1)) == check; + } + + static int getStandardUPCEANChecksum(CharSequence s) throws FormatException { + int length = s.length(); + int sum = 0; + for (int i = length - 1; i >= 0; i -= 2) { + int digit = s.charAt(i) - '0'; + if (digit < 0 || digit > 9) { + throw FormatException.getFormatInstance(); + } + sum += digit; + } + sum *= 3; + for (int i = length - 2; i >= 0; i -= 2) { + int digit = s.charAt(i) - '0'; + if (digit < 0 || digit > 9) { + throw FormatException.getFormatInstance(); + } + sum += digit; + } + return (1000 - sum) % 10; + } + + int[] decodeEnd(BitArray row, int endStart) throws NotFoundException { + return findGuardPattern(row, endStart, false, START_END_PATTERN); + } + + static int[] findGuardPattern(BitArray row, + int rowOffset, + boolean whiteFirst, + int[] pattern) throws NotFoundException { + return findGuardPattern(row, rowOffset, whiteFirst, pattern, new int[pattern.length]); + } + + /** + * @param row row of black/white values to search + * @param rowOffset position to start search + * @param whiteFirst if true, indicates that the pattern specifies white/black/white/... + * pixel counts, otherwise, it is interpreted as black/white/black/... + * @param pattern pattern of counts of number of black and white pixels that are being + * searched for as a pattern + * @param counters array of counters, as long as pattern, to re-use + * @return start/end horizontal offset of guard pattern, as an array of two ints + * @throws NotFoundException if pattern is not found + */ + private static int[] findGuardPattern(BitArray row, + int rowOffset, + boolean whiteFirst, + int[] pattern, + int[] counters) throws NotFoundException { + int width = row.getSize(); + rowOffset = whiteFirst ? row.getNextUnset(rowOffset) : row.getNextSet(rowOffset); + int counterPosition = 0; + int patternStart = rowOffset; + int patternLength = pattern.length; + boolean isWhite = whiteFirst; + for (int x = rowOffset; x < width; x++) { + if (row.get(x) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { + return new int[]{patternStart, x}; + } + patternStart += counters[0] + counters[1]; + System.arraycopy(counters, 2, counters, 0, counterPosition - 1); + counters[counterPosition - 1] = 0; + counters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Attempts to decode a single UPC/EAN-encoded digit. + * + * @param row row of black/white values to decode + * @param counters the counts of runs of observed black/white/black/... values + * @param rowOffset horizontal offset to start decoding from + * @param patterns the set of patterns to use to decode -- sometimes different encodings + * for the digits 0-9 are used, and this indicates the encodings for 0 to 9 that should + * be used + * @return horizontal offset of first pixel beyond the decoded digit + * @throws NotFoundException if digit cannot be decoded + */ + static int decodeDigit(BitArray row, int[] counters, int rowOffset, int[][] patterns) + throws NotFoundException { + recordPattern(row, rowOffset, counters); + float bestVariance = MAX_AVG_VARIANCE; // worst variance we'll accept + int bestMatch = -1; + int max = patterns.length; + for (int i = 0; i < max; i++) { + int[] pattern = patterns[i]; + float variance = patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE); + if (variance < bestVariance) { + bestVariance = variance; + bestMatch = i; + } + } + if (bestMatch >= 0) { + return bestMatch; + } else { + throw NotFoundException.getNotFoundInstance(); + } + } + + /** + * Get the format of this decoder. + * + * @return The 1D format. + */ + abstract BarcodeFormat getBarcodeFormat(); + + /** + * Subclasses override this to decode the portion of a barcode between the start + * and end guard patterns. + * + * @param row row of black/white values to search + * @param startRange start/end offset of start guard pattern + * @param resultString {@link StringBuilder} to append decoded chars to + * @return horizontal offset of first pixel after the "middle" that was decoded + * @throws NotFoundException if decoding could not complete successfully + */ + protected abstract int decodeMiddle(BitArray row, + int[] startRange, + StringBuilder resultString) throws NotFoundException; + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEANWriter.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEANWriter.java new file mode 100644 index 0000000..6e9907a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEANWriter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +/** + *

Encapsulates functionality and implementation that is common to UPC and EAN families + * of one-dimensional barcodes.

+ * + * @author aripollak@gmail.com (Ari Pollak) + * @author dsbnatut@gmail.com (Kazuki Nishiura) + */ +public abstract class UPCEANWriter extends OneDimensionalCodeWriter { + + @Override + public int getDefaultMargin() { + // Use a different default more appropriate for UPC/EAN + return 9; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEReader.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEReader.java new file mode 100644 index 0000000..ab8e39c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEReader.java @@ -0,0 +1,182 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + *

Implements decoding of the UPC-E format.

+ *

This is a great reference for + * UPC-E information.

+ * + * @author Sean Owen + */ +public final class UPCEReader extends UPCEANReader { + + /** + * The pattern that marks the middle, and end, of a UPC-E pattern. + * There is no "second half" to a UPC-E barcode. + */ + private static final int[] MIDDLE_END_PATTERN = {1, 1, 1, 1, 1, 1}; + + // For an UPC-E barcode, the final digit is represented by the parities used + // to encode the middle six digits, according to the table below. + // + // Parity of next 6 digits + // Digit 0 1 2 3 4 5 + // 0 Even Even Even Odd Odd Odd + // 1 Even Even Odd Even Odd Odd + // 2 Even Even Odd Odd Even Odd + // 3 Even Even Odd Odd Odd Even + // 4 Even Odd Even Even Odd Odd + // 5 Even Odd Odd Even Even Odd + // 6 Even Odd Odd Odd Even Even + // 7 Even Odd Even Odd Even Odd + // 8 Even Odd Even Odd Odd Even + // 9 Even Odd Odd Even Odd Even + // + // The encoding is represented by the following array, which is a bit pattern + // using Odd = 0 and Even = 1. For example, 5 is represented by: + // + // Odd Even Even Odd Odd Even + // in binary: + // 0 1 1 0 0 1 == 0x19 + // + + /** + * See {@link #L_AND_G_PATTERNS}; these values similarly represent patterns of + * even-odd parity encodings of digits that imply both the number system (0 or 1) + * used, and the check digit. + */ + static final int[][] NUMSYS_AND_CHECK_DIGIT_PATTERNS = { + {0x38, 0x34, 0x32, 0x31, 0x2C, 0x26, 0x23, 0x2A, 0x29, 0x25}, + {0x07, 0x0B, 0x0D, 0x0E, 0x13, 0x19, 0x1C, 0x15, 0x16, 0x1A} + }; + + private final int[] decodeMiddleCounters; + + public UPCEReader() { + decodeMiddleCounters = new int[4]; + } + + @Override + protected int decodeMiddle(BitArray row, int[] startRange, StringBuilder result) + throws NotFoundException { + int[] counters = decodeMiddleCounters; + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + int end = row.getSize(); + int rowOffset = startRange[1]; + + int lgPatternFound = 0; + + for (int x = 0; x < 6 && rowOffset < end; x++) { + int bestMatch = decodeDigit(row, counters, rowOffset, L_AND_G_PATTERNS); + result.append((char) ('0' + bestMatch % 10)); + for (int counter : counters) { + rowOffset += counter; + } + if (bestMatch >= 10) { + lgPatternFound |= 1 << (5 - x); + } + } + + determineNumSysAndCheckDigit(result, lgPatternFound); + + return rowOffset; + } + + @Override + protected int[] decodeEnd(BitArray row, int endStart) throws NotFoundException { + return findGuardPattern(row, endStart, true, MIDDLE_END_PATTERN); + } + + @Override + protected boolean checkChecksum(String s) throws FormatException { + return super.checkChecksum(convertUPCEtoUPCA(s)); + } + + private static void determineNumSysAndCheckDigit(StringBuilder resultString, int lgPatternFound) + throws NotFoundException { + + for (int numSys = 0; numSys <= 1; numSys++) { + for (int d = 0; d < 10; d++) { + if (lgPatternFound == NUMSYS_AND_CHECK_DIGIT_PATTERNS[numSys][d]) { + resultString.insert(0, (char) ('0' + numSys)); + resultString.append((char) ('0' + d)); + return; + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + + @Override + BarcodeFormat getBarcodeFormat() { + return BarcodeFormat.UPC_E; + } + + /** + * Expands a UPC-E value back into its full, equivalent UPC-A code value. + * + * @param upce UPC-E code as string of digits + * @return equivalent UPC-A code as string of digits + */ + public static String convertUPCEtoUPCA(String upce) { + char[] upceChars = new char[6]; + upce.getChars(1, 7, upceChars, 0); + StringBuilder result = new StringBuilder(12); + result.append(upce.charAt(0)); + char lastChar = upceChars[5]; + switch (lastChar) { + case '0': + case '1': + case '2': + result.append(upceChars, 0, 2); + result.append(lastChar); + result.append("0000"); + result.append(upceChars, 2, 3); + break; + case '3': + result.append(upceChars, 0, 3); + result.append("00000"); + result.append(upceChars, 3, 2); + break; + case '4': + result.append(upceChars, 0, 4); + result.append("00000"); + result.append(upceChars[4]); + break; + default: + result.append(upceChars, 0, 5); + result.append("0000"); + result.append(lastChar); + break; + } + // Only append check digit in conversion if supplied + if (upce.length() >= 8) { + result.append(upce.charAt(7)); + } + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/UPCEWriter.java b/rubylib/src/main/java/com/google/zxing/oned/UPCEWriter.java new file mode 100644 index 0000000..825cfe4 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/UPCEWriter.java @@ -0,0 +1,104 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned; + +import java.util.Map; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; + +/** + * This object renders an UPC-E code as a {@link BitMatrix}. + * + * @author 0979097955s@gmail.com (RX) + */ +public final class UPCEWriter extends UPCEANWriter { + + private static final int CODE_WIDTH = 3 + // start guard + (7 * 6) + // bars + 6; // end guard + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.UPC_E) { + throw new IllegalArgumentException("Can only encode UPC_E, but got " + format); + } + + return super.encode(contents, format, width, height, hints); + } + + @Override + public boolean[] encode(String contents) { + int length = contents.length(); + switch (length) { + case 7: + // No check digit present, calculate it and add it + int check; + try { + check = UPCEANReader.getStandardUPCEANChecksum(UPCEReader.convertUPCEtoUPCA(contents)); + } catch (FormatException fe) { + throw new IllegalArgumentException(fe); + } + contents += check; + break; + case 8: + try { + if (!UPCEANReader.checkStandardUPCEANChecksum(contents)) { + throw new IllegalArgumentException("Contents do not pass checksum"); + } + } catch (FormatException ignored) { + throw new IllegalArgumentException("Illegal contents"); + } + break; + default: + throw new IllegalArgumentException( + "Requested contents should be 8 digits long, but got " + length); + } + + int firstDigit = Character.digit(contents.charAt(0), 10); + if (firstDigit != 0 && firstDigit != 1) { + throw new IllegalArgumentException("Number system must be 0 or 1"); + } + + int checkDigit = Character.digit(contents.charAt(7), 10); + int parities = UPCEReader.NUMSYS_AND_CHECK_DIGIT_PATTERNS[firstDigit][checkDigit]; + boolean[] result = new boolean[CODE_WIDTH]; + int pos = 0; + + pos += appendPattern(result, pos, UPCEANReader.START_END_PATTERN, true); + + for (int i = 1; i <= 6; i++) { + int digit = Character.digit(contents.charAt(i), 10); + if ((parities >> (6 - i) & 1) == 1) { + digit += 10; + } + pos += appendPattern(result, pos, UPCEANReader.L_AND_G_PATTERNS[digit], false); + } + + appendPattern(result, pos, UPCEANReader.END_PATTERN, false); + + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/AbstractRSSReader.java b/rubylib/src/main/java/com/google/zxing/oned/rss/AbstractRSSReader.java new file mode 100644 index 0000000..5c77512 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/AbstractRSSReader.java @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.oned.OneDReader; + +/** + * Superclass of {@link OneDReader} implementations that read barcodes in the RSS family + * of formats. + */ +public abstract class AbstractRSSReader extends OneDReader { + + private static final float MAX_AVG_VARIANCE = 0.2f; + private static final float MAX_INDIVIDUAL_VARIANCE = 0.45f; + + private static final float MIN_FINDER_PATTERN_RATIO = 9.5f / 12.0f; + private static final float MAX_FINDER_PATTERN_RATIO = 12.5f / 14.0f; + + private final int[] decodeFinderCounters; + private final int[] dataCharacterCounters; + private final float[] oddRoundingErrors; + private final float[] evenRoundingErrors; + private final int[] oddCounts; + private final int[] evenCounts; + + protected AbstractRSSReader() { + decodeFinderCounters = new int[4]; + dataCharacterCounters = new int[8]; + oddRoundingErrors = new float[4]; + evenRoundingErrors = new float[4]; + oddCounts = new int[dataCharacterCounters.length / 2]; + evenCounts = new int[dataCharacterCounters.length / 2]; + } + + protected final int[] getDecodeFinderCounters() { + return decodeFinderCounters; + } + + protected final int[] getDataCharacterCounters() { + return dataCharacterCounters; + } + + protected final float[] getOddRoundingErrors() { + return oddRoundingErrors; + } + + protected final float[] getEvenRoundingErrors() { + return evenRoundingErrors; + } + + protected final int[] getOddCounts() { + return oddCounts; + } + + protected final int[] getEvenCounts() { + return evenCounts; + } + + protected static int parseFinderValue(int[] counters, + int[][] finderPatterns) throws NotFoundException { + for (int value = 0; value < finderPatterns.length; value++) { + if (patternMatchVariance(counters, finderPatterns[value], MAX_INDIVIDUAL_VARIANCE) < + MAX_AVG_VARIANCE) { + return value; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + /** + * @param array values to sum + * @return sum of values + * @deprecated call {@link MathUtils#sum(int[])} + */ + @Deprecated + protected static int count(int[] array) { + return MathUtils.sum(array); + } + + protected static void increment(int[] array, float[] errors) { + int index = 0; + float biggestError = errors[0]; + for (int i = 1; i < array.length; i++) { + if (errors[i] > biggestError) { + biggestError = errors[i]; + index = i; + } + } + array[index]++; + } + + protected static void decrement(int[] array, float[] errors) { + int index = 0; + float biggestError = errors[0]; + for (int i = 1; i < array.length; i++) { + if (errors[i] < biggestError) { + biggestError = errors[i]; + index = i; + } + } + array[index]--; + } + + protected static boolean isFinderPattern(int[] counters) { + int firstTwoSum = counters[0] + counters[1]; + int sum = firstTwoSum + counters[2] + counters[3]; + float ratio = firstTwoSum / (float) sum; + if (ratio >= MIN_FINDER_PATTERN_RATIO && ratio <= MAX_FINDER_PATTERN_RATIO) { + // passes ratio test in spec, but see if the counts are unreasonable + int minCounter = Integer.MAX_VALUE; + int maxCounter = Integer.MIN_VALUE; + for (int counter : counters) { + if (counter > maxCounter) { + maxCounter = counter; + } + if (counter < minCounter) { + minCounter = counter; + } + } + return maxCounter < 10 * minCounter; + } + return false; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/DataCharacter.java b/rubylib/src/main/java/com/google/zxing/oned/rss/DataCharacter.java new file mode 100644 index 0000000..1c3c502 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/DataCharacter.java @@ -0,0 +1,59 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +/** + * Encapsulates a since character value in an RSS barcode, including its checksum information. + */ +public class DataCharacter { + + private final int value; + private final int checksumPortion; + + public DataCharacter(int value, int checksumPortion) { + this.value = value; + this.checksumPortion = checksumPortion; + } + + public final int getValue() { + return value; + } + + public final int getChecksumPortion() { + return checksumPortion; + } + + @Override + public final String toString() { + return value + "(" + checksumPortion + ')'; + } + + @Override + public final boolean equals(Object o) { + if (!(o instanceof DataCharacter)) { + return false; + } + DataCharacter that = (DataCharacter) o; + return value == that.value && checksumPortion == that.checksumPortion; + } + + @Override + public final int hashCode() { + return value ^ checksumPortion; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/FinderPattern.java b/rubylib/src/main/java/com/google/zxing/oned/rss/FinderPattern.java new file mode 100644 index 0000000..56a8d72 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/FinderPattern.java @@ -0,0 +1,65 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +import com.google.zxing.ResultPoint; + +/** + * Encapsulates an RSS barcode finder pattern, including its start/end position and row. + */ +public final class FinderPattern { + + private final int value; + private final int[] startEnd; + private final ResultPoint[] resultPoints; + + public FinderPattern(int value, int[] startEnd, int start, int end, int rowNumber) { + this.value = value; + this.startEnd = startEnd; + this.resultPoints = new ResultPoint[] { + new ResultPoint(start, rowNumber), + new ResultPoint(end, rowNumber), + }; + } + + public int getValue() { + return value; + } + + public int[] getStartEnd() { + return startEnd; + } + + public ResultPoint[] getResultPoints() { + return resultPoints; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FinderPattern)) { + return false; + } + FinderPattern that = (FinderPattern) o; + return value == that.value; + } + + @Override + public int hashCode() { + return value; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/Pair.java b/rubylib/src/main/java/com/google/zxing/oned/rss/Pair.java new file mode 100644 index 0000000..e2371d2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/Pair.java @@ -0,0 +1,41 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +final class Pair extends DataCharacter { + + private final FinderPattern finderPattern; + private int count; + + Pair(int value, int checksumPortion, FinderPattern finderPattern) { + super(value, checksumPortion); + this.finderPattern = finderPattern; + } + + FinderPattern getFinderPattern() { + return finderPattern; + } + + int getCount() { + return count; + } + + void incrementCount() { + count++; + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/RSS14Reader.java b/rubylib/src/main/java/com/google/zxing/oned/rss/RSS14Reader.java new file mode 100644 index 0000000..db87b8f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/RSS14Reader.java @@ -0,0 +1,473 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.detector.MathUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Decodes RSS-14, including truncated and stacked variants. See ISO/IEC 24724:2006. + */ +public final class RSS14Reader extends AbstractRSSReader { + + private static final int[] OUTSIDE_EVEN_TOTAL_SUBSET = {1,10,34,70,126}; + private static final int[] INSIDE_ODD_TOTAL_SUBSET = {4,20,48,81}; + private static final int[] OUTSIDE_GSUM = {0,161,961,2015,2715}; + private static final int[] INSIDE_GSUM = {0,336,1036,1516}; + private static final int[] OUTSIDE_ODD_WIDEST = {8,6,4,3,1}; + private static final int[] INSIDE_ODD_WIDEST = {2,4,6,8}; + + private static final int[][] FINDER_PATTERNS = { + {3,8,2,1}, + {3,5,5,1}, + {3,3,7,1}, + {3,1,9,1}, + {2,7,4,1}, + {2,5,6,1}, + {2,3,8,1}, + {1,5,7,1}, + {1,3,9,1}, + }; + + private final List possibleLeftPairs; + private final List possibleRightPairs; + + public RSS14Reader() { + possibleLeftPairs = new ArrayList<>(); + possibleRightPairs = new ArrayList<>(); + } + + @Override + public Result decodeRow(int rowNumber, + BitArray row, + Map hints) throws NotFoundException { + Pair leftPair = decodePair(row, false, rowNumber, hints); + addOrTally(possibleLeftPairs, leftPair); + row.reverse(); + Pair rightPair = decodePair(row, true, rowNumber, hints); + addOrTally(possibleRightPairs, rightPair); + row.reverse(); + for (Pair left : possibleLeftPairs) { + if (left.getCount() > 1) { + for (Pair right : possibleRightPairs) { + if (right.getCount() > 1 && checkChecksum(left, right)) { + return constructResult(left, right); + } + } + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static void addOrTally(Collection possiblePairs, Pair pair) { + if (pair == null) { + return; + } + boolean found = false; + for (Pair other : possiblePairs) { + if (other.getValue() == pair.getValue()) { + other.incrementCount(); + found = true; + break; + } + } + if (!found) { + possiblePairs.add(pair); + } + } + + @Override + public void reset() { + possibleLeftPairs.clear(); + possibleRightPairs.clear(); + } + + private static Result constructResult(Pair leftPair, Pair rightPair) { + long symbolValue = 4537077L * leftPair.getValue() + rightPair.getValue(); + String text = String.valueOf(symbolValue); + + StringBuilder buffer = new StringBuilder(14); + for (int i = 13 - text.length(); i > 0; i--) { + buffer.append('0'); + } + buffer.append(text); + + int checkDigit = 0; + for (int i = 0; i < 13; i++) { + int digit = buffer.charAt(i) - '0'; + checkDigit += (i & 0x01) == 0 ? 3 * digit : digit; + } + checkDigit = 10 - (checkDigit % 10); + if (checkDigit == 10) { + checkDigit = 0; + } + buffer.append(checkDigit); + + ResultPoint[] leftPoints = leftPair.getFinderPattern().getResultPoints(); + ResultPoint[] rightPoints = rightPair.getFinderPattern().getResultPoints(); + return new Result( + buffer.toString(), + null, + new ResultPoint[] { leftPoints[0], leftPoints[1], rightPoints[0], rightPoints[1], }, + BarcodeFormat.RSS_14); + } + + private static boolean checkChecksum(Pair leftPair, Pair rightPair) { + //int leftFPValue = leftPair.getFinderPattern().getValue(); + //int rightFPValue = rightPair.getFinderPattern().getValue(); + //if ((leftFPValue == 0 && rightFPValue == 8) || + // (leftFPValue == 8 && rightFPValue == 0)) { + //} + int checkValue = (leftPair.getChecksumPortion() + 16 * rightPair.getChecksumPortion()) % 79; + int targetCheckValue = + 9 * leftPair.getFinderPattern().getValue() + rightPair.getFinderPattern().getValue(); + if (targetCheckValue > 72) { + targetCheckValue--; + } + if (targetCheckValue > 8) { + targetCheckValue--; + } + return checkValue == targetCheckValue; + } + + private Pair decodePair(BitArray row, boolean right, int rowNumber, Map hints) { + try { + int[] startEnd = findFinderPattern(row, right); + FinderPattern pattern = parseFoundFinderPattern(row, rowNumber, right, startEnd); + + ResultPointCallback resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + if (resultPointCallback != null) { + float center = (startEnd[0] + startEnd[1]) / 2.0f; + if (right) { + // row is actually reversed + center = row.getSize() - 1 - center; + } + resultPointCallback.foundPossibleResultPoint(new ResultPoint(center, rowNumber)); + } + + DataCharacter outside = decodeDataCharacter(row, pattern, true); + DataCharacter inside = decodeDataCharacter(row, pattern, false); + return new Pair(1597 * outside.getValue() + inside.getValue(), + outside.getChecksumPortion() + 4 * inside.getChecksumPortion(), + pattern); + } catch (NotFoundException ignored) { + return null; + } + } + + private DataCharacter decodeDataCharacter(BitArray row, FinderPattern pattern, boolean outsideChar) + throws NotFoundException { + + int[] counters = getDataCharacterCounters(); + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + counters[4] = 0; + counters[5] = 0; + counters[6] = 0; + counters[7] = 0; + + if (outsideChar) { + recordPatternInReverse(row, pattern.getStartEnd()[0], counters); + } else { + recordPattern(row, pattern.getStartEnd()[1] + 1, counters); + // reverse it + for (int i = 0, j = counters.length - 1; i < j; i++, j--) { + int temp = counters[i]; + counters[i] = counters[j]; + counters[j] = temp; + } + } + + int numModules = outsideChar ? 16 : 15; + float elementWidth = MathUtils.sum(counters) / (float) numModules; + + int[] oddCounts = this.getOddCounts(); + int[] evenCounts = this.getEvenCounts(); + float[] oddRoundingErrors = this.getOddRoundingErrors(); + float[] evenRoundingErrors = this.getEvenRoundingErrors(); + + for (int i = 0; i < counters.length; i++) { + float value = counters[i] / elementWidth; + int count = (int) (value + 0.5f); // Round + if (count < 1) { + count = 1; + } else if (count > 8) { + count = 8; + } + int offset = i / 2; + if ((i & 0x01) == 0) { + oddCounts[offset] = count; + oddRoundingErrors[offset] = value - count; + } else { + evenCounts[offset] = count; + evenRoundingErrors[offset] = value - count; + } + } + + adjustOddEvenCounts(outsideChar, numModules); + + int oddSum = 0; + int oddChecksumPortion = 0; + for (int i = oddCounts.length - 1; i >= 0; i--) { + oddChecksumPortion *= 9; + oddChecksumPortion += oddCounts[i]; + oddSum += oddCounts[i]; + } + int evenChecksumPortion = 0; + int evenSum = 0; + for (int i = evenCounts.length - 1; i >= 0; i--) { + evenChecksumPortion *= 9; + evenChecksumPortion += evenCounts[i]; + evenSum += evenCounts[i]; + } + int checksumPortion = oddChecksumPortion + 3 * evenChecksumPortion; + + if (outsideChar) { + if ((oddSum & 0x01) != 0 || oddSum > 12 || oddSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + int group = (12 - oddSum) / 2; + int oddWidest = OUTSIDE_ODD_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, false); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, true); + int tEven = OUTSIDE_EVEN_TOTAL_SUBSET[group]; + int gSum = OUTSIDE_GSUM[group]; + return new DataCharacter(vOdd * tEven + vEven + gSum, checksumPortion); + } else { + if ((evenSum & 0x01) != 0 || evenSum > 10 || evenSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + int group = (10 - evenSum) / 2; + int oddWidest = INSIDE_ODD_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false); + int tOdd = INSIDE_ODD_TOTAL_SUBSET[group]; + int gSum = INSIDE_GSUM[group]; + return new DataCharacter(vEven * tOdd + vOdd + gSum, checksumPortion); + } + + } + + private int[] findFinderPattern(BitArray row, boolean rightFinderPattern) + throws NotFoundException { + + int[] counters = getDecodeFinderCounters(); + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + + int width = row.getSize(); + boolean isWhite = false; + int rowOffset = 0; + while (rowOffset < width) { + isWhite = !row.get(rowOffset); + if (rightFinderPattern == isWhite) { + // Will encounter white first when searching for right finder pattern + break; + } + rowOffset++; + } + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + if (row.get(x) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == 3) { + if (isFinderPattern(counters)) { + return new int[]{patternStart, x}; + } + patternStart += counters[0] + counters[1]; + counters[0] = counters[2]; + counters[1] = counters[3]; + counters[2] = 0; + counters[3] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + + } + + private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean right, int[] startEnd) + throws NotFoundException { + // Actually we found elements 2-5 + boolean firstIsBlack = row.get(startEnd[0]); + int firstElementStart = startEnd[0] - 1; + // Locate element 1 + while (firstElementStart >= 0 && firstIsBlack != row.get(firstElementStart)) { + firstElementStart--; + } + firstElementStart++; + int firstCounter = startEnd[0] - firstElementStart; + // Make 'counters' hold 1-4 + int[] counters = getDecodeFinderCounters(); + System.arraycopy(counters, 0, counters, 1, counters.length - 1); + counters[0] = firstCounter; + int value = parseFinderValue(counters, FINDER_PATTERNS); + int start = firstElementStart; + int end = startEnd[1]; + if (right) { + // row is actually reversed + start = row.getSize() - 1 - start; + end = row.getSize() - 1 - end; + } + return new FinderPattern(value, new int[] {firstElementStart, startEnd[1]}, start, end, rowNumber); + } + + private void adjustOddEvenCounts(boolean outsideChar, int numModules) throws NotFoundException { + + int oddSum = MathUtils.sum(getOddCounts()); + int evenSum = MathUtils.sum(getEvenCounts()); + + boolean incrementOdd = false; + boolean decrementOdd = false; + boolean incrementEven = false; + boolean decrementEven = false; + + if (outsideChar) { + if (oddSum > 12) { + decrementOdd = true; + } else if (oddSum < 4) { + incrementOdd = true; + } + if (evenSum > 12) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + } else { + if (oddSum > 11) { + decrementOdd = true; + } else if (oddSum < 5) { + incrementOdd = true; + } + if (evenSum > 10) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + } + + int mismatch = oddSum + evenSum - numModules; + boolean oddParityBad = (oddSum & 0x01) == (outsideChar ? 1 : 0); + boolean evenParityBad = (evenSum & 0x01) == 1; + /*if (mismatch == 2) { + if (!(oddParityBad && evenParityBad)) { + throw ReaderException.getInstance(); + } + decrementOdd = true; + decrementEven = true; + } else if (mismatch == -2) { + if (!(oddParityBad && evenParityBad)) { + throw ReaderException.getInstance(); + } + incrementOdd = true; + incrementEven = true; + } else */ if (mismatch == 1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementEven = true; + } + } else if (mismatch == -1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementEven = true; + } + } else if (mismatch == 0) { + if (oddParityBad) { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Both bad + if (oddSum < evenSum) { + incrementOdd = true; + decrementEven = true; + } else { + decrementOdd = true; + incrementEven = true; + } + } else { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Nothing to do! + } + } else { + throw NotFoundException.getNotFoundInstance(); + } + + if (incrementOdd) { + if (decrementOdd) { + throw NotFoundException.getNotFoundInstance(); + } + increment(getOddCounts(), getOddRoundingErrors()); + } + if (decrementOdd) { + decrement(getOddCounts(), getOddRoundingErrors()); + } + if (incrementEven) { + if (decrementEven) { + throw NotFoundException.getNotFoundInstance(); + } + increment(getEvenCounts(), getOddRoundingErrors()); + } + if (decrementEven) { + decrement(getEvenCounts(), getEvenRoundingErrors()); + } + + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/RSSUtils.java b/rubylib/src/main/java/com/google/zxing/oned/rss/RSSUtils.java new file mode 100644 index 0000000..676a3bd --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/RSSUtils.java @@ -0,0 +1,129 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss; + +/** Adapted from listings in ISO/IEC 24724 Appendix B and Appendix G. */ +public final class RSSUtils { + + private RSSUtils() {} + + /* + static int[] getRSSwidths(int val, int n, int elements, int maxWidth, boolean noNarrow) { + int[] widths = new int[elements]; + int bar; + int narrowMask = 0; + for (bar = 0; bar < elements - 1; bar++) { + narrowMask |= 1 << bar; + int elmWidth = 1; + int subVal; + while (true) { + subVal = combins(n - elmWidth - 1, elements - bar - 2); + if (noNarrow && (narrowMask == 0) && + (n - elmWidth - (elements - bar - 1) >= elements - bar - 1)) { + subVal -= combins(n - elmWidth - (elements - bar), elements - bar - 2); + } + if (elements - bar - 1 > 1) { + int lessVal = 0; + for (int mxwElement = n - elmWidth - (elements - bar - 2); + mxwElement > maxWidth; + mxwElement--) { + lessVal += combins(n - elmWidth - mxwElement - 1, elements - bar - 3); + } + subVal -= lessVal * (elements - 1 - bar); + } else if (n - elmWidth > maxWidth) { + subVal--; + } + val -= subVal; + if (val < 0) { + break; + } + elmWidth++; + narrowMask &= ~(1 << bar); + } + val += subVal; + n -= elmWidth; + widths[bar] = elmWidth; + } + widths[bar] = n; + return widths; + } + */ + + public static int getRSSvalue(int[] widths, int maxWidth, boolean noNarrow) { + int n = 0; + for (int width : widths) { + n += width; + } + int val = 0; + int narrowMask = 0; + int elements = widths.length; + for (int bar = 0; bar < elements - 1; bar++) { + int elmWidth; + for (elmWidth = 1, narrowMask |= 1 << bar; + elmWidth < widths[bar]; + elmWidth++, narrowMask &= ~(1 << bar)) { + int subVal = combins(n - elmWidth - 1, elements - bar - 2); + if (noNarrow && (narrowMask == 0) && + (n - elmWidth - (elements - bar - 1) >= elements - bar - 1)) { + subVal -= combins(n - elmWidth - (elements - bar), + elements - bar - 2); + } + if (elements - bar - 1 > 1) { + int lessVal = 0; + for (int mxwElement = n - elmWidth - (elements - bar - 2); + mxwElement > maxWidth; mxwElement--) { + lessVal += combins(n - elmWidth - mxwElement - 1, + elements - bar - 3); + } + subVal -= lessVal * (elements - 1 - bar); + } else if (n - elmWidth > maxWidth) { + subVal--; + } + val += subVal; + } + n -= elmWidth; + } + return val; + } + + private static int combins(int n, int r) { + int maxDenom; + int minDenom; + if (n - r > r) { + minDenom = r; + maxDenom = n - r; + } else { + minDenom = n - r; + maxDenom = r; + } + int val = 1; + int j = 1; + for (int i = n; i > maxDenom; i--) { + val *= i; + if (j <= minDenom) { + val /= j; + j++; + } + } + while (j <= minDenom) { + val /= j; + j++; + } + return val; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java new file mode 100644 index 0000000..293af30 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/BitArrayBuilder.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import com.google.zxing.common.BitArray; + +import java.util.List; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class BitArrayBuilder { + + private BitArrayBuilder() { + } + + static BitArray buildBitArray(List pairs) { + int charNumber = (pairs.size() * 2) - 1; + if (pairs.get(pairs.size() - 1).getRightChar() == null) { + charNumber -= 1; + } + + int size = 12 * charNumber; + + BitArray binary = new BitArray(size); + int accPos = 0; + + ExpandedPair firstPair = pairs.get(0); + int firstValue = firstPair.getRightChar().getValue(); + for (int i = 11; i >= 0; --i) { + if ((firstValue & (1 << i)) != 0) { + binary.set(accPos); + } + accPos++; + } + + for (int i = 1; i < pairs.size(); ++i) { + ExpandedPair currentPair = pairs.get(i); + + int leftValue = currentPair.getLeftChar().getValue(); + for (int j = 11; j >= 0; --j) { + if ((leftValue & (1 << j)) != 0) { + binary.set(accPos); + } + accPos++; + } + + if (currentPair.getRightChar() != null) { + int rightValue = currentPair.getRightChar().getValue(); + for (int j = 11; j >= 0; --j) { + if ((rightValue & (1 << j)) != 0) { + binary.set(accPos); + } + accPos++; + } + } + } + return binary; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedPair.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedPair.java new file mode 100644 index 0000000..2e944b2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedPair.java @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import com.google.zxing.oned.rss.DataCharacter; +import com.google.zxing.oned.rss.FinderPattern; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class ExpandedPair { + + private final boolean mayBeLast; + private final DataCharacter leftChar; + private final DataCharacter rightChar; + private final FinderPattern finderPattern; + + ExpandedPair(DataCharacter leftChar, + DataCharacter rightChar, + FinderPattern finderPattern, + boolean mayBeLast) { + this.leftChar = leftChar; + this.rightChar = rightChar; + this.finderPattern = finderPattern; + this.mayBeLast = mayBeLast; + } + + boolean mayBeLast() { + return this.mayBeLast; + } + + DataCharacter getLeftChar() { + return this.leftChar; + } + + DataCharacter getRightChar() { + return this.rightChar; + } + + FinderPattern getFinderPattern() { + return this.finderPattern; + } + + public boolean mustBeLast() { + return this.rightChar == null; + } + + @Override + public String toString() { + return + "[ " + leftChar + " , " + rightChar + " : " + + (finderPattern == null ? "null" : finderPattern.getValue()) + " ]"; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof ExpandedPair)) { + return false; + } + ExpandedPair that = (ExpandedPair) o; + return + equalsOrNull(leftChar, that.leftChar) && + equalsOrNull(rightChar, that.rightChar) && + equalsOrNull(finderPattern, that.finderPattern); + } + + private static boolean equalsOrNull(Object o1, Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + + @Override + public int hashCode() { + return hashNotNull(leftChar) ^ hashNotNull(rightChar) ^ hashNotNull(finderPattern); + } + + private static int hashNotNull(Object o) { + return o == null ? 0 : o.hashCode(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedRow.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedRow.java new file mode 100644 index 0000000..adb8ff8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/ExpandedRow.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.oned.rss.expanded; + +import java.util.ArrayList; +import java.util.List; + +/** + * One row of an RSS Expanded Stacked symbol, consisting of 1+ expanded pairs. + */ +final class ExpandedRow { + + private final List pairs; + private final int rowNumber; + /** Did this row of the image have to be reversed (mirrored) to recognize the pairs? */ + private final boolean wasReversed; + + ExpandedRow(List pairs, int rowNumber, boolean wasReversed) { + this.pairs = new ArrayList<>(pairs); + this.rowNumber = rowNumber; + this.wasReversed = wasReversed; + } + + List getPairs() { + return this.pairs; + } + + int getRowNumber() { + return this.rowNumber; + } + + boolean isReversed() { + return this.wasReversed; + } + + boolean isEquivalent(List otherPairs) { + return this.pairs.equals(otherPairs); + } + + @Override + public String toString() { + return "{ " + pairs + " }"; + } + + /** + * Two rows are equal if they contain the same pairs in the same order. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof ExpandedRow)) { + return false; + } + ExpandedRow that = (ExpandedRow) o; + return this.pairs.equals(that.getPairs()) && wasReversed == that.wasReversed; + } + + @Override + public int hashCode() { + return pairs.hashCode() ^ Boolean.valueOf(wasReversed).hashCode(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java new file mode 100644 index 0000000..2146b48 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/RSSExpandedReader.java @@ -0,0 +1,779 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Result; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.oned.rss.AbstractRSSReader; +import com.google.zxing.oned.rss.DataCharacter; +import com.google.zxing.oned.rss.FinderPattern; +import com.google.zxing.oned.rss.RSSUtils; +import com.google.zxing.oned.rss.expanded.decoders.AbstractExpandedDecoder; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Collections; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +public final class RSSExpandedReader extends AbstractRSSReader { + + private static final int[] SYMBOL_WIDEST = {7, 5, 4, 3, 1}; + private static final int[] EVEN_TOTAL_SUBSET = {4, 20, 52, 104, 204}; + private static final int[] GSUM = {0, 348, 1388, 2948, 3988}; + + private static final int[][] FINDER_PATTERNS = { + {1,8,4,1}, // A + {3,6,4,1}, // B + {3,4,6,1}, // C + {3,2,8,1}, // D + {2,6,5,1}, // E + {2,2,9,1} // F + }; + + private static final int[][] WEIGHTS = { + { 1, 3, 9, 27, 81, 32, 96, 77}, + { 20, 60, 180, 118, 143, 7, 21, 63}, + {189, 145, 13, 39, 117, 140, 209, 205}, + {193, 157, 49, 147, 19, 57, 171, 91}, + { 62, 186, 136, 197, 169, 85, 44, 132}, + {185, 133, 188, 142, 4, 12, 36, 108}, + {113, 128, 173, 97, 80, 29, 87, 50}, + {150, 28, 84, 41, 123, 158, 52, 156}, + { 46, 138, 203, 187, 139, 206, 196, 166}, + { 76, 17, 51, 153, 37, 111, 122, 155}, + { 43, 129, 176, 106, 107, 110, 119, 146}, + { 16, 48, 144, 10, 30, 90, 59, 177}, + {109, 116, 137, 200, 178, 112, 125, 164}, + { 70, 210, 208, 202, 184, 130, 179, 115}, + {134, 191, 151, 31, 93, 68, 204, 190}, + {148, 22, 66, 198, 172, 94, 71, 2}, + { 6, 18, 54, 162, 64, 192,154, 40}, + {120, 149, 25, 75, 14, 42,126, 167}, + { 79, 26, 78, 23, 69, 207,199, 175}, + {103, 98, 83, 38, 114, 131, 182, 124}, + {161, 61, 183, 127, 170, 88, 53, 159}, + { 55, 165, 73, 8, 24, 72, 5, 15}, + { 45, 135, 194, 160, 58, 174, 100, 89} + }; + + private static final int FINDER_PAT_A = 0; + private static final int FINDER_PAT_B = 1; + private static final int FINDER_PAT_C = 2; + private static final int FINDER_PAT_D = 3; + private static final int FINDER_PAT_E = 4; + private static final int FINDER_PAT_F = 5; + + private static final int[][] FINDER_PATTERN_SEQUENCES = { + { FINDER_PAT_A, FINDER_PAT_A }, + { FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B }, + { FINDER_PAT_A, FINDER_PAT_C, FINDER_PAT_B, FINDER_PAT_D }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_C }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_E, FINDER_PAT_B, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + { FINDER_PAT_A, FINDER_PAT_A, FINDER_PAT_B, FINDER_PAT_B, FINDER_PAT_C, FINDER_PAT_D, FINDER_PAT_D, FINDER_PAT_E, FINDER_PAT_E, FINDER_PAT_F, FINDER_PAT_F }, + }; + + private static final int MAX_PAIRS = 11; + + private final List pairs = new ArrayList<>(MAX_PAIRS); + private final List rows = new ArrayList<>(); + private final int [] startEnd = new int[2]; + private boolean startFromEven; + + @Override + public Result decodeRow(int rowNumber, + BitArray row, + Map hints) throws NotFoundException, FormatException { + // Rows can start with even pattern in case in prev rows there where odd number of patters. + // So lets try twice + this.pairs.clear(); + this.startFromEven = false; + try { + return constructResult(decodeRow2pairs(rowNumber, row)); + } catch (NotFoundException e) { + // OK + } + + this.pairs.clear(); + this.startFromEven = true; + return constructResult(decodeRow2pairs(rowNumber, row)); + } + + @Override + public void reset() { + this.pairs.clear(); + this.rows.clear(); + } + + // Not private for testing + List decodeRow2pairs(int rowNumber, BitArray row) throws NotFoundException { + try { + while (true) { + ExpandedPair nextPair = retrieveNextPair(row, this.pairs, rowNumber); + this.pairs.add(nextPair); + // exit this loop when retrieveNextPair() fails and throws + } + } catch (NotFoundException nfe) { + if (this.pairs.isEmpty()) { + throw nfe; + } + } + + // TODO: verify sequence of finder patterns as in checkPairSequence() + if (checkChecksum()) { + return this.pairs; + } + + boolean tryStackedDecode = !this.rows.isEmpty(); + storeRow(rowNumber, false); // TODO: deal with reversed rows + if (tryStackedDecode) { + // When the image is 180-rotated, then rows are sorted in wrong direction. + // Try twice with both the directions. + List ps = checkRows(false); + if (ps != null) { + return ps; + } + ps = checkRows(true); + if (ps != null) { + return ps; + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + private List checkRows(boolean reverse) { + // Limit number of rows we are checking + // We use recursive algorithm with pure complexity and don't want it to take forever + // Stacked barcode can have up to 11 rows, so 25 seems reasonable enough + if (this.rows.size() > 25) { + this.rows.clear(); // We will never have a chance to get result, so clear it + return null; + } + + this.pairs.clear(); + if (reverse) { + Collections.reverse(this.rows); + } + + List ps = null; + try { + ps = checkRows(new ArrayList(), 0); + } catch (NotFoundException e) { + // OK + } + + if (reverse) { + Collections.reverse(this.rows); + } + + return ps; + } + + // Try to construct a valid rows sequence + // Recursion is used to implement backtracking + private List checkRows(List collectedRows, int currentRow) throws NotFoundException { + for (int i = currentRow; i < rows.size(); i++) { + ExpandedRow row = rows.get(i); + this.pairs.clear(); + for (ExpandedRow collectedRow : collectedRows) { + this.pairs.addAll(collectedRow.getPairs()); + } + this.pairs.addAll(row.getPairs()); + + if (!isValidSequence(this.pairs)) { + continue; + } + + if (checkChecksum()) { + return this.pairs; + } + + List rs = new ArrayList<>(); + rs.addAll(collectedRows); + rs.add(row); + try { + // Recursion: try to add more rows + return checkRows(rs, i + 1); + } catch (NotFoundException e) { + // We failed, try the next candidate + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + // Whether the pairs form a valid find pattern sequence, + // either complete or a prefix + private static boolean isValidSequence(List pairs) { + for (int[] sequence : FINDER_PATTERN_SEQUENCES) { + if (pairs.size() > sequence.length) { + continue; + } + + boolean stop = true; + for (int j = 0; j < pairs.size(); j++) { + if (pairs.get(j).getFinderPattern().getValue() != sequence[j]) { + stop = false; + break; + } + } + + if (stop) { + return true; + } + } + + return false; + } + + private void storeRow(int rowNumber, boolean wasReversed) { + // Discard if duplicate above or below; otherwise insert in order by row number. + int insertPos = 0; + boolean prevIsSame = false; + boolean nextIsSame = false; + while (insertPos < this.rows.size()) { + ExpandedRow erow = this.rows.get(insertPos); + if (erow.getRowNumber() > rowNumber) { + nextIsSame = erow.isEquivalent(this.pairs); + break; + } + prevIsSame = erow.isEquivalent(this.pairs); + insertPos++; + } + if (nextIsSame || prevIsSame) { + return; + } + + // When the row was partially decoded (e.g. 2 pairs found instead of 3), + // it will prevent us from detecting the barcode. + // Try to merge partial rows + + // Check whether the row is part of an allready detected row + if (isPartialRow(this.pairs, this.rows)) { + return; + } + + this.rows.add(insertPos, new ExpandedRow(this.pairs, rowNumber, wasReversed)); + + removePartialRows(this.pairs, this.rows); + } + + // Remove all the rows that contains only specified pairs + private static void removePartialRows(List pairs, List rows) { + for (Iterator iterator = rows.iterator(); iterator.hasNext();) { + ExpandedRow r = iterator.next(); + if (r.getPairs().size() == pairs.size()) { + continue; + } + boolean allFound = true; + for (ExpandedPair p : r.getPairs()) { + boolean found = false; + for (ExpandedPair pp : pairs) { + if (p.equals(pp)) { + found = true; + break; + } + } + if (!found) { + allFound = false; + break; + } + } + if (allFound) { + // 'pairs' contains all the pairs from the row 'r' + iterator.remove(); + } + } + } + + // Returns true when one of the rows already contains all the pairs + private static boolean isPartialRow(Iterable pairs, Iterable rows) { + for (ExpandedRow r : rows) { + boolean allFound = true; + for (ExpandedPair p : pairs) { + boolean found = false; + for (ExpandedPair pp : r.getPairs()) { + if (p.equals(pp)) { + found = true; + break; + } + } + if (!found) { + allFound = false; + break; + } + } + if (allFound) { + // the row 'r' contain all the pairs from 'pairs' + return true; + } + } + return false; + } + + // Only used for unit testing + List getRows() { + return this.rows; + } + + // Not private for unit testing + static Result constructResult(List pairs) throws NotFoundException, FormatException { + BitArray binary = BitArrayBuilder.buildBitArray(pairs); + + AbstractExpandedDecoder decoder = AbstractExpandedDecoder.createDecoder(binary); + String resultingString = decoder.parseInformation(); + + ResultPoint[] firstPoints = pairs.get(0).getFinderPattern().getResultPoints(); + ResultPoint[] lastPoints = pairs.get(pairs.size() - 1).getFinderPattern().getResultPoints(); + + return new Result( + resultingString, + null, + new ResultPoint[]{firstPoints[0], firstPoints[1], lastPoints[0], lastPoints[1]}, + BarcodeFormat.RSS_EXPANDED + ); + } + + private boolean checkChecksum() { + ExpandedPair firstPair = this.pairs.get(0); + DataCharacter checkCharacter = firstPair.getLeftChar(); + DataCharacter firstCharacter = firstPair.getRightChar(); + + if (firstCharacter == null) { + return false; + } + + int checksum = firstCharacter.getChecksumPortion(); + int s = 2; + + for (int i = 1; i < this.pairs.size(); ++i) { + ExpandedPair currentPair = this.pairs.get(i); + checksum += currentPair.getLeftChar().getChecksumPortion(); + s++; + DataCharacter currentRightChar = currentPair.getRightChar(); + if (currentRightChar != null) { + checksum += currentRightChar.getChecksumPortion(); + s++; + } + } + + checksum %= 211; + + int checkCharacterValue = 211 * (s - 4) + checksum; + + return checkCharacterValue == checkCharacter.getValue(); + } + + private static int getNextSecondBar(BitArray row, int initialPos) { + int currentPos; + if (row.get(initialPos)) { + currentPos = row.getNextUnset(initialPos); + currentPos = row.getNextSet(currentPos); + } else { + currentPos = row.getNextSet(initialPos); + currentPos = row.getNextUnset(currentPos); + } + return currentPos; + } + + // not private for testing + ExpandedPair retrieveNextPair(BitArray row, List previousPairs, int rowNumber) + throws NotFoundException { + boolean isOddPattern = previousPairs.size() % 2 == 0; + if (startFromEven) { + isOddPattern = !isOddPattern; + } + + FinderPattern pattern; + + boolean keepFinding = true; + int forcedOffset = -1; + do { + this.findNextPair(row, previousPairs, forcedOffset); + pattern = parseFoundFinderPattern(row, rowNumber, isOddPattern); + if (pattern == null) { + forcedOffset = getNextSecondBar(row, this.startEnd[0]); + } else { + keepFinding = false; + } + } while (keepFinding); + + // When stacked symbol is split over multiple rows, there's no way to guess if this pair can be last or not. + // boolean mayBeLast = checkPairSequence(previousPairs, pattern); + + DataCharacter leftChar = this.decodeDataCharacter(row, pattern, isOddPattern, true); + + if (!previousPairs.isEmpty() && previousPairs.get(previousPairs.size() - 1).mustBeLast()) { + throw NotFoundException.getNotFoundInstance(); + } + + DataCharacter rightChar; + try { + rightChar = this.decodeDataCharacter(row, pattern, isOddPattern, false); + } catch (NotFoundException ignored) { + rightChar = null; + } + return new ExpandedPair(leftChar, rightChar, pattern, true); + } + + private void findNextPair(BitArray row, List previousPairs, int forcedOffset) + throws NotFoundException { + int[] counters = this.getDecodeFinderCounters(); + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + + int width = row.getSize(); + + int rowOffset; + if (forcedOffset >= 0) { + rowOffset = forcedOffset; + } else if (previousPairs.isEmpty()) { + rowOffset = 0; + } else { + ExpandedPair lastPair = previousPairs.get(previousPairs.size() - 1); + rowOffset = lastPair.getFinderPattern().getStartEnd()[1]; + } + boolean searchingEvenPair = previousPairs.size() % 2 != 0; + if (startFromEven) { + searchingEvenPair = !searchingEvenPair; + } + + boolean isWhite = false; + while (rowOffset < width) { + isWhite = !row.get(rowOffset); + if (!isWhite) { + break; + } + rowOffset++; + } + + int counterPosition = 0; + int patternStart = rowOffset; + for (int x = rowOffset; x < width; x++) { + if (row.get(x) != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == 3) { + if (searchingEvenPair) { + reverseCounters(counters); + } + + if (isFinderPattern(counters)) { + this.startEnd[0] = patternStart; + this.startEnd[1] = x; + return; + } + + if (searchingEvenPair) { + reverseCounters(counters); + } + + patternStart += counters[0] + counters[1]; + counters[0] = counters[2]; + counters[1] = counters[3]; + counters[2] = 0; + counters[3] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + throw NotFoundException.getNotFoundInstance(); + } + + private static void reverseCounters(int [] counters) { + int length = counters.length; + for (int i = 0; i < length / 2; ++i) { + int tmp = counters[i]; + counters[i] = counters[length - i - 1]; + counters[length - i - 1] = tmp; + } + } + + private FinderPattern parseFoundFinderPattern(BitArray row, int rowNumber, boolean oddPattern) { + // Actually we found elements 2-5. + int firstCounter; + int start; + int end; + + if (oddPattern) { + // If pattern number is odd, we need to locate element 1 *before* the current block. + + int firstElementStart = this.startEnd[0] - 1; + // Locate element 1 + while (firstElementStart >= 0 && !row.get(firstElementStart)) { + firstElementStart--; + } + + firstElementStart++; + firstCounter = this.startEnd[0] - firstElementStart; + start = firstElementStart; + end = this.startEnd[1]; + + } else { + // If pattern number is even, the pattern is reversed, so we need to locate element 1 *after* the current block. + + start = this.startEnd[0]; + + end = row.getNextUnset(this.startEnd[1] + 1); + firstCounter = end - this.startEnd[1]; + } + + // Make 'counters' hold 1-4 + int [] counters = this.getDecodeFinderCounters(); + System.arraycopy(counters, 0, counters, 1, counters.length - 1); + + counters[0] = firstCounter; + int value; + try { + value = parseFinderValue(counters, FINDER_PATTERNS); + } catch (NotFoundException ignored) { + return null; + } + return new FinderPattern(value, new int[] {start, end}, start, end, rowNumber); + } + + DataCharacter decodeDataCharacter(BitArray row, + FinderPattern pattern, + boolean isOddPattern, + boolean leftChar) throws NotFoundException { + int[] counters = this.getDataCharacterCounters(); + counters[0] = 0; + counters[1] = 0; + counters[2] = 0; + counters[3] = 0; + counters[4] = 0; + counters[5] = 0; + counters[6] = 0; + counters[7] = 0; + + if (leftChar) { + recordPatternInReverse(row, pattern.getStartEnd()[0], counters); + } else { + recordPattern(row, pattern.getStartEnd()[1], counters); + // reverse it + for (int i = 0, j = counters.length - 1; i < j; i++, j--) { + int temp = counters[i]; + counters[i] = counters[j]; + counters[j] = temp; + } + } //counters[] has the pixels of the module + + int numModules = 17; //left and right data characters have all the same length + float elementWidth = MathUtils.sum(counters) / (float) numModules; + + // Sanity check: element width for pattern and the character should match + float expectedElementWidth = (pattern.getStartEnd()[1] - pattern.getStartEnd()[0]) / 15.0f; + if (Math.abs(elementWidth - expectedElementWidth) / expectedElementWidth > 0.3f) { + throw NotFoundException.getNotFoundInstance(); + } + + int[] oddCounts = this.getOddCounts(); + int[] evenCounts = this.getEvenCounts(); + float[] oddRoundingErrors = this.getOddRoundingErrors(); + float[] evenRoundingErrors = this.getEvenRoundingErrors(); + + for (int i = 0; i < counters.length; i++) { + float value = 1.0f * counters[i] / elementWidth; + int count = (int) (value + 0.5f); // Round + if (count < 1) { + if (value < 0.3f) { + throw NotFoundException.getNotFoundInstance(); + } + count = 1; + } else if (count > 8) { + if (value > 8.7f) { + throw NotFoundException.getNotFoundInstance(); + } + count = 8; + } + int offset = i / 2; + if ((i & 0x01) == 0) { + oddCounts[offset] = count; + oddRoundingErrors[offset] = value - count; + } else { + evenCounts[offset] = count; + evenRoundingErrors[offset] = value - count; + } + } + + adjustOddEvenCounts(numModules); + + int weightRowNumber = 4 * pattern.getValue() + (isOddPattern ? 0 : 2) + (leftChar ? 0 : 1) - 1; + + int oddSum = 0; + int oddChecksumPortion = 0; + for (int i = oddCounts.length - 1; i >= 0; i--) { + if (isNotA1left(pattern, isOddPattern, leftChar)) { + int weight = WEIGHTS[weightRowNumber][2 * i]; + oddChecksumPortion += oddCounts[i] * weight; + } + oddSum += oddCounts[i]; + } + int evenChecksumPortion = 0; + //int evenSum = 0; + for (int i = evenCounts.length - 1; i >= 0; i--) { + if (isNotA1left(pattern, isOddPattern, leftChar)) { + int weight = WEIGHTS[weightRowNumber][2 * i + 1]; + evenChecksumPortion += evenCounts[i] * weight; + } + //evenSum += evenCounts[i]; + } + int checksumPortion = oddChecksumPortion + evenChecksumPortion; + + if ((oddSum & 0x01) != 0 || oddSum > 13 || oddSum < 4) { + throw NotFoundException.getNotFoundInstance(); + } + + int group = (13 - oddSum) / 2; + int oddWidest = SYMBOL_WIDEST[group]; + int evenWidest = 9 - oddWidest; + int vOdd = RSSUtils.getRSSvalue(oddCounts, oddWidest, true); + int vEven = RSSUtils.getRSSvalue(evenCounts, evenWidest, false); + int tEven = EVEN_TOTAL_SUBSET[group]; + int gSum = GSUM[group]; + int value = vOdd * tEven + vEven + gSum; + + return new DataCharacter(value, checksumPortion); + } + + private static boolean isNotA1left(FinderPattern pattern, boolean isOddPattern, boolean leftChar) { + // A1: pattern.getValue is 0 (A), and it's an oddPattern, and it is a left char + return !(pattern.getValue() == 0 && isOddPattern && leftChar); + } + + private void adjustOddEvenCounts(int numModules) throws NotFoundException { + + int oddSum = MathUtils.sum(this.getOddCounts()); + int evenSum = MathUtils.sum(this.getEvenCounts()); + + boolean incrementOdd = false; + boolean decrementOdd = false; + + if (oddSum > 13) { + decrementOdd = true; + } else if (oddSum < 4) { + incrementOdd = true; + } + boolean incrementEven = false; + boolean decrementEven = false; + if (evenSum > 13) { + decrementEven = true; + } else if (evenSum < 4) { + incrementEven = true; + } + + int mismatch = oddSum + evenSum - numModules; + boolean oddParityBad = (oddSum & 0x01) == 1; + boolean evenParityBad = (evenSum & 0x01) == 0; + if (mismatch == 1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + decrementEven = true; + } + } else if (mismatch == -1) { + if (oddParityBad) { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementOdd = true; + } else { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + incrementEven = true; + } + } else if (mismatch == 0) { + if (oddParityBad) { + if (!evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Both bad + if (oddSum < evenSum) { + incrementOdd = true; + decrementEven = true; + } else { + decrementOdd = true; + incrementEven = true; + } + } else { + if (evenParityBad) { + throw NotFoundException.getNotFoundInstance(); + } + // Nothing to do! + } + } else { + throw NotFoundException.getNotFoundInstance(); + } + + if (incrementOdd) { + if (decrementOdd) { + throw NotFoundException.getNotFoundInstance(); + } + increment(this.getOddCounts(), this.getOddRoundingErrors()); + } + if (decrementOdd) { + decrement(this.getOddCounts(), this.getOddRoundingErrors()); + } + if (incrementEven) { + if (decrementEven) { + throw NotFoundException.getNotFoundInstance(); + } + increment(this.getEvenCounts(), this.getOddRoundingErrors()); + } + if (decrementEven) { + decrement(this.getEvenCounts(), this.getEvenRoundingErrors()); + } + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java new file mode 100644 index 0000000..759cf96 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013103decoder.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI013103decoder extends AI013x0xDecoder { + + AI013103decoder(BitArray information) { + super(information); + } + + @Override + protected void addWeightCode(StringBuilder buf, int weight) { + buf.append("(3103)"); + } + + @Override + protected int checkWeight(int weight) { + return weight; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java new file mode 100644 index 0000000..eb50f7a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01320xDecoder.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01320xDecoder extends AI013x0xDecoder { + + AI01320xDecoder(BitArray information) { + super(information); + } + + @Override + protected void addWeightCode(StringBuilder buf, int weight) { + if (weight < 10000) { + buf.append("(3202)"); + } else { + buf.append("(3203)"); + } + } + + @Override + protected int checkWeight(int weight) { + if (weight < 10000) { + return weight; + } + return weight - 10000; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java new file mode 100644 index 0000000..9ea5bba --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01392xDecoder.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01392xDecoder extends AI01decoder { + + private static final int HEADER_SIZE = 5 + 1 + 2; + private static final int LAST_DIGIT_SIZE = 2; + + AI01392xDecoder(BitArray information) { + super(information); + } + + @Override + public String parseInformation() throws NotFoundException, FormatException { + if (this.getInformation().getSize() < HEADER_SIZE + GTIN_SIZE) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuilder buf = new StringBuilder(); + + encodeCompressedGtin(buf, HEADER_SIZE); + + int lastAIdigit = + this.getGeneralDecoder().extractNumericValueFromBitArray(HEADER_SIZE + GTIN_SIZE, LAST_DIGIT_SIZE); + buf.append("(392"); + buf.append(lastAIdigit); + buf.append(')'); + + DecodedInformation decodedInformation = + this.getGeneralDecoder().decodeGeneralPurposeField(HEADER_SIZE + GTIN_SIZE + LAST_DIGIT_SIZE, null); + buf.append(decodedInformation.getNewString()); + + return buf.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java new file mode 100644 index 0000000..9c7ad5c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01393xDecoder.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class AI01393xDecoder extends AI01decoder { + + private static final int HEADER_SIZE = 5 + 1 + 2; + private static final int LAST_DIGIT_SIZE = 2; + private static final int FIRST_THREE_DIGITS_SIZE = 10; + + AI01393xDecoder(BitArray information) { + super(information); + } + + @Override + public String parseInformation() throws NotFoundException, FormatException { + if (this.getInformation().getSize() < HEADER_SIZE + GTIN_SIZE) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuilder buf = new StringBuilder(); + + encodeCompressedGtin(buf, HEADER_SIZE); + + int lastAIdigit = + this.getGeneralDecoder().extractNumericValueFromBitArray(HEADER_SIZE + GTIN_SIZE, LAST_DIGIT_SIZE); + + buf.append("(393"); + buf.append(lastAIdigit); + buf.append(')'); + + int firstThreeDigits = + this.getGeneralDecoder().extractNumericValueFromBitArray(HEADER_SIZE + GTIN_SIZE + LAST_DIGIT_SIZE, FIRST_THREE_DIGITS_SIZE); + if (firstThreeDigits / 100 == 0) { + buf.append('0'); + } + if (firstThreeDigits / 10 == 0) { + buf.append('0'); + } + buf.append(firstThreeDigits); + + DecodedInformation generalInformation = + this.getGeneralDecoder().decodeGeneralPurposeField(HEADER_SIZE + GTIN_SIZE + LAST_DIGIT_SIZE + FIRST_THREE_DIGITS_SIZE, null); + buf.append(generalInformation.getNewString()); + + return buf.toString(); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java new file mode 100644 index 0000000..b8e406a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0x1xDecoder.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AI013x0x1xDecoder extends AI01weightDecoder { + + private static final int HEADER_SIZE = 7 + 1; + private static final int WEIGHT_SIZE = 20; + private static final int DATE_SIZE = 16; + + private final String dateCode; + private final String firstAIdigits; + + AI013x0x1xDecoder(BitArray information, String firstAIdigits, String dateCode) { + super(information); + this.dateCode = dateCode; + this.firstAIdigits = firstAIdigits; + } + + @Override + public String parseInformation() throws NotFoundException { + if (this.getInformation().getSize() != HEADER_SIZE + GTIN_SIZE + WEIGHT_SIZE + DATE_SIZE) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuilder buf = new StringBuilder(); + + encodeCompressedGtin(buf, HEADER_SIZE); + encodeCompressedWeight(buf, HEADER_SIZE + GTIN_SIZE, WEIGHT_SIZE); + encodeCompressedDate(buf, HEADER_SIZE + GTIN_SIZE + WEIGHT_SIZE); + + return buf.toString(); + } + + private void encodeCompressedDate(StringBuilder buf, int currentPos) { + int numericDate = this.getGeneralDecoder().extractNumericValueFromBitArray(currentPos, DATE_SIZE); + if (numericDate == 38400) { + return; + } + + buf.append('('); + buf.append(this.dateCode); + buf.append(')'); + + int day = numericDate % 32; + numericDate /= 32; + int month = numericDate % 12 + 1; + numericDate /= 12; + int year = numericDate; + + if (year / 10 == 0) { + buf.append('0'); + } + buf.append(year); + if (month / 10 == 0) { + buf.append('0'); + } + buf.append(month); + if (day / 10 == 0) { + buf.append('0'); + } + buf.append(day); + } + + @Override + protected void addWeightCode(StringBuilder buf, int weight) { + buf.append('('); + buf.append(this.firstAIdigits); + buf.append(weight / 100000); + buf.append(')'); + } + + @Override + protected int checkWeight(int weight) { + return weight % 100000; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java new file mode 100644 index 0000000..54d32d6 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI013x0xDecoder.java @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class AI013x0xDecoder extends AI01weightDecoder { + + private static final int HEADER_SIZE = 4 + 1; + private static final int WEIGHT_SIZE = 15; + + AI013x0xDecoder(BitArray information) { + super(information); + } + + @Override + public String parseInformation() throws NotFoundException { + if (this.getInformation().getSize() != HEADER_SIZE + GTIN_SIZE + WEIGHT_SIZE) { + throw NotFoundException.getNotFoundInstance(); + } + + StringBuilder buf = new StringBuilder(); + + encodeCompressedGtin(buf, HEADER_SIZE); + encodeCompressedWeight(buf, HEADER_SIZE + GTIN_SIZE, WEIGHT_SIZE); + + return buf.toString(); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java new file mode 100644 index 0000000..27222a3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01AndOtherAIs.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AI01AndOtherAIs extends AI01decoder { + + private static final int HEADER_SIZE = 1 + 1 + 2; //first bit encodes the linkage flag, + //the second one is the encodation method, and the other two are for the variable length + AI01AndOtherAIs(BitArray information) { + super(information); + } + + @Override + public String parseInformation() throws NotFoundException, FormatException { + StringBuilder buff = new StringBuilder(); + + buff.append("(01)"); + int initialGtinPosition = buff.length(); + int firstGtinDigit = this.getGeneralDecoder().extractNumericValueFromBitArray(HEADER_SIZE, 4); + buff.append(firstGtinDigit); + + this.encodeCompressedGtinWithoutAI(buff, HEADER_SIZE + 4, initialGtinPosition); + + return this.getGeneralDecoder().decodeAllCodes(buff, HEADER_SIZE + 44); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java new file mode 100644 index 0000000..795015c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01decoder.java @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +abstract class AI01decoder extends AbstractExpandedDecoder { + + static final int GTIN_SIZE = 40; + + AI01decoder(BitArray information) { + super(information); + } + + final void encodeCompressedGtin(StringBuilder buf, int currentPos) { + buf.append("(01)"); + int initialPosition = buf.length(); + buf.append('9'); + + encodeCompressedGtinWithoutAI(buf, currentPos, initialPosition); + } + + final void encodeCompressedGtinWithoutAI(StringBuilder buf, int currentPos, int initialBufferPosition) { + for (int i = 0; i < 4; ++i) { + int currentBlock = this.getGeneralDecoder().extractNumericValueFromBitArray(currentPos + 10 * i, 10); + if (currentBlock / 100 == 0) { + buf.append('0'); + } + if (currentBlock / 10 == 0) { + buf.append('0'); + } + buf.append(currentBlock); + } + + appendCheckDigit(buf, initialBufferPosition); + } + + private static void appendCheckDigit(StringBuilder buf, int currentPos) { + int checkDigit = 0; + for (int i = 0; i < 13; i++) { + int digit = buf.charAt(i + currentPos) - '0'; + checkDigit += (i & 0x01) == 0 ? 3 * digit : digit; + } + + checkDigit = 10 - (checkDigit % 10); + if (checkDigit == 10) { + checkDigit = 0; + } + + buf.append(checkDigit); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java new file mode 100644 index 0000000..96f9464 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AI01weightDecoder.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class AI01weightDecoder extends AI01decoder { + + AI01weightDecoder(BitArray information) { + super(information); + } + + final void encodeCompressedWeight(StringBuilder buf, int currentPos, int weightSize) { + int originalWeightNumeric = this.getGeneralDecoder().extractNumericValueFromBitArray(currentPos, weightSize); + addWeightCode(buf, originalWeightNumeric); + + int weightNumeric = checkWeight(originalWeightNumeric); + + int currentDivisor = 100000; + for (int i = 0; i < 5; ++i) { + if (weightNumeric / currentDivisor == 0) { + buf.append('0'); + } + currentDivisor /= 10; + } + buf.append(weightNumeric); + } + + protected abstract void addWeightCode(StringBuilder buf, int weight); + + protected abstract int checkWeight(int weight); + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java new file mode 100644 index 0000000..771b42b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AbstractExpandedDecoder.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +public abstract class AbstractExpandedDecoder { + + private final BitArray information; + private final GeneralAppIdDecoder generalDecoder; + + AbstractExpandedDecoder(BitArray information) { + this.information = information; + this.generalDecoder = new GeneralAppIdDecoder(information); + } + + protected final BitArray getInformation() { + return information; + } + + protected final GeneralAppIdDecoder getGeneralDecoder() { + return generalDecoder; + } + + public abstract String parseInformation() throws NotFoundException, FormatException; + + public static AbstractExpandedDecoder createDecoder(BitArray information) { + if (information.get(1)) { + return new AI01AndOtherAIs(information); + } + if (!information.get(2)) { + return new AnyAIDecoder(information); + } + + int fourBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 4); + + switch (fourBitEncodationMethod) { + case 4: return new AI013103decoder(information); + case 5: return new AI01320xDecoder(information); + } + + int fiveBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 5); + switch (fiveBitEncodationMethod) { + case 12: return new AI01392xDecoder(information); + case 13: return new AI01393xDecoder(information); + } + + int sevenBitEncodationMethod = GeneralAppIdDecoder.extractNumericValueFromBitArray(information, 1, 7); + switch (sevenBitEncodationMethod) { + case 56: return new AI013x0x1xDecoder(information, "310", "11"); + case 57: return new AI013x0x1xDecoder(information, "320", "11"); + case 58: return new AI013x0x1xDecoder(information, "310", "13"); + case 59: return new AI013x0x1xDecoder(information, "320", "13"); + case 60: return new AI013x0x1xDecoder(information, "310", "15"); + case 61: return new AI013x0x1xDecoder(information, "320", "15"); + case 62: return new AI013x0x1xDecoder(information, "310", "17"); + case 63: return new AI013x0x1xDecoder(information, "320", "17"); + } + + throw new IllegalStateException("unknown decoder: " + information); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java new file mode 100644 index 0000000..2074d8f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/AnyAIDecoder.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class AnyAIDecoder extends AbstractExpandedDecoder { + + private static final int HEADER_SIZE = 2 + 1 + 2; + + AnyAIDecoder(BitArray information) { + super(information); + } + + @Override + public String parseInformation() throws NotFoundException, FormatException { + StringBuilder buf = new StringBuilder(); + return this.getGeneralDecoder().decodeAllCodes(buf, HEADER_SIZE); + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java new file mode 100644 index 0000000..a959355 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/BlockParsedResult.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class BlockParsedResult { + + private final DecodedInformation decodedInformation; + private final boolean finished; + + BlockParsedResult(boolean finished) { + this(null, finished); + } + + BlockParsedResult(DecodedInformation information, boolean finished) { + this.finished = finished; + this.decodedInformation = information; + } + + DecodedInformation getDecodedInformation() { + return this.decodedInformation; + } + + boolean isFinished() { + return this.finished; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java new file mode 100644 index 0000000..34e92e1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/CurrentParsingState.java @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +final class CurrentParsingState { + + private int position; + private State encoding; + + private enum State { + NUMERIC, + ALPHA, + ISO_IEC_646 + } + + CurrentParsingState() { + this.position = 0; + this.encoding = State.NUMERIC; + } + + int getPosition() { + return position; + } + + void setPosition(int position) { + this.position = position; + } + + void incrementPosition(int delta) { + position += delta; + } + + boolean isAlpha() { + return this.encoding == State.ALPHA; + } + + boolean isNumeric() { + return this.encoding == State.NUMERIC; + } + + boolean isIsoIec646() { + return this.encoding == State.ISO_IEC_646; + } + + void setNumeric() { + this.encoding = State.NUMERIC; + } + + void setAlpha() { + this.encoding = State.ALPHA; + } + + void setIsoIec646() { + this.encoding = State.ISO_IEC_646; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java new file mode 100644 index 0000000..2dd8995 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedChar.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedChar extends DecodedObject { + + private final char value; + + static final char FNC1 = '$'; // It's not in Alphanumeric neither in ISO/IEC 646 charset + + DecodedChar(int newPosition, char value) { + super(newPosition); + this.value = value; + } + + char getValue() { + return this.value; + } + + boolean isFNC1() { + return this.value == FNC1; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java new file mode 100644 index 0000000..0ed90fd --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedInformation.java @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedInformation extends DecodedObject { + + private final String newString; + private final int remainingValue; + private final boolean remaining; + + DecodedInformation(int newPosition, String newString) { + super(newPosition); + this.newString = newString; + this.remaining = false; + this.remainingValue = 0; + } + + DecodedInformation(int newPosition, String newString, int remainingValue) { + super(newPosition); + this.remaining = true; + this.remainingValue = remainingValue; + this.newString = newString; + } + + String getNewString() { + return this.newString; + } + + boolean isRemaining() { + return this.remaining; + } + + int getRemainingValue() { + return this.remainingValue; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java new file mode 100644 index 0000000..df38d94 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedNumeric.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class DecodedNumeric extends DecodedObject { + + private final int firstDigit; + private final int secondDigit; + + static final int FNC1 = 10; + + DecodedNumeric(int newPosition, int firstDigit, int secondDigit) throws FormatException { + super(newPosition); + + if (firstDigit < 0 || firstDigit > 10 || secondDigit < 0 || secondDigit > 10) { + throw FormatException.getFormatInstance(); + } + + this.firstDigit = firstDigit; + this.secondDigit = secondDigit; + } + + int getFirstDigit() { + return this.firstDigit; + } + + int getSecondDigit() { + return this.secondDigit; + } + + int getValue() { + return this.firstDigit * 10 + this.secondDigit; + } + + boolean isFirstDigitFNC1() { + return this.firstDigit == FNC1; + } + + boolean isSecondDigitFNC1() { + return this.secondDigit == FNC1; + } + + boolean isAnyFNC1() { + return this.firstDigit == FNC1 || this.secondDigit == FNC1; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java new file mode 100644 index 0000000..cf7d7da --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/DecodedObject.java @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + */ +abstract class DecodedObject { + + private final int newPosition; + + DecodedObject(int newPosition) { + this.newPosition = newPosition; + } + + final int getNewPosition() { + return this.newPosition; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java new file mode 100644 index 0000000..b62202d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/FieldParser.java @@ -0,0 +1,291 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.NotFoundException; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class FieldParser { + + private static final Object VARIABLE_LENGTH = new Object(); + + private static final Object [][] TWO_DIGIT_DATA_LENGTH = { + // "DIGITS", new Integer(LENGTH) + // or + // "DIGITS", VARIABLE_LENGTH, new Integer(MAX_SIZE) + + { "00", 18}, + { "01", 14}, + { "02", 14}, + + { "10", VARIABLE_LENGTH, 20}, + { "11", 6}, + { "12", 6}, + { "13", 6}, + { "15", 6}, + { "17", 6}, + + { "20", 2}, + { "21", VARIABLE_LENGTH, 20}, + { "22", VARIABLE_LENGTH, 29}, + + { "30", VARIABLE_LENGTH, 8}, + { "37", VARIABLE_LENGTH, 8}, + + //internal company codes + { "90", VARIABLE_LENGTH, 30}, + { "91", VARIABLE_LENGTH, 30}, + { "92", VARIABLE_LENGTH, 30}, + { "93", VARIABLE_LENGTH, 30}, + { "94", VARIABLE_LENGTH, 30}, + { "95", VARIABLE_LENGTH, 30}, + { "96", VARIABLE_LENGTH, 30}, + { "97", VARIABLE_LENGTH, 30}, + { "98", VARIABLE_LENGTH, 30}, + { "99", VARIABLE_LENGTH, 30}, + }; + + private static final Object [][] THREE_DIGIT_DATA_LENGTH = { + // Same format as above + + { "240", VARIABLE_LENGTH, 30}, + { "241", VARIABLE_LENGTH, 30}, + { "242", VARIABLE_LENGTH, 6}, + { "250", VARIABLE_LENGTH, 30}, + { "251", VARIABLE_LENGTH, 30}, + { "253", VARIABLE_LENGTH, 17}, + { "254", VARIABLE_LENGTH, 20}, + + { "400", VARIABLE_LENGTH, 30}, + { "401", VARIABLE_LENGTH, 30}, + { "402", 17}, + { "403", VARIABLE_LENGTH, 30}, + { "410", 13}, + { "411", 13}, + { "412", 13}, + { "413", 13}, + { "414", 13}, + { "420", VARIABLE_LENGTH, 20}, + { "421", VARIABLE_LENGTH, 15}, + { "422", 3}, + { "423", VARIABLE_LENGTH, 15}, + { "424", 3}, + { "425", 3}, + { "426", 3}, + }; + + private static final Object [][] THREE_DIGIT_PLUS_DIGIT_DATA_LENGTH = { + // Same format as above + + { "310", 6}, + { "311", 6}, + { "312", 6}, + { "313", 6}, + { "314", 6}, + { "315", 6}, + { "316", 6}, + { "320", 6}, + { "321", 6}, + { "322", 6}, + { "323", 6}, + { "324", 6}, + { "325", 6}, + { "326", 6}, + { "327", 6}, + { "328", 6}, + { "329", 6}, + { "330", 6}, + { "331", 6}, + { "332", 6}, + { "333", 6}, + { "334", 6}, + { "335", 6}, + { "336", 6}, + { "340", 6}, + { "341", 6}, + { "342", 6}, + { "343", 6}, + { "344", 6}, + { "345", 6}, + { "346", 6}, + { "347", 6}, + { "348", 6}, + { "349", 6}, + { "350", 6}, + { "351", 6}, + { "352", 6}, + { "353", 6}, + { "354", 6}, + { "355", 6}, + { "356", 6}, + { "357", 6}, + { "360", 6}, + { "361", 6}, + { "362", 6}, + { "363", 6}, + { "364", 6}, + { "365", 6}, + { "366", 6}, + { "367", 6}, + { "368", 6}, + { "369", 6}, + { "390", VARIABLE_LENGTH, 15}, + { "391", VARIABLE_LENGTH, 18}, + { "392", VARIABLE_LENGTH, 15}, + { "393", VARIABLE_LENGTH, 18}, + { "703", VARIABLE_LENGTH, 30}, + }; + + private static final Object [][] FOUR_DIGIT_DATA_LENGTH = { + // Same format as above + + { "7001", 13}, + { "7002", VARIABLE_LENGTH, 30}, + { "7003", 10}, + + { "8001", 14}, + { "8002", VARIABLE_LENGTH, 20}, + { "8003", VARIABLE_LENGTH, 30}, + { "8004", VARIABLE_LENGTH, 30}, + { "8005", 6}, + { "8006", 18}, + { "8007", VARIABLE_LENGTH, 30}, + { "8008", VARIABLE_LENGTH, 12}, + { "8018", 18}, + { "8020", VARIABLE_LENGTH, 25}, + { "8100", 6}, + { "8101", 10}, + { "8102", 2}, + { "8110", VARIABLE_LENGTH, 70}, + { "8200", VARIABLE_LENGTH, 70}, + }; + + private FieldParser() { + } + + static String parseFieldsInGeneralPurpose(String rawInformation) throws NotFoundException { + if (rawInformation.isEmpty()) { + return null; + } + + // Processing 2-digit AIs + + if (rawInformation.length() < 2) { + throw NotFoundException.getNotFoundInstance(); + } + + String firstTwoDigits = rawInformation.substring(0, 2); + + for (Object[] dataLength : TWO_DIGIT_DATA_LENGTH) { + if (dataLength[0].equals(firstTwoDigits)) { + if (dataLength[1] == VARIABLE_LENGTH) { + return processVariableAI(2, (Integer) dataLength[2], rawInformation); + } + return processFixedAI(2, (Integer) dataLength[1], rawInformation); + } + } + + if (rawInformation.length() < 3) { + throw NotFoundException.getNotFoundInstance(); + } + + String firstThreeDigits = rawInformation.substring(0, 3); + + for (Object[] dataLength : THREE_DIGIT_DATA_LENGTH) { + if (dataLength[0].equals(firstThreeDigits)) { + if (dataLength[1] == VARIABLE_LENGTH) { + return processVariableAI(3, (Integer) dataLength[2], rawInformation); + } + return processFixedAI(3, (Integer) dataLength[1], rawInformation); + } + } + + + for (Object[] dataLength : THREE_DIGIT_PLUS_DIGIT_DATA_LENGTH) { + if (dataLength[0].equals(firstThreeDigits)) { + if (dataLength[1] == VARIABLE_LENGTH) { + return processVariableAI(4, (Integer) dataLength[2], rawInformation); + } + return processFixedAI(4, (Integer) dataLength[1], rawInformation); + } + } + + if (rawInformation.length() < 4) { + throw NotFoundException.getNotFoundInstance(); + } + + String firstFourDigits = rawInformation.substring(0, 4); + + for (Object[] dataLength : FOUR_DIGIT_DATA_LENGTH) { + if (dataLength[0].equals(firstFourDigits)) { + if (dataLength[1] == VARIABLE_LENGTH) { + return processVariableAI(4, (Integer) dataLength[2], rawInformation); + } + return processFixedAI(4, (Integer) dataLength[1], rawInformation); + } + } + + throw NotFoundException.getNotFoundInstance(); + } + + private static String processFixedAI(int aiSize, int fieldSize, String rawInformation) throws NotFoundException { + if (rawInformation.length() < aiSize) { + throw NotFoundException.getNotFoundInstance(); + } + + String ai = rawInformation.substring(0, aiSize); + + if (rawInformation.length() < aiSize + fieldSize) { + throw NotFoundException.getNotFoundInstance(); + } + + String field = rawInformation.substring(aiSize, aiSize + fieldSize); + String remaining = rawInformation.substring(aiSize + fieldSize); + String result = '(' + ai + ')' + field; + String parsedAI = parseFieldsInGeneralPurpose(remaining); + return parsedAI == null ? result : result + parsedAI; + } + + private static String processVariableAI(int aiSize, int variableFieldSize, String rawInformation) + throws NotFoundException { + String ai = rawInformation.substring(0, aiSize); + int maxSize; + if (rawInformation.length() < aiSize + variableFieldSize) { + maxSize = rawInformation.length(); + } else { + maxSize = aiSize + variableFieldSize; + } + String field = rawInformation.substring(aiSize, maxSize); + String remaining = rawInformation.substring(maxSize); + String result = '(' + ai + ')' + field; + String parsedAI = parseFieldsInGeneralPurpose(remaining); + return parsedAI == null ? result : result + parsedAI; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/GeneralAppIdDecoder.java b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/GeneralAppIdDecoder.java new file mode 100644 index 0000000..f7a2457 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/oned/rss/expanded/decoders/GeneralAppIdDecoder.java @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2010 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * These authors would like to acknowledge the Spanish Ministry of Industry, + * Tourism and Trade, for the support in the project TSI020301-2008-2 + * "PIRAmIDE: Personalizable Interactions with Resources on AmI-enabled + * Mobile Dynamic Environments", led by Treelogic + * ( http://www.treelogic.com/ ): + * + * http://www.piramidepse.com/ + */ + +package com.google.zxing.oned.rss.expanded.decoders; + +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.common.BitArray; + +/** + * @author Pablo Orduña, University of Deusto (pablo.orduna@deusto.es) + * @author Eduardo Castillejo, University of Deusto (eduardo.castillejo@deusto.es) + */ +final class GeneralAppIdDecoder { + + private final BitArray information; + private final CurrentParsingState current = new CurrentParsingState(); + private final StringBuilder buffer = new StringBuilder(); + + GeneralAppIdDecoder(BitArray information) { + this.information = information; + } + + String decodeAllCodes(StringBuilder buff, int initialPosition) throws NotFoundException, FormatException { + int currentPosition = initialPosition; + String remaining = null; + do { + DecodedInformation info = this.decodeGeneralPurposeField(currentPosition, remaining); + String parsedFields = FieldParser.parseFieldsInGeneralPurpose(info.getNewString()); + if (parsedFields != null) { + buff.append(parsedFields); + } + if (info.isRemaining()) { + remaining = String.valueOf(info.getRemainingValue()); + } else { + remaining = null; + } + + if (currentPosition == info.getNewPosition()) { // No step forward! + break; + } + currentPosition = info.getNewPosition(); + } while (true); + + return buff.toString(); + } + + private boolean isStillNumeric(int pos) { + // It's numeric if it still has 7 positions + // and one of the first 4 bits is "1". + if (pos + 7 > this.information.getSize()) { + return pos + 4 <= this.information.getSize(); + } + + for (int i = pos; i < pos + 3; ++i) { + if (this.information.get(i)) { + return true; + } + } + + return this.information.get(pos + 3); + } + + private DecodedNumeric decodeNumeric(int pos) throws FormatException { + if (pos + 7 > this.information.getSize()) { + int numeric = extractNumericValueFromBitArray(pos, 4); + if (numeric == 0) { + return new DecodedNumeric(this.information.getSize(), DecodedNumeric.FNC1, DecodedNumeric.FNC1); + } + return new DecodedNumeric(this.information.getSize(), numeric - 1, DecodedNumeric.FNC1); + } + int numeric = extractNumericValueFromBitArray(pos, 7); + + int digit1 = (numeric - 8) / 11; + int digit2 = (numeric - 8) % 11; + + return new DecodedNumeric(pos + 7, digit1, digit2); + } + + int extractNumericValueFromBitArray(int pos, int bits) { + return extractNumericValueFromBitArray(this.information, pos, bits); + } + + static int extractNumericValueFromBitArray(BitArray information, int pos, int bits) { + int value = 0; + for (int i = 0; i < bits; ++i) { + if (information.get(pos + i)) { + value |= 1 << (bits - i - 1); + } + } + + return value; + } + + DecodedInformation decodeGeneralPurposeField(int pos, String remaining) throws FormatException { + this.buffer.setLength(0); + + if (remaining != null) { + this.buffer.append(remaining); + } + + this.current.setPosition(pos); + + DecodedInformation lastDecoded = parseBlocks(); + if (lastDecoded != null && lastDecoded.isRemaining()) { + return new DecodedInformation(this.current.getPosition(), this.buffer.toString(), lastDecoded.getRemainingValue()); + } + return new DecodedInformation(this.current.getPosition(), this.buffer.toString()); + } + + private DecodedInformation parseBlocks() throws FormatException { + boolean isFinished; + BlockParsedResult result; + do { + int initialPosition = current.getPosition(); + + if (current.isAlpha()) { + result = parseAlphaBlock(); + isFinished = result.isFinished(); + } else if (current.isIsoIec646()) { + result = parseIsoIec646Block(); + isFinished = result.isFinished(); + } else { // it must be numeric + result = parseNumericBlock(); + isFinished = result.isFinished(); + } + + boolean positionChanged = initialPosition != current.getPosition(); + if (!positionChanged && !isFinished) { + break; + } + } while (!isFinished); + + return result.getDecodedInformation(); + } + + private BlockParsedResult parseNumericBlock() throws FormatException { + while (isStillNumeric(current.getPosition())) { + DecodedNumeric numeric = decodeNumeric(current.getPosition()); + current.setPosition(numeric.getNewPosition()); + + if (numeric.isFirstDigitFNC1()) { + DecodedInformation information; + if (numeric.isSecondDigitFNC1()) { + information = new DecodedInformation(current.getPosition(), buffer.toString()); + } else { + information = new DecodedInformation(current.getPosition(), buffer.toString(), numeric.getSecondDigit()); + } + return new BlockParsedResult(information, true); + } + buffer.append(numeric.getFirstDigit()); + + if (numeric.isSecondDigitFNC1()) { + DecodedInformation information = new DecodedInformation(current.getPosition(), buffer.toString()); + return new BlockParsedResult(information, true); + } + buffer.append(numeric.getSecondDigit()); + } + + if (isNumericToAlphaNumericLatch(current.getPosition())) { + current.setAlpha(); + current.incrementPosition(4); + } + return new BlockParsedResult(false); + } + + private BlockParsedResult parseIsoIec646Block() throws FormatException { + while (isStillIsoIec646(current.getPosition())) { + DecodedChar iso = decodeIsoIec646(current.getPosition()); + current.setPosition(iso.getNewPosition()); + + if (iso.isFNC1()) { + DecodedInformation information = new DecodedInformation(current.getPosition(), buffer.toString()); + return new BlockParsedResult(information, true); + } + buffer.append(iso.getValue()); + } + + if (isAlphaOr646ToNumericLatch(current.getPosition())) { + current.incrementPosition(3); + current.setNumeric(); + } else if (isAlphaTo646ToAlphaLatch(current.getPosition())) { + if (current.getPosition() + 5 < this.information.getSize()) { + current.incrementPosition(5); + } else { + current.setPosition(this.information.getSize()); + } + + current.setAlpha(); + } + return new BlockParsedResult(false); + } + + private BlockParsedResult parseAlphaBlock() { + while (isStillAlpha(current.getPosition())) { + DecodedChar alpha = decodeAlphanumeric(current.getPosition()); + current.setPosition(alpha.getNewPosition()); + + if (alpha.isFNC1()) { + DecodedInformation information = new DecodedInformation(current.getPosition(), buffer.toString()); + return new BlockParsedResult(information, true); //end of the char block + } + + buffer.append(alpha.getValue()); + } + + if (isAlphaOr646ToNumericLatch(current.getPosition())) { + current.incrementPosition(3); + current.setNumeric(); + } else if (isAlphaTo646ToAlphaLatch(current.getPosition())) { + if (current.getPosition() + 5 < this.information.getSize()) { + current.incrementPosition(5); + } else { + current.setPosition(this.information.getSize()); + } + + current.setIsoIec646(); + } + return new BlockParsedResult(false); + } + + private boolean isStillIsoIec646(int pos) { + if (pos + 5 > this.information.getSize()) { + return false; + } + + int fiveBitValue = extractNumericValueFromBitArray(pos, 5); + if (fiveBitValue >= 5 && fiveBitValue < 16) { + return true; + } + + if (pos + 7 > this.information.getSize()) { + return false; + } + + int sevenBitValue = extractNumericValueFromBitArray(pos, 7); + if (sevenBitValue >= 64 && sevenBitValue < 116) { + return true; + } + + if (pos + 8 > this.information.getSize()) { + return false; + } + + int eightBitValue = extractNumericValueFromBitArray(pos, 8); + return eightBitValue >= 232 && eightBitValue < 253; + + } + + private DecodedChar decodeIsoIec646(int pos) throws FormatException { + int fiveBitValue = extractNumericValueFromBitArray(pos, 5); + if (fiveBitValue == 15) { + return new DecodedChar(pos + 5, DecodedChar.FNC1); + } + + if (fiveBitValue >= 5 && fiveBitValue < 15) { + return new DecodedChar(pos + 5, (char) ('0' + fiveBitValue - 5)); + } + + int sevenBitValue = extractNumericValueFromBitArray(pos, 7); + + if (sevenBitValue >= 64 && sevenBitValue < 90) { + return new DecodedChar(pos + 7, (char) (sevenBitValue + 1)); + } + + if (sevenBitValue >= 90 && sevenBitValue < 116) { + return new DecodedChar(pos + 7, (char) (sevenBitValue + 7)); + } + + int eightBitValue = extractNumericValueFromBitArray(pos, 8); + char c; + switch (eightBitValue) { + case 232: + c = '!'; + break; + case 233: + c = '"'; + break; + case 234: + c = '%'; + break; + case 235: + c = '&'; + break; + case 236: + c = '\''; + break; + case 237: + c = '('; + break; + case 238: + c = ')'; + break; + case 239: + c = '*'; + break; + case 240: + c = '+'; + break; + case 241: + c = ','; + break; + case 242: + c = '-'; + break; + case 243: + c = '.'; + break; + case 244: + c = '/'; + break; + case 245: + c = ':'; + break; + case 246: + c = ';'; + break; + case 247: + c = '<'; + break; + case 248: + c = '='; + break; + case 249: + c = '>'; + break; + case 250: + c = '?'; + break; + case 251: + c = '_'; + break; + case 252: + c = ' '; + break; + default: + throw FormatException.getFormatInstance(); + } + return new DecodedChar(pos + 8, c); + } + + private boolean isStillAlpha(int pos) { + if (pos + 5 > this.information.getSize()) { + return false; + } + + // We now check if it's a valid 5-bit value (0..9 and FNC1) + int fiveBitValue = extractNumericValueFromBitArray(pos, 5); + if (fiveBitValue >= 5 && fiveBitValue < 16) { + return true; + } + + if (pos + 6 > this.information.getSize()) { + return false; + } + + int sixBitValue = extractNumericValueFromBitArray(pos, 6); + return sixBitValue >= 16 && sixBitValue < 63; // 63 not included + } + + private DecodedChar decodeAlphanumeric(int pos) { + int fiveBitValue = extractNumericValueFromBitArray(pos, 5); + if (fiveBitValue == 15) { + return new DecodedChar(pos + 5, DecodedChar.FNC1); + } + + if (fiveBitValue >= 5 && fiveBitValue < 15) { + return new DecodedChar(pos + 5, (char) ('0' + fiveBitValue - 5)); + } + + int sixBitValue = extractNumericValueFromBitArray(pos, 6); + + if (sixBitValue >= 32 && sixBitValue < 58) { + return new DecodedChar(pos + 6, (char) (sixBitValue + 33)); + } + + char c; + switch (sixBitValue) { + case 58: + c = '*'; + break; + case 59: + c = ','; + break; + case 60: + c = '-'; + break; + case 61: + c = '.'; + break; + case 62: + c = '/'; + break; + default: + throw new IllegalStateException("Decoding invalid alphanumeric value: " + sixBitValue); + } + return new DecodedChar(pos + 6, c); + } + + private boolean isAlphaTo646ToAlphaLatch(int pos) { + if (pos + 1 > this.information.getSize()) { + return false; + } + + for (int i = 0; i < 5 && i + pos < this.information.getSize(); ++i) { + if (i == 2) { + if (!this.information.get(pos + 2)) { + return false; + } + } else if (this.information.get(pos + i)) { + return false; + } + } + + return true; + } + + private boolean isAlphaOr646ToNumericLatch(int pos) { + // Next is alphanumeric if there are 3 positions and they are all zeros + if (pos + 3 > this.information.getSize()) { + return false; + } + + for (int i = pos; i < pos + 3; ++i) { + if (this.information.get(i)) { + return false; + } + } + return true; + } + + private boolean isNumericToAlphaNumericLatch(int pos) { + // Next is alphanumeric if there are 4 positions and they are all zeros, or + // if there is a subset of this just before the end of the symbol + if (pos + 1 > this.information.getSize()) { + return false; + } + + for (int i = 0; i < 4 && i + pos < this.information.getSize(); ++i) { + if (this.information.get(pos + i)) { + return false; + } + } + return true; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Common.java b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Common.java new file mode 100644 index 0000000..5e7cafd --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Common.java @@ -0,0 +1,463 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.google.zxing.pdf417; + +import java.util.Arrays; +import java.util.Collection; + +import com.google.zxing.common.detector.MathUtils; + +/** + * @author SITA Lab (kevin.osullivan@sita.aero) + * @author Guenther Grau + */ +public final class PDF417Common { + + public static final int NUMBER_OF_CODEWORDS = 929; + // Maximum Codewords (Data + Error). + public static final int MAX_CODEWORDS_IN_BARCODE = NUMBER_OF_CODEWORDS - 1; + public static final int MIN_ROWS_IN_BARCODE = 3; + public static final int MAX_ROWS_IN_BARCODE = 90; + // One left row indication column + max 30 data columns + one right row indicator column + //public static final int MAX_CODEWORDS_IN_ROW = 32; + public static final int MODULES_IN_CODEWORD = 17; + public static final int MODULES_IN_STOP_PATTERN = 18; + public static final int BARS_IN_MODULE = 8; + + private static final int[] EMPTY_INT_ARRAY = {}; + + private PDF417Common() { + } + + /** + * @param moduleBitCount values to sum + * @return sum of values + * @deprecated call {@link MathUtils#sum(int[])} + */ + @Deprecated + public static int getBitCountSum(int[] moduleBitCount) { + return MathUtils.sum(moduleBitCount); + } + + public static int[] toIntArray(Collection list) { + if (list == null || list.isEmpty()) { + return EMPTY_INT_ARRAY; + } + int[] result = new int[list.size()]; + int i = 0; + for (Integer integer : list) { + result[i++] = integer; + } + return result; + } + + /** + * @param symbol encoded symbol to translate to a codeword + * @return the codeword corresponding to the symbol. + */ + public static int getCodeword(int symbol) { + int i = Arrays.binarySearch(SYMBOL_TABLE, symbol & 0x3FFFF); + if (i < 0) { + return -1; + } + return (CODEWORD_TABLE[i] - 1) % NUMBER_OF_CODEWORDS; + } + + /** + * The sorted table of all possible symbols. Extracted from the PDF417 + * specification. The index of a symbol in this table corresponds to the + * index into the codeword table. + */ + public static final int[] SYMBOL_TABLE = { + 0x1025e, 0x1027a, 0x1029e, 0x102bc, 0x102f2, 0x102f4, 0x1032e, 0x1034e, 0x1035c, 0x10396, 0x103a6, 0x103ac, + 0x10422, 0x10428, 0x10436, 0x10442, 0x10444, 0x10448, 0x10450, 0x1045e, 0x10466, 0x1046c, 0x1047a, 0x10482, + 0x1049e, 0x104a0, 0x104bc, 0x104c6, 0x104d8, 0x104ee, 0x104f2, 0x104f4, 0x10504, 0x10508, 0x10510, 0x1051e, + 0x10520, 0x1053c, 0x10540, 0x10578, 0x10586, 0x1058c, 0x10598, 0x105b0, 0x105be, 0x105ce, 0x105dc, 0x105e2, + 0x105e4, 0x105e8, 0x105f6, 0x1062e, 0x1064e, 0x1065c, 0x1068e, 0x1069c, 0x106b8, 0x106de, 0x106fa, 0x10716, + 0x10726, 0x1072c, 0x10746, 0x1074c, 0x10758, 0x1076e, 0x10792, 0x10794, 0x107a2, 0x107a4, 0x107a8, 0x107b6, + 0x10822, 0x10828, 0x10842, 0x10848, 0x10850, 0x1085e, 0x10866, 0x1086c, 0x1087a, 0x10882, 0x10884, 0x10890, + 0x1089e, 0x108a0, 0x108bc, 0x108c6, 0x108cc, 0x108d8, 0x108ee, 0x108f2, 0x108f4, 0x10902, 0x10908, 0x1091e, + 0x10920, 0x1093c, 0x10940, 0x10978, 0x10986, 0x10998, 0x109b0, 0x109be, 0x109ce, 0x109dc, 0x109e2, 0x109e4, + 0x109e8, 0x109f6, 0x10a08, 0x10a10, 0x10a1e, 0x10a20, 0x10a3c, 0x10a40, 0x10a78, 0x10af0, 0x10b06, 0x10b0c, + 0x10b18, 0x10b30, 0x10b3e, 0x10b60, 0x10b7c, 0x10b8e, 0x10b9c, 0x10bb8, 0x10bc2, 0x10bc4, 0x10bc8, 0x10bd0, + 0x10bde, 0x10be6, 0x10bec, 0x10c2e, 0x10c4e, 0x10c5c, 0x10c62, 0x10c64, 0x10c68, 0x10c76, 0x10c8e, 0x10c9c, + 0x10cb8, 0x10cc2, 0x10cc4, 0x10cc8, 0x10cd0, 0x10cde, 0x10ce6, 0x10cec, 0x10cfa, 0x10d0e, 0x10d1c, 0x10d38, + 0x10d70, 0x10d7e, 0x10d82, 0x10d84, 0x10d88, 0x10d90, 0x10d9e, 0x10da0, 0x10dbc, 0x10dc6, 0x10dcc, 0x10dd8, + 0x10dee, 0x10df2, 0x10df4, 0x10e16, 0x10e26, 0x10e2c, 0x10e46, 0x10e58, 0x10e6e, 0x10e86, 0x10e8c, 0x10e98, + 0x10eb0, 0x10ebe, 0x10ece, 0x10edc, 0x10f0a, 0x10f12, 0x10f14, 0x10f22, 0x10f28, 0x10f36, 0x10f42, 0x10f44, + 0x10f48, 0x10f50, 0x10f5e, 0x10f66, 0x10f6c, 0x10fb2, 0x10fb4, 0x11022, 0x11028, 0x11042, 0x11048, 0x11050, + 0x1105e, 0x1107a, 0x11082, 0x11084, 0x11090, 0x1109e, 0x110a0, 0x110bc, 0x110c6, 0x110cc, 0x110d8, 0x110ee, + 0x110f2, 0x110f4, 0x11102, 0x1111e, 0x11120, 0x1113c, 0x11140, 0x11178, 0x11186, 0x11198, 0x111b0, 0x111be, + 0x111ce, 0x111dc, 0x111e2, 0x111e4, 0x111e8, 0x111f6, 0x11208, 0x1121e, 0x11220, 0x11278, 0x112f0, 0x1130c, + 0x11330, 0x1133e, 0x11360, 0x1137c, 0x1138e, 0x1139c, 0x113b8, 0x113c2, 0x113c8, 0x113d0, 0x113de, 0x113e6, + 0x113ec, 0x11408, 0x11410, 0x1141e, 0x11420, 0x1143c, 0x11440, 0x11478, 0x114f0, 0x115e0, 0x1160c, 0x11618, + 0x11630, 0x1163e, 0x11660, 0x1167c, 0x116c0, 0x116f8, 0x1171c, 0x11738, 0x11770, 0x1177e, 0x11782, 0x11784, + 0x11788, 0x11790, 0x1179e, 0x117a0, 0x117bc, 0x117c6, 0x117cc, 0x117d8, 0x117ee, 0x1182e, 0x11834, 0x1184e, + 0x1185c, 0x11862, 0x11864, 0x11868, 0x11876, 0x1188e, 0x1189c, 0x118b8, 0x118c2, 0x118c8, 0x118d0, 0x118de, + 0x118e6, 0x118ec, 0x118fa, 0x1190e, 0x1191c, 0x11938, 0x11970, 0x1197e, 0x11982, 0x11984, 0x11990, 0x1199e, + 0x119a0, 0x119bc, 0x119c6, 0x119cc, 0x119d8, 0x119ee, 0x119f2, 0x119f4, 0x11a0e, 0x11a1c, 0x11a38, 0x11a70, + 0x11a7e, 0x11ae0, 0x11afc, 0x11b08, 0x11b10, 0x11b1e, 0x11b20, 0x11b3c, 0x11b40, 0x11b78, 0x11b8c, 0x11b98, + 0x11bb0, 0x11bbe, 0x11bce, 0x11bdc, 0x11be2, 0x11be4, 0x11be8, 0x11bf6, 0x11c16, 0x11c26, 0x11c2c, 0x11c46, + 0x11c4c, 0x11c58, 0x11c6e, 0x11c86, 0x11c98, 0x11cb0, 0x11cbe, 0x11cce, 0x11cdc, 0x11ce2, 0x11ce4, 0x11ce8, + 0x11cf6, 0x11d06, 0x11d0c, 0x11d18, 0x11d30, 0x11d3e, 0x11d60, 0x11d7c, 0x11d8e, 0x11d9c, 0x11db8, 0x11dc4, + 0x11dc8, 0x11dd0, 0x11dde, 0x11de6, 0x11dec, 0x11dfa, 0x11e0a, 0x11e12, 0x11e14, 0x11e22, 0x11e24, 0x11e28, + 0x11e36, 0x11e42, 0x11e44, 0x11e50, 0x11e5e, 0x11e66, 0x11e6c, 0x11e82, 0x11e84, 0x11e88, 0x11e90, 0x11e9e, + 0x11ea0, 0x11ebc, 0x11ec6, 0x11ecc, 0x11ed8, 0x11eee, 0x11f1a, 0x11f2e, 0x11f32, 0x11f34, 0x11f4e, 0x11f5c, + 0x11f62, 0x11f64, 0x11f68, 0x11f76, 0x12048, 0x1205e, 0x12082, 0x12084, 0x12090, 0x1209e, 0x120a0, 0x120bc, + 0x120d8, 0x120f2, 0x120f4, 0x12108, 0x1211e, 0x12120, 0x1213c, 0x12140, 0x12178, 0x12186, 0x12198, 0x121b0, + 0x121be, 0x121e2, 0x121e4, 0x121e8, 0x121f6, 0x12204, 0x12210, 0x1221e, 0x12220, 0x12278, 0x122f0, 0x12306, + 0x1230c, 0x12330, 0x1233e, 0x12360, 0x1237c, 0x1238e, 0x1239c, 0x123b8, 0x123c2, 0x123c8, 0x123d0, 0x123e6, + 0x123ec, 0x1241e, 0x12420, 0x1243c, 0x124f0, 0x125e0, 0x12618, 0x1263e, 0x12660, 0x1267c, 0x126c0, 0x126f8, + 0x12738, 0x12770, 0x1277e, 0x12782, 0x12784, 0x12790, 0x1279e, 0x127a0, 0x127bc, 0x127c6, 0x127cc, 0x127d8, + 0x127ee, 0x12820, 0x1283c, 0x12840, 0x12878, 0x128f0, 0x129e0, 0x12bc0, 0x12c18, 0x12c30, 0x12c3e, 0x12c60, + 0x12c7c, 0x12cc0, 0x12cf8, 0x12df0, 0x12e1c, 0x12e38, 0x12e70, 0x12e7e, 0x12ee0, 0x12efc, 0x12f04, 0x12f08, + 0x12f10, 0x12f20, 0x12f3c, 0x12f40, 0x12f78, 0x12f86, 0x12f8c, 0x12f98, 0x12fb0, 0x12fbe, 0x12fce, 0x12fdc, + 0x1302e, 0x1304e, 0x1305c, 0x13062, 0x13068, 0x1308e, 0x1309c, 0x130b8, 0x130c2, 0x130c8, 0x130d0, 0x130de, + 0x130ec, 0x130fa, 0x1310e, 0x13138, 0x13170, 0x1317e, 0x13182, 0x13184, 0x13190, 0x1319e, 0x131a0, 0x131bc, + 0x131c6, 0x131cc, 0x131d8, 0x131f2, 0x131f4, 0x1320e, 0x1321c, 0x13270, 0x1327e, 0x132e0, 0x132fc, 0x13308, + 0x1331e, 0x13320, 0x1333c, 0x13340, 0x13378, 0x13386, 0x13398, 0x133b0, 0x133be, 0x133ce, 0x133dc, 0x133e2, + 0x133e4, 0x133e8, 0x133f6, 0x1340e, 0x1341c, 0x13438, 0x13470, 0x1347e, 0x134e0, 0x134fc, 0x135c0, 0x135f8, + 0x13608, 0x13610, 0x1361e, 0x13620, 0x1363c, 0x13640, 0x13678, 0x136f0, 0x1370c, 0x13718, 0x13730, 0x1373e, + 0x13760, 0x1377c, 0x1379c, 0x137b8, 0x137c2, 0x137c4, 0x137c8, 0x137d0, 0x137de, 0x137e6, 0x137ec, 0x13816, + 0x13826, 0x1382c, 0x13846, 0x1384c, 0x13858, 0x1386e, 0x13874, 0x13886, 0x13898, 0x138b0, 0x138be, 0x138ce, + 0x138dc, 0x138e2, 0x138e4, 0x138e8, 0x13906, 0x1390c, 0x13930, 0x1393e, 0x13960, 0x1397c, 0x1398e, 0x1399c, + 0x139b8, 0x139c8, 0x139d0, 0x139de, 0x139e6, 0x139ec, 0x139fa, 0x13a06, 0x13a0c, 0x13a18, 0x13a30, 0x13a3e, + 0x13a60, 0x13a7c, 0x13ac0, 0x13af8, 0x13b0e, 0x13b1c, 0x13b38, 0x13b70, 0x13b7e, 0x13b88, 0x13b90, 0x13b9e, + 0x13ba0, 0x13bbc, 0x13bcc, 0x13bd8, 0x13bee, 0x13bf2, 0x13bf4, 0x13c12, 0x13c14, 0x13c22, 0x13c24, 0x13c28, + 0x13c36, 0x13c42, 0x13c48, 0x13c50, 0x13c5e, 0x13c66, 0x13c6c, 0x13c82, 0x13c84, 0x13c90, 0x13c9e, 0x13ca0, + 0x13cbc, 0x13cc6, 0x13ccc, 0x13cd8, 0x13cee, 0x13d02, 0x13d04, 0x13d08, 0x13d10, 0x13d1e, 0x13d20, 0x13d3c, + 0x13d40, 0x13d78, 0x13d86, 0x13d8c, 0x13d98, 0x13db0, 0x13dbe, 0x13dce, 0x13ddc, 0x13de4, 0x13de8, 0x13df6, + 0x13e1a, 0x13e2e, 0x13e32, 0x13e34, 0x13e4e, 0x13e5c, 0x13e62, 0x13e64, 0x13e68, 0x13e76, 0x13e8e, 0x13e9c, + 0x13eb8, 0x13ec2, 0x13ec4, 0x13ec8, 0x13ed0, 0x13ede, 0x13ee6, 0x13eec, 0x13f26, 0x13f2c, 0x13f3a, 0x13f46, + 0x13f4c, 0x13f58, 0x13f6e, 0x13f72, 0x13f74, 0x14082, 0x1409e, 0x140a0, 0x140bc, 0x14104, 0x14108, 0x14110, + 0x1411e, 0x14120, 0x1413c, 0x14140, 0x14178, 0x1418c, 0x14198, 0x141b0, 0x141be, 0x141e2, 0x141e4, 0x141e8, + 0x14208, 0x14210, 0x1421e, 0x14220, 0x1423c, 0x14240, 0x14278, 0x142f0, 0x14306, 0x1430c, 0x14318, 0x14330, + 0x1433e, 0x14360, 0x1437c, 0x1438e, 0x143c2, 0x143c4, 0x143c8, 0x143d0, 0x143e6, 0x143ec, 0x14408, 0x14410, + 0x1441e, 0x14420, 0x1443c, 0x14440, 0x14478, 0x144f0, 0x145e0, 0x1460c, 0x14618, 0x14630, 0x1463e, 0x14660, + 0x1467c, 0x146c0, 0x146f8, 0x1471c, 0x14738, 0x14770, 0x1477e, 0x14782, 0x14784, 0x14788, 0x14790, 0x147a0, + 0x147bc, 0x147c6, 0x147cc, 0x147d8, 0x147ee, 0x14810, 0x14820, 0x1483c, 0x14840, 0x14878, 0x148f0, 0x149e0, + 0x14bc0, 0x14c30, 0x14c3e, 0x14c60, 0x14c7c, 0x14cc0, 0x14cf8, 0x14df0, 0x14e38, 0x14e70, 0x14e7e, 0x14ee0, + 0x14efc, 0x14f04, 0x14f08, 0x14f10, 0x14f1e, 0x14f20, 0x14f3c, 0x14f40, 0x14f78, 0x14f86, 0x14f8c, 0x14f98, + 0x14fb0, 0x14fce, 0x14fdc, 0x15020, 0x15040, 0x15078, 0x150f0, 0x151e0, 0x153c0, 0x15860, 0x1587c, 0x158c0, + 0x158f8, 0x159f0, 0x15be0, 0x15c70, 0x15c7e, 0x15ce0, 0x15cfc, 0x15dc0, 0x15df8, 0x15e08, 0x15e10, 0x15e20, + 0x15e40, 0x15e78, 0x15ef0, 0x15f0c, 0x15f18, 0x15f30, 0x15f60, 0x15f7c, 0x15f8e, 0x15f9c, 0x15fb8, 0x1604e, + 0x1605c, 0x1608e, 0x1609c, 0x160b8, 0x160c2, 0x160c4, 0x160c8, 0x160de, 0x1610e, 0x1611c, 0x16138, 0x16170, + 0x1617e, 0x16184, 0x16188, 0x16190, 0x1619e, 0x161a0, 0x161bc, 0x161c6, 0x161cc, 0x161d8, 0x161f2, 0x161f4, + 0x1620e, 0x1621c, 0x16238, 0x16270, 0x1627e, 0x162e0, 0x162fc, 0x16304, 0x16308, 0x16310, 0x1631e, 0x16320, + 0x1633c, 0x16340, 0x16378, 0x16386, 0x1638c, 0x16398, 0x163b0, 0x163be, 0x163ce, 0x163dc, 0x163e2, 0x163e4, + 0x163e8, 0x163f6, 0x1640e, 0x1641c, 0x16438, 0x16470, 0x1647e, 0x164e0, 0x164fc, 0x165c0, 0x165f8, 0x16610, + 0x1661e, 0x16620, 0x1663c, 0x16640, 0x16678, 0x166f0, 0x16718, 0x16730, 0x1673e, 0x16760, 0x1677c, 0x1678e, + 0x1679c, 0x167b8, 0x167c2, 0x167c4, 0x167c8, 0x167d0, 0x167de, 0x167e6, 0x167ec, 0x1681c, 0x16838, 0x16870, + 0x168e0, 0x168fc, 0x169c0, 0x169f8, 0x16bf0, 0x16c10, 0x16c1e, 0x16c20, 0x16c3c, 0x16c40, 0x16c78, 0x16cf0, + 0x16de0, 0x16e18, 0x16e30, 0x16e3e, 0x16e60, 0x16e7c, 0x16ec0, 0x16ef8, 0x16f1c, 0x16f38, 0x16f70, 0x16f7e, + 0x16f84, 0x16f88, 0x16f90, 0x16f9e, 0x16fa0, 0x16fbc, 0x16fc6, 0x16fcc, 0x16fd8, 0x17026, 0x1702c, 0x17046, + 0x1704c, 0x17058, 0x1706e, 0x17086, 0x1708c, 0x17098, 0x170b0, 0x170be, 0x170ce, 0x170dc, 0x170e8, 0x17106, + 0x1710c, 0x17118, 0x17130, 0x1713e, 0x17160, 0x1717c, 0x1718e, 0x1719c, 0x171b8, 0x171c2, 0x171c4, 0x171c8, + 0x171d0, 0x171de, 0x171e6, 0x171ec, 0x171fa, 0x17206, 0x1720c, 0x17218, 0x17230, 0x1723e, 0x17260, 0x1727c, + 0x172c0, 0x172f8, 0x1730e, 0x1731c, 0x17338, 0x17370, 0x1737e, 0x17388, 0x17390, 0x1739e, 0x173a0, 0x173bc, + 0x173cc, 0x173d8, 0x173ee, 0x173f2, 0x173f4, 0x1740c, 0x17418, 0x17430, 0x1743e, 0x17460, 0x1747c, 0x174c0, + 0x174f8, 0x175f0, 0x1760e, 0x1761c, 0x17638, 0x17670, 0x1767e, 0x176e0, 0x176fc, 0x17708, 0x17710, 0x1771e, + 0x17720, 0x1773c, 0x17740, 0x17778, 0x17798, 0x177b0, 0x177be, 0x177dc, 0x177e2, 0x177e4, 0x177e8, 0x17822, + 0x17824, 0x17828, 0x17836, 0x17842, 0x17844, 0x17848, 0x17850, 0x1785e, 0x17866, 0x1786c, 0x17882, 0x17884, + 0x17888, 0x17890, 0x1789e, 0x178a0, 0x178bc, 0x178c6, 0x178cc, 0x178d8, 0x178ee, 0x178f2, 0x178f4, 0x17902, + 0x17904, 0x17908, 0x17910, 0x1791e, 0x17920, 0x1793c, 0x17940, 0x17978, 0x17986, 0x1798c, 0x17998, 0x179b0, + 0x179be, 0x179ce, 0x179dc, 0x179e2, 0x179e4, 0x179e8, 0x179f6, 0x17a04, 0x17a08, 0x17a10, 0x17a1e, 0x17a20, + 0x17a3c, 0x17a40, 0x17a78, 0x17af0, 0x17b06, 0x17b0c, 0x17b18, 0x17b30, 0x17b3e, 0x17b60, 0x17b7c, 0x17b8e, + 0x17b9c, 0x17bb8, 0x17bc4, 0x17bc8, 0x17bd0, 0x17bde, 0x17be6, 0x17bec, 0x17c2e, 0x17c32, 0x17c34, 0x17c4e, + 0x17c5c, 0x17c62, 0x17c64, 0x17c68, 0x17c76, 0x17c8e, 0x17c9c, 0x17cb8, 0x17cc2, 0x17cc4, 0x17cc8, 0x17cd0, + 0x17cde, 0x17ce6, 0x17cec, 0x17d0e, 0x17d1c, 0x17d38, 0x17d70, 0x17d82, 0x17d84, 0x17d88, 0x17d90, 0x17d9e, + 0x17da0, 0x17dbc, 0x17dc6, 0x17dcc, 0x17dd8, 0x17dee, 0x17e26, 0x17e2c, 0x17e3a, 0x17e46, 0x17e4c, 0x17e58, + 0x17e6e, 0x17e72, 0x17e74, 0x17e86, 0x17e8c, 0x17e98, 0x17eb0, 0x17ece, 0x17edc, 0x17ee2, 0x17ee4, 0x17ee8, + 0x17ef6, 0x1813a, 0x18172, 0x18174, 0x18216, 0x18226, 0x1823a, 0x1824c, 0x18258, 0x1826e, 0x18272, 0x18274, + 0x18298, 0x182be, 0x182e2, 0x182e4, 0x182e8, 0x182f6, 0x1835e, 0x1837a, 0x183ae, 0x183d6, 0x18416, 0x18426, + 0x1842c, 0x1843a, 0x18446, 0x18458, 0x1846e, 0x18472, 0x18474, 0x18486, 0x184b0, 0x184be, 0x184ce, 0x184dc, + 0x184e2, 0x184e4, 0x184e8, 0x184f6, 0x18506, 0x1850c, 0x18518, 0x18530, 0x1853e, 0x18560, 0x1857c, 0x1858e, + 0x1859c, 0x185b8, 0x185c2, 0x185c4, 0x185c8, 0x185d0, 0x185de, 0x185e6, 0x185ec, 0x185fa, 0x18612, 0x18614, + 0x18622, 0x18628, 0x18636, 0x18642, 0x18650, 0x1865e, 0x1867a, 0x18682, 0x18684, 0x18688, 0x18690, 0x1869e, + 0x186a0, 0x186bc, 0x186c6, 0x186cc, 0x186d8, 0x186ee, 0x186f2, 0x186f4, 0x1872e, 0x1874e, 0x1875c, 0x18796, + 0x187a6, 0x187ac, 0x187d2, 0x187d4, 0x18826, 0x1882c, 0x1883a, 0x18846, 0x1884c, 0x18858, 0x1886e, 0x18872, + 0x18874, 0x18886, 0x18898, 0x188b0, 0x188be, 0x188ce, 0x188dc, 0x188e2, 0x188e4, 0x188e8, 0x188f6, 0x1890c, + 0x18930, 0x1893e, 0x18960, 0x1897c, 0x1898e, 0x189b8, 0x189c2, 0x189c8, 0x189d0, 0x189de, 0x189e6, 0x189ec, + 0x189fa, 0x18a18, 0x18a30, 0x18a3e, 0x18a60, 0x18a7c, 0x18ac0, 0x18af8, 0x18b1c, 0x18b38, 0x18b70, 0x18b7e, + 0x18b82, 0x18b84, 0x18b88, 0x18b90, 0x18b9e, 0x18ba0, 0x18bbc, 0x18bc6, 0x18bcc, 0x18bd8, 0x18bee, 0x18bf2, + 0x18bf4, 0x18c22, 0x18c24, 0x18c28, 0x18c36, 0x18c42, 0x18c48, 0x18c50, 0x18c5e, 0x18c66, 0x18c7a, 0x18c82, + 0x18c84, 0x18c90, 0x18c9e, 0x18ca0, 0x18cbc, 0x18ccc, 0x18cf2, 0x18cf4, 0x18d04, 0x18d08, 0x18d10, 0x18d1e, + 0x18d20, 0x18d3c, 0x18d40, 0x18d78, 0x18d86, 0x18d98, 0x18dce, 0x18de2, 0x18de4, 0x18de8, 0x18e2e, 0x18e32, + 0x18e34, 0x18e4e, 0x18e5c, 0x18e62, 0x18e64, 0x18e68, 0x18e8e, 0x18e9c, 0x18eb8, 0x18ec2, 0x18ec4, 0x18ec8, + 0x18ed0, 0x18efa, 0x18f16, 0x18f26, 0x18f2c, 0x18f46, 0x18f4c, 0x18f58, 0x18f6e, 0x18f8a, 0x18f92, 0x18f94, + 0x18fa2, 0x18fa4, 0x18fa8, 0x18fb6, 0x1902c, 0x1903a, 0x19046, 0x1904c, 0x19058, 0x19072, 0x19074, 0x19086, + 0x19098, 0x190b0, 0x190be, 0x190ce, 0x190dc, 0x190e2, 0x190e8, 0x190f6, 0x19106, 0x1910c, 0x19130, 0x1913e, + 0x19160, 0x1917c, 0x1918e, 0x1919c, 0x191b8, 0x191c2, 0x191c8, 0x191d0, 0x191de, 0x191e6, 0x191ec, 0x191fa, + 0x19218, 0x1923e, 0x19260, 0x1927c, 0x192c0, 0x192f8, 0x19338, 0x19370, 0x1937e, 0x19382, 0x19384, 0x19390, + 0x1939e, 0x193a0, 0x193bc, 0x193c6, 0x193cc, 0x193d8, 0x193ee, 0x193f2, 0x193f4, 0x19430, 0x1943e, 0x19460, + 0x1947c, 0x194c0, 0x194f8, 0x195f0, 0x19638, 0x19670, 0x1967e, 0x196e0, 0x196fc, 0x19702, 0x19704, 0x19708, + 0x19710, 0x19720, 0x1973c, 0x19740, 0x19778, 0x19786, 0x1978c, 0x19798, 0x197b0, 0x197be, 0x197ce, 0x197dc, + 0x197e2, 0x197e4, 0x197e8, 0x19822, 0x19824, 0x19842, 0x19848, 0x19850, 0x1985e, 0x19866, 0x1987a, 0x19882, + 0x19884, 0x19890, 0x1989e, 0x198a0, 0x198bc, 0x198cc, 0x198f2, 0x198f4, 0x19902, 0x19908, 0x1991e, 0x19920, + 0x1993c, 0x19940, 0x19978, 0x19986, 0x19998, 0x199ce, 0x199e2, 0x199e4, 0x199e8, 0x19a08, 0x19a10, 0x19a1e, + 0x19a20, 0x19a3c, 0x19a40, 0x19a78, 0x19af0, 0x19b18, 0x19b3e, 0x19b60, 0x19b9c, 0x19bc2, 0x19bc4, 0x19bc8, + 0x19bd0, 0x19be6, 0x19c2e, 0x19c34, 0x19c4e, 0x19c5c, 0x19c62, 0x19c64, 0x19c68, 0x19c8e, 0x19c9c, 0x19cb8, + 0x19cc2, 0x19cc8, 0x19cd0, 0x19ce6, 0x19cfa, 0x19d0e, 0x19d1c, 0x19d38, 0x19d70, 0x19d7e, 0x19d82, 0x19d84, + 0x19d88, 0x19d90, 0x19da0, 0x19dcc, 0x19df2, 0x19df4, 0x19e16, 0x19e26, 0x19e2c, 0x19e46, 0x19e4c, 0x19e58, + 0x19e74, 0x19e86, 0x19e8c, 0x19e98, 0x19eb0, 0x19ebe, 0x19ece, 0x19ee2, 0x19ee4, 0x19ee8, 0x19f0a, 0x19f12, + 0x19f14, 0x19f22, 0x19f24, 0x19f28, 0x19f42, 0x19f44, 0x19f48, 0x19f50, 0x19f5e, 0x19f6c, 0x19f9a, 0x19fae, + 0x19fb2, 0x19fb4, 0x1a046, 0x1a04c, 0x1a072, 0x1a074, 0x1a086, 0x1a08c, 0x1a098, 0x1a0b0, 0x1a0be, 0x1a0e2, + 0x1a0e4, 0x1a0e8, 0x1a0f6, 0x1a106, 0x1a10c, 0x1a118, 0x1a130, 0x1a13e, 0x1a160, 0x1a17c, 0x1a18e, 0x1a19c, + 0x1a1b8, 0x1a1c2, 0x1a1c4, 0x1a1c8, 0x1a1d0, 0x1a1de, 0x1a1e6, 0x1a1ec, 0x1a218, 0x1a230, 0x1a23e, 0x1a260, + 0x1a27c, 0x1a2c0, 0x1a2f8, 0x1a31c, 0x1a338, 0x1a370, 0x1a37e, 0x1a382, 0x1a384, 0x1a388, 0x1a390, 0x1a39e, + 0x1a3a0, 0x1a3bc, 0x1a3c6, 0x1a3cc, 0x1a3d8, 0x1a3ee, 0x1a3f2, 0x1a3f4, 0x1a418, 0x1a430, 0x1a43e, 0x1a460, + 0x1a47c, 0x1a4c0, 0x1a4f8, 0x1a5f0, 0x1a61c, 0x1a638, 0x1a670, 0x1a67e, 0x1a6e0, 0x1a6fc, 0x1a702, 0x1a704, + 0x1a708, 0x1a710, 0x1a71e, 0x1a720, 0x1a73c, 0x1a740, 0x1a778, 0x1a786, 0x1a78c, 0x1a798, 0x1a7b0, 0x1a7be, + 0x1a7ce, 0x1a7dc, 0x1a7e2, 0x1a7e4, 0x1a7e8, 0x1a830, 0x1a860, 0x1a87c, 0x1a8c0, 0x1a8f8, 0x1a9f0, 0x1abe0, + 0x1ac70, 0x1ac7e, 0x1ace0, 0x1acfc, 0x1adc0, 0x1adf8, 0x1ae04, 0x1ae08, 0x1ae10, 0x1ae20, 0x1ae3c, 0x1ae40, + 0x1ae78, 0x1aef0, 0x1af06, 0x1af0c, 0x1af18, 0x1af30, 0x1af3e, 0x1af60, 0x1af7c, 0x1af8e, 0x1af9c, 0x1afb8, + 0x1afc4, 0x1afc8, 0x1afd0, 0x1afde, 0x1b042, 0x1b05e, 0x1b07a, 0x1b082, 0x1b084, 0x1b088, 0x1b090, 0x1b09e, + 0x1b0a0, 0x1b0bc, 0x1b0cc, 0x1b0f2, 0x1b0f4, 0x1b102, 0x1b104, 0x1b108, 0x1b110, 0x1b11e, 0x1b120, 0x1b13c, + 0x1b140, 0x1b178, 0x1b186, 0x1b198, 0x1b1ce, 0x1b1e2, 0x1b1e4, 0x1b1e8, 0x1b204, 0x1b208, 0x1b210, 0x1b21e, + 0x1b220, 0x1b23c, 0x1b240, 0x1b278, 0x1b2f0, 0x1b30c, 0x1b33e, 0x1b360, 0x1b39c, 0x1b3c2, 0x1b3c4, 0x1b3c8, + 0x1b3d0, 0x1b3e6, 0x1b410, 0x1b41e, 0x1b420, 0x1b43c, 0x1b440, 0x1b478, 0x1b4f0, 0x1b5e0, 0x1b618, 0x1b660, + 0x1b67c, 0x1b6c0, 0x1b738, 0x1b782, 0x1b784, 0x1b788, 0x1b790, 0x1b79e, 0x1b7a0, 0x1b7cc, 0x1b82e, 0x1b84e, + 0x1b85c, 0x1b88e, 0x1b89c, 0x1b8b8, 0x1b8c2, 0x1b8c4, 0x1b8c8, 0x1b8d0, 0x1b8e6, 0x1b8fa, 0x1b90e, 0x1b91c, + 0x1b938, 0x1b970, 0x1b97e, 0x1b982, 0x1b984, 0x1b988, 0x1b990, 0x1b99e, 0x1b9a0, 0x1b9cc, 0x1b9f2, 0x1b9f4, + 0x1ba0e, 0x1ba1c, 0x1ba38, 0x1ba70, 0x1ba7e, 0x1bae0, 0x1bafc, 0x1bb08, 0x1bb10, 0x1bb20, 0x1bb3c, 0x1bb40, + 0x1bb98, 0x1bbce, 0x1bbe2, 0x1bbe4, 0x1bbe8, 0x1bc16, 0x1bc26, 0x1bc2c, 0x1bc46, 0x1bc4c, 0x1bc58, 0x1bc72, + 0x1bc74, 0x1bc86, 0x1bc8c, 0x1bc98, 0x1bcb0, 0x1bcbe, 0x1bcce, 0x1bce2, 0x1bce4, 0x1bce8, 0x1bd06, 0x1bd0c, + 0x1bd18, 0x1bd30, 0x1bd3e, 0x1bd60, 0x1bd7c, 0x1bd9c, 0x1bdc2, 0x1bdc4, 0x1bdc8, 0x1bdd0, 0x1bde6, 0x1bdfa, + 0x1be12, 0x1be14, 0x1be22, 0x1be24, 0x1be28, 0x1be42, 0x1be44, 0x1be48, 0x1be50, 0x1be5e, 0x1be66, 0x1be82, + 0x1be84, 0x1be88, 0x1be90, 0x1be9e, 0x1bea0, 0x1bebc, 0x1becc, 0x1bef4, 0x1bf1a, 0x1bf2e, 0x1bf32, 0x1bf34, + 0x1bf4e, 0x1bf5c, 0x1bf62, 0x1bf64, 0x1bf68, 0x1c09a, 0x1c0b2, 0x1c0b4, 0x1c11a, 0x1c132, 0x1c134, 0x1c162, + 0x1c164, 0x1c168, 0x1c176, 0x1c1ba, 0x1c21a, 0x1c232, 0x1c234, 0x1c24e, 0x1c25c, 0x1c262, 0x1c264, 0x1c268, + 0x1c276, 0x1c28e, 0x1c2c2, 0x1c2c4, 0x1c2c8, 0x1c2d0, 0x1c2de, 0x1c2e6, 0x1c2ec, 0x1c2fa, 0x1c316, 0x1c326, + 0x1c33a, 0x1c346, 0x1c34c, 0x1c372, 0x1c374, 0x1c41a, 0x1c42e, 0x1c432, 0x1c434, 0x1c44e, 0x1c45c, 0x1c462, + 0x1c464, 0x1c468, 0x1c476, 0x1c48e, 0x1c49c, 0x1c4b8, 0x1c4c2, 0x1c4c8, 0x1c4d0, 0x1c4de, 0x1c4e6, 0x1c4ec, + 0x1c4fa, 0x1c51c, 0x1c538, 0x1c570, 0x1c57e, 0x1c582, 0x1c584, 0x1c588, 0x1c590, 0x1c59e, 0x1c5a0, 0x1c5bc, + 0x1c5c6, 0x1c5cc, 0x1c5d8, 0x1c5ee, 0x1c5f2, 0x1c5f4, 0x1c616, 0x1c626, 0x1c62c, 0x1c63a, 0x1c646, 0x1c64c, + 0x1c658, 0x1c66e, 0x1c672, 0x1c674, 0x1c686, 0x1c68c, 0x1c698, 0x1c6b0, 0x1c6be, 0x1c6ce, 0x1c6dc, 0x1c6e2, + 0x1c6e4, 0x1c6e8, 0x1c712, 0x1c714, 0x1c722, 0x1c728, 0x1c736, 0x1c742, 0x1c744, 0x1c748, 0x1c750, 0x1c75e, + 0x1c766, 0x1c76c, 0x1c77a, 0x1c7ae, 0x1c7d6, 0x1c7ea, 0x1c81a, 0x1c82e, 0x1c832, 0x1c834, 0x1c84e, 0x1c85c, + 0x1c862, 0x1c864, 0x1c868, 0x1c876, 0x1c88e, 0x1c89c, 0x1c8b8, 0x1c8c2, 0x1c8c8, 0x1c8d0, 0x1c8de, 0x1c8e6, + 0x1c8ec, 0x1c8fa, 0x1c90e, 0x1c938, 0x1c970, 0x1c97e, 0x1c982, 0x1c984, 0x1c990, 0x1c99e, 0x1c9a0, 0x1c9bc, + 0x1c9c6, 0x1c9cc, 0x1c9d8, 0x1c9ee, 0x1c9f2, 0x1c9f4, 0x1ca38, 0x1ca70, 0x1ca7e, 0x1cae0, 0x1cafc, 0x1cb02, + 0x1cb04, 0x1cb08, 0x1cb10, 0x1cb20, 0x1cb3c, 0x1cb40, 0x1cb78, 0x1cb86, 0x1cb8c, 0x1cb98, 0x1cbb0, 0x1cbbe, + 0x1cbce, 0x1cbdc, 0x1cbe2, 0x1cbe4, 0x1cbe8, 0x1cbf6, 0x1cc16, 0x1cc26, 0x1cc2c, 0x1cc3a, 0x1cc46, 0x1cc58, + 0x1cc72, 0x1cc74, 0x1cc86, 0x1ccb0, 0x1ccbe, 0x1ccce, 0x1cce2, 0x1cce4, 0x1cce8, 0x1cd06, 0x1cd0c, 0x1cd18, + 0x1cd30, 0x1cd3e, 0x1cd60, 0x1cd7c, 0x1cd9c, 0x1cdc2, 0x1cdc4, 0x1cdc8, 0x1cdd0, 0x1cdde, 0x1cde6, 0x1cdfa, + 0x1ce22, 0x1ce28, 0x1ce42, 0x1ce50, 0x1ce5e, 0x1ce66, 0x1ce7a, 0x1ce82, 0x1ce84, 0x1ce88, 0x1ce90, 0x1ce9e, + 0x1cea0, 0x1cebc, 0x1cecc, 0x1cef2, 0x1cef4, 0x1cf2e, 0x1cf32, 0x1cf34, 0x1cf4e, 0x1cf5c, 0x1cf62, 0x1cf64, + 0x1cf68, 0x1cf96, 0x1cfa6, 0x1cfac, 0x1cfca, 0x1cfd2, 0x1cfd4, 0x1d02e, 0x1d032, 0x1d034, 0x1d04e, 0x1d05c, + 0x1d062, 0x1d064, 0x1d068, 0x1d076, 0x1d08e, 0x1d09c, 0x1d0b8, 0x1d0c2, 0x1d0c4, 0x1d0c8, 0x1d0d0, 0x1d0de, + 0x1d0e6, 0x1d0ec, 0x1d0fa, 0x1d11c, 0x1d138, 0x1d170, 0x1d17e, 0x1d182, 0x1d184, 0x1d188, 0x1d190, 0x1d19e, + 0x1d1a0, 0x1d1bc, 0x1d1c6, 0x1d1cc, 0x1d1d8, 0x1d1ee, 0x1d1f2, 0x1d1f4, 0x1d21c, 0x1d238, 0x1d270, 0x1d27e, + 0x1d2e0, 0x1d2fc, 0x1d302, 0x1d304, 0x1d308, 0x1d310, 0x1d31e, 0x1d320, 0x1d33c, 0x1d340, 0x1d378, 0x1d386, + 0x1d38c, 0x1d398, 0x1d3b0, 0x1d3be, 0x1d3ce, 0x1d3dc, 0x1d3e2, 0x1d3e4, 0x1d3e8, 0x1d3f6, 0x1d470, 0x1d47e, + 0x1d4e0, 0x1d4fc, 0x1d5c0, 0x1d5f8, 0x1d604, 0x1d608, 0x1d610, 0x1d620, 0x1d640, 0x1d678, 0x1d6f0, 0x1d706, + 0x1d70c, 0x1d718, 0x1d730, 0x1d73e, 0x1d760, 0x1d77c, 0x1d78e, 0x1d79c, 0x1d7b8, 0x1d7c2, 0x1d7c4, 0x1d7c8, + 0x1d7d0, 0x1d7de, 0x1d7e6, 0x1d7ec, 0x1d826, 0x1d82c, 0x1d83a, 0x1d846, 0x1d84c, 0x1d858, 0x1d872, 0x1d874, + 0x1d886, 0x1d88c, 0x1d898, 0x1d8b0, 0x1d8be, 0x1d8ce, 0x1d8e2, 0x1d8e4, 0x1d8e8, 0x1d8f6, 0x1d90c, 0x1d918, + 0x1d930, 0x1d93e, 0x1d960, 0x1d97c, 0x1d99c, 0x1d9c2, 0x1d9c4, 0x1d9c8, 0x1d9d0, 0x1d9e6, 0x1d9fa, 0x1da0c, + 0x1da18, 0x1da30, 0x1da3e, 0x1da60, 0x1da7c, 0x1dac0, 0x1daf8, 0x1db38, 0x1db82, 0x1db84, 0x1db88, 0x1db90, + 0x1db9e, 0x1dba0, 0x1dbcc, 0x1dbf2, 0x1dbf4, 0x1dc22, 0x1dc42, 0x1dc44, 0x1dc48, 0x1dc50, 0x1dc5e, 0x1dc66, + 0x1dc7a, 0x1dc82, 0x1dc84, 0x1dc88, 0x1dc90, 0x1dc9e, 0x1dca0, 0x1dcbc, 0x1dccc, 0x1dcf2, 0x1dcf4, 0x1dd04, + 0x1dd08, 0x1dd10, 0x1dd1e, 0x1dd20, 0x1dd3c, 0x1dd40, 0x1dd78, 0x1dd86, 0x1dd98, 0x1ddce, 0x1dde2, 0x1dde4, + 0x1dde8, 0x1de2e, 0x1de32, 0x1de34, 0x1de4e, 0x1de5c, 0x1de62, 0x1de64, 0x1de68, 0x1de8e, 0x1de9c, 0x1deb8, + 0x1dec2, 0x1dec4, 0x1dec8, 0x1ded0, 0x1dee6, 0x1defa, 0x1df16, 0x1df26, 0x1df2c, 0x1df46, 0x1df4c, 0x1df58, + 0x1df72, 0x1df74, 0x1df8a, 0x1df92, 0x1df94, 0x1dfa2, 0x1dfa4, 0x1dfa8, 0x1e08a, 0x1e092, 0x1e094, 0x1e0a2, + 0x1e0a4, 0x1e0a8, 0x1e0b6, 0x1e0da, 0x1e10a, 0x1e112, 0x1e114, 0x1e122, 0x1e124, 0x1e128, 0x1e136, 0x1e142, + 0x1e144, 0x1e148, 0x1e150, 0x1e166, 0x1e16c, 0x1e17a, 0x1e19a, 0x1e1b2, 0x1e1b4, 0x1e20a, 0x1e212, 0x1e214, + 0x1e222, 0x1e224, 0x1e228, 0x1e236, 0x1e242, 0x1e248, 0x1e250, 0x1e25e, 0x1e266, 0x1e26c, 0x1e27a, 0x1e282, + 0x1e284, 0x1e288, 0x1e290, 0x1e2a0, 0x1e2bc, 0x1e2c6, 0x1e2cc, 0x1e2d8, 0x1e2ee, 0x1e2f2, 0x1e2f4, 0x1e31a, + 0x1e332, 0x1e334, 0x1e35c, 0x1e362, 0x1e364, 0x1e368, 0x1e3ba, 0x1e40a, 0x1e412, 0x1e414, 0x1e422, 0x1e428, + 0x1e436, 0x1e442, 0x1e448, 0x1e450, 0x1e45e, 0x1e466, 0x1e46c, 0x1e47a, 0x1e482, 0x1e484, 0x1e490, 0x1e49e, + 0x1e4a0, 0x1e4bc, 0x1e4c6, 0x1e4cc, 0x1e4d8, 0x1e4ee, 0x1e4f2, 0x1e4f4, 0x1e502, 0x1e504, 0x1e508, 0x1e510, + 0x1e51e, 0x1e520, 0x1e53c, 0x1e540, 0x1e578, 0x1e586, 0x1e58c, 0x1e598, 0x1e5b0, 0x1e5be, 0x1e5ce, 0x1e5dc, + 0x1e5e2, 0x1e5e4, 0x1e5e8, 0x1e5f6, 0x1e61a, 0x1e62e, 0x1e632, 0x1e634, 0x1e64e, 0x1e65c, 0x1e662, 0x1e668, + 0x1e68e, 0x1e69c, 0x1e6b8, 0x1e6c2, 0x1e6c4, 0x1e6c8, 0x1e6d0, 0x1e6e6, 0x1e6fa, 0x1e716, 0x1e726, 0x1e72c, + 0x1e73a, 0x1e746, 0x1e74c, 0x1e758, 0x1e772, 0x1e774, 0x1e792, 0x1e794, 0x1e7a2, 0x1e7a4, 0x1e7a8, 0x1e7b6, + 0x1e812, 0x1e814, 0x1e822, 0x1e824, 0x1e828, 0x1e836, 0x1e842, 0x1e844, 0x1e848, 0x1e850, 0x1e85e, 0x1e866, + 0x1e86c, 0x1e87a, 0x1e882, 0x1e884, 0x1e888, 0x1e890, 0x1e89e, 0x1e8a0, 0x1e8bc, 0x1e8c6, 0x1e8cc, 0x1e8d8, + 0x1e8ee, 0x1e8f2, 0x1e8f4, 0x1e902, 0x1e904, 0x1e908, 0x1e910, 0x1e920, 0x1e93c, 0x1e940, 0x1e978, 0x1e986, + 0x1e98c, 0x1e998, 0x1e9b0, 0x1e9be, 0x1e9ce, 0x1e9dc, 0x1e9e2, 0x1e9e4, 0x1e9e8, 0x1e9f6, 0x1ea04, 0x1ea08, + 0x1ea10, 0x1ea20, 0x1ea40, 0x1ea78, 0x1eaf0, 0x1eb06, 0x1eb0c, 0x1eb18, 0x1eb30, 0x1eb3e, 0x1eb60, 0x1eb7c, + 0x1eb8e, 0x1eb9c, 0x1ebb8, 0x1ebc2, 0x1ebc4, 0x1ebc8, 0x1ebd0, 0x1ebde, 0x1ebe6, 0x1ebec, 0x1ec1a, 0x1ec2e, + 0x1ec32, 0x1ec34, 0x1ec4e, 0x1ec5c, 0x1ec62, 0x1ec64, 0x1ec68, 0x1ec8e, 0x1ec9c, 0x1ecb8, 0x1ecc2, 0x1ecc4, + 0x1ecc8, 0x1ecd0, 0x1ece6, 0x1ecfa, 0x1ed0e, 0x1ed1c, 0x1ed38, 0x1ed70, 0x1ed7e, 0x1ed82, 0x1ed84, 0x1ed88, + 0x1ed90, 0x1ed9e, 0x1eda0, 0x1edcc, 0x1edf2, 0x1edf4, 0x1ee16, 0x1ee26, 0x1ee2c, 0x1ee3a, 0x1ee46, 0x1ee4c, + 0x1ee58, 0x1ee6e, 0x1ee72, 0x1ee74, 0x1ee86, 0x1ee8c, 0x1ee98, 0x1eeb0, 0x1eebe, 0x1eece, 0x1eedc, 0x1eee2, + 0x1eee4, 0x1eee8, 0x1ef12, 0x1ef22, 0x1ef24, 0x1ef28, 0x1ef36, 0x1ef42, 0x1ef44, 0x1ef48, 0x1ef50, 0x1ef5e, + 0x1ef66, 0x1ef6c, 0x1ef7a, 0x1efae, 0x1efb2, 0x1efb4, 0x1efd6, 0x1f096, 0x1f0a6, 0x1f0ac, 0x1f0ba, 0x1f0ca, + 0x1f0d2, 0x1f0d4, 0x1f116, 0x1f126, 0x1f12c, 0x1f13a, 0x1f146, 0x1f14c, 0x1f158, 0x1f16e, 0x1f172, 0x1f174, + 0x1f18a, 0x1f192, 0x1f194, 0x1f1a2, 0x1f1a4, 0x1f1a8, 0x1f1da, 0x1f216, 0x1f226, 0x1f22c, 0x1f23a, 0x1f246, + 0x1f258, 0x1f26e, 0x1f272, 0x1f274, 0x1f286, 0x1f28c, 0x1f298, 0x1f2b0, 0x1f2be, 0x1f2ce, 0x1f2dc, 0x1f2e2, + 0x1f2e4, 0x1f2e8, 0x1f2f6, 0x1f30a, 0x1f312, 0x1f314, 0x1f322, 0x1f328, 0x1f342, 0x1f344, 0x1f348, 0x1f350, + 0x1f35e, 0x1f366, 0x1f37a, 0x1f39a, 0x1f3ae, 0x1f3b2, 0x1f3b4, 0x1f416, 0x1f426, 0x1f42c, 0x1f43a, 0x1f446, + 0x1f44c, 0x1f458, 0x1f46e, 0x1f472, 0x1f474, 0x1f486, 0x1f48c, 0x1f498, 0x1f4b0, 0x1f4be, 0x1f4ce, 0x1f4dc, + 0x1f4e2, 0x1f4e4, 0x1f4e8, 0x1f4f6, 0x1f506, 0x1f50c, 0x1f518, 0x1f530, 0x1f53e, 0x1f560, 0x1f57c, 0x1f58e, + 0x1f59c, 0x1f5b8, 0x1f5c2, 0x1f5c4, 0x1f5c8, 0x1f5d0, 0x1f5de, 0x1f5e6, 0x1f5ec, 0x1f5fa, 0x1f60a, 0x1f612, + 0x1f614, 0x1f622, 0x1f624, 0x1f628, 0x1f636, 0x1f642, 0x1f644, 0x1f648, 0x1f650, 0x1f65e, 0x1f666, 0x1f67a, + 0x1f682, 0x1f684, 0x1f688, 0x1f690, 0x1f69e, 0x1f6a0, 0x1f6bc, 0x1f6cc, 0x1f6f2, 0x1f6f4, 0x1f71a, 0x1f72e, + 0x1f732, 0x1f734, 0x1f74e, 0x1f75c, 0x1f762, 0x1f764, 0x1f768, 0x1f776, 0x1f796, 0x1f7a6, 0x1f7ac, 0x1f7ba, + 0x1f7d2, 0x1f7d4, 0x1f89a, 0x1f8ae, 0x1f8b2, 0x1f8b4, 0x1f8d6, 0x1f8ea, 0x1f91a, 0x1f92e, 0x1f932, 0x1f934, + 0x1f94e, 0x1f95c, 0x1f962, 0x1f964, 0x1f968, 0x1f976, 0x1f996, 0x1f9a6, 0x1f9ac, 0x1f9ba, 0x1f9ca, 0x1f9d2, + 0x1f9d4, 0x1fa1a, 0x1fa2e, 0x1fa32, 0x1fa34, 0x1fa4e, 0x1fa5c, 0x1fa62, 0x1fa64, 0x1fa68, 0x1fa76, 0x1fa8e, + 0x1fa9c, 0x1fab8, 0x1fac2, 0x1fac4, 0x1fac8, 0x1fad0, 0x1fade, 0x1fae6, 0x1faec, 0x1fb16, 0x1fb26, 0x1fb2c, + 0x1fb3a, 0x1fb46, 0x1fb4c, 0x1fb58, 0x1fb6e, 0x1fb72, 0x1fb74, 0x1fb8a, 0x1fb92, 0x1fb94, 0x1fba2, 0x1fba4, + 0x1fba8, 0x1fbb6, 0x1fbda}; + + /** + * This table contains to codewords for all symbols. + */ + private static final int[] CODEWORD_TABLE = { + 2627, 1819, 2622, 2621, 1813, 1812, 2729, 2724, 2723, 2779, 2774, 2773, 902, 896, 908, 868, 865, 861, 859, 2511, + 873, 871, 1780, 835, 2493, 825, 2491, 842, 837, 844, 1764, 1762, 811, 810, 809, 2483, 807, 2482, 806, 2480, 815, + 814, 813, 812, 2484, 817, 816, 1745, 1744, 1742, 1746, 2655, 2637, 2635, 2626, 2625, 2623, 2628, 1820, 2752, + 2739, 2737, 2728, 2727, 2725, 2730, 2785, 2783, 2778, 2777, 2775, 2780, 787, 781, 747, 739, 736, 2413, 754, 752, + 1719, 692, 689, 681, 2371, 678, 2369, 700, 697, 694, 703, 1688, 1686, 642, 638, 2343, 631, 2341, 627, 2338, 651, + 646, 643, 2345, 654, 652, 1652, 1650, 1647, 1654, 601, 599, 2322, 596, 2321, 594, 2319, 2317, 611, 610, 608, 606, + 2324, 603, 2323, 615, 614, 612, 1617, 1616, 1614, 1612, 616, 1619, 1618, 2575, 2538, 2536, 905, 901, 898, 909, + 2509, 2507, 2504, 870, 867, 864, 860, 2512, 875, 872, 1781, 2490, 2489, 2487, 2485, 1748, 836, 834, 832, 830, + 2494, 827, 2492, 843, 841, 839, 845, 1765, 1763, 2701, 2676, 2674, 2653, 2648, 2656, 2634, 2633, 2631, 2629, + 1821, 2638, 2636, 2770, 2763, 2761, 2750, 2745, 2753, 2736, 2735, 2733, 2731, 1848, 2740, 2738, 2786, 2784, 591, + 588, 576, 569, 566, 2296, 1590, 537, 534, 526, 2276, 522, 2274, 545, 542, 539, 548, 1572, 1570, 481, 2245, 466, + 2242, 462, 2239, 492, 485, 482, 2249, 496, 494, 1534, 1531, 1528, 1538, 413, 2196, 406, 2191, 2188, 425, 419, + 2202, 415, 2199, 432, 430, 427, 1472, 1467, 1464, 433, 1476, 1474, 368, 367, 2160, 365, 2159, 362, 2157, 2155, + 2152, 378, 377, 375, 2166, 372, 2165, 369, 2162, 383, 381, 379, 2168, 1419, 1418, 1416, 1414, 385, 1411, 384, + 1423, 1422, 1420, 1424, 2461, 802, 2441, 2439, 790, 786, 783, 794, 2409, 2406, 2403, 750, 742, 738, 2414, 756, + 753, 1720, 2367, 2365, 2362, 2359, 1663, 693, 691, 684, 2373, 680, 2370, 702, 699, 696, 704, 1690, 1687, 2337, + 2336, 2334, 2332, 1624, 2329, 1622, 640, 637, 2344, 634, 2342, 630, 2340, 650, 648, 645, 2346, 655, 653, 1653, + 1651, 1649, 1655, 2612, 2597, 2595, 2571, 2568, 2565, 2576, 2534, 2529, 2526, 1787, 2540, 2537, 907, 904, 900, + 910, 2503, 2502, 2500, 2498, 1768, 2495, 1767, 2510, 2508, 2506, 869, 866, 863, 2513, 876, 874, 1782, 2720, 2713, + 2711, 2697, 2694, 2691, 2702, 2672, 2670, 2664, 1828, 2678, 2675, 2647, 2646, 2644, 2642, 1823, 2639, 1822, 2654, + 2652, 2650, 2657, 2771, 1855, 2765, 2762, 1850, 1849, 2751, 2749, 2747, 2754, 353, 2148, 344, 342, 336, 2142, + 332, 2140, 345, 1375, 1373, 306, 2130, 299, 2128, 295, 2125, 319, 314, 311, 2132, 1354, 1352, 1349, 1356, 262, + 257, 2101, 253, 2096, 2093, 274, 273, 267, 2107, 263, 2104, 280, 278, 275, 1316, 1311, 1308, 1320, 1318, 2052, + 202, 2050, 2044, 2040, 219, 2063, 212, 2060, 208, 2055, 224, 221, 2066, 1260, 1258, 1252, 231, 1248, 229, 1266, + 1264, 1261, 1268, 155, 1998, 153, 1996, 1994, 1991, 1988, 165, 164, 2007, 162, 2006, 159, 2003, 2000, 172, 171, + 169, 2012, 166, 2010, 1186, 1184, 1182, 1179, 175, 1176, 173, 1192, 1191, 1189, 1187, 176, 1194, 1193, 2313, + 2307, 2305, 592, 589, 2294, 2292, 2289, 578, 572, 568, 2297, 580, 1591, 2272, 2267, 2264, 1547, 538, 536, 529, + 2278, 525, 2275, 547, 544, 541, 1574, 1571, 2237, 2235, 2229, 1493, 2225, 1489, 478, 2247, 470, 2244, 465, 2241, + 493, 488, 484, 2250, 498, 495, 1536, 1533, 1530, 1539, 2187, 2186, 2184, 2182, 1432, 2179, 1430, 2176, 1427, 414, + 412, 2197, 409, 2195, 405, 2193, 2190, 426, 424, 421, 2203, 418, 2201, 431, 429, 1473, 1471, 1469, 1466, 434, + 1477, 1475, 2478, 2472, 2470, 2459, 2457, 2454, 2462, 803, 2437, 2432, 2429, 1726, 2443, 2440, 792, 789, 785, + 2401, 2399, 2393, 1702, 2389, 1699, 2411, 2408, 2405, 745, 741, 2415, 758, 755, 1721, 2358, 2357, 2355, 2353, + 1661, 2350, 1660, 2347, 1657, 2368, 2366, 2364, 2361, 1666, 690, 687, 2374, 683, 2372, 701, 698, 705, 1691, 1689, + 2619, 2617, 2610, 2608, 2605, 2613, 2593, 2588, 2585, 1803, 2599, 2596, 2563, 2561, 2555, 1797, 2551, 1795, 2573, + 2570, 2567, 2577, 2525, 2524, 2522, 2520, 1786, 2517, 1785, 2514, 1783, 2535, 2533, 2531, 2528, 1788, 2541, 2539, + 906, 903, 911, 2721, 1844, 2715, 2712, 1838, 1836, 2699, 2696, 2693, 2703, 1827, 1826, 1824, 2673, 2671, 2669, + 2666, 1829, 2679, 2677, 1858, 1857, 2772, 1854, 1853, 1851, 1856, 2766, 2764, 143, 1987, 139, 1986, 135, 133, + 131, 1984, 128, 1983, 125, 1981, 138, 137, 136, 1985, 1133, 1132, 1130, 112, 110, 1974, 107, 1973, 104, 1971, + 1969, 122, 121, 119, 117, 1977, 114, 1976, 124, 1115, 1114, 1112, 1110, 1117, 1116, 84, 83, 1953, 81, 1952, 78, + 1950, 1948, 1945, 94, 93, 91, 1959, 88, 1958, 85, 1955, 99, 97, 95, 1961, 1086, 1085, 1083, 1081, 1078, 100, + 1090, 1089, 1087, 1091, 49, 47, 1917, 44, 1915, 1913, 1910, 1907, 59, 1926, 56, 1925, 53, 1922, 1919, 66, 64, + 1931, 61, 1929, 1042, 1040, 1038, 71, 1035, 70, 1032, 68, 1048, 1047, 1045, 1043, 1050, 1049, 12, 10, 1869, 1867, + 1864, 1861, 21, 1880, 19, 1877, 1874, 1871, 28, 1888, 25, 1886, 22, 1883, 982, 980, 977, 974, 32, 30, 991, 989, + 987, 984, 34, 995, 994, 992, 2151, 2150, 2147, 2146, 2144, 356, 355, 354, 2149, 2139, 2138, 2136, 2134, 1359, + 343, 341, 338, 2143, 335, 2141, 348, 347, 346, 1376, 1374, 2124, 2123, 2121, 2119, 1326, 2116, 1324, 310, 308, + 305, 2131, 302, 2129, 298, 2127, 320, 318, 316, 313, 2133, 322, 321, 1355, 1353, 1351, 1357, 2092, 2091, 2089, + 2087, 1276, 2084, 1274, 2081, 1271, 259, 2102, 256, 2100, 252, 2098, 2095, 272, 269, 2108, 266, 2106, 281, 279, + 277, 1317, 1315, 1313, 1310, 282, 1321, 1319, 2039, 2037, 2035, 2032, 1203, 2029, 1200, 1197, 207, 2053, 205, + 2051, 201, 2049, 2046, 2043, 220, 218, 2064, 215, 2062, 211, 2059, 228, 226, 223, 2069, 1259, 1257, 1254, 232, + 1251, 230, 1267, 1265, 1263, 2316, 2315, 2312, 2311, 2309, 2314, 2304, 2303, 2301, 2299, 1593, 2308, 2306, 590, + 2288, 2287, 2285, 2283, 1578, 2280, 1577, 2295, 2293, 2291, 579, 577, 574, 571, 2298, 582, 581, 1592, 2263, 2262, + 2260, 2258, 1545, 2255, 1544, 2252, 1541, 2273, 2271, 2269, 2266, 1550, 535, 532, 2279, 528, 2277, 546, 543, 549, + 1575, 1573, 2224, 2222, 2220, 1486, 2217, 1485, 2214, 1482, 1479, 2238, 2236, 2234, 2231, 1496, 2228, 1492, 480, + 477, 2248, 473, 2246, 469, 2243, 490, 487, 2251, 497, 1537, 1535, 1532, 2477, 2476, 2474, 2479, 2469, 2468, 2466, + 2464, 1730, 2473, 2471, 2453, 2452, 2450, 2448, 1729, 2445, 1728, 2460, 2458, 2456, 2463, 805, 804, 2428, 2427, + 2425, 2423, 1725, 2420, 1724, 2417, 1722, 2438, 2436, 2434, 2431, 1727, 2444, 2442, 793, 791, 788, 795, 2388, + 2386, 2384, 1697, 2381, 1696, 2378, 1694, 1692, 2402, 2400, 2398, 2395, 1703, 2392, 1701, 2412, 2410, 2407, 751, + 748, 744, 2416, 759, 757, 1807, 2620, 2618, 1806, 1805, 2611, 2609, 2607, 2614, 1802, 1801, 1799, 2594, 2592, + 2590, 2587, 1804, 2600, 2598, 1794, 1793, 1791, 1789, 2564, 2562, 2560, 2557, 1798, 2554, 1796, 2574, 2572, 2569, + 2578, 1847, 1846, 2722, 1843, 1842, 1840, 1845, 2716, 2714, 1835, 1834, 1832, 1830, 1839, 1837, 2700, 2698, 2695, + 2704, 1817, 1811, 1810, 897, 862, 1777, 829, 826, 838, 1760, 1758, 808, 2481, 1741, 1740, 1738, 1743, 2624, 1818, + 2726, 2776, 782, 740, 737, 1715, 686, 679, 695, 1682, 1680, 639, 628, 2339, 647, 644, 1645, 1643, 1640, 1648, + 602, 600, 597, 595, 2320, 593, 2318, 609, 607, 604, 1611, 1610, 1608, 1606, 613, 1615, 1613, 2328, 926, 924, 892, + 886, 899, 857, 850, 2505, 1778, 824, 823, 821, 819, 2488, 818, 2486, 833, 831, 828, 840, 1761, 1759, 2649, 2632, + 2630, 2746, 2734, 2732, 2782, 2781, 570, 567, 1587, 531, 527, 523, 540, 1566, 1564, 476, 467, 463, 2240, 486, + 483, 1524, 1521, 1518, 1529, 411, 403, 2192, 399, 2189, 423, 416, 1462, 1457, 1454, 428, 1468, 1465, 2210, 366, + 363, 2158, 360, 2156, 357, 2153, 376, 373, 370, 2163, 1410, 1409, 1407, 1405, 382, 1402, 380, 1417, 1415, 1412, + 1421, 2175, 2174, 777, 774, 771, 784, 732, 725, 722, 2404, 743, 1716, 676, 674, 668, 2363, 665, 2360, 685, 1684, + 1681, 626, 624, 622, 2335, 620, 2333, 617, 2330, 641, 635, 649, 1646, 1644, 1642, 2566, 928, 925, 2530, 2527, + 894, 891, 888, 2501, 2499, 2496, 858, 856, 854, 851, 1779, 2692, 2668, 2665, 2645, 2643, 2640, 2651, 2768, 2759, + 2757, 2744, 2743, 2741, 2748, 352, 1382, 340, 337, 333, 1371, 1369, 307, 300, 296, 2126, 315, 312, 1347, 1342, + 1350, 261, 258, 250, 2097, 246, 2094, 271, 268, 264, 1306, 1301, 1298, 276, 1312, 1309, 2115, 203, 2048, 195, + 2045, 191, 2041, 213, 209, 2056, 1246, 1244, 1238, 225, 1234, 222, 1256, 1253, 1249, 1262, 2080, 2079, 154, 1997, + 150, 1995, 147, 1992, 1989, 163, 160, 2004, 156, 2001, 1175, 1174, 1172, 1170, 1167, 170, 1164, 167, 1185, 1183, + 1180, 1177, 174, 1190, 1188, 2025, 2024, 2022, 587, 586, 564, 559, 556, 2290, 573, 1588, 520, 518, 512, 2268, + 508, 2265, 530, 1568, 1565, 461, 457, 2233, 450, 2230, 446, 2226, 479, 471, 489, 1526, 1523, 1520, 397, 395, + 2185, 392, 2183, 389, 2180, 2177, 410, 2194, 402, 422, 1463, 1461, 1459, 1456, 1470, 2455, 799, 2433, 2430, 779, + 776, 773, 2397, 2394, 2390, 734, 728, 724, 746, 1717, 2356, 2354, 2351, 2348, 1658, 677, 675, 673, 670, 667, 688, + 1685, 1683, 2606, 2589, 2586, 2559, 2556, 2552, 927, 2523, 2521, 2518, 2515, 1784, 2532, 895, 893, 890, 2718, + 2709, 2707, 2689, 2687, 2684, 2663, 2662, 2660, 2658, 1825, 2667, 2769, 1852, 2760, 2758, 142, 141, 1139, 1138, + 134, 132, 129, 126, 1982, 1129, 1128, 1126, 1131, 113, 111, 108, 105, 1972, 101, 1970, 120, 118, 115, 1109, 1108, + 1106, 1104, 123, 1113, 1111, 82, 79, 1951, 75, 1949, 72, 1946, 92, 89, 86, 1956, 1077, 1076, 1074, 1072, 98, + 1069, 96, 1084, 1082, 1079, 1088, 1968, 1967, 48, 45, 1916, 42, 1914, 39, 1911, 1908, 60, 57, 54, 1923, 50, 1920, + 1031, 1030, 1028, 1026, 67, 1023, 65, 1020, 62, 1041, 1039, 1036, 1033, 69, 1046, 1044, 1944, 1943, 1941, 11, 9, + 1868, 7, 1865, 1862, 1859, 20, 1878, 16, 1875, 13, 1872, 970, 968, 966, 963, 29, 960, 26, 23, 983, 981, 978, 975, + 33, 971, 31, 990, 988, 985, 1906, 1904, 1902, 993, 351, 2145, 1383, 331, 330, 328, 326, 2137, 323, 2135, 339, + 1372, 1370, 294, 293, 291, 289, 2122, 286, 2120, 283, 2117, 309, 303, 317, 1348, 1346, 1344, 245, 244, 242, 2090, + 239, 2088, 236, 2085, 2082, 260, 2099, 249, 270, 1307, 1305, 1303, 1300, 1314, 189, 2038, 186, 2036, 183, 2033, + 2030, 2026, 206, 198, 2047, 194, 216, 1247, 1245, 1243, 1240, 227, 1237, 1255, 2310, 2302, 2300, 2286, 2284, + 2281, 565, 563, 561, 558, 575, 1589, 2261, 2259, 2256, 2253, 1542, 521, 519, 517, 514, 2270, 511, 533, 1569, + 1567, 2223, 2221, 2218, 2215, 1483, 2211, 1480, 459, 456, 453, 2232, 449, 474, 491, 1527, 1525, 1522, 2475, 2467, + 2465, 2451, 2449, 2446, 801, 800, 2426, 2424, 2421, 2418, 1723, 2435, 780, 778, 775, 2387, 2385, 2382, 2379, + 1695, 2375, 1693, 2396, 735, 733, 730, 727, 749, 1718, 2616, 2615, 2604, 2603, 2601, 2584, 2583, 2581, 2579, + 1800, 2591, 2550, 2549, 2547, 2545, 1792, 2542, 1790, 2558, 929, 2719, 1841, 2710, 2708, 1833, 1831, 2690, 2688, + 2686, 1815, 1809, 1808, 1774, 1756, 1754, 1737, 1736, 1734, 1739, 1816, 1711, 1676, 1674, 633, 629, 1638, 1636, + 1633, 1641, 598, 1605, 1604, 1602, 1600, 605, 1609, 1607, 2327, 887, 853, 1775, 822, 820, 1757, 1755, 1584, 524, + 1560, 1558, 468, 464, 1514, 1511, 1508, 1519, 408, 404, 400, 1452, 1447, 1444, 417, 1458, 1455, 2208, 364, 361, + 358, 2154, 1401, 1400, 1398, 1396, 374, 1393, 371, 1408, 1406, 1403, 1413, 2173, 2172, 772, 726, 723, 1712, 672, + 669, 666, 682, 1678, 1675, 625, 623, 621, 618, 2331, 636, 632, 1639, 1637, 1635, 920, 918, 884, 880, 889, 849, + 848, 847, 846, 2497, 855, 852, 1776, 2641, 2742, 2787, 1380, 334, 1367, 1365, 301, 297, 1340, 1338, 1335, 1343, + 255, 251, 247, 1296, 1291, 1288, 265, 1302, 1299, 2113, 204, 196, 192, 2042, 1232, 1230, 1224, 214, 1220, 210, + 1242, 1239, 1235, 1250, 2077, 2075, 151, 148, 1993, 144, 1990, 1163, 1162, 1160, 1158, 1155, 161, 1152, 157, + 1173, 1171, 1168, 1165, 168, 1181, 1178, 2021, 2020, 2018, 2023, 585, 560, 557, 1585, 516, 509, 1562, 1559, 458, + 447, 2227, 472, 1516, 1513, 1510, 398, 396, 393, 390, 2181, 386, 2178, 407, 1453, 1451, 1449, 1446, 420, 1460, + 2209, 769, 764, 720, 712, 2391, 729, 1713, 664, 663, 661, 659, 2352, 656, 2349, 671, 1679, 1677, 2553, 922, 919, + 2519, 2516, 885, 883, 881, 2685, 2661, 2659, 2767, 2756, 2755, 140, 1137, 1136, 130, 127, 1125, 1124, 1122, 1127, + 109, 106, 102, 1103, 1102, 1100, 1098, 116, 1107, 1105, 1980, 80, 76, 73, 1947, 1068, 1067, 1065, 1063, 90, 1060, + 87, 1075, 1073, 1070, 1080, 1966, 1965, 46, 43, 40, 1912, 36, 1909, 1019, 1018, 1016, 1014, 58, 1011, 55, 1008, + 51, 1029, 1027, 1024, 1021, 63, 1037, 1034, 1940, 1939, 1937, 1942, 8, 1866, 4, 1863, 1, 1860, 956, 954, 952, + 949, 946, 17, 14, 969, 967, 964, 961, 27, 957, 24, 979, 976, 972, 1901, 1900, 1898, 1896, 986, 1905, 1903, 350, + 349, 1381, 329, 327, 324, 1368, 1366, 292, 290, 287, 284, 2118, 304, 1341, 1339, 1337, 1345, 243, 240, 237, 2086, + 233, 2083, 254, 1297, 1295, 1293, 1290, 1304, 2114, 190, 187, 184, 2034, 180, 2031, 177, 2027, 199, 1233, 1231, + 1229, 1226, 217, 1223, 1241, 2078, 2076, 584, 555, 554, 552, 550, 2282, 562, 1586, 507, 506, 504, 502, 2257, 499, + 2254, 515, 1563, 1561, 445, 443, 441, 2219, 438, 2216, 435, 2212, 460, 454, 475, 1517, 1515, 1512, 2447, 798, + 797, 2422, 2419, 770, 768, 766, 2383, 2380, 2376, 721, 719, 717, 714, 731, 1714, 2602, 2582, 2580, 2548, 2546, + 2543, 923, 921, 2717, 2706, 2705, 2683, 2682, 2680, 1771, 1752, 1750, 1733, 1732, 1731, 1735, 1814, 1707, 1670, + 1668, 1631, 1629, 1626, 1634, 1599, 1598, 1596, 1594, 1603, 1601, 2326, 1772, 1753, 1751, 1581, 1554, 1552, 1504, + 1501, 1498, 1509, 1442, 1437, 1434, 401, 1448, 1445, 2206, 1392, 1391, 1389, 1387, 1384, 359, 1399, 1397, 1394, + 1404, 2171, 2170, 1708, 1672, 1669, 619, 1632, 1630, 1628, 1773, 1378, 1363, 1361, 1333, 1328, 1336, 1286, 1281, + 1278, 248, 1292, 1289, 2111, 1218, 1216, 1210, 197, 1206, 193, 1228, 1225, 1221, 1236, 2073, 2071, 1151, 1150, + 1148, 1146, 152, 1143, 149, 1140, 145, 1161, 1159, 1156, 1153, 158, 1169, 1166, 2017, 2016, 2014, 2019, 1582, + 510, 1556, 1553, 452, 448, 1506, 1500, 394, 391, 387, 1443, 1441, 1439, 1436, 1450, 2207, 765, 716, 713, 1709, + 662, 660, 657, 1673, 1671, 916, 914, 879, 878, 877, 882, 1135, 1134, 1121, 1120, 1118, 1123, 1097, 1096, 1094, + 1092, 103, 1101, 1099, 1979, 1059, 1058, 1056, 1054, 77, 1051, 74, 1066, 1064, 1061, 1071, 1964, 1963, 1007, + 1006, 1004, 1002, 999, 41, 996, 37, 1017, 1015, 1012, 1009, 52, 1025, 1022, 1936, 1935, 1933, 1938, 942, 940, + 938, 935, 932, 5, 2, 955, 953, 950, 947, 18, 943, 15, 965, 962, 958, 1895, 1894, 1892, 1890, 973, 1899, 1897, + 1379, 325, 1364, 1362, 288, 285, 1334, 1332, 1330, 241, 238, 234, 1287, 1285, 1283, 1280, 1294, 2112, 188, 185, + 181, 178, 2028, 1219, 1217, 1215, 1212, 200, 1209, 1227, 2074, 2072, 583, 553, 551, 1583, 505, 503, 500, 513, + 1557, 1555, 444, 442, 439, 436, 2213, 455, 451, 1507, 1505, 1502, 796, 763, 762, 760, 767, 711, 710, 708, 706, + 2377, 718, 715, 1710, 2544, 917, 915, 2681, 1627, 1597, 1595, 2325, 1769, 1749, 1747, 1499, 1438, 1435, 2204, + 1390, 1388, 1385, 1395, 2169, 2167, 1704, 1665, 1662, 1625, 1623, 1620, 1770, 1329, 1282, 1279, 2109, 1214, 1207, + 1222, 2068, 2065, 1149, 1147, 1144, 1141, 146, 1157, 1154, 2013, 2011, 2008, 2015, 1579, 1549, 1546, 1495, 1487, + 1433, 1431, 1428, 1425, 388, 1440, 2205, 1705, 658, 1667, 1664, 1119, 1095, 1093, 1978, 1057, 1055, 1052, 1062, + 1962, 1960, 1005, 1003, 1000, 997, 38, 1013, 1010, 1932, 1930, 1927, 1934, 941, 939, 936, 933, 6, 930, 3, 951, + 948, 944, 1889, 1887, 1884, 1881, 959, 1893, 1891, 35, 1377, 1360, 1358, 1327, 1325, 1322, 1331, 1277, 1275, + 1272, 1269, 235, 1284, 2110, 1205, 1204, 1201, 1198, 182, 1195, 179, 1213, 2070, 2067, 1580, 501, 1551, 1548, + 440, 437, 1497, 1494, 1490, 1503, 761, 709, 707, 1706, 913, 912, 2198, 1386, 2164, 2161, 1621, 1766, 2103, 1208, + 2058, 2054, 1145, 1142, 2005, 2002, 1999, 2009, 1488, 1429, 1426, 2200, 1698, 1659, 1656, 1975, 1053, 1957, 1954, + 1001, 998, 1924, 1921, 1918, 1928, 937, 934, 931, 1879, 1876, 1873, 1870, 945, 1885, 1882, 1323, 1273, 1270, + 2105, 1202, 1199, 1196, 1211, 2061, 2057, 1576, 1543, 1540, 1484, 1481, 1478, 1491, 1700}; +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Reader.java b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Reader.java new file mode 100644 index 0000000..c0eeb28 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Reader.java @@ -0,0 +1,135 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.multi.MultipleBarcodeReader; +import com.google.zxing.pdf417.decoder.PDF417ScanningDecoder; +import com.google.zxing.pdf417.detector.Detector; +import com.google.zxing.pdf417.detector.PDF417DetectorResult; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This implementation can detect and decode PDF417 codes in an image. + * + * @author Guenther Grau + */ +public final class PDF417Reader implements Reader, MultipleBarcodeReader { + + /** + * Locates and decodes a PDF417 code in an image. + * + * @return a String representing the content encoded by the PDF417 code + * @throws NotFoundException if a PDF417 code cannot be found, + * @throws FormatException if a PDF417 cannot be decoded + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, FormatException, ChecksumException { + return decode(image, null); + } + + @Override + public Result decode(BinaryBitmap image, Map hints) throws NotFoundException, FormatException, + ChecksumException { + Result[] result = decode(image, hints, false); + if (result == null || result.length == 0 || result[0] == null) { + throw NotFoundException.getNotFoundInstance(); + } + return result[0]; + } + + @Override + public Result[] decodeMultiple(BinaryBitmap image) throws NotFoundException { + return decodeMultiple(image, null); + } + + @Override + public Result[] decodeMultiple(BinaryBitmap image, Map hints) throws NotFoundException { + try { + return decode(image, hints, true); + } catch (FormatException | ChecksumException ignored) { + throw NotFoundException.getNotFoundInstance(); + } + } + + private static Result[] decode(BinaryBitmap image, Map hints, boolean multiple) + throws NotFoundException, FormatException, ChecksumException { + List results = new ArrayList<>(); + PDF417DetectorResult detectorResult = Detector.detect(image, hints, multiple); + for (ResultPoint[] points : detectorResult.getPoints()) { + DecoderResult decoderResult = PDF417ScanningDecoder.decode(detectorResult.getBits(), points[4], points[5], + points[6], points[7], getMinCodewordWidth(points), getMaxCodewordWidth(points)); + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.PDF_417); + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, decoderResult.getECLevel()); + PDF417ResultMetadata pdf417ResultMetadata = (PDF417ResultMetadata) decoderResult.getOther(); + if (pdf417ResultMetadata != null) { + result.putMetadata(ResultMetadataType.PDF417_EXTRA_METADATA, pdf417ResultMetadata); + } + results.add(result); + } + return results.toArray(new Result[results.size()]); + } + + private static int getMaxWidth(ResultPoint p1, ResultPoint p2) { + if (p1 == null || p2 == null) { + return 0; + } + return (int) Math.abs(p1.getX() - p2.getX()); + } + + private static int getMinWidth(ResultPoint p1, ResultPoint p2) { + if (p1 == null || p2 == null) { + return Integer.MAX_VALUE; + } + return (int) Math.abs(p1.getX() - p2.getX()); + } + + private static int getMaxCodewordWidth(ResultPoint[] p) { + return Math.max( + Math.max(getMaxWidth(p[0], p[4]), getMaxWidth(p[6], p[2]) * PDF417Common.MODULES_IN_CODEWORD / + PDF417Common.MODULES_IN_STOP_PATTERN), + Math.max(getMaxWidth(p[1], p[5]), getMaxWidth(p[7], p[3]) * PDF417Common.MODULES_IN_CODEWORD / + PDF417Common.MODULES_IN_STOP_PATTERN)); + } + + private static int getMinCodewordWidth(ResultPoint[] p) { + return Math.min( + Math.min(getMinWidth(p[0], p[4]), getMinWidth(p[6], p[2]) * PDF417Common.MODULES_IN_CODEWORD / + PDF417Common.MODULES_IN_STOP_PATTERN), + Math.min(getMinWidth(p[1], p[5]), getMinWidth(p[7], p[3]) * PDF417Common.MODULES_IN_CODEWORD / + PDF417Common.MODULES_IN_STOP_PATTERN)); + } + + @Override + public void reset() { + // nothing needs to be reset + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/PDF417ResultMetadata.java b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417ResultMetadata.java new file mode 100644 index 0000000..2e1c4ee --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417ResultMetadata.java @@ -0,0 +1,61 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417; + +/** + * @author Guenther Grau + */ +public final class PDF417ResultMetadata { + + private int segmentIndex; + private String fileId; + private int[] optionalData; + private boolean lastSegment; + + public int getSegmentIndex() { + return segmentIndex; + } + + public void setSegmentIndex(int segmentIndex) { + this.segmentIndex = segmentIndex; + } + + public String getFileId() { + return fileId; + } + + public void setFileId(String fileId) { + this.fileId = fileId; + } + + public int[] getOptionalData() { + return optionalData; + } + + public void setOptionalData(int[] optionalData) { + this.optionalData = optionalData; + } + + public boolean isLastSegment() { + return lastSegment; + } + + public void setLastSegment(boolean lastSegment) { + this.lastSegment = lastSegment; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Writer.java b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Writer.java new file mode 100644 index 0000000..1251d02 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/PDF417Writer.java @@ -0,0 +1,177 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.pdf417.encoder.Compaction; +import com.google.zxing.pdf417.encoder.Dimensions; +import com.google.zxing.pdf417.encoder.PDF417; + +import java.nio.charset.Charset; +import java.util.Map; + +/** + * @author Jacob Haynes + * @author qwandor@google.com (Andrew Walbran) + */ +public final class PDF417Writer implements Writer { + + /** + * default white space (margin) around the code + */ + private static final int WHITE_SPACE = 30; + + /** + * default error correction level + */ + private static final int DEFAULT_ERROR_CORRECTION_LEVEL = 2; + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + if (format != BarcodeFormat.PDF_417) { + throw new IllegalArgumentException("Can only encode PDF_417, but got " + format); + } + + PDF417 encoder = new PDF417(); + int margin = WHITE_SPACE; + int errorCorrectionLevel = DEFAULT_ERROR_CORRECTION_LEVEL; + + if (hints != null) { + if (hints.containsKey(EncodeHintType.PDF417_COMPACT)) { + encoder.setCompact(Boolean.valueOf(hints.get(EncodeHintType.PDF417_COMPACT).toString())); + } + if (hints.containsKey(EncodeHintType.PDF417_COMPACTION)) { + encoder.setCompaction(Compaction.valueOf(hints.get(EncodeHintType.PDF417_COMPACTION).toString())); + } + if (hints.containsKey(EncodeHintType.PDF417_DIMENSIONS)) { + Dimensions dimensions = (Dimensions) hints.get(EncodeHintType.PDF417_DIMENSIONS); + encoder.setDimensions(dimensions.getMaxCols(), + dimensions.getMinCols(), + dimensions.getMaxRows(), + dimensions.getMinRows()); + } + if (hints.containsKey(EncodeHintType.MARGIN)) { + margin = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString()); + } + if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) { + errorCorrectionLevel = Integer.parseInt(hints.get(EncodeHintType.ERROR_CORRECTION).toString()); + } + if (hints.containsKey(EncodeHintType.CHARACTER_SET)) { + Charset encoding = Charset.forName(hints.get(EncodeHintType.CHARACTER_SET).toString()); + encoder.setEncoding(encoding); + } + } + + return bitMatrixFromEncoder(encoder, contents, errorCorrectionLevel, width, height, margin); + } + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height) throws WriterException { + return encode(contents, format, width, height, null); + } + + /** + * Takes encoder, accounts for width/height, and retrieves bit matrix + */ + private static BitMatrix bitMatrixFromEncoder(PDF417 encoder, + String contents, + int errorCorrectionLevel, + int width, + int height, + int margin) throws WriterException { + encoder.generateBarcodeLogic(contents, errorCorrectionLevel); + + int aspectRatio = 4; + byte[][] originalScale = encoder.getBarcodeMatrix().getScaledMatrix(1, aspectRatio); + boolean rotated = false; + if ((height > width) != (originalScale[0].length < originalScale.length)) { + originalScale = rotateArray(originalScale); + rotated = true; + } + + int scaleX = width / originalScale[0].length; + int scaleY = height / originalScale.length; + + int scale; + if (scaleX < scaleY) { + scale = scaleX; + } else { + scale = scaleY; + } + + if (scale > 1) { + byte[][] scaledMatrix = + encoder.getBarcodeMatrix().getScaledMatrix(scale, scale * aspectRatio); + if (rotated) { + scaledMatrix = rotateArray(scaledMatrix); + } + return bitMatrixFromBitArray(scaledMatrix, margin); + } + return bitMatrixFromBitArray(originalScale, margin); + } + + /** + * This takes an array holding the values of the PDF 417 + * + * @param input a byte array of information with 0 is black, and 1 is white + * @param margin border around the barcode + * @return BitMatrix of the input + */ + private static BitMatrix bitMatrixFromBitArray(byte[][] input, int margin) { + // Creates the bit matrix with extra space for whitespace + BitMatrix output = new BitMatrix(input[0].length + 2 * margin, input.length + 2 * margin); + output.clear(); + for (int y = 0, yOutput = output.getHeight() - margin - 1; y < input.length; y++, yOutput--) { + byte[] inputY = input[y]; + for (int x = 0; x < input[0].length; x++) { + // Zero is white in the byte matrix + if (inputY[x] == 1) { + output.set(x + margin, yOutput); + } + } + } + return output; + } + + /** + * Takes and rotates the it 90 degrees + */ + private static byte[][] rotateArray(byte[][] bitarray) { + byte[][] temp = new byte[bitarray[0].length][bitarray.length]; + for (int ii = 0; ii < bitarray.length; ii++) { + // This makes the direction consistent on screen when rotating the + // screen; + int inverseii = bitarray.length - ii - 1; + for (int jj = 0; jj < bitarray[0].length; jj++) { + temp[jj][inverseii] = bitarray[ii][jj]; + } + } + return temp; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeMetadata.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeMetadata.java new file mode 100644 index 0000000..9c1acbf --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeMetadata.java @@ -0,0 +1,58 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +/** + * @author Guenther Grau + */ +final class BarcodeMetadata { + + private final int columnCount; + private final int errorCorrectionLevel; + private final int rowCountUpperPart; + private final int rowCountLowerPart; + private final int rowCount; + + BarcodeMetadata(int columnCount, int rowCountUpperPart, int rowCountLowerPart, int errorCorrectionLevel) { + this.columnCount = columnCount; + this.errorCorrectionLevel = errorCorrectionLevel; + this.rowCountUpperPart = rowCountUpperPart; + this.rowCountLowerPart = rowCountLowerPart; + this.rowCount = rowCountUpperPart + rowCountLowerPart; + } + + int getColumnCount() { + return columnCount; + } + + int getErrorCorrectionLevel() { + return errorCorrectionLevel; + } + + int getRowCount() { + return rowCount; + } + + int getRowCountUpperPart() { + return rowCountUpperPart; + } + + int getRowCountLowerPart() { + return rowCountLowerPart; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeValue.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeValue.java new file mode 100644 index 0000000..55a1c68 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BarcodeValue.java @@ -0,0 +1,68 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.pdf417.PDF417Common; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +/** + * @author Guenther Grau + */ +final class BarcodeValue { + private final Map values = new HashMap<>(); + + /** + * Add an occurrence of a value + */ + void setValue(int value) { + Integer confidence = values.get(value); + if (confidence == null) { + confidence = 0; + } + confidence++; + values.put(value, confidence); + } + + /** + * Determines the maximum occurrence of a set value and returns all values which were set with this occurrence. + * @return an array of int, containing the values with the highest occurrence, or null, if no value was set + */ + int[] getValue() { + int maxConfidence = -1; + Collection result = new ArrayList<>(); + for (Entry entry : values.entrySet()) { + if (entry.getValue() > maxConfidence) { + maxConfidence = entry.getValue(); + result.clear(); + result.add(entry.getKey()); + } else if (entry.getValue() == maxConfidence) { + result.add(entry.getKey()); + } + } + return PDF417Common.toIntArray(result); + } + + Integer getConfidence(int value) { + return values.get(value); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BoundingBox.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BoundingBox.java new file mode 100644 index 0000000..b773145 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/BoundingBox.java @@ -0,0 +1,176 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +/** + * @author Guenther Grau + */ +final class BoundingBox { + + private BitMatrix image; + private ResultPoint topLeft; + private ResultPoint bottomLeft; + private ResultPoint topRight; + private ResultPoint bottomRight; + private int minX; + private int maxX; + private int minY; + private int maxY; + + BoundingBox(BitMatrix image, + ResultPoint topLeft, + ResultPoint bottomLeft, + ResultPoint topRight, + ResultPoint bottomRight) throws NotFoundException { + if ((topLeft == null && topRight == null) || + (bottomLeft == null && bottomRight == null) || + (topLeft != null && bottomLeft == null) || + (topRight != null && bottomRight == null)) { + throw NotFoundException.getNotFoundInstance(); + } + init(image, topLeft, bottomLeft, topRight, bottomRight); + } + + BoundingBox(BoundingBox boundingBox) { + init(boundingBox.image, boundingBox.topLeft, boundingBox.bottomLeft, boundingBox.topRight, boundingBox.bottomRight); + } + + private void init(BitMatrix image, + ResultPoint topLeft, + ResultPoint bottomLeft, + ResultPoint topRight, + ResultPoint bottomRight) { + this.image = image; + this.topLeft = topLeft; + this.bottomLeft = bottomLeft; + this.topRight = topRight; + this.bottomRight = bottomRight; + calculateMinMaxValues(); + } + + static BoundingBox merge(BoundingBox leftBox, BoundingBox rightBox) throws NotFoundException { + if (leftBox == null) { + return rightBox; + } + if (rightBox == null) { + return leftBox; + } + return new BoundingBox(leftBox.image, leftBox.topLeft, leftBox.bottomLeft, rightBox.topRight, rightBox.bottomRight); + } + + BoundingBox addMissingRows(int missingStartRows, int missingEndRows, boolean isLeft) throws NotFoundException { + ResultPoint newTopLeft = topLeft; + ResultPoint newBottomLeft = bottomLeft; + ResultPoint newTopRight = topRight; + ResultPoint newBottomRight = bottomRight; + + if (missingStartRows > 0) { + ResultPoint top = isLeft ? topLeft : topRight; + int newMinY = (int) top.getY() - missingStartRows; + if (newMinY < 0) { + newMinY = 0; + } + ResultPoint newTop = new ResultPoint(top.getX(), newMinY); + if (isLeft) { + newTopLeft = newTop; + } else { + newTopRight = newTop; + } + } + + if (missingEndRows > 0) { + ResultPoint bottom = isLeft ? bottomLeft : bottomRight; + int newMaxY = (int) bottom.getY() + missingEndRows; + if (newMaxY >= image.getHeight()) { + newMaxY = image.getHeight() - 1; + } + ResultPoint newBottom = new ResultPoint(bottom.getX(), newMaxY); + if (isLeft) { + newBottomLeft = newBottom; + } else { + newBottomRight = newBottom; + } + } + + calculateMinMaxValues(); + return new BoundingBox(image, newTopLeft, newBottomLeft, newTopRight, newBottomRight); + } + + private void calculateMinMaxValues() { + if (topLeft == null) { + topLeft = new ResultPoint(0, topRight.getY()); + bottomLeft = new ResultPoint(0, bottomRight.getY()); + } else if (topRight == null) { + topRight = new ResultPoint(image.getWidth() - 1, topLeft.getY()); + bottomRight = new ResultPoint(image.getWidth() - 1, bottomLeft.getY()); + } + + minX = (int) Math.min(topLeft.getX(), bottomLeft.getX()); + maxX = (int) Math.max(topRight.getX(), bottomRight.getX()); + minY = (int) Math.min(topLeft.getY(), topRight.getY()); + maxY = (int) Math.max(bottomLeft.getY(), bottomRight.getY()); + } + + /* + void setTopRight(ResultPoint topRight) { + this.topRight = topRight; + calculateMinMaxValues(); + } + + void setBottomRight(ResultPoint bottomRight) { + this.bottomRight = bottomRight; + calculateMinMaxValues(); + } + */ + + int getMinX() { + return minX; + } + + int getMaxX() { + return maxX; + } + + int getMinY() { + return minY; + } + + int getMaxY() { + return maxY; + } + + ResultPoint getTopLeft() { + return topLeft; + } + + ResultPoint getTopRight() { + return topRight; + } + + ResultPoint getBottomLeft() { + return bottomLeft; + } + + ResultPoint getBottomRight() { + return bottomRight; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/Codeword.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/Codeword.java new file mode 100644 index 0000000..bb5477d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/Codeword.java @@ -0,0 +1,84 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +/** + * @author Guenther Grau + */ +final class Codeword { + + private static final int BARCODE_ROW_UNKNOWN = -1; + + private final int startX; + private final int endX; + private final int bucket; + private final int value; + private int rowNumber = BARCODE_ROW_UNKNOWN; + + Codeword(int startX, int endX, int bucket, int value) { + this.startX = startX; + this.endX = endX; + this.bucket = bucket; + this.value = value; + } + + boolean hasValidRowNumber() { + return isValidRowNumber(rowNumber); + } + + boolean isValidRowNumber(int rowNumber) { + return rowNumber != BARCODE_ROW_UNKNOWN && bucket == (rowNumber % 3) * 3; + } + + void setRowNumberAsRowIndicatorColumn() { + rowNumber = (value / 30) * 3 + bucket / 3; + } + + int getWidth() { + return endX - startX; + } + + int getStartX() { + return startX; + } + + int getEndX() { + return endX; + } + + int getBucket() { + return bucket; + } + + int getValue() { + return value; + } + + int getRowNumber() { + return rowNumber; + } + + void setRowNumber(int rowNumber) { + this.rowNumber = rowNumber; + } + + @Override + public String toString() { + return rowNumber + "|" + value; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..7cc8fbc --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DecodedBitStreamParser.java @@ -0,0 +1,677 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.pdf417.PDF417ResultMetadata; + +import java.io.ByteArrayOutputStream; +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + *

This class contains the methods for decoding the PDF417 codewords.

+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + * @author Guenther Grau + */ +final class DecodedBitStreamParser { + + private enum Mode { + ALPHA, + LOWER, + MIXED, + PUNCT, + ALPHA_SHIFT, + PUNCT_SHIFT + } + + private static final int TEXT_COMPACTION_MODE_LATCH = 900; + private static final int BYTE_COMPACTION_MODE_LATCH = 901; + private static final int NUMERIC_COMPACTION_MODE_LATCH = 902; + private static final int BYTE_COMPACTION_MODE_LATCH_6 = 924; + private static final int ECI_USER_DEFINED = 925; + private static final int ECI_GENERAL_PURPOSE = 926; + private static final int ECI_CHARSET = 927; + private static final int BEGIN_MACRO_PDF417_CONTROL_BLOCK = 928; + private static final int BEGIN_MACRO_PDF417_OPTIONAL_FIELD = 923; + private static final int MACRO_PDF417_TERMINATOR = 922; + private static final int MODE_SHIFT_TO_BYTE_COMPACTION_MODE = 913; + private static final int MAX_NUMERIC_CODEWORDS = 15; + + private static final int PL = 25; + private static final int LL = 27; + private static final int AS = 27; + private static final int ML = 28; + private static final int AL = 28; + private static final int PS = 29; + private static final int PAL = 29; + + private static final char[] PUNCT_CHARS = + ";<>@[\\]_`~!\r\t,:\n-.$/\"|*()?{}'".toCharArray(); + + private static final char[] MIXED_CHARS = + "0123456789&\r\t,:#-.$/+%*=^".toCharArray(); + + private static final Charset DEFAULT_ENCODING = Charset.forName("ISO-8859-1"); + + /** + * Table containing values for the exponent of 900. + * This is used in the numeric compaction decode algorithm. + */ + private static final BigInteger[] EXP900; + static { + EXP900 = new BigInteger[16]; + EXP900[0] = BigInteger.ONE; + BigInteger nineHundred = BigInteger.valueOf(900); + EXP900[1] = nineHundred; + for (int i = 2; i < EXP900.length; i++) { + EXP900[i] = EXP900[i - 1].multiply(nineHundred); + } + } + + private static final int NUMBER_OF_SEQUENCE_CODEWORDS = 2; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(int[] codewords, String ecLevel) throws FormatException { + StringBuilder result = new StringBuilder(codewords.length * 2); + Charset encoding = DEFAULT_ENCODING; + // Get compaction mode + int codeIndex = 1; + int code = codewords[codeIndex++]; + PDF417ResultMetadata resultMetadata = new PDF417ResultMetadata(); + while (codeIndex < codewords[0]) { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: + codeIndex = textCompaction(codewords, codeIndex, result); + break; + case BYTE_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH_6: + codeIndex = byteCompaction(code, codewords, encoding, codeIndex, result); + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + result.append((char) codewords[codeIndex++]); + break; + case NUMERIC_COMPACTION_MODE_LATCH: + codeIndex = numericCompaction(codewords, codeIndex, result); + break; + case ECI_CHARSET: + CharacterSetECI charsetECI = + CharacterSetECI.getCharacterSetECIByValue(codewords[codeIndex++]); + encoding = Charset.forName(charsetECI.name()); + break; + case ECI_GENERAL_PURPOSE: + // Can't do anything with generic ECI; skip its 2 characters + codeIndex += 2; + break; + case ECI_USER_DEFINED: + // Can't do anything with user ECI; skip its 1 character + codeIndex ++; + break; + case BEGIN_MACRO_PDF417_CONTROL_BLOCK: + codeIndex = decodeMacroBlock(codewords, codeIndex, resultMetadata); + break; + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + case MACRO_PDF417_TERMINATOR: + // Should not see these outside a macro block + throw FormatException.getFormatInstance(); + default: + // Default to text compaction. During testing numerous barcodes + // appeared to be missing the starting mode. In these cases defaulting + // to text compaction seems to work. + codeIndex--; + codeIndex = textCompaction(codewords, codeIndex, result); + break; + } + if (codeIndex < codewords.length) { + code = codewords[codeIndex++]; + } else { + throw FormatException.getFormatInstance(); + } + } + if (result.length() == 0) { + throw FormatException.getFormatInstance(); + } + DecoderResult decoderResult = new DecoderResult(null, result.toString(), null, ecLevel); + decoderResult.setOther(resultMetadata); + return decoderResult; + } + + private static int decodeMacroBlock(int[] codewords, int codeIndex, PDF417ResultMetadata resultMetadata) + throws FormatException { + if (codeIndex + NUMBER_OF_SEQUENCE_CODEWORDS > codewords[0]) { + // we must have at least two bytes left for the segment index + throw FormatException.getFormatInstance(); + } + int[] segmentIndexArray = new int[NUMBER_OF_SEQUENCE_CODEWORDS]; + for (int i = 0; i < NUMBER_OF_SEQUENCE_CODEWORDS; i++, codeIndex++) { + segmentIndexArray[i] = codewords[codeIndex]; + } + resultMetadata.setSegmentIndex(Integer.parseInt(decodeBase900toBase10(segmentIndexArray, + NUMBER_OF_SEQUENCE_CODEWORDS))); + + StringBuilder fileId = new StringBuilder(); + codeIndex = textCompaction(codewords, codeIndex, fileId); + resultMetadata.setFileId(fileId.toString()); + + switch (codewords[codeIndex]) { + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + codeIndex++; + int[] additionalOptionCodeWords = new int[codewords[0] - codeIndex]; + int additionalOptionCodeWordsIndex = 0; + + boolean end = false; + while ((codeIndex < codewords[0]) && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + additionalOptionCodeWords[additionalOptionCodeWordsIndex++] = code; + } else { + switch (code) { + case MACRO_PDF417_TERMINATOR: + resultMetadata.setLastSegment(true); + codeIndex++; + end = true; + break; + default: + throw FormatException.getFormatInstance(); + } + } + } + resultMetadata.setOptionalData(Arrays.copyOf(additionalOptionCodeWords, additionalOptionCodeWordsIndex)); + break; + case MACRO_PDF417_TERMINATOR: + resultMetadata.setLastSegment(true); + codeIndex++; + break; + } + + return codeIndex; + } + + /** + * Text Compaction mode (see 5.4.1.5) permits all printable ASCII characters to be + * encoded, i.e. values 32 - 126 inclusive in accordance with ISO/IEC 646 (IRV), as + * well as selected control characters. + * + * @param codewords The array of codewords (data + error) + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int textCompaction(int[] codewords, int codeIndex, StringBuilder result) { + // 2 character per codeword + int[] textCompactionData = new int[(codewords[0] - codeIndex) * 2]; + // Used to hold the byte compaction value if there is a mode shift + int[] byteCompactionData = new int[(codewords[0] - codeIndex) * 2]; + + int index = 0; + boolean end = false; + while ((codeIndex < codewords[0]) && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + textCompactionData[index] = code / 30; + textCompactionData[index + 1] = code % 30; + index += 2; + } else { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: + // reinitialize text compaction mode to alpha sub mode + textCompactionData[index++] = TEXT_COMPACTION_MODE_LATCH; + break; + case BYTE_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH_6: + case NUMERIC_COMPACTION_MODE_LATCH: + case BEGIN_MACRO_PDF417_CONTROL_BLOCK: + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + case MACRO_PDF417_TERMINATOR: + codeIndex--; + end = true; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + // The Mode Shift codeword 913 shall cause a temporary + // switch from Text Compaction mode to Byte Compaction mode. + // This switch shall be in effect for only the next codeword, + // after which the mode shall revert to the prevailing sub-mode + // of the Text Compaction mode. Codeword 913 is only available + // in Text Compaction mode; its use is described in 5.4.2.4. + textCompactionData[index] = MODE_SHIFT_TO_BYTE_COMPACTION_MODE; + code = codewords[codeIndex++]; + byteCompactionData[index] = code; + index++; + break; + } + } + } + decodeTextCompaction(textCompactionData, byteCompactionData, index, result); + return codeIndex; + } + + /** + * The Text Compaction mode includes all the printable ASCII characters + * (i.e. values from 32 to 126) and three ASCII control characters: HT or tab + * (ASCII value 9), LF or line feed (ASCII value 10), and CR or carriage + * return (ASCII value 13). The Text Compaction mode also includes various latch + * and shift characters which are used exclusively within the mode. The Text + * Compaction mode encodes up to 2 characters per codeword. The compaction rules + * for converting data into PDF417 codewords are defined in 5.4.2.2. The sub-mode + * switches are defined in 5.4.2.3. + * + * @param textCompactionData The text compaction data. + * @param byteCompactionData The byte compaction data if there + * was a mode shift. + * @param length The size of the text compaction and byte compaction data. + * @param result The decoded data is appended to the result. + */ + private static void decodeTextCompaction(int[] textCompactionData, + int[] byteCompactionData, + int length, + StringBuilder result) { + // Beginning from an initial state of the Alpha sub-mode + // The default compaction mode for PDF417 in effect at the start of each symbol shall always be Text + // Compaction mode Alpha sub-mode (uppercase alphabetic). A latch codeword from another mode to the Text + // Compaction mode shall always switch to the Text Compaction Alpha sub-mode. + Mode subMode = Mode.ALPHA; + Mode priorToShiftMode = Mode.ALPHA; + int i = 0; + while (i < length) { + int subModeCh = textCompactionData[i]; + char ch = 0; + switch (subMode) { + case ALPHA: + // Alpha (uppercase alphabetic) + if (subModeCh < 26) { + // Upper case Alpha Character + ch = (char) ('A' + subModeCh); + } else { + switch (subModeCh) { + case 26: + ch = ' '; + break; + case LL: + subMode = Mode.LOWER; + break; + case ML: + subMode = Mode.MIXED; + break; + case PS: + // Shift to punctuation + priorToShiftMode = subMode; + subMode = Mode.PUNCT_SHIFT; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + result.append((char) byteCompactionData[i]); + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + + case LOWER: + // Lower (lowercase alphabetic) + if (subModeCh < 26) { + ch = (char) ('a' + subModeCh); + } else { + switch (subModeCh) { + case 26: + ch = ' '; + break; + case AS: + // Shift to alpha + priorToShiftMode = subMode; + subMode = Mode.ALPHA_SHIFT; + break; + case ML: + subMode = Mode.MIXED; + break; + case PS: + // Shift to punctuation + priorToShiftMode = subMode; + subMode = Mode.PUNCT_SHIFT; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + // TODO Does this need to use the current character encoding? See other occurrences below + result.append((char) byteCompactionData[i]); + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + + case MIXED: + // Mixed (numeric and some punctuation) + if (subModeCh < PL) { + ch = MIXED_CHARS[subModeCh]; + } else { + switch (subModeCh) { + case PL: + subMode = Mode.PUNCT; + break; + case 26: + ch = ' '; + break; + case LL: + subMode = Mode.LOWER; + break; + case AL: + subMode = Mode.ALPHA; + break; + case PS: + // Shift to punctuation + priorToShiftMode = subMode; + subMode = Mode.PUNCT_SHIFT; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + result.append((char) byteCompactionData[i]); + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + + case PUNCT: + // Punctuation + if (subModeCh < PAL) { + ch = PUNCT_CHARS[subModeCh]; + } else { + switch (subModeCh) { + case PAL: + subMode = Mode.ALPHA; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + result.append((char) byteCompactionData[i]); + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + + case ALPHA_SHIFT: + // Restore sub-mode + subMode = priorToShiftMode; + if (subModeCh < 26) { + ch = (char) ('A' + subModeCh); + } else { + switch (subModeCh) { + case 26: + ch = ' '; + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + + case PUNCT_SHIFT: + // Restore sub-mode + subMode = priorToShiftMode; + if (subModeCh < PAL) { + ch = PUNCT_CHARS[subModeCh]; + } else { + switch (subModeCh) { + case PAL: + subMode = Mode.ALPHA; + break; + case MODE_SHIFT_TO_BYTE_COMPACTION_MODE: + // PS before Shift-to-Byte is used as a padding character, + // see 5.4.2.4 of the specification + result.append((char) byteCompactionData[i]); + break; + case TEXT_COMPACTION_MODE_LATCH: + subMode = Mode.ALPHA; + break; + } + } + break; + } + if (ch != 0) { + // Append decoded character to result + result.append(ch); + } + i++; + } + } + + /** + * Byte Compaction mode (see 5.4.3) permits all 256 possible 8-bit byte values to be encoded. + * This includes all ASCII characters value 0 to 127 inclusive and provides for international + * character set support. + * + * @param mode The byte compaction mode i.e. 901 or 924 + * @param codewords The array of codewords (data + error) + * @param encoding Currently active character encoding + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int byteCompaction(int mode, + int[] codewords, + Charset encoding, + int codeIndex, + StringBuilder result) { + ByteArrayOutputStream decodedBytes = new ByteArrayOutputStream(); + int count = 0; + long value = 0; + boolean end = false; + + switch (mode) { + case BYTE_COMPACTION_MODE_LATCH: + // Total number of Byte Compaction characters to be encoded + // is not a multiple of 6 + + int[] byteCompactedCodewords = new int[6]; + int nextCode = codewords[codeIndex++]; + while ((codeIndex < codewords[0]) && !end) { + byteCompactedCodewords[count++] = nextCode; + // Base 900 + value = 900 * value + nextCode; + nextCode = codewords[codeIndex++]; + // perhaps it should be ok to check only nextCode >= TEXT_COMPACTION_MODE_LATCH + switch (nextCode) { + case TEXT_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH: + case NUMERIC_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH_6: + case BEGIN_MACRO_PDF417_CONTROL_BLOCK: + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + case MACRO_PDF417_TERMINATOR: + codeIndex--; + end = true; + break; + default: + if ((count % 5 == 0) && (count > 0)) { + // Decode every 5 codewords + // Convert to Base 256 + for (int j = 0; j < 6; ++j) { + decodedBytes.write((byte) (value >> (8 * (5 - j)))); + } + value = 0; + count = 0; + } + break; + } + } + + // if the end of all codewords is reached the last codeword needs to be added + if (codeIndex == codewords[0] && nextCode < TEXT_COMPACTION_MODE_LATCH) { + byteCompactedCodewords[count++] = nextCode; + } + + // If Byte Compaction mode is invoked with codeword 901, + // the last group of codewords is interpreted directly + // as one byte per codeword, without compaction. + for (int i = 0; i < count; i++) { + decodedBytes.write((byte) byteCompactedCodewords[i]); + } + + break; + + case BYTE_COMPACTION_MODE_LATCH_6: + // Total number of Byte Compaction characters to be encoded + // is an integer multiple of 6 + while (codeIndex < codewords[0] && !end) { + int code = codewords[codeIndex++]; + if (code < TEXT_COMPACTION_MODE_LATCH) { + count++; + // Base 900 + value = 900 * value + code; + } else { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH: + case NUMERIC_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH_6: + case BEGIN_MACRO_PDF417_CONTROL_BLOCK: + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + case MACRO_PDF417_TERMINATOR: + codeIndex--; + end = true; + break; + } + } + if ((count % 5 == 0) && (count > 0)) { + // Decode every 5 codewords + // Convert to Base 256 + for (int j = 0; j < 6; ++j) { + decodedBytes.write((byte) (value >> (8 * (5 - j)))); + } + value = 0; + count = 0; + } + } + break; + } + result.append(new String(decodedBytes.toByteArray(), encoding)); + return codeIndex; + } + + /** + * Numeric Compaction mode (see 5.4.4) permits efficient encoding of numeric data strings. + * + * @param codewords The array of codewords (data + error) + * @param codeIndex The current index into the codeword array. + * @param result The decoded data is appended to the result. + * @return The next index into the codeword array. + */ + private static int numericCompaction(int[] codewords, int codeIndex, StringBuilder result) throws FormatException { + int count = 0; + boolean end = false; + + int[] numericCodewords = new int[MAX_NUMERIC_CODEWORDS]; + + while (codeIndex < codewords[0] && !end) { + int code = codewords[codeIndex++]; + if (codeIndex == codewords[0]) { + end = true; + } + if (code < TEXT_COMPACTION_MODE_LATCH) { + numericCodewords[count] = code; + count++; + } else { + switch (code) { + case TEXT_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH: + case BYTE_COMPACTION_MODE_LATCH_6: + case BEGIN_MACRO_PDF417_CONTROL_BLOCK: + case BEGIN_MACRO_PDF417_OPTIONAL_FIELD: + case MACRO_PDF417_TERMINATOR: + codeIndex--; + end = true; + break; + } + } + if ((count % MAX_NUMERIC_CODEWORDS == 0 || code == NUMERIC_COMPACTION_MODE_LATCH || end) && count > 0) { + // Re-invoking Numeric Compaction mode (by using codeword 902 + // while in Numeric Compaction mode) serves to terminate the + // current Numeric Compaction mode grouping as described in 5.4.4.2, + // and then to start a new one grouping. + result.append(decodeBase900toBase10(numericCodewords, count)); + count = 0; + } + } + return codeIndex; + } + + /** + * Convert a list of Numeric Compacted codewords from Base 900 to Base 10. + * + * @param codewords The array of codewords + * @param count The number of codewords + * @return The decoded string representing the Numeric data. + */ + /* + EXAMPLE + Encode the fifteen digit numeric string 000213298174000 + Prefix the numeric string with a 1 and set the initial value of + t = 1 000 213 298 174 000 + Calculate codeword 0 + d0 = 1 000 213 298 174 000 mod 900 = 200 + + t = 1 000 213 298 174 000 div 900 = 1 111 348 109 082 + Calculate codeword 1 + d1 = 1 111 348 109 082 mod 900 = 282 + + t = 1 111 348 109 082 div 900 = 1 234 831 232 + Calculate codeword 2 + d2 = 1 234 831 232 mod 900 = 632 + + t = 1 234 831 232 div 900 = 1 372 034 + Calculate codeword 3 + d3 = 1 372 034 mod 900 = 434 + + t = 1 372 034 div 900 = 1 524 + Calculate codeword 4 + d4 = 1 524 mod 900 = 624 + + t = 1 524 div 900 = 1 + Calculate codeword 5 + d5 = 1 mod 900 = 1 + t = 1 div 900 = 0 + Codeword sequence is: 1, 624, 434, 632, 282, 200 + + Decode the above codewords involves + 1 x 900 power of 5 + 624 x 900 power of 4 + 434 x 900 power of 3 + + 632 x 900 power of 2 + 282 x 900 power of 1 + 200 x 900 power of 0 = 1000213298174000 + + Remove leading 1 => Result is 000213298174000 + */ + private static String decodeBase900toBase10(int[] codewords, int count) throws FormatException { + BigInteger result = BigInteger.ZERO; + for (int i = 0; i < count; i++) { + result = result.add(EXP900[count - i - 1].multiply(BigInteger.valueOf(codewords[i]))); + } + String resultString = result.toString(); + if (resultString.charAt(0) != '1') { + throw FormatException.getFormatInstance(); + } + return resultString.substring(1); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResult.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResult.java new file mode 100644 index 0000000..dea45f6 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResult.java @@ -0,0 +1,296 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.pdf417.PDF417Common; + +import java.util.Formatter; + +/** + * @author Guenther Grau + */ +final class DetectionResult { + + private static final int ADJUST_ROW_NUMBER_SKIP = 2; + + private final BarcodeMetadata barcodeMetadata; + private final DetectionResultColumn[] detectionResultColumns; + private BoundingBox boundingBox; + private final int barcodeColumnCount; + + DetectionResult(BarcodeMetadata barcodeMetadata, BoundingBox boundingBox) { + this.barcodeMetadata = barcodeMetadata; + this.barcodeColumnCount = barcodeMetadata.getColumnCount(); + this.boundingBox = boundingBox; + detectionResultColumns = new DetectionResultColumn[barcodeColumnCount + 2]; + } + + DetectionResultColumn[] getDetectionResultColumns() { + adjustIndicatorColumnRowNumbers(detectionResultColumns[0]); + adjustIndicatorColumnRowNumbers(detectionResultColumns[barcodeColumnCount + 1]); + int unadjustedCodewordCount = PDF417Common.MAX_CODEWORDS_IN_BARCODE; + int previousUnadjustedCount; + do { + previousUnadjustedCount = unadjustedCodewordCount; + unadjustedCodewordCount = adjustRowNumbers(); + } while (unadjustedCodewordCount > 0 && unadjustedCodewordCount < previousUnadjustedCount); + return detectionResultColumns; + } + + private void adjustIndicatorColumnRowNumbers(DetectionResultColumn detectionResultColumn) { + if (detectionResultColumn != null) { + ((DetectionResultRowIndicatorColumn) detectionResultColumn) + .adjustCompleteIndicatorColumnRowNumbers(barcodeMetadata); + } + } + + // TODO ensure that no detected codewords with unknown row number are left + // we should be able to estimate the row height and use it as a hint for the row number + // we should also fill the rows top to bottom and bottom to top + /** + * @return number of codewords which don't have a valid row number. Note that the count is not accurate as codewords + * will be counted several times. It just serves as an indicator to see when we can stop adjusting row numbers + */ + private int adjustRowNumbers() { + int unadjustedCount = adjustRowNumbersByRow(); + if (unadjustedCount == 0) { + return 0; + } + for (int barcodeColumn = 1; barcodeColumn < barcodeColumnCount + 1; barcodeColumn++) { + Codeword[] codewords = detectionResultColumns[barcodeColumn].getCodewords(); + for (int codewordsRow = 0; codewordsRow < codewords.length; codewordsRow++) { + if (codewords[codewordsRow] == null) { + continue; + } + if (!codewords[codewordsRow].hasValidRowNumber()) { + adjustRowNumbers(barcodeColumn, codewordsRow, codewords); + } + } + } + return unadjustedCount; + } + + private int adjustRowNumbersByRow() { + adjustRowNumbersFromBothRI(); + // TODO we should only do full row adjustments if row numbers of left and right row indicator column match. + // Maybe it's even better to calculated the height (in codeword rows) and divide it by the number of barcode + // rows. This, together with the LRI and RRI row numbers should allow us to get a good estimate where a row + // number starts and ends. + int unadjustedCount = adjustRowNumbersFromLRI(); + return unadjustedCount + adjustRowNumbersFromRRI(); + } + + private void adjustRowNumbersFromBothRI() { + if (detectionResultColumns[0] == null || detectionResultColumns[barcodeColumnCount + 1] == null) { + return; + } + Codeword[] LRIcodewords = detectionResultColumns[0].getCodewords(); + Codeword[] RRIcodewords = detectionResultColumns[barcodeColumnCount + 1].getCodewords(); + for (int codewordsRow = 0; codewordsRow < LRIcodewords.length; codewordsRow++) { + if (LRIcodewords[codewordsRow] != null && + RRIcodewords[codewordsRow] != null && + LRIcodewords[codewordsRow].getRowNumber() == RRIcodewords[codewordsRow].getRowNumber()) { + for (int barcodeColumn = 1; barcodeColumn <= barcodeColumnCount; barcodeColumn++) { + Codeword codeword = detectionResultColumns[barcodeColumn].getCodewords()[codewordsRow]; + if (codeword == null) { + continue; + } + codeword.setRowNumber(LRIcodewords[codewordsRow].getRowNumber()); + if (!codeword.hasValidRowNumber()) { + detectionResultColumns[barcodeColumn].getCodewords()[codewordsRow] = null; + } + } + } + } + } + + private int adjustRowNumbersFromRRI() { + if (detectionResultColumns[barcodeColumnCount + 1] == null) { + return 0; + } + int unadjustedCount = 0; + Codeword[] codewords = detectionResultColumns[barcodeColumnCount + 1].getCodewords(); + for (int codewordsRow = 0; codewordsRow < codewords.length; codewordsRow++) { + if (codewords[codewordsRow] == null) { + continue; + } + int rowIndicatorRowNumber = codewords[codewordsRow].getRowNumber(); + int invalidRowCounts = 0; + for (int barcodeColumn = barcodeColumnCount + 1; barcodeColumn > 0 && invalidRowCounts < ADJUST_ROW_NUMBER_SKIP; barcodeColumn--) { + Codeword codeword = detectionResultColumns[barcodeColumn].getCodewords()[codewordsRow]; + if (codeword != null) { + invalidRowCounts = adjustRowNumberIfValid(rowIndicatorRowNumber, invalidRowCounts, codeword); + if (!codeword.hasValidRowNumber()) { + unadjustedCount++; + } + } + } + } + return unadjustedCount; + } + + private int adjustRowNumbersFromLRI() { + if (detectionResultColumns[0] == null) { + return 0; + } + int unadjustedCount = 0; + Codeword[] codewords = detectionResultColumns[0].getCodewords(); + for (int codewordsRow = 0; codewordsRow < codewords.length; codewordsRow++) { + if (codewords[codewordsRow] == null) { + continue; + } + int rowIndicatorRowNumber = codewords[codewordsRow].getRowNumber(); + int invalidRowCounts = 0; + for (int barcodeColumn = 1; barcodeColumn < barcodeColumnCount + 1 && invalidRowCounts < ADJUST_ROW_NUMBER_SKIP; barcodeColumn++) { + Codeword codeword = detectionResultColumns[barcodeColumn].getCodewords()[codewordsRow]; + if (codeword != null) { + invalidRowCounts = adjustRowNumberIfValid(rowIndicatorRowNumber, invalidRowCounts, codeword); + if (!codeword.hasValidRowNumber()) { + unadjustedCount++; + } + } + } + } + return unadjustedCount; + } + + private static int adjustRowNumberIfValid(int rowIndicatorRowNumber, int invalidRowCounts, Codeword codeword) { + if (codeword == null) { + return invalidRowCounts; + } + if (!codeword.hasValidRowNumber()) { + if (codeword.isValidRowNumber(rowIndicatorRowNumber)) { + codeword.setRowNumber(rowIndicatorRowNumber); + invalidRowCounts = 0; + } else { + ++invalidRowCounts; + } + } + return invalidRowCounts; + } + + private void adjustRowNumbers(int barcodeColumn, int codewordsRow, Codeword[] codewords) { + Codeword codeword = codewords[codewordsRow]; + Codeword[] previousColumnCodewords = detectionResultColumns[barcodeColumn - 1].getCodewords(); + Codeword[] nextColumnCodewords = previousColumnCodewords; + if (detectionResultColumns[barcodeColumn + 1] != null) { + nextColumnCodewords = detectionResultColumns[barcodeColumn + 1].getCodewords(); + } + + Codeword[] otherCodewords = new Codeword[14]; + + otherCodewords[2] = previousColumnCodewords[codewordsRow]; + otherCodewords[3] = nextColumnCodewords[codewordsRow]; + + if (codewordsRow > 0) { + otherCodewords[0] = codewords[codewordsRow - 1]; + otherCodewords[4] = previousColumnCodewords[codewordsRow - 1]; + otherCodewords[5] = nextColumnCodewords[codewordsRow - 1]; + } + if (codewordsRow > 1) { + otherCodewords[8] = codewords[codewordsRow - 2]; + otherCodewords[10] = previousColumnCodewords[codewordsRow - 2]; + otherCodewords[11] = nextColumnCodewords[codewordsRow - 2]; + } + if (codewordsRow < codewords.length - 1) { + otherCodewords[1] = codewords[codewordsRow + 1]; + otherCodewords[6] = previousColumnCodewords[codewordsRow + 1]; + otherCodewords[7] = nextColumnCodewords[codewordsRow + 1]; + } + if (codewordsRow < codewords.length - 2) { + otherCodewords[9] = codewords[codewordsRow + 2]; + otherCodewords[12] = previousColumnCodewords[codewordsRow + 2]; + otherCodewords[13] = nextColumnCodewords[codewordsRow + 2]; + } + for (Codeword otherCodeword : otherCodewords) { + if (adjustRowNumber(codeword, otherCodeword)) { + return; + } + } + } + + /** + * @return true, if row number was adjusted, false otherwise + */ + private static boolean adjustRowNumber(Codeword codeword, Codeword otherCodeword) { + if (otherCodeword == null) { + return false; + } + if (otherCodeword.hasValidRowNumber() && otherCodeword.getBucket() == codeword.getBucket()) { + codeword.setRowNumber(otherCodeword.getRowNumber()); + return true; + } + return false; + } + + int getBarcodeColumnCount() { + return barcodeColumnCount; + } + + int getBarcodeRowCount() { + return barcodeMetadata.getRowCount(); + } + + int getBarcodeECLevel() { + return barcodeMetadata.getErrorCorrectionLevel(); + } + + void setBoundingBox(BoundingBox boundingBox) { + this.boundingBox = boundingBox; + } + + BoundingBox getBoundingBox() { + return boundingBox; + } + + void setDetectionResultColumn(int barcodeColumn, DetectionResultColumn detectionResultColumn) { + detectionResultColumns[barcodeColumn] = detectionResultColumn; + } + + DetectionResultColumn getDetectionResultColumn(int barcodeColumn) { + return detectionResultColumns[barcodeColumn]; + } + + @Override + public String toString() { + DetectionResultColumn rowIndicatorColumn = detectionResultColumns[0]; + if (rowIndicatorColumn == null) { + rowIndicatorColumn = detectionResultColumns[barcodeColumnCount + 1]; + } + Formatter formatter = new Formatter(); + for (int codewordsRow = 0; codewordsRow < rowIndicatorColumn.getCodewords().length; codewordsRow++) { + formatter.format("CW %3d:", codewordsRow); + for (int barcodeColumn = 0; barcodeColumn < barcodeColumnCount + 2; barcodeColumn++) { + if (detectionResultColumns[barcodeColumn] == null) { + formatter.format(" | "); + continue; + } + Codeword codeword = detectionResultColumns[barcodeColumn].getCodewords()[codewordsRow]; + if (codeword == null) { + formatter.format(" | "); + continue; + } + formatter.format(" %3d|%3d", codeword.getRowNumber(), codeword.getValue()); + } + formatter.format("%n"); + } + String result = formatter.toString(); + formatter.close(); + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultColumn.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultColumn.java new file mode 100644 index 0000000..e0ea96d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultColumn.java @@ -0,0 +1,96 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import java.util.Formatter; + +/** + * @author Guenther Grau + */ +class DetectionResultColumn { + + private static final int MAX_NEARBY_DISTANCE = 5; + + private final BoundingBox boundingBox; + private final Codeword[] codewords; + + DetectionResultColumn(BoundingBox boundingBox) { + this.boundingBox = new BoundingBox(boundingBox); + codewords = new Codeword[boundingBox.getMaxY() - boundingBox.getMinY() + 1]; + } + + final Codeword getCodewordNearby(int imageRow) { + Codeword codeword = getCodeword(imageRow); + if (codeword != null) { + return codeword; + } + for (int i = 1; i < MAX_NEARBY_DISTANCE; i++) { + int nearImageRow = imageRowToCodewordIndex(imageRow) - i; + if (nearImageRow >= 0) { + codeword = codewords[nearImageRow]; + if (codeword != null) { + return codeword; + } + } + nearImageRow = imageRowToCodewordIndex(imageRow) + i; + if (nearImageRow < codewords.length) { + codeword = codewords[nearImageRow]; + if (codeword != null) { + return codeword; + } + } + } + return null; + } + + final int imageRowToCodewordIndex(int imageRow) { + return imageRow - boundingBox.getMinY(); + } + + final void setCodeword(int imageRow, Codeword codeword) { + codewords[imageRowToCodewordIndex(imageRow)] = codeword; + } + + final Codeword getCodeword(int imageRow) { + return codewords[imageRowToCodewordIndex(imageRow)]; + } + + final BoundingBox getBoundingBox() { + return boundingBox; + } + + final Codeword[] getCodewords() { + return codewords; + } + + @Override + public String toString() { + Formatter formatter = new Formatter(); + int row = 0; + for (Codeword codeword : codewords) { + if (codeword == null) { + formatter.format("%3d: | %n", row++); + continue; + } + formatter.format("%3d: %3d|%3d%n", row++, codeword.getRowNumber(), codeword.getValue()); + } + String result = formatter.toString(); + formatter.close(); + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultRowIndicatorColumn.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultRowIndicatorColumn.java new file mode 100644 index 0000000..89fd929 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/DetectionResultRowIndicatorColumn.java @@ -0,0 +1,267 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.ResultPoint; +import com.google.zxing.pdf417.PDF417Common; + +/** + * @author Guenther Grau + */ +final class DetectionResultRowIndicatorColumn extends DetectionResultColumn { + + private final boolean isLeft; + + DetectionResultRowIndicatorColumn(BoundingBox boundingBox, boolean isLeft) { + super(boundingBox); + this.isLeft = isLeft; + } + + private void setRowNumbers() { + for (Codeword codeword : getCodewords()) { + if (codeword != null) { + codeword.setRowNumberAsRowIndicatorColumn(); + } + } + } + + // TODO implement properly + // TODO maybe we should add missing codewords to store the correct row number to make + // finding row numbers for other columns easier + // use row height count to make detection of invalid row numbers more reliable + void adjustCompleteIndicatorColumnRowNumbers(BarcodeMetadata barcodeMetadata) { + Codeword[] codewords = getCodewords(); + setRowNumbers(); + removeIncorrectCodewords(codewords, barcodeMetadata); + BoundingBox boundingBox = getBoundingBox(); + ResultPoint top = isLeft ? boundingBox.getTopLeft() : boundingBox.getTopRight(); + ResultPoint bottom = isLeft ? boundingBox.getBottomLeft() : boundingBox.getBottomRight(); + int firstRow = imageRowToCodewordIndex((int) top.getY()); + int lastRow = imageRowToCodewordIndex((int) bottom.getY()); + // We need to be careful using the average row height. Barcode could be skewed so that we have smaller and + // taller rows + //float averageRowHeight = (lastRow - firstRow) / (float) barcodeMetadata.getRowCount(); + int barcodeRow = -1; + int maxRowHeight = 1; + int currentRowHeight = 0; + for (int codewordsRow = firstRow; codewordsRow < lastRow; codewordsRow++) { + if (codewords[codewordsRow] == null) { + continue; + } + Codeword codeword = codewords[codewordsRow]; + + // float expectedRowNumber = (codewordsRow - firstRow) / averageRowHeight; + // if (Math.abs(codeword.getRowNumber() - expectedRowNumber) > 2) { + // SimpleLog.log(LEVEL.WARNING, + // "Removing codeword, rowNumberSkew too high, codeword[" + codewordsRow + "]: Expected Row: " + + // expectedRowNumber + ", RealRow: " + codeword.getRowNumber() + ", value: " + codeword.getValue()); + // codewords[codewordsRow] = null; + // } + + int rowDifference = codeword.getRowNumber() - barcodeRow; + + // TODO improve handling with case where first row indicator doesn't start with 0 + + if (rowDifference == 0) { + currentRowHeight++; + } else if (rowDifference == 1) { + maxRowHeight = Math.max(maxRowHeight, currentRowHeight); + currentRowHeight = 1; + barcodeRow = codeword.getRowNumber(); + } else if (rowDifference < 0 || + codeword.getRowNumber() >= barcodeMetadata.getRowCount() || + rowDifference > codewordsRow) { + codewords[codewordsRow] = null; + } else { + int checkedRows; + if (maxRowHeight > 2) { + checkedRows = (maxRowHeight - 2) * rowDifference; + } else { + checkedRows = rowDifference; + } + boolean closePreviousCodewordFound = checkedRows >= codewordsRow; + for (int i = 1; i <= checkedRows && !closePreviousCodewordFound; i++) { + // there must be (height * rowDifference) number of codewords missing. For now we assume height = 1. + // This should hopefully get rid of most problems already. + closePreviousCodewordFound = codewords[codewordsRow - i] != null; + } + if (closePreviousCodewordFound) { + codewords[codewordsRow] = null; + } else { + barcodeRow = codeword.getRowNumber(); + currentRowHeight = 1; + } + } + } + //return (int) (averageRowHeight + 0.5); + } + + int[] getRowHeights() { + BarcodeMetadata barcodeMetadata = getBarcodeMetadata(); + if (barcodeMetadata == null) { + return null; + } + adjustIncompleteIndicatorColumnRowNumbers(barcodeMetadata); + int[] result = new int[barcodeMetadata.getRowCount()]; + for (Codeword codeword : getCodewords()) { + if (codeword != null) { + int rowNumber = codeword.getRowNumber(); + if (rowNumber >= result.length) { + // We have more rows than the barcode metadata allows for, ignore them. + continue; + } + result[rowNumber]++; + } // else throw exception? + } + return result; + } + + // TODO maybe we should add missing codewords to store the correct row number to make + // finding row numbers for other columns easier + // use row height count to make detection of invalid row numbers more reliable + private void adjustIncompleteIndicatorColumnRowNumbers(BarcodeMetadata barcodeMetadata) { + BoundingBox boundingBox = getBoundingBox(); + ResultPoint top = isLeft ? boundingBox.getTopLeft() : boundingBox.getTopRight(); + ResultPoint bottom = isLeft ? boundingBox.getBottomLeft() : boundingBox.getBottomRight(); + int firstRow = imageRowToCodewordIndex((int) top.getY()); + int lastRow = imageRowToCodewordIndex((int) bottom.getY()); + //float averageRowHeight = (lastRow - firstRow) / (float) barcodeMetadata.getRowCount(); + Codeword[] codewords = getCodewords(); + int barcodeRow = -1; + int maxRowHeight = 1; + int currentRowHeight = 0; + for (int codewordsRow = firstRow; codewordsRow < lastRow; codewordsRow++) { + if (codewords[codewordsRow] == null) { + continue; + } + Codeword codeword = codewords[codewordsRow]; + + codeword.setRowNumberAsRowIndicatorColumn(); + + int rowDifference = codeword.getRowNumber() - barcodeRow; + + // TODO improve handling with case where first row indicator doesn't start with 0 + + if (rowDifference == 0) { + currentRowHeight++; + } else if (rowDifference == 1) { + maxRowHeight = Math.max(maxRowHeight, currentRowHeight); + currentRowHeight = 1; + barcodeRow = codeword.getRowNumber(); + } else if (codeword.getRowNumber() >= barcodeMetadata.getRowCount()) { + codewords[codewordsRow] = null; + } else { + barcodeRow = codeword.getRowNumber(); + currentRowHeight = 1; + } + } + //return (int) (averageRowHeight + 0.5); + } + + BarcodeMetadata getBarcodeMetadata() { + Codeword[] codewords = getCodewords(); + BarcodeValue barcodeColumnCount = new BarcodeValue(); + BarcodeValue barcodeRowCountUpperPart = new BarcodeValue(); + BarcodeValue barcodeRowCountLowerPart = new BarcodeValue(); + BarcodeValue barcodeECLevel = new BarcodeValue(); + for (Codeword codeword : codewords) { + if (codeword == null) { + continue; + } + codeword.setRowNumberAsRowIndicatorColumn(); + int rowIndicatorValue = codeword.getValue() % 30; + int codewordRowNumber = codeword.getRowNumber(); + if (!isLeft) { + codewordRowNumber += 2; + } + switch (codewordRowNumber % 3) { + case 0: + barcodeRowCountUpperPart.setValue(rowIndicatorValue * 3 + 1); + break; + case 1: + barcodeECLevel.setValue(rowIndicatorValue / 3); + barcodeRowCountLowerPart.setValue(rowIndicatorValue % 3); + break; + case 2: + barcodeColumnCount.setValue(rowIndicatorValue + 1); + break; + } + } + // Maybe we should check if we have ambiguous values? + if ((barcodeColumnCount.getValue().length == 0) || + (barcodeRowCountUpperPart.getValue().length == 0) || + (barcodeRowCountLowerPart.getValue().length == 0) || + (barcodeECLevel.getValue().length == 0) || + barcodeColumnCount.getValue()[0] < 1 || + barcodeRowCountUpperPart.getValue()[0] + barcodeRowCountLowerPart.getValue()[0] < PDF417Common.MIN_ROWS_IN_BARCODE || + barcodeRowCountUpperPart.getValue()[0] + barcodeRowCountLowerPart.getValue()[0] > PDF417Common.MAX_ROWS_IN_BARCODE) { + return null; + } + BarcodeMetadata barcodeMetadata = new BarcodeMetadata(barcodeColumnCount.getValue()[0], + barcodeRowCountUpperPart.getValue()[0], barcodeRowCountLowerPart.getValue()[0], barcodeECLevel.getValue()[0]); + removeIncorrectCodewords(codewords, barcodeMetadata); + return barcodeMetadata; + } + + private void removeIncorrectCodewords(Codeword[] codewords, BarcodeMetadata barcodeMetadata) { + // Remove codewords which do not match the metadata + // TODO Maybe we should keep the incorrect codewords for the start and end positions? + for (int codewordRow = 0; codewordRow < codewords.length; codewordRow++) { + Codeword codeword = codewords[codewordRow]; + if (codewords[codewordRow] == null) { + continue; + } + int rowIndicatorValue = codeword.getValue() % 30; + int codewordRowNumber = codeword.getRowNumber(); + if (codewordRowNumber > barcodeMetadata.getRowCount()) { + codewords[codewordRow] = null; + continue; + } + if (!isLeft) { + codewordRowNumber += 2; + } + switch (codewordRowNumber % 3) { + case 0: + if (rowIndicatorValue * 3 + 1 != barcodeMetadata.getRowCountUpperPart()) { + codewords[codewordRow] = null; + } + break; + case 1: + if (rowIndicatorValue / 3 != barcodeMetadata.getErrorCorrectionLevel() || + rowIndicatorValue % 3 != barcodeMetadata.getRowCountLowerPart()) { + codewords[codewordRow] = null; + } + break; + case 2: + if (rowIndicatorValue + 1 != barcodeMetadata.getColumnCount()) { + codewords[codewordRow] = null; + } + break; + } + } + } + + boolean isLeft() { + return isLeft; + } + + @Override + public String toString() { + return "IsLeft: " + isLeft + '\n' + super.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417CodewordDecoder.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417CodewordDecoder.java new file mode 100644 index 0000000..e4dee58 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417CodewordDecoder.java @@ -0,0 +1,120 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.pdf417.PDF417Common; + +/** + * @author Guenther Grau + * @author creatale GmbH (christoph.schulz@creatale.de) + */ +final class PDF417CodewordDecoder { + + private static final float[][] RATIOS_TABLE = + new float[PDF417Common.SYMBOL_TABLE.length][PDF417Common.BARS_IN_MODULE]; + + static { + // Pre-computes the symbol ratio table. + for (int i = 0; i < PDF417Common.SYMBOL_TABLE.length; i++) { + int currentSymbol = PDF417Common.SYMBOL_TABLE[i]; + int currentBit = currentSymbol & 0x1; + for (int j = 0; j < PDF417Common.BARS_IN_MODULE; j++) { + float size = 0.0f; + while ((currentSymbol & 0x1) == currentBit) { + size += 1.0f; + currentSymbol >>= 1; + } + currentBit = currentSymbol & 0x1; + RATIOS_TABLE[i][PDF417Common.BARS_IN_MODULE - j - 1] = size / PDF417Common.MODULES_IN_CODEWORD; + } + } + } + + private PDF417CodewordDecoder() { + } + + static int getDecodedValue(int[] moduleBitCount) { + int decodedValue = getDecodedCodewordValue(sampleBitCounts(moduleBitCount)); + if (decodedValue != -1) { + return decodedValue; + } + return getClosestDecodedValue(moduleBitCount); + } + + private static int[] sampleBitCounts(int[] moduleBitCount) { + float bitCountSum = MathUtils.sum(moduleBitCount); + int[] result = new int[PDF417Common.BARS_IN_MODULE]; + int bitCountIndex = 0; + int sumPreviousBits = 0; + for (int i = 0; i < PDF417Common.MODULES_IN_CODEWORD; i++) { + float sampleIndex = + bitCountSum / (2 * PDF417Common.MODULES_IN_CODEWORD) + + (i * bitCountSum) / PDF417Common.MODULES_IN_CODEWORD; + if (sumPreviousBits + moduleBitCount[bitCountIndex] <= sampleIndex) { + sumPreviousBits += moduleBitCount[bitCountIndex]; + bitCountIndex++; + } + result[bitCountIndex]++; + } + return result; + } + + private static int getDecodedCodewordValue(int[] moduleBitCount) { + int decodedValue = getBitValue(moduleBitCount); + return PDF417Common.getCodeword(decodedValue) == -1 ? -1 : decodedValue; + } + + private static int getBitValue(int[] moduleBitCount) { + long result = 0; + for (int i = 0; i < moduleBitCount.length; i++) { + for (int bit = 0; bit < moduleBitCount[i]; bit++) { + result = (result << 1) | (i % 2 == 0 ? 1 : 0); + } + } + return (int) result; + } + + private static int getClosestDecodedValue(int[] moduleBitCount) { + int bitCountSum = MathUtils.sum(moduleBitCount); + float[] bitCountRatios = new float[PDF417Common.BARS_IN_MODULE]; + if (bitCountSum > 1) { + for (int i = 0; i < bitCountRatios.length; i++) { + bitCountRatios[i] = moduleBitCount[i] / (float) bitCountSum; + } + } + float bestMatchError = Float.MAX_VALUE; + int bestMatch = -1; + for (int j = 0; j < RATIOS_TABLE.length; j++) { + float error = 0.0f; + float[] ratioTableRow = RATIOS_TABLE[j]; + for (int k = 0; k < PDF417Common.BARS_IN_MODULE; k++) { + float diff = ratioTableRow[k] - bitCountRatios[k]; + error += diff * diff; + if (error >= bestMatchError) { + break; + } + } + if (error < bestMatchError) { + bestMatchError = error; + bestMatch = PDF417Common.SYMBOL_TABLE[j]; + } + } + return bestMatch; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417ScanningDecoder.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417ScanningDecoder.java new file mode 100644 index 0000000..4504355 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/PDF417ScanningDecoder.java @@ -0,0 +1,632 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.pdf417.PDF417Common; +import com.google.zxing.pdf417.decoder.ec.ErrorCorrection; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Formatter; +import java.util.List; + +/** + * @author Guenther Grau + */ +public final class PDF417ScanningDecoder { + + private static final int CODEWORD_SKEW_SIZE = 2; + + private static final int MAX_ERRORS = 3; + private static final int MAX_EC_CODEWORDS = 512; + private static final ErrorCorrection errorCorrection = new ErrorCorrection(); + + private PDF417ScanningDecoder() { + } + + // TODO don't pass in minCodewordWidth and maxCodewordWidth, pass in barcode columns for start and stop pattern + // columns. That way width can be deducted from the pattern column. + // This approach also allows to detect more details about the barcode, e.g. if a bar type (white or black) is wider + // than it should be. This can happen if the scanner used a bad blackpoint. + public static DecoderResult decode(BitMatrix image, + ResultPoint imageTopLeft, + ResultPoint imageBottomLeft, + ResultPoint imageTopRight, + ResultPoint imageBottomRight, + int minCodewordWidth, + int maxCodewordWidth) throws NotFoundException, FormatException, ChecksumException { + BoundingBox boundingBox = new BoundingBox(image, imageTopLeft, imageBottomLeft, imageTopRight, imageBottomRight); + DetectionResultRowIndicatorColumn leftRowIndicatorColumn = null; + DetectionResultRowIndicatorColumn rightRowIndicatorColumn = null; + DetectionResult detectionResult = null; + for (int i = 0; i < 2; i++) { + if (imageTopLeft != null) { + leftRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopLeft, true, minCodewordWidth, + maxCodewordWidth); + } + if (imageTopRight != null) { + rightRowIndicatorColumn = getRowIndicatorColumn(image, boundingBox, imageTopRight, false, minCodewordWidth, + maxCodewordWidth); + } + detectionResult = merge(leftRowIndicatorColumn, rightRowIndicatorColumn); + if (detectionResult == null) { + throw NotFoundException.getNotFoundInstance(); + } + if (i == 0 && detectionResult.getBoundingBox() != null && + (detectionResult.getBoundingBox().getMinY() < boundingBox.getMinY() || detectionResult.getBoundingBox() + .getMaxY() > boundingBox.getMaxY())) { + boundingBox = detectionResult.getBoundingBox(); + } else { + detectionResult.setBoundingBox(boundingBox); + break; + } + } + int maxBarcodeColumn = detectionResult.getBarcodeColumnCount() + 1; + detectionResult.setDetectionResultColumn(0, leftRowIndicatorColumn); + detectionResult.setDetectionResultColumn(maxBarcodeColumn, rightRowIndicatorColumn); + + boolean leftToRight = leftRowIndicatorColumn != null; + for (int barcodeColumnCount = 1; barcodeColumnCount <= maxBarcodeColumn; barcodeColumnCount++) { + int barcodeColumn = leftToRight ? barcodeColumnCount : maxBarcodeColumn - barcodeColumnCount; + if (detectionResult.getDetectionResultColumn(barcodeColumn) != null) { + // This will be the case for the opposite row indicator column, which doesn't need to be decoded again. + continue; + } + DetectionResultColumn detectionResultColumn; + if (barcodeColumn == 0 || barcodeColumn == maxBarcodeColumn) { + detectionResultColumn = new DetectionResultRowIndicatorColumn(boundingBox, barcodeColumn == 0); + } else { + detectionResultColumn = new DetectionResultColumn(boundingBox); + } + detectionResult.setDetectionResultColumn(barcodeColumn, detectionResultColumn); + int startColumn = -1; + int previousStartColumn = startColumn; + // TODO start at a row for which we know the start position, then detect upwards and downwards from there. + for (int imageRow = boundingBox.getMinY(); imageRow <= boundingBox.getMaxY(); imageRow++) { + startColumn = getStartColumn(detectionResult, barcodeColumn, imageRow, leftToRight); + if (startColumn < 0 || startColumn > boundingBox.getMaxX()) { + if (previousStartColumn == -1) { + continue; + } + startColumn = previousStartColumn; + } + Codeword codeword = detectCodeword(image, boundingBox.getMinX(), boundingBox.getMaxX(), leftToRight, + startColumn, imageRow, minCodewordWidth, maxCodewordWidth); + if (codeword != null) { + detectionResultColumn.setCodeword(imageRow, codeword); + previousStartColumn = startColumn; + minCodewordWidth = Math.min(minCodewordWidth, codeword.getWidth()); + maxCodewordWidth = Math.max(maxCodewordWidth, codeword.getWidth()); + } + } + } + return createDecoderResult(detectionResult); + } + + private static DetectionResult merge(DetectionResultRowIndicatorColumn leftRowIndicatorColumn, + DetectionResultRowIndicatorColumn rightRowIndicatorColumn) + throws NotFoundException { + if (leftRowIndicatorColumn == null && rightRowIndicatorColumn == null) { + return null; + } + BarcodeMetadata barcodeMetadata = getBarcodeMetadata(leftRowIndicatorColumn, rightRowIndicatorColumn); + if (barcodeMetadata == null) { + return null; + } + BoundingBox boundingBox = BoundingBox.merge(adjustBoundingBox(leftRowIndicatorColumn), + adjustBoundingBox(rightRowIndicatorColumn)); + return new DetectionResult(barcodeMetadata, boundingBox); + } + + private static BoundingBox adjustBoundingBox(DetectionResultRowIndicatorColumn rowIndicatorColumn) + throws NotFoundException { + if (rowIndicatorColumn == null) { + return null; + } + int[] rowHeights = rowIndicatorColumn.getRowHeights(); + if (rowHeights == null) { + return null; + } + int maxRowHeight = getMax(rowHeights); + int missingStartRows = 0; + for (int rowHeight : rowHeights) { + missingStartRows += maxRowHeight - rowHeight; + if (rowHeight > 0) { + break; + } + } + Codeword[] codewords = rowIndicatorColumn.getCodewords(); + for (int row = 0; missingStartRows > 0 && codewords[row] == null; row++) { + missingStartRows--; + } + int missingEndRows = 0; + for (int row = rowHeights.length - 1; row >= 0; row--) { + missingEndRows += maxRowHeight - rowHeights[row]; + if (rowHeights[row] > 0) { + break; + } + } + for (int row = codewords.length - 1; missingEndRows > 0 && codewords[row] == null; row--) { + missingEndRows--; + } + return rowIndicatorColumn.getBoundingBox().addMissingRows(missingStartRows, missingEndRows, + rowIndicatorColumn.isLeft()); + } + + private static int getMax(int[] values) { + int maxValue = -1; + for (int value : values) { + maxValue = Math.max(maxValue, value); + } + return maxValue; + } + + private static BarcodeMetadata getBarcodeMetadata(DetectionResultRowIndicatorColumn leftRowIndicatorColumn, + DetectionResultRowIndicatorColumn rightRowIndicatorColumn) { + BarcodeMetadata leftBarcodeMetadata; + if (leftRowIndicatorColumn == null || + (leftBarcodeMetadata = leftRowIndicatorColumn.getBarcodeMetadata()) == null) { + return rightRowIndicatorColumn == null ? null : rightRowIndicatorColumn.getBarcodeMetadata(); + } + BarcodeMetadata rightBarcodeMetadata; + if (rightRowIndicatorColumn == null || + (rightBarcodeMetadata = rightRowIndicatorColumn.getBarcodeMetadata()) == null) { + return leftBarcodeMetadata; + } + + if (leftBarcodeMetadata.getColumnCount() != rightBarcodeMetadata.getColumnCount() && + leftBarcodeMetadata.getErrorCorrectionLevel() != rightBarcodeMetadata.getErrorCorrectionLevel() && + leftBarcodeMetadata.getRowCount() != rightBarcodeMetadata.getRowCount()) { + return null; + } + return leftBarcodeMetadata; + } + + private static DetectionResultRowIndicatorColumn getRowIndicatorColumn(BitMatrix image, + BoundingBox boundingBox, + ResultPoint startPoint, + boolean leftToRight, + int minCodewordWidth, + int maxCodewordWidth) { + DetectionResultRowIndicatorColumn rowIndicatorColumn = new DetectionResultRowIndicatorColumn(boundingBox, + leftToRight); + for (int i = 0; i < 2; i++) { + int increment = i == 0 ? 1 : -1; + int startColumn = (int) startPoint.getX(); + for (int imageRow = (int) startPoint.getY(); imageRow <= boundingBox.getMaxY() && + imageRow >= boundingBox.getMinY(); imageRow += increment) { + Codeword codeword = detectCodeword(image, 0, image.getWidth(), leftToRight, startColumn, imageRow, + minCodewordWidth, maxCodewordWidth); + if (codeword != null) { + rowIndicatorColumn.setCodeword(imageRow, codeword); + if (leftToRight) { + startColumn = codeword.getStartX(); + } else { + startColumn = codeword.getEndX(); + } + } + } + } + return rowIndicatorColumn; + } + + private static void adjustCodewordCount(DetectionResult detectionResult, BarcodeValue[][] barcodeMatrix) + throws NotFoundException { + BarcodeValue barcodeMatrix01 = barcodeMatrix[0][1]; + int[] numberOfCodewords = barcodeMatrix01.getValue(); + int calculatedNumberOfCodewords = detectionResult.getBarcodeColumnCount() * + detectionResult.getBarcodeRowCount() - + getNumberOfECCodeWords(detectionResult.getBarcodeECLevel()); + if (numberOfCodewords.length == 0) { + if (calculatedNumberOfCodewords < 1 || calculatedNumberOfCodewords > PDF417Common.MAX_CODEWORDS_IN_BARCODE) { + throw NotFoundException.getNotFoundInstance(); + } + barcodeMatrix01.setValue(calculatedNumberOfCodewords); + } else if (numberOfCodewords[0] != calculatedNumberOfCodewords) { + // The calculated one is more reliable as it is derived from the row indicator columns + barcodeMatrix01.setValue(calculatedNumberOfCodewords); + } + } + + private static DecoderResult createDecoderResult(DetectionResult detectionResult) throws FormatException, + ChecksumException, NotFoundException { + BarcodeValue[][] barcodeMatrix = createBarcodeMatrix(detectionResult); + adjustCodewordCount(detectionResult, barcodeMatrix); + Collection erasures = new ArrayList<>(); + int[] codewords = new int[detectionResult.getBarcodeRowCount() * detectionResult.getBarcodeColumnCount()]; + List ambiguousIndexValuesList = new ArrayList<>(); + List ambiguousIndexesList = new ArrayList<>(); + for (int row = 0; row < detectionResult.getBarcodeRowCount(); row++) { + for (int column = 0; column < detectionResult.getBarcodeColumnCount(); column++) { + int[] values = barcodeMatrix[row][column + 1].getValue(); + int codewordIndex = row * detectionResult.getBarcodeColumnCount() + column; + if (values.length == 0) { + erasures.add(codewordIndex); + } else if (values.length == 1) { + codewords[codewordIndex] = values[0]; + } else { + ambiguousIndexesList.add(codewordIndex); + ambiguousIndexValuesList.add(values); + } + } + } + int[][] ambiguousIndexValues = new int[ambiguousIndexValuesList.size()][]; + for (int i = 0; i < ambiguousIndexValues.length; i++) { + ambiguousIndexValues[i] = ambiguousIndexValuesList.get(i); + } + return createDecoderResultFromAmbiguousValues(detectionResult.getBarcodeECLevel(), codewords, + PDF417Common.toIntArray(erasures), PDF417Common.toIntArray(ambiguousIndexesList), ambiguousIndexValues); + } + + /** + * This method deals with the fact, that the decoding process doesn't always yield a single most likely value. The + * current error correction implementation doesn't deal with erasures very well, so it's better to provide a value + * for these ambiguous codewords instead of treating it as an erasure. The problem is that we don't know which of + * the ambiguous values to choose. We try decode using the first value, and if that fails, we use another of the + * ambiguous values and try to decode again. This usually only happens on very hard to read and decode barcodes, + * so decoding the normal barcodes is not affected by this. + * + * @param erasureArray contains the indexes of erasures + * @param ambiguousIndexes array with the indexes that have more than one most likely value + * @param ambiguousIndexValues two dimensional array that contains the ambiguous values. The first dimension must + * be the same length as the ambiguousIndexes array + */ + private static DecoderResult createDecoderResultFromAmbiguousValues(int ecLevel, + int[] codewords, + int[] erasureArray, + int[] ambiguousIndexes, + int[][] ambiguousIndexValues) + throws FormatException, ChecksumException { + int[] ambiguousIndexCount = new int[ambiguousIndexes.length]; + + int tries = 100; + while (tries-- > 0) { + for (int i = 0; i < ambiguousIndexCount.length; i++) { + codewords[ambiguousIndexes[i]] = ambiguousIndexValues[i][ambiguousIndexCount[i]]; + } + try { + return decodeCodewords(codewords, ecLevel, erasureArray); + } catch (ChecksumException ignored) { + // + } + if (ambiguousIndexCount.length == 0) { + throw ChecksumException.getChecksumInstance(); + } + for (int i = 0; i < ambiguousIndexCount.length; i++) { + if (ambiguousIndexCount[i] < ambiguousIndexValues[i].length - 1) { + ambiguousIndexCount[i]++; + break; + } else { + ambiguousIndexCount[i] = 0; + if (i == ambiguousIndexCount.length - 1) { + throw ChecksumException.getChecksumInstance(); + } + } + } + } + throw ChecksumException.getChecksumInstance(); + } + + private static BarcodeValue[][] createBarcodeMatrix(DetectionResult detectionResult) { + BarcodeValue[][] barcodeMatrix = + new BarcodeValue[detectionResult.getBarcodeRowCount()][detectionResult.getBarcodeColumnCount() + 2]; + for (int row = 0; row < barcodeMatrix.length; row++) { + for (int column = 0; column < barcodeMatrix[row].length; column++) { + barcodeMatrix[row][column] = new BarcodeValue(); + } + } + + int column = 0; + for (DetectionResultColumn detectionResultColumn : detectionResult.getDetectionResultColumns()) { + if (detectionResultColumn != null) { + for (Codeword codeword : detectionResultColumn.getCodewords()) { + if (codeword != null) { + int rowNumber = codeword.getRowNumber(); + if (rowNumber >= 0) { + if (rowNumber >= barcodeMatrix.length) { + // We have more rows than the barcode metadata allows for, ignore them. + continue; + } + barcodeMatrix[rowNumber][column].setValue(codeword.getValue()); + } + } + } + } + column++; + } + return barcodeMatrix; + } + + private static boolean isValidBarcodeColumn(DetectionResult detectionResult, int barcodeColumn) { + return barcodeColumn >= 0 && barcodeColumn <= detectionResult.getBarcodeColumnCount() + 1; + } + + private static int getStartColumn(DetectionResult detectionResult, + int barcodeColumn, + int imageRow, + boolean leftToRight) { + int offset = leftToRight ? 1 : -1; + Codeword codeword = null; + if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) { + codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodeword(imageRow); + } + if (codeword != null) { + return leftToRight ? codeword.getEndX() : codeword.getStartX(); + } + codeword = detectionResult.getDetectionResultColumn(barcodeColumn).getCodewordNearby(imageRow); + if (codeword != null) { + return leftToRight ? codeword.getStartX() : codeword.getEndX(); + } + if (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) { + codeword = detectionResult.getDetectionResultColumn(barcodeColumn - offset).getCodewordNearby(imageRow); + } + if (codeword != null) { + return leftToRight ? codeword.getEndX() : codeword.getStartX(); + } + int skippedColumns = 0; + + while (isValidBarcodeColumn(detectionResult, barcodeColumn - offset)) { + barcodeColumn -= offset; + for (Codeword previousRowCodeword : detectionResult.getDetectionResultColumn(barcodeColumn).getCodewords()) { + if (previousRowCodeword != null) { + return (leftToRight ? previousRowCodeword.getEndX() : previousRowCodeword.getStartX()) + + offset * + skippedColumns * + (previousRowCodeword.getEndX() - previousRowCodeword.getStartX()); + } + } + skippedColumns++; + } + return leftToRight ? detectionResult.getBoundingBox().getMinX() : detectionResult.getBoundingBox().getMaxX(); + } + + private static Codeword detectCodeword(BitMatrix image, + int minColumn, + int maxColumn, + boolean leftToRight, + int startColumn, + int imageRow, + int minCodewordWidth, + int maxCodewordWidth) { + startColumn = adjustCodewordStartColumn(image, minColumn, maxColumn, leftToRight, startColumn, imageRow); + // we usually know fairly exact now how long a codeword is. We should provide minimum and maximum expected length + // and try to adjust the read pixels, e.g. remove single pixel errors or try to cut off exceeding pixels. + // min and maxCodewordWidth should not be used as they are calculated for the whole barcode an can be inaccurate + // for the current position + int[] moduleBitCount = getModuleBitCount(image, minColumn, maxColumn, leftToRight, startColumn, imageRow); + if (moduleBitCount == null) { + return null; + } + int endColumn; + int codewordBitCount = MathUtils.sum(moduleBitCount); + if (leftToRight) { + endColumn = startColumn + codewordBitCount; + } else { + for (int i = 0; i < moduleBitCount.length / 2; i++) { + int tmpCount = moduleBitCount[i]; + moduleBitCount[i] = moduleBitCount[moduleBitCount.length - 1 - i]; + moduleBitCount[moduleBitCount.length - 1 - i] = tmpCount; + } + endColumn = startColumn; + startColumn = endColumn - codewordBitCount; + } + // TODO implement check for width and correction of black and white bars + // use start (and maybe stop pattern) to determine if black bars are wider than white bars. If so, adjust. + // should probably done only for codewords with a lot more than 17 bits. + // The following fixes 10-1.png, which has wide black bars and small white bars + // for (int i = 0; i < moduleBitCount.length; i++) { + // if (i % 2 == 0) { + // moduleBitCount[i]--; + // } else { + // moduleBitCount[i]++; + // } + // } + + // We could also use the width of surrounding codewords for more accurate results, but this seems + // sufficient for now + if (!checkCodewordSkew(codewordBitCount, minCodewordWidth, maxCodewordWidth)) { + // We could try to use the startX and endX position of the codeword in the same column in the previous row, + // create the bit count from it and normalize it to 8. This would help with single pixel errors. + return null; + } + + int decodedValue = PDF417CodewordDecoder.getDecodedValue(moduleBitCount); + int codeword = PDF417Common.getCodeword(decodedValue); + if (codeword == -1) { + return null; + } + return new Codeword(startColumn, endColumn, getCodewordBucketNumber(decodedValue), codeword); + } + + private static int[] getModuleBitCount(BitMatrix image, + int minColumn, + int maxColumn, + boolean leftToRight, + int startColumn, + int imageRow) { + int imageColumn = startColumn; + int[] moduleBitCount = new int[8]; + int moduleNumber = 0; + int increment = leftToRight ? 1 : -1; + boolean previousPixelValue = leftToRight; + while ((leftToRight ? imageColumn < maxColumn : imageColumn >= minColumn) && + moduleNumber < moduleBitCount.length) { + if (image.get(imageColumn, imageRow) == previousPixelValue) { + moduleBitCount[moduleNumber]++; + imageColumn += increment; + } else { + moduleNumber++; + previousPixelValue = !previousPixelValue; + } + } + if (moduleNumber == moduleBitCount.length || + ((imageColumn == (leftToRight ? maxColumn : minColumn)) && + moduleNumber == moduleBitCount.length - 1)) { + return moduleBitCount; + } + return null; + } + + private static int getNumberOfECCodeWords(int barcodeECLevel) { + return 2 << barcodeECLevel; + } + + private static int adjustCodewordStartColumn(BitMatrix image, + int minColumn, + int maxColumn, + boolean leftToRight, + int codewordStartColumn, + int imageRow) { + int correctedStartColumn = codewordStartColumn; + int increment = leftToRight ? -1 : 1; + // there should be no black pixels before the start column. If there are, then we need to start earlier. + for (int i = 0; i < 2; i++) { + while ((leftToRight ? correctedStartColumn >= minColumn : correctedStartColumn < maxColumn) && + leftToRight == image.get(correctedStartColumn, imageRow)) { + if (Math.abs(codewordStartColumn - correctedStartColumn) > CODEWORD_SKEW_SIZE) { + return codewordStartColumn; + } + correctedStartColumn += increment; + } + increment = -increment; + leftToRight = !leftToRight; + } + return correctedStartColumn; + } + + private static boolean checkCodewordSkew(int codewordSize, int minCodewordWidth, int maxCodewordWidth) { + return minCodewordWidth - CODEWORD_SKEW_SIZE <= codewordSize && + codewordSize <= maxCodewordWidth + CODEWORD_SKEW_SIZE; + } + + private static DecoderResult decodeCodewords(int[] codewords, int ecLevel, int[] erasures) throws FormatException, + ChecksumException { + if (codewords.length == 0) { + throw FormatException.getFormatInstance(); + } + + int numECCodewords = 1 << (ecLevel + 1); + int correctedErrorsCount = correctErrors(codewords, erasures, numECCodewords); + verifyCodewordCount(codewords, numECCodewords); + + // Decode the codewords + DecoderResult decoderResult = DecodedBitStreamParser.decode(codewords, String.valueOf(ecLevel)); + decoderResult.setErrorsCorrected(correctedErrorsCount); + decoderResult.setErasures(erasures.length); + return decoderResult; + } + + /** + *

Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place.

+ * + * @param codewords data and error correction codewords + * @param erasures positions of any known erasures + * @param numECCodewords number of error correction codewords that are available in codewords + * @throws ChecksumException if error correction fails + */ + private static int correctErrors(int[] codewords, int[] erasures, int numECCodewords) throws ChecksumException { + if (erasures != null && + erasures.length > numECCodewords / 2 + MAX_ERRORS || + numECCodewords < 0 || + numECCodewords > MAX_EC_CODEWORDS) { + // Too many errors or EC Codewords is corrupted + throw ChecksumException.getChecksumInstance(); + } + return errorCorrection.decode(codewords, numECCodewords, erasures); + } + + /** + * Verify that all is OK with the codeword array. + */ + private static void verifyCodewordCount(int[] codewords, int numECCodewords) throws FormatException { + if (codewords.length < 4) { + // Codeword array size should be at least 4 allowing for + // Count CW, At least one Data CW, Error Correction CW, Error Correction CW + throw FormatException.getFormatInstance(); + } + // The first codeword, the Symbol Length Descriptor, shall always encode the total number of data + // codewords in the symbol, including the Symbol Length Descriptor itself, data codewords and pad + // codewords, but excluding the number of error correction codewords. + int numberOfCodewords = codewords[0]; + if (numberOfCodewords > codewords.length) { + throw FormatException.getFormatInstance(); + } + if (numberOfCodewords == 0) { + // Reset to the length of the array - 8 (Allow for at least level 3 Error Correction (8 Error Codewords) + if (numECCodewords < codewords.length) { + codewords[0] = codewords.length - numECCodewords; + } else { + throw FormatException.getFormatInstance(); + } + } + } + + private static int[] getBitCountForCodeword(int codeword) { + int[] result = new int[8]; + int previousValue = 0; + int i = result.length - 1; + while (true) { + if ((codeword & 0x1) != previousValue) { + previousValue = codeword & 0x1; + i--; + if (i < 0) { + break; + } + } + result[i]++; + codeword >>= 1; + } + return result; + } + + private static int getCodewordBucketNumber(int codeword) { + return getCodewordBucketNumber(getBitCountForCodeword(codeword)); + } + + private static int getCodewordBucketNumber(int[] moduleBitCount) { + return (moduleBitCount[0] - moduleBitCount[2] + moduleBitCount[4] - moduleBitCount[6] + 9) % 9; + } + + public static String toString(BarcodeValue[][] barcodeMatrix) { + Formatter formatter = new Formatter(); + for (int row = 0; row < barcodeMatrix.length; row++) { + formatter.format("Row %2d: ", row); + for (int column = 0; column < barcodeMatrix[row].length; column++) { + BarcodeValue barcodeValue = barcodeMatrix[row][column]; + if (barcodeValue.getValue().length == 0) { + formatter.format(" ", (Object[]) null); + } else { + formatter.format("%4d(%2d)", barcodeValue.getValue()[0], + barcodeValue.getConfidence(barcodeValue.getValue()[0])); + } + } + formatter.format("%n"); + } + String result = formatter.toString(); + formatter.close(); + return result; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ErrorCorrection.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ErrorCorrection.java new file mode 100644 index 0000000..e34cb7f --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ErrorCorrection.java @@ -0,0 +1,187 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder.ec; + +import com.google.zxing.ChecksumException; + +/** + *

PDF417 error correction implementation.

+ * + *

This example + * is quite useful in understanding the algorithm.

+ * + * @author Sean Owen + * @see com.google.zxing.common.reedsolomon.ReedSolomonDecoder + */ +public final class ErrorCorrection { + + private final ModulusGF field; + + public ErrorCorrection() { + this.field = ModulusGF.PDF417_GF; + } + + /** + * @param received received codewords + * @param numECCodewords number of those codewords used for EC + * @param erasures location of erasures + * @return number of errors + * @throws ChecksumException if errors cannot be corrected, maybe because of too many errors + */ + public int decode(int[] received, + int numECCodewords, + int[] erasures) throws ChecksumException { + + ModulusPoly poly = new ModulusPoly(field, received); + int[] S = new int[numECCodewords]; + boolean error = false; + for (int i = numECCodewords; i > 0; i--) { + int eval = poly.evaluateAt(field.exp(i)); + S[numECCodewords - i] = eval; + if (eval != 0) { + error = true; + } + } + + if (!error) { + return 0; + } + + ModulusPoly knownErrors = field.getOne(); + if (erasures != null) { + for (int erasure : erasures) { + int b = field.exp(received.length - 1 - erasure); + // Add (1 - bx) term: + ModulusPoly term = new ModulusPoly(field, new int[]{field.subtract(0, b), 1}); + knownErrors = knownErrors.multiply(term); + } + } + + ModulusPoly syndrome = new ModulusPoly(field, S); + //syndrome = syndrome.multiply(knownErrors); + + ModulusPoly[] sigmaOmega = + runEuclideanAlgorithm(field.buildMonomial(numECCodewords, 1), syndrome, numECCodewords); + ModulusPoly sigma = sigmaOmega[0]; + ModulusPoly omega = sigmaOmega[1]; + + //sigma = sigma.multiply(knownErrors); + + int[] errorLocations = findErrorLocations(sigma); + int[] errorMagnitudes = findErrorMagnitudes(omega, sigma, errorLocations); + + for (int i = 0; i < errorLocations.length; i++) { + int position = received.length - 1 - field.log(errorLocations[i]); + if (position < 0) { + throw ChecksumException.getChecksumInstance(); + } + received[position] = field.subtract(received[position], errorMagnitudes[i]); + } + return errorLocations.length; + } + + private ModulusPoly[] runEuclideanAlgorithm(ModulusPoly a, ModulusPoly b, int R) + throws ChecksumException { + // Assume a's degree is >= b's + if (a.getDegree() < b.getDegree()) { + ModulusPoly temp = a; + a = b; + b = temp; + } + + ModulusPoly rLast = a; + ModulusPoly r = b; + ModulusPoly tLast = field.getZero(); + ModulusPoly t = field.getOne(); + + // Run Euclidean algorithm until r's degree is less than R/2 + while (r.getDegree() >= R / 2) { + ModulusPoly rLastLast = rLast; + ModulusPoly tLastLast = tLast; + rLast = r; + tLast = t; + + // Divide rLastLast by rLast, with quotient in q and remainder in r + if (rLast.isZero()) { + // Oops, Euclidean algorithm already terminated? + throw ChecksumException.getChecksumInstance(); + } + r = rLastLast; + ModulusPoly q = field.getZero(); + int denominatorLeadingTerm = rLast.getCoefficient(rLast.getDegree()); + int dltInverse = field.inverse(denominatorLeadingTerm); + while (r.getDegree() >= rLast.getDegree() && !r.isZero()) { + int degreeDiff = r.getDegree() - rLast.getDegree(); + int scale = field.multiply(r.getCoefficient(r.getDegree()), dltInverse); + q = q.add(field.buildMonomial(degreeDiff, scale)); + r = r.subtract(rLast.multiplyByMonomial(degreeDiff, scale)); + } + + t = q.multiply(tLast).subtract(tLastLast).negative(); + } + + int sigmaTildeAtZero = t.getCoefficient(0); + if (sigmaTildeAtZero == 0) { + throw ChecksumException.getChecksumInstance(); + } + + int inverse = field.inverse(sigmaTildeAtZero); + ModulusPoly sigma = t.multiply(inverse); + ModulusPoly omega = r.multiply(inverse); + return new ModulusPoly[]{sigma, omega}; + } + + private int[] findErrorLocations(ModulusPoly errorLocator) throws ChecksumException { + // This is a direct application of Chien's search + int numErrors = errorLocator.getDegree(); + int[] result = new int[numErrors]; + int e = 0; + for (int i = 1; i < field.getSize() && e < numErrors; i++) { + if (errorLocator.evaluateAt(i) == 0) { + result[e] = field.inverse(i); + e++; + } + } + if (e != numErrors) { + throw ChecksumException.getChecksumInstance(); + } + return result; + } + + private int[] findErrorMagnitudes(ModulusPoly errorEvaluator, + ModulusPoly errorLocator, + int[] errorLocations) { + int errorLocatorDegree = errorLocator.getDegree(); + int[] formalDerivativeCoefficients = new int[errorLocatorDegree]; + for (int i = 1; i <= errorLocatorDegree; i++) { + formalDerivativeCoefficients[errorLocatorDegree - i] = + field.multiply(i, errorLocator.getCoefficient(i)); + } + ModulusPoly formalDerivative = new ModulusPoly(field, formalDerivativeCoefficients); + + // This is directly applying Forney's Formula + int s = errorLocations.length; + int[] result = new int[s]; + for (int i = 0; i < s; i++) { + int xiInverse = field.inverse(errorLocations[i]); + int numerator = field.subtract(0, errorEvaluator.evaluateAt(xiInverse)); + int denominator = field.inverse(formalDerivative.evaluateAt(xiInverse)); + result[i] = field.multiply(numerator, denominator); + } + return result; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusGF.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusGF.java new file mode 100644 index 0000000..61c86c5 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusGF.java @@ -0,0 +1,112 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder.ec; + +import com.google.zxing.pdf417.PDF417Common; + +/** + *

A field based on powers of a generator integer, modulo some modulus.

+ * + * @author Sean Owen + * @see com.google.zxing.common.reedsolomon.GenericGF + */ +public final class ModulusGF { + + public static final ModulusGF PDF417_GF = new ModulusGF(PDF417Common.NUMBER_OF_CODEWORDS, 3); + + private final int[] expTable; + private final int[] logTable; + private final ModulusPoly zero; + private final ModulusPoly one; + private final int modulus; + + private ModulusGF(int modulus, int generator) { + this.modulus = modulus; + expTable = new int[modulus]; + logTable = new int[modulus]; + int x = 1; + for (int i = 0; i < modulus; i++) { + expTable[i] = x; + x = (x * generator) % modulus; + } + for (int i = 0; i < modulus - 1; i++) { + logTable[expTable[i]] = i; + } + // logTable[0] == 0 but this should never be used + zero = new ModulusPoly(this, new int[]{0}); + one = new ModulusPoly(this, new int[]{1}); + } + + + ModulusPoly getZero() { + return zero; + } + + ModulusPoly getOne() { + return one; + } + + ModulusPoly buildMonomial(int degree, int coefficient) { + if (degree < 0) { + throw new IllegalArgumentException(); + } + if (coefficient == 0) { + return zero; + } + int[] coefficients = new int[degree + 1]; + coefficients[0] = coefficient; + return new ModulusPoly(this, coefficients); + } + + int add(int a, int b) { + return (a + b) % modulus; + } + + int subtract(int a, int b) { + return (modulus + a - b) % modulus; + } + + int exp(int a) { + return expTable[a]; + } + + int log(int a) { + if (a == 0) { + throw new IllegalArgumentException(); + } + return logTable[a]; + } + + int inverse(int a) { + if (a == 0) { + throw new ArithmeticException(); + } + return expTable[modulus - logTable[a] - 1]; + } + + int multiply(int a, int b) { + if (a == 0 || b == 0) { + return 0; + } + return expTable[(logTable[a] + logTable[b]) % (modulus - 1)]; + } + + int getSize() { + return modulus; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusPoly.java b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusPoly.java new file mode 100644 index 0000000..b01b6ec --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/decoder/ec/ModulusPoly.java @@ -0,0 +1,262 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.decoder.ec; + +/** + * @author Sean Owen + * @see com.google.zxing.common.reedsolomon.GenericGFPoly + */ +final class ModulusPoly { + + private final ModulusGF field; + private final int[] coefficients; + + ModulusPoly(ModulusGF field, int[] coefficients) { + if (coefficients.length == 0) { + throw new IllegalArgumentException(); + } + this.field = field; + int coefficientsLength = coefficients.length; + if (coefficientsLength > 1 && coefficients[0] == 0) { + // Leading term must be non-zero for anything except the constant polynomial "0" + int firstNonZero = 1; + while (firstNonZero < coefficientsLength && coefficients[firstNonZero] == 0) { + firstNonZero++; + } + if (firstNonZero == coefficientsLength) { + this.coefficients = new int[]{0}; + } else { + this.coefficients = new int[coefficientsLength - firstNonZero]; + System.arraycopy(coefficients, + firstNonZero, + this.coefficients, + 0, + this.coefficients.length); + } + } else { + this.coefficients = coefficients; + } + } + + int[] getCoefficients() { + return coefficients; + } + + /** + * @return degree of this polynomial + */ + int getDegree() { + return coefficients.length - 1; + } + + /** + * @return true iff this polynomial is the monomial "0" + */ + boolean isZero() { + return coefficients[0] == 0; + } + + /** + * @return coefficient of x^degree term in this polynomial + */ + int getCoefficient(int degree) { + return coefficients[coefficients.length - 1 - degree]; + } + + /** + * @return evaluation of this polynomial at a given point + */ + int evaluateAt(int a) { + if (a == 0) { + // Just return the x^0 coefficient + return getCoefficient(0); + } + if (a == 1) { + // Just the sum of the coefficients + int result = 0; + for (int coefficient : coefficients) { + result = field.add(result, coefficient); + } + return result; + } + int result = coefficients[0]; + int size = coefficients.length; + for (int i = 1; i < size; i++) { + result = field.add(field.multiply(a, result), coefficients[i]); + } + return result; + } + + ModulusPoly add(ModulusPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("ModulusPolys do not have same ModulusGF field"); + } + if (isZero()) { + return other; + } + if (other.isZero()) { + return this; + } + + int[] smallerCoefficients = this.coefficients; + int[] largerCoefficients = other.coefficients; + if (smallerCoefficients.length > largerCoefficients.length) { + int[] temp = smallerCoefficients; + smallerCoefficients = largerCoefficients; + largerCoefficients = temp; + } + int[] sumDiff = new int[largerCoefficients.length]; + int lengthDiff = largerCoefficients.length - smallerCoefficients.length; + // Copy high-order terms only found in higher-degree polynomial's coefficients + System.arraycopy(largerCoefficients, 0, sumDiff, 0, lengthDiff); + + for (int i = lengthDiff; i < largerCoefficients.length; i++) { + sumDiff[i] = field.add(smallerCoefficients[i - lengthDiff], largerCoefficients[i]); + } + + return new ModulusPoly(field, sumDiff); + } + + ModulusPoly subtract(ModulusPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("ModulusPolys do not have same ModulusGF field"); + } + if (other.isZero()) { + return this; + } + return add(other.negative()); + } + + ModulusPoly multiply(ModulusPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("ModulusPolys do not have same ModulusGF field"); + } + if (isZero() || other.isZero()) { + return field.getZero(); + } + int[] aCoefficients = this.coefficients; + int aLength = aCoefficients.length; + int[] bCoefficients = other.coefficients; + int bLength = bCoefficients.length; + int[] product = new int[aLength + bLength - 1]; + for (int i = 0; i < aLength; i++) { + int aCoeff = aCoefficients[i]; + for (int j = 0; j < bLength; j++) { + product[i + j] = field.add(product[i + j], field.multiply(aCoeff, bCoefficients[j])); + } + } + return new ModulusPoly(field, product); + } + + ModulusPoly negative() { + int size = coefficients.length; + int[] negativeCoefficients = new int[size]; + for (int i = 0; i < size; i++) { + negativeCoefficients[i] = field.subtract(0, coefficients[i]); + } + return new ModulusPoly(field, negativeCoefficients); + } + + ModulusPoly multiply(int scalar) { + if (scalar == 0) { + return field.getZero(); + } + if (scalar == 1) { + return this; + } + int size = coefficients.length; + int[] product = new int[size]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], scalar); + } + return new ModulusPoly(field, product); + } + + ModulusPoly multiplyByMonomial(int degree, int coefficient) { + if (degree < 0) { + throw new IllegalArgumentException(); + } + if (coefficient == 0) { + return field.getZero(); + } + int size = coefficients.length; + int[] product = new int[size + degree]; + for (int i = 0; i < size; i++) { + product[i] = field.multiply(coefficients[i], coefficient); + } + return new ModulusPoly(field, product); + } + + /* + ModulusPoly[] divide(ModulusPoly other) { + if (!field.equals(other.field)) { + throw new IllegalArgumentException("ModulusPolys do not have same ModulusGF field"); + } + if (other.isZero()) { + throw new IllegalArgumentException("Divide by 0"); + } + + ModulusPoly quotient = field.getZero(); + ModulusPoly remainder = this; + + int denominatorLeadingTerm = other.getCoefficient(other.getDegree()); + int inverseDenominatorLeadingTerm = field.inverse(denominatorLeadingTerm); + + while (remainder.getDegree() >= other.getDegree() && !remainder.isZero()) { + int degreeDifference = remainder.getDegree() - other.getDegree(); + int scale = field.multiply(remainder.getCoefficient(remainder.getDegree()), inverseDenominatorLeadingTerm); + ModulusPoly term = other.multiplyByMonomial(degreeDifference, scale); + ModulusPoly iterationQuotient = field.buildMonomial(degreeDifference, scale); + quotient = quotient.add(iterationQuotient); + remainder = remainder.subtract(term); + } + + return new ModulusPoly[] { quotient, remainder }; + } + */ + + @Override + public String toString() { + StringBuilder result = new StringBuilder(8 * getDegree()); + for (int degree = getDegree(); degree >= 0; degree--) { + int coefficient = getCoefficient(degree); + if (coefficient != 0) { + if (coefficient < 0) { + result.append(" - "); + coefficient = -coefficient; + } else { + if (result.length() > 0) { + result.append(" + "); + } + } + if (degree == 0 || coefficient != 1) { + result.append(coefficient); + } + if (degree != 0) { + if (degree == 1) { + result.append('x'); + } else { + result.append("x^"); + result.append(degree); + } + } + } + } + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/detector/Detector.java b/rubylib/src/main/java/com/google/zxing/pdf417/detector/Detector.java new file mode 100644 index 0000000..5f65663 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/detector/Detector.java @@ -0,0 +1,340 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.detector; + +import com.google.zxing.BinaryBitmap; +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +/** + *

Encapsulates logic that can detect a PDF417 Code in an image, even if the + * PDF417 Code is rotated or skewed, or partially obscured.

+ * + * @author SITA Lab (kevin.osullivan@sita.aero) + * @author dswitkin@google.com (Daniel Switkin) + * @author Guenther Grau + */ +public final class Detector { + + private static final int[] INDEXES_START_PATTERN = {0, 4, 1, 5}; + private static final int[] INDEXES_STOP_PATTERN = {6, 2, 7, 3}; + private static final float MAX_AVG_VARIANCE = 0.42f; + private static final float MAX_INDIVIDUAL_VARIANCE = 0.8f; + + // B S B S B S B S Bar/Space pattern + // 11111111 0 1 0 1 0 1 000 + private static final int[] START_PATTERN = {8, 1, 1, 1, 1, 1, 1, 3}; + // 1111111 0 1 000 1 0 1 00 1 + private static final int[] STOP_PATTERN = {7, 1, 1, 3, 1, 1, 1, 2, 1}; + private static final int MAX_PIXEL_DRIFT = 3; + private static final int MAX_PATTERN_DRIFT = 5; + // if we set the value too low, then we don't detect the correct height of the bar if the start patterns are damaged. + // if we set the value too high, then we might detect the start pattern from a neighbor barcode. + private static final int SKIPPED_ROW_COUNT_MAX = 25; + // A PDF471 barcode should have at least 3 rows, with each row being >= 3 times the module width. Therefore it should be at least + // 9 pixels tall. To be conservative, we use about half the size to ensure we don't miss it. + private static final int ROW_STEP = 5; + private static final int BARCODE_MIN_HEIGHT = 10; + + private Detector() { + } + + /** + *

Detects a PDF417 Code in an image. Only checks 0 and 180 degree rotations.

+ * + * @param image barcode image to decode + * @param hints optional hints to detector + * @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will + * be found and returned + * @return {@link PDF417DetectorResult} encapsulating results of detecting a PDF417 code + * @throws NotFoundException if no PDF417 Code can be found + */ + public static PDF417DetectorResult detect(BinaryBitmap image, Map hints, boolean multiple) + throws NotFoundException { + // TODO detection improvement, tryHarder could try several different luminance thresholds/blackpoints or even + // different binarizers + //boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + + BitMatrix bitMatrix = image.getBlackMatrix(); + + List barcodeCoordinates = detect(multiple, bitMatrix); + if (barcodeCoordinates.isEmpty()) { + bitMatrix = bitMatrix.clone(); + bitMatrix.rotate180(); + barcodeCoordinates = detect(multiple, bitMatrix); + } + return new PDF417DetectorResult(bitMatrix, barcodeCoordinates); + } + + /** + * Detects PDF417 codes in an image. Only checks 0 degree rotation + * @param multiple if true, then the image is searched for multiple codes. If false, then at most one code will + * be found and returned + * @param bitMatrix bit matrix to detect barcodes in + * @return List of ResultPoint arrays containing the coordinates of found barcodes + */ + private static List detect(boolean multiple, BitMatrix bitMatrix) { + List barcodeCoordinates = new ArrayList<>(); + int row = 0; + int column = 0; + boolean foundBarcodeInRow = false; + while (row < bitMatrix.getHeight()) { + ResultPoint[] vertices = findVertices(bitMatrix, row, column); + + if (vertices[0] == null && vertices[3] == null) { + if (!foundBarcodeInRow) { + // we didn't find any barcode so that's the end of searching + break; + } + // we didn't find a barcode starting at the given column and row. Try again from the first column and slightly + // below the lowest barcode we found so far. + foundBarcodeInRow = false; + column = 0; + for (ResultPoint[] barcodeCoordinate : barcodeCoordinates) { + if (barcodeCoordinate[1] != null) { + row = (int) Math.max(row, barcodeCoordinate[1].getY()); + } + if (barcodeCoordinate[3] != null) { + row = Math.max(row, (int) barcodeCoordinate[3].getY()); + } + } + row += ROW_STEP; + continue; + } + foundBarcodeInRow = true; + barcodeCoordinates.add(vertices); + if (!multiple) { + break; + } + // if we didn't find a right row indicator column, then continue the search for the next barcode after the + // start pattern of the barcode just found. + if (vertices[2] != null) { + column = (int) vertices[2].getX(); + row = (int) vertices[2].getY(); + } else { + column = (int) vertices[4].getX(); + row = (int) vertices[4].getY(); + } + } + return barcodeCoordinates; + } + + /** + * Locate the vertices and the codewords area of a black blob using the Start + * and Stop patterns as locators. + * + * @param matrix the scanned barcode image. + * @return an array containing the vertices: + * vertices[0] x, y top left barcode + * vertices[1] x, y bottom left barcode + * vertices[2] x, y top right barcode + * vertices[3] x, y bottom right barcode + * vertices[4] x, y top left codeword area + * vertices[5] x, y bottom left codeword area + * vertices[6] x, y top right codeword area + * vertices[7] x, y bottom right codeword area + */ + private static ResultPoint[] findVertices(BitMatrix matrix, int startRow, int startColumn) { + int height = matrix.getHeight(); + int width = matrix.getWidth(); + + ResultPoint[] result = new ResultPoint[8]; + copyToResult(result, findRowsWithPattern(matrix, height, width, startRow, startColumn, START_PATTERN), + INDEXES_START_PATTERN); + + if (result[4] != null) { + startColumn = (int) result[4].getX(); + startRow = (int) result[4].getY(); + } + copyToResult(result, findRowsWithPattern(matrix, height, width, startRow, startColumn, STOP_PATTERN), + INDEXES_STOP_PATTERN); + return result; + } + + private static void copyToResult(ResultPoint[] result, ResultPoint[] tmpResult, int[] destinationIndexes) { + for (int i = 0; i < destinationIndexes.length; i++) { + result[destinationIndexes[i]] = tmpResult[i]; + } + } + + private static ResultPoint[] findRowsWithPattern(BitMatrix matrix, + int height, + int width, + int startRow, + int startColumn, + int[] pattern) { + ResultPoint[] result = new ResultPoint[4]; + boolean found = false; + int[] counters = new int[pattern.length]; + for (; startRow < height; startRow += ROW_STEP) { + int[] loc = findGuardPattern(matrix, startColumn, startRow, width, false, pattern, counters); + if (loc != null) { + while (startRow > 0) { + int[] previousRowLoc = findGuardPattern(matrix, startColumn, --startRow, width, false, pattern, counters); + if (previousRowLoc != null) { + loc = previousRowLoc; + } else { + startRow++; + break; + } + } + result[0] = new ResultPoint(loc[0], startRow); + result[1] = new ResultPoint(loc[1], startRow); + found = true; + break; + } + } + int stopRow = startRow + 1; + // Last row of the current symbol that contains pattern + if (found) { + int skippedRowCount = 0; + int[] previousRowLoc = {(int) result[0].getX(), (int) result[1].getX()}; + for (; stopRow < height; stopRow++) { + int[] loc = findGuardPattern(matrix, previousRowLoc[0], stopRow, width, false, pattern, counters); + // a found pattern is only considered to belong to the same barcode if the start and end positions + // don't differ too much. Pattern drift should be not bigger than two for consecutive rows. With + // a higher number of skipped rows drift could be larger. To keep it simple for now, we allow a slightly + // larger drift and don't check for skipped rows. + if (loc != null && + Math.abs(previousRowLoc[0] - loc[0]) < MAX_PATTERN_DRIFT && + Math.abs(previousRowLoc[1] - loc[1]) < MAX_PATTERN_DRIFT) { + previousRowLoc = loc; + skippedRowCount = 0; + } else { + if (skippedRowCount > SKIPPED_ROW_COUNT_MAX) { + break; + } else { + skippedRowCount++; + } + } + } + stopRow -= skippedRowCount + 1; + result[2] = new ResultPoint(previousRowLoc[0], stopRow); + result[3] = new ResultPoint(previousRowLoc[1], stopRow); + } + if (stopRow - startRow < BARCODE_MIN_HEIGHT) { + Arrays.fill(result, null); + } + return result; + } + + /** + * @param matrix row of black/white values to search + * @param column x position to start search + * @param row y position to start search + * @param width the number of pixels to search on this row + * @param pattern pattern of counts of number of black and white pixels that are + * being searched for as a pattern + * @param counters array of counters, as long as pattern, to re-use + * @return start/end horizontal offset of guard pattern, as an array of two ints. + */ + private static int[] findGuardPattern(BitMatrix matrix, + int column, + int row, + int width, + boolean whiteFirst, + int[] pattern, + int[] counters) { + Arrays.fill(counters, 0, counters.length, 0); + int patternStart = column; + int pixelDrift = 0; + + // if there are black pixels left of the current pixel shift to the left, but only for MAX_PIXEL_DRIFT pixels + while (matrix.get(patternStart, row) && patternStart > 0 && pixelDrift++ < MAX_PIXEL_DRIFT) { + patternStart--; + } + int x = patternStart; + int counterPosition = 0; + int patternLength = pattern.length; + for (boolean isWhite = whiteFirst; x < width; x++) { + boolean pixel = matrix.get(x, row); + if (pixel != isWhite) { + counters[counterPosition]++; + } else { + if (counterPosition == patternLength - 1) { + if (patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { + return new int[] {patternStart, x}; + } + patternStart += counters[0] + counters[1]; + System.arraycopy(counters, 2, counters, 0, counterPosition - 1); + counters[counterPosition - 1] = 0; + counters[counterPosition] = 0; + counterPosition--; + } else { + counterPosition++; + } + counters[counterPosition] = 1; + isWhite = !isWhite; + } + } + if (counterPosition == patternLength - 1 && + patternMatchVariance(counters, pattern, MAX_INDIVIDUAL_VARIANCE) < MAX_AVG_VARIANCE) { + return new int[] {patternStart, x - 1}; + } + return null; + } + + /** + * Determines how closely a set of observed counts of runs of black/white + * values matches a given target pattern. This is reported as the ratio of + * the total variance from the expected pattern proportions across all + * pattern elements, to the length of the pattern. + * + * @param counters observed counters + * @param pattern expected pattern + * @param maxIndividualVariance The most any counter can differ before we give up + * @return ratio of total variance between counters and pattern compared to total pattern size + */ + private static float patternMatchVariance(int[] counters, int[] pattern, float maxIndividualVariance) { + int numCounters = counters.length; + int total = 0; + int patternLength = 0; + for (int i = 0; i < numCounters; i++) { + total += counters[i]; + patternLength += pattern[i]; + } + if (total < patternLength) { + // If we don't even have one pixel per unit of bar width, assume this + // is too small to reliably match, so fail: + return Float.POSITIVE_INFINITY; + } + // We're going to fake floating-point math in integers. We just need to use more bits. + // Scale up patternLength so that intermediate values below like scaledCounter will have + // more "significant digits". + float unitBarWidth = (float) total / patternLength; + maxIndividualVariance *= unitBarWidth; + + float totalVariance = 0.0f; + for (int x = 0; x < numCounters; x++) { + int counter = counters[x]; + float scaledPattern = pattern[x] * unitBarWidth; + float variance = counter > scaledPattern ? counter - scaledPattern : scaledPattern - counter; + if (variance > maxIndividualVariance) { + return Float.POSITIVE_INFINITY; + } + totalVariance += variance; + } + return totalVariance / total; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/detector/PDF417DetectorResult.java b/rubylib/src/main/java/com/google/zxing/pdf417/detector/PDF417DetectorResult.java new file mode 100644 index 0000000..081b778 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/detector/PDF417DetectorResult.java @@ -0,0 +1,45 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.detector; + +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; + +import java.util.List; + +/** + * @author Guenther Grau + */ +public final class PDF417DetectorResult { + + private final BitMatrix bits; + private final List points; + + public PDF417DetectorResult(BitMatrix bits, List points) { + this.bits = bits; + this.points = points; + } + + public BitMatrix getBits() { + return bits; + } + + public List getPoints() { + return points; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeMatrix.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeMatrix.java new file mode 100644 index 0000000..cf1c11b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeMatrix.java @@ -0,0 +1,82 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.encoder; + +/** + * Holds all of the information for a barcode in a format where it can be easily accessible + * + * @author Jacob Haynes + */ +public final class BarcodeMatrix { + + private final BarcodeRow[] matrix; + private int currentRow; + private final int height; + private final int width; + + /** + * @param height the height of the matrix (Rows) + * @param width the width of the matrix (Cols) + */ + BarcodeMatrix(int height, int width) { + matrix = new BarcodeRow[height]; + //Initializes the array to the correct width + for (int i = 0, matrixLength = matrix.length; i < matrixLength; i++) { + matrix[i] = new BarcodeRow((width + 4) * 17 + 1); + } + this.width = width * 17; + this.height = height; + this.currentRow = -1; + } + + void set(int x, int y, byte value) { + matrix[y].set(x, value); + } + + /* + void setMatrix(int x, int y, boolean black) { + set(x, y, (byte) (black ? 1 : 0)); + } + */ + + void startRow() { + ++currentRow; + } + + BarcodeRow getCurrentRow() { + return matrix[currentRow]; + } + + public byte[][] getMatrix() { + return getScaledMatrix(1, 1); + } + + /* + public byte[][] getScaledMatrix(int scale) { + return getScaledMatrix(scale, scale); + } + */ + + public byte[][] getScaledMatrix(int xScale, int yScale) { + byte[][] matrixOut = new byte[height * yScale][width * xScale]; + int yMax = height * yScale; + for (int i = 0; i < yMax; i++) { + matrixOut[yMax - i - 1] = matrix[i / yScale].getScaledRow(xScale); + } + return matrixOut; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeRow.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeRow.java new file mode 100644 index 0000000..c4d6772 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/BarcodeRow.java @@ -0,0 +1,85 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.encoder; + +/** + * @author Jacob Haynes + */ +final class BarcodeRow { + + private final byte[] row; + //A tacker for position in the bar + private int currentLocation; + + /** + * Creates a Barcode row of the width + */ + BarcodeRow(int width) { + this.row = new byte[width]; + currentLocation = 0; + } + + /** + * Sets a specific location in the bar + * + * @param x The location in the bar + * @param value Black if true, white if false; + */ + void set(int x, byte value) { + row[x] = value; + } + + /** + * Sets a specific location in the bar + * + * @param x The location in the bar + * @param black Black if true, white if false; + */ + private void set(int x, boolean black) { + row[x] = (byte) (black ? 1 : 0); + } + + /** + * @param black A boolean which is true if the bar black false if it is white + * @param width How many spots wide the bar is. + */ + void addBar(boolean black, int width) { + for (int ii = 0; ii < width; ii++) { + set(currentLocation++, black); + } + } + + /* + byte[] getRow() { + return row; + } + */ + + /** + * This function scales the row + * + * @param scale How much you want the image to be scaled, must be greater than or equal to 1. + * @return the scaled row + */ + byte[] getScaledRow(int scale) { + byte[] output = new byte[row.length * scale]; + for (int i = 0; i < output.length; i++) { + output[i] = row[i / scale]; + } + return output; + } +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Compaction.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Compaction.java new file mode 100644 index 0000000..67cf4b8 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Compaction.java @@ -0,0 +1,29 @@ +/* + * Copyright 2011 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.encoder; + +/** + * Represents possible PDF417 barcode compaction types. + */ +public enum Compaction { + + AUTO, + TEXT, + BYTE, + NUMERIC + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Dimensions.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Dimensions.java new file mode 100644 index 0000000..83a736b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/Dimensions.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.pdf417.encoder; + +/** + * Data object to specify the minimum and maximum number of rows and columns for a PDF417 barcode. + * + * @author qwandor@google.com (Andrew Walbran) + */ +public final class Dimensions { + + private final int minCols; + private final int maxCols; + private final int minRows; + private final int maxRows; + + public Dimensions(int minCols, int maxCols, int minRows, int maxRows) { + this.minCols = minCols; + this.maxCols = maxCols; + this.minRows = minRows; + this.maxRows = maxRows; + } + + public int getMinCols() { + return minCols; + } + + public int getMaxCols() { + return maxCols; + } + + public int getMinRows() { + return minRows; + } + + public int getMaxRows() { + return maxRows; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java new file mode 100644 index 0000000..514b3cb --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417.java @@ -0,0 +1,768 @@ +/* + * Copyright 2006 Jeremias Maerki in part, and ZXing Authors in part + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file has been modified from its original form in Barcode4J. + */ + +package com.google.zxing.pdf417.encoder; + +import com.google.zxing.WriterException; + +import java.nio.charset.Charset; + +/** + * Top-level class for the logic part of the PDF417 implementation. + */ +public final class PDF417 { + + /** + * The start pattern (17 bits) + */ + private static final int START_PATTERN = 0x1fea8; + /** + * The stop pattern (18 bits) + */ + private static final int STOP_PATTERN = 0x3fa29; + + /** + * The codeword table from the Annex A of ISO/IEC 15438:2001(E). + */ + private static final int[][] CODEWORD_TABLE = { + {0x1d5c0, 0x1eaf0, 0x1f57c, 0x1d4e0, 0x1ea78, 0x1f53e, + 0x1a8c0, 0x1d470, 0x1a860, 0x15040, 0x1a830, 0x15020, + 0x1adc0, 0x1d6f0, 0x1eb7c, 0x1ace0, 0x1d678, 0x1eb3e, + 0x158c0, 0x1ac70, 0x15860, 0x15dc0, 0x1aef0, 0x1d77c, + 0x15ce0, 0x1ae78, 0x1d73e, 0x15c70, 0x1ae3c, 0x15ef0, + 0x1af7c, 0x15e78, 0x1af3e, 0x15f7c, 0x1f5fa, 0x1d2e0, + 0x1e978, 0x1f4be, 0x1a4c0, 0x1d270, 0x1e93c, 0x1a460, + 0x1d238, 0x14840, 0x1a430, 0x1d21c, 0x14820, 0x1a418, + 0x14810, 0x1a6e0, 0x1d378, 0x1e9be, 0x14cc0, 0x1a670, + 0x1d33c, 0x14c60, 0x1a638, 0x1d31e, 0x14c30, 0x1a61c, + 0x14ee0, 0x1a778, 0x1d3be, 0x14e70, 0x1a73c, 0x14e38, + 0x1a71e, 0x14f78, 0x1a7be, 0x14f3c, 0x14f1e, 0x1a2c0, + 0x1d170, 0x1e8bc, 0x1a260, 0x1d138, 0x1e89e, 0x14440, + 0x1a230, 0x1d11c, 0x14420, 0x1a218, 0x14410, 0x14408, + 0x146c0, 0x1a370, 0x1d1bc, 0x14660, 0x1a338, 0x1d19e, + 0x14630, 0x1a31c, 0x14618, 0x1460c, 0x14770, 0x1a3bc, + 0x14738, 0x1a39e, 0x1471c, 0x147bc, 0x1a160, 0x1d0b8, + 0x1e85e, 0x14240, 0x1a130, 0x1d09c, 0x14220, 0x1a118, + 0x1d08e, 0x14210, 0x1a10c, 0x14208, 0x1a106, 0x14360, + 0x1a1b8, 0x1d0de, 0x14330, 0x1a19c, 0x14318, 0x1a18e, + 0x1430c, 0x14306, 0x1a1de, 0x1438e, 0x14140, 0x1a0b0, + 0x1d05c, 0x14120, 0x1a098, 0x1d04e, 0x14110, 0x1a08c, + 0x14108, 0x1a086, 0x14104, 0x141b0, 0x14198, 0x1418c, + 0x140a0, 0x1d02e, 0x1a04c, 0x1a046, 0x14082, 0x1cae0, + 0x1e578, 0x1f2be, 0x194c0, 0x1ca70, 0x1e53c, 0x19460, + 0x1ca38, 0x1e51e, 0x12840, 0x19430, 0x12820, 0x196e0, + 0x1cb78, 0x1e5be, 0x12cc0, 0x19670, 0x1cb3c, 0x12c60, + 0x19638, 0x12c30, 0x12c18, 0x12ee0, 0x19778, 0x1cbbe, + 0x12e70, 0x1973c, 0x12e38, 0x12e1c, 0x12f78, 0x197be, + 0x12f3c, 0x12fbe, 0x1dac0, 0x1ed70, 0x1f6bc, 0x1da60, + 0x1ed38, 0x1f69e, 0x1b440, 0x1da30, 0x1ed1c, 0x1b420, + 0x1da18, 0x1ed0e, 0x1b410, 0x1da0c, 0x192c0, 0x1c970, + 0x1e4bc, 0x1b6c0, 0x19260, 0x1c938, 0x1e49e, 0x1b660, + 0x1db38, 0x1ed9e, 0x16c40, 0x12420, 0x19218, 0x1c90e, + 0x16c20, 0x1b618, 0x16c10, 0x126c0, 0x19370, 0x1c9bc, + 0x16ec0, 0x12660, 0x19338, 0x1c99e, 0x16e60, 0x1b738, + 0x1db9e, 0x16e30, 0x12618, 0x16e18, 0x12770, 0x193bc, + 0x16f70, 0x12738, 0x1939e, 0x16f38, 0x1b79e, 0x16f1c, + 0x127bc, 0x16fbc, 0x1279e, 0x16f9e, 0x1d960, 0x1ecb8, + 0x1f65e, 0x1b240, 0x1d930, 0x1ec9c, 0x1b220, 0x1d918, + 0x1ec8e, 0x1b210, 0x1d90c, 0x1b208, 0x1b204, 0x19160, + 0x1c8b8, 0x1e45e, 0x1b360, 0x19130, 0x1c89c, 0x16640, + 0x12220, 0x1d99c, 0x1c88e, 0x16620, 0x12210, 0x1910c, + 0x16610, 0x1b30c, 0x19106, 0x12204, 0x12360, 0x191b8, + 0x1c8de, 0x16760, 0x12330, 0x1919c, 0x16730, 0x1b39c, + 0x1918e, 0x16718, 0x1230c, 0x12306, 0x123b8, 0x191de, + 0x167b8, 0x1239c, 0x1679c, 0x1238e, 0x1678e, 0x167de, + 0x1b140, 0x1d8b0, 0x1ec5c, 0x1b120, 0x1d898, 0x1ec4e, + 0x1b110, 0x1d88c, 0x1b108, 0x1d886, 0x1b104, 0x1b102, + 0x12140, 0x190b0, 0x1c85c, 0x16340, 0x12120, 0x19098, + 0x1c84e, 0x16320, 0x1b198, 0x1d8ce, 0x16310, 0x12108, + 0x19086, 0x16308, 0x1b186, 0x16304, 0x121b0, 0x190dc, + 0x163b0, 0x12198, 0x190ce, 0x16398, 0x1b1ce, 0x1638c, + 0x12186, 0x16386, 0x163dc, 0x163ce, 0x1b0a0, 0x1d858, + 0x1ec2e, 0x1b090, 0x1d84c, 0x1b088, 0x1d846, 0x1b084, + 0x1b082, 0x120a0, 0x19058, 0x1c82e, 0x161a0, 0x12090, + 0x1904c, 0x16190, 0x1b0cc, 0x19046, 0x16188, 0x12084, + 0x16184, 0x12082, 0x120d8, 0x161d8, 0x161cc, 0x161c6, + 0x1d82c, 0x1d826, 0x1b042, 0x1902c, 0x12048, 0x160c8, + 0x160c4, 0x160c2, 0x18ac0, 0x1c570, 0x1e2bc, 0x18a60, + 0x1c538, 0x11440, 0x18a30, 0x1c51c, 0x11420, 0x18a18, + 0x11410, 0x11408, 0x116c0, 0x18b70, 0x1c5bc, 0x11660, + 0x18b38, 0x1c59e, 0x11630, 0x18b1c, 0x11618, 0x1160c, + 0x11770, 0x18bbc, 0x11738, 0x18b9e, 0x1171c, 0x117bc, + 0x1179e, 0x1cd60, 0x1e6b8, 0x1f35e, 0x19a40, 0x1cd30, + 0x1e69c, 0x19a20, 0x1cd18, 0x1e68e, 0x19a10, 0x1cd0c, + 0x19a08, 0x1cd06, 0x18960, 0x1c4b8, 0x1e25e, 0x19b60, + 0x18930, 0x1c49c, 0x13640, 0x11220, 0x1cd9c, 0x1c48e, + 0x13620, 0x19b18, 0x1890c, 0x13610, 0x11208, 0x13608, + 0x11360, 0x189b8, 0x1c4de, 0x13760, 0x11330, 0x1cdde, + 0x13730, 0x19b9c, 0x1898e, 0x13718, 0x1130c, 0x1370c, + 0x113b8, 0x189de, 0x137b8, 0x1139c, 0x1379c, 0x1138e, + 0x113de, 0x137de, 0x1dd40, 0x1eeb0, 0x1f75c, 0x1dd20, + 0x1ee98, 0x1f74e, 0x1dd10, 0x1ee8c, 0x1dd08, 0x1ee86, + 0x1dd04, 0x19940, 0x1ccb0, 0x1e65c, 0x1bb40, 0x19920, + 0x1eedc, 0x1e64e, 0x1bb20, 0x1dd98, 0x1eece, 0x1bb10, + 0x19908, 0x1cc86, 0x1bb08, 0x1dd86, 0x19902, 0x11140, + 0x188b0, 0x1c45c, 0x13340, 0x11120, 0x18898, 0x1c44e, + 0x17740, 0x13320, 0x19998, 0x1ccce, 0x17720, 0x1bb98, + 0x1ddce, 0x18886, 0x17710, 0x13308, 0x19986, 0x17708, + 0x11102, 0x111b0, 0x188dc, 0x133b0, 0x11198, 0x188ce, + 0x177b0, 0x13398, 0x199ce, 0x17798, 0x1bbce, 0x11186, + 0x13386, 0x111dc, 0x133dc, 0x111ce, 0x177dc, 0x133ce, + 0x1dca0, 0x1ee58, 0x1f72e, 0x1dc90, 0x1ee4c, 0x1dc88, + 0x1ee46, 0x1dc84, 0x1dc82, 0x198a0, 0x1cc58, 0x1e62e, + 0x1b9a0, 0x19890, 0x1ee6e, 0x1b990, 0x1dccc, 0x1cc46, + 0x1b988, 0x19884, 0x1b984, 0x19882, 0x1b982, 0x110a0, + 0x18858, 0x1c42e, 0x131a0, 0x11090, 0x1884c, 0x173a0, + 0x13190, 0x198cc, 0x18846, 0x17390, 0x1b9cc, 0x11084, + 0x17388, 0x13184, 0x11082, 0x13182, 0x110d8, 0x1886e, + 0x131d8, 0x110cc, 0x173d8, 0x131cc, 0x110c6, 0x173cc, + 0x131c6, 0x110ee, 0x173ee, 0x1dc50, 0x1ee2c, 0x1dc48, + 0x1ee26, 0x1dc44, 0x1dc42, 0x19850, 0x1cc2c, 0x1b8d0, + 0x19848, 0x1cc26, 0x1b8c8, 0x1dc66, 0x1b8c4, 0x19842, + 0x1b8c2, 0x11050, 0x1882c, 0x130d0, 0x11048, 0x18826, + 0x171d0, 0x130c8, 0x19866, 0x171c8, 0x1b8e6, 0x11042, + 0x171c4, 0x130c2, 0x171c2, 0x130ec, 0x171ec, 0x171e6, + 0x1ee16, 0x1dc22, 0x1cc16, 0x19824, 0x19822, 0x11028, + 0x13068, 0x170e8, 0x11022, 0x13062, 0x18560, 0x10a40, + 0x18530, 0x10a20, 0x18518, 0x1c28e, 0x10a10, 0x1850c, + 0x10a08, 0x18506, 0x10b60, 0x185b8, 0x1c2de, 0x10b30, + 0x1859c, 0x10b18, 0x1858e, 0x10b0c, 0x10b06, 0x10bb8, + 0x185de, 0x10b9c, 0x10b8e, 0x10bde, 0x18d40, 0x1c6b0, + 0x1e35c, 0x18d20, 0x1c698, 0x18d10, 0x1c68c, 0x18d08, + 0x1c686, 0x18d04, 0x10940, 0x184b0, 0x1c25c, 0x11b40, + 0x10920, 0x1c6dc, 0x1c24e, 0x11b20, 0x18d98, 0x1c6ce, + 0x11b10, 0x10908, 0x18486, 0x11b08, 0x18d86, 0x10902, + 0x109b0, 0x184dc, 0x11bb0, 0x10998, 0x184ce, 0x11b98, + 0x18dce, 0x11b8c, 0x10986, 0x109dc, 0x11bdc, 0x109ce, + 0x11bce, 0x1cea0, 0x1e758, 0x1f3ae, 0x1ce90, 0x1e74c, + 0x1ce88, 0x1e746, 0x1ce84, 0x1ce82, 0x18ca0, 0x1c658, + 0x19da0, 0x18c90, 0x1c64c, 0x19d90, 0x1cecc, 0x1c646, + 0x19d88, 0x18c84, 0x19d84, 0x18c82, 0x19d82, 0x108a0, + 0x18458, 0x119a0, 0x10890, 0x1c66e, 0x13ba0, 0x11990, + 0x18ccc, 0x18446, 0x13b90, 0x19dcc, 0x10884, 0x13b88, + 0x11984, 0x10882, 0x11982, 0x108d8, 0x1846e, 0x119d8, + 0x108cc, 0x13bd8, 0x119cc, 0x108c6, 0x13bcc, 0x119c6, + 0x108ee, 0x119ee, 0x13bee, 0x1ef50, 0x1f7ac, 0x1ef48, + 0x1f7a6, 0x1ef44, 0x1ef42, 0x1ce50, 0x1e72c, 0x1ded0, + 0x1ef6c, 0x1e726, 0x1dec8, 0x1ef66, 0x1dec4, 0x1ce42, + 0x1dec2, 0x18c50, 0x1c62c, 0x19cd0, 0x18c48, 0x1c626, + 0x1bdd0, 0x19cc8, 0x1ce66, 0x1bdc8, 0x1dee6, 0x18c42, + 0x1bdc4, 0x19cc2, 0x1bdc2, 0x10850, 0x1842c, 0x118d0, + 0x10848, 0x18426, 0x139d0, 0x118c8, 0x18c66, 0x17bd0, + 0x139c8, 0x19ce6, 0x10842, 0x17bc8, 0x1bde6, 0x118c2, + 0x17bc4, 0x1086c, 0x118ec, 0x10866, 0x139ec, 0x118e6, + 0x17bec, 0x139e6, 0x17be6, 0x1ef28, 0x1f796, 0x1ef24, + 0x1ef22, 0x1ce28, 0x1e716, 0x1de68, 0x1ef36, 0x1de64, + 0x1ce22, 0x1de62, 0x18c28, 0x1c616, 0x19c68, 0x18c24, + 0x1bce8, 0x19c64, 0x18c22, 0x1bce4, 0x19c62, 0x1bce2, + 0x10828, 0x18416, 0x11868, 0x18c36, 0x138e8, 0x11864, + 0x10822, 0x179e8, 0x138e4, 0x11862, 0x179e4, 0x138e2, + 0x179e2, 0x11876, 0x179f6, 0x1ef12, 0x1de34, 0x1de32, + 0x19c34, 0x1bc74, 0x1bc72, 0x11834, 0x13874, 0x178f4, + 0x178f2, 0x10540, 0x10520, 0x18298, 0x10510, 0x10508, + 0x10504, 0x105b0, 0x10598, 0x1058c, 0x10586, 0x105dc, + 0x105ce, 0x186a0, 0x18690, 0x1c34c, 0x18688, 0x1c346, + 0x18684, 0x18682, 0x104a0, 0x18258, 0x10da0, 0x186d8, + 0x1824c, 0x10d90, 0x186cc, 0x10d88, 0x186c6, 0x10d84, + 0x10482, 0x10d82, 0x104d8, 0x1826e, 0x10dd8, 0x186ee, + 0x10dcc, 0x104c6, 0x10dc6, 0x104ee, 0x10dee, 0x1c750, + 0x1c748, 0x1c744, 0x1c742, 0x18650, 0x18ed0, 0x1c76c, + 0x1c326, 0x18ec8, 0x1c766, 0x18ec4, 0x18642, 0x18ec2, + 0x10450, 0x10cd0, 0x10448, 0x18226, 0x11dd0, 0x10cc8, + 0x10444, 0x11dc8, 0x10cc4, 0x10442, 0x11dc4, 0x10cc2, + 0x1046c, 0x10cec, 0x10466, 0x11dec, 0x10ce6, 0x11de6, + 0x1e7a8, 0x1e7a4, 0x1e7a2, 0x1c728, 0x1cf68, 0x1e7b6, + 0x1cf64, 0x1c722, 0x1cf62, 0x18628, 0x1c316, 0x18e68, + 0x1c736, 0x19ee8, 0x18e64, 0x18622, 0x19ee4, 0x18e62, + 0x19ee2, 0x10428, 0x18216, 0x10c68, 0x18636, 0x11ce8, + 0x10c64, 0x10422, 0x13de8, 0x11ce4, 0x10c62, 0x13de4, + 0x11ce2, 0x10436, 0x10c76, 0x11cf6, 0x13df6, 0x1f7d4, + 0x1f7d2, 0x1e794, 0x1efb4, 0x1e792, 0x1efb2, 0x1c714, + 0x1cf34, 0x1c712, 0x1df74, 0x1cf32, 0x1df72, 0x18614, + 0x18e34, 0x18612, 0x19e74, 0x18e32, 0x1bef4}, + {0x1f560, 0x1fab8, 0x1ea40, 0x1f530, 0x1fa9c, 0x1ea20, + 0x1f518, 0x1fa8e, 0x1ea10, 0x1f50c, 0x1ea08, 0x1f506, + 0x1ea04, 0x1eb60, 0x1f5b8, 0x1fade, 0x1d640, 0x1eb30, + 0x1f59c, 0x1d620, 0x1eb18, 0x1f58e, 0x1d610, 0x1eb0c, + 0x1d608, 0x1eb06, 0x1d604, 0x1d760, 0x1ebb8, 0x1f5de, + 0x1ae40, 0x1d730, 0x1eb9c, 0x1ae20, 0x1d718, 0x1eb8e, + 0x1ae10, 0x1d70c, 0x1ae08, 0x1d706, 0x1ae04, 0x1af60, + 0x1d7b8, 0x1ebde, 0x15e40, 0x1af30, 0x1d79c, 0x15e20, + 0x1af18, 0x1d78e, 0x15e10, 0x1af0c, 0x15e08, 0x1af06, + 0x15f60, 0x1afb8, 0x1d7de, 0x15f30, 0x1af9c, 0x15f18, + 0x1af8e, 0x15f0c, 0x15fb8, 0x1afde, 0x15f9c, 0x15f8e, + 0x1e940, 0x1f4b0, 0x1fa5c, 0x1e920, 0x1f498, 0x1fa4e, + 0x1e910, 0x1f48c, 0x1e908, 0x1f486, 0x1e904, 0x1e902, + 0x1d340, 0x1e9b0, 0x1f4dc, 0x1d320, 0x1e998, 0x1f4ce, + 0x1d310, 0x1e98c, 0x1d308, 0x1e986, 0x1d304, 0x1d302, + 0x1a740, 0x1d3b0, 0x1e9dc, 0x1a720, 0x1d398, 0x1e9ce, + 0x1a710, 0x1d38c, 0x1a708, 0x1d386, 0x1a704, 0x1a702, + 0x14f40, 0x1a7b0, 0x1d3dc, 0x14f20, 0x1a798, 0x1d3ce, + 0x14f10, 0x1a78c, 0x14f08, 0x1a786, 0x14f04, 0x14fb0, + 0x1a7dc, 0x14f98, 0x1a7ce, 0x14f8c, 0x14f86, 0x14fdc, + 0x14fce, 0x1e8a0, 0x1f458, 0x1fa2e, 0x1e890, 0x1f44c, + 0x1e888, 0x1f446, 0x1e884, 0x1e882, 0x1d1a0, 0x1e8d8, + 0x1f46e, 0x1d190, 0x1e8cc, 0x1d188, 0x1e8c6, 0x1d184, + 0x1d182, 0x1a3a0, 0x1d1d8, 0x1e8ee, 0x1a390, 0x1d1cc, + 0x1a388, 0x1d1c6, 0x1a384, 0x1a382, 0x147a0, 0x1a3d8, + 0x1d1ee, 0x14790, 0x1a3cc, 0x14788, 0x1a3c6, 0x14784, + 0x14782, 0x147d8, 0x1a3ee, 0x147cc, 0x147c6, 0x147ee, + 0x1e850, 0x1f42c, 0x1e848, 0x1f426, 0x1e844, 0x1e842, + 0x1d0d0, 0x1e86c, 0x1d0c8, 0x1e866, 0x1d0c4, 0x1d0c2, + 0x1a1d0, 0x1d0ec, 0x1a1c8, 0x1d0e6, 0x1a1c4, 0x1a1c2, + 0x143d0, 0x1a1ec, 0x143c8, 0x1a1e6, 0x143c4, 0x143c2, + 0x143ec, 0x143e6, 0x1e828, 0x1f416, 0x1e824, 0x1e822, + 0x1d068, 0x1e836, 0x1d064, 0x1d062, 0x1a0e8, 0x1d076, + 0x1a0e4, 0x1a0e2, 0x141e8, 0x1a0f6, 0x141e4, 0x141e2, + 0x1e814, 0x1e812, 0x1d034, 0x1d032, 0x1a074, 0x1a072, + 0x1e540, 0x1f2b0, 0x1f95c, 0x1e520, 0x1f298, 0x1f94e, + 0x1e510, 0x1f28c, 0x1e508, 0x1f286, 0x1e504, 0x1e502, + 0x1cb40, 0x1e5b0, 0x1f2dc, 0x1cb20, 0x1e598, 0x1f2ce, + 0x1cb10, 0x1e58c, 0x1cb08, 0x1e586, 0x1cb04, 0x1cb02, + 0x19740, 0x1cbb0, 0x1e5dc, 0x19720, 0x1cb98, 0x1e5ce, + 0x19710, 0x1cb8c, 0x19708, 0x1cb86, 0x19704, 0x19702, + 0x12f40, 0x197b0, 0x1cbdc, 0x12f20, 0x19798, 0x1cbce, + 0x12f10, 0x1978c, 0x12f08, 0x19786, 0x12f04, 0x12fb0, + 0x197dc, 0x12f98, 0x197ce, 0x12f8c, 0x12f86, 0x12fdc, + 0x12fce, 0x1f6a0, 0x1fb58, 0x16bf0, 0x1f690, 0x1fb4c, + 0x169f8, 0x1f688, 0x1fb46, 0x168fc, 0x1f684, 0x1f682, + 0x1e4a0, 0x1f258, 0x1f92e, 0x1eda0, 0x1e490, 0x1fb6e, + 0x1ed90, 0x1f6cc, 0x1f246, 0x1ed88, 0x1e484, 0x1ed84, + 0x1e482, 0x1ed82, 0x1c9a0, 0x1e4d8, 0x1f26e, 0x1dba0, + 0x1c990, 0x1e4cc, 0x1db90, 0x1edcc, 0x1e4c6, 0x1db88, + 0x1c984, 0x1db84, 0x1c982, 0x1db82, 0x193a0, 0x1c9d8, + 0x1e4ee, 0x1b7a0, 0x19390, 0x1c9cc, 0x1b790, 0x1dbcc, + 0x1c9c6, 0x1b788, 0x19384, 0x1b784, 0x19382, 0x1b782, + 0x127a0, 0x193d8, 0x1c9ee, 0x16fa0, 0x12790, 0x193cc, + 0x16f90, 0x1b7cc, 0x193c6, 0x16f88, 0x12784, 0x16f84, + 0x12782, 0x127d8, 0x193ee, 0x16fd8, 0x127cc, 0x16fcc, + 0x127c6, 0x16fc6, 0x127ee, 0x1f650, 0x1fb2c, 0x165f8, + 0x1f648, 0x1fb26, 0x164fc, 0x1f644, 0x1647e, 0x1f642, + 0x1e450, 0x1f22c, 0x1ecd0, 0x1e448, 0x1f226, 0x1ecc8, + 0x1f666, 0x1ecc4, 0x1e442, 0x1ecc2, 0x1c8d0, 0x1e46c, + 0x1d9d0, 0x1c8c8, 0x1e466, 0x1d9c8, 0x1ece6, 0x1d9c4, + 0x1c8c2, 0x1d9c2, 0x191d0, 0x1c8ec, 0x1b3d0, 0x191c8, + 0x1c8e6, 0x1b3c8, 0x1d9e6, 0x1b3c4, 0x191c2, 0x1b3c2, + 0x123d0, 0x191ec, 0x167d0, 0x123c8, 0x191e6, 0x167c8, + 0x1b3e6, 0x167c4, 0x123c2, 0x167c2, 0x123ec, 0x167ec, + 0x123e6, 0x167e6, 0x1f628, 0x1fb16, 0x162fc, 0x1f624, + 0x1627e, 0x1f622, 0x1e428, 0x1f216, 0x1ec68, 0x1f636, + 0x1ec64, 0x1e422, 0x1ec62, 0x1c868, 0x1e436, 0x1d8e8, + 0x1c864, 0x1d8e4, 0x1c862, 0x1d8e2, 0x190e8, 0x1c876, + 0x1b1e8, 0x1d8f6, 0x1b1e4, 0x190e2, 0x1b1e2, 0x121e8, + 0x190f6, 0x163e8, 0x121e4, 0x163e4, 0x121e2, 0x163e2, + 0x121f6, 0x163f6, 0x1f614, 0x1617e, 0x1f612, 0x1e414, + 0x1ec34, 0x1e412, 0x1ec32, 0x1c834, 0x1d874, 0x1c832, + 0x1d872, 0x19074, 0x1b0f4, 0x19072, 0x1b0f2, 0x120f4, + 0x161f4, 0x120f2, 0x161f2, 0x1f60a, 0x1e40a, 0x1ec1a, + 0x1c81a, 0x1d83a, 0x1903a, 0x1b07a, 0x1e2a0, 0x1f158, + 0x1f8ae, 0x1e290, 0x1f14c, 0x1e288, 0x1f146, 0x1e284, + 0x1e282, 0x1c5a0, 0x1e2d8, 0x1f16e, 0x1c590, 0x1e2cc, + 0x1c588, 0x1e2c6, 0x1c584, 0x1c582, 0x18ba0, 0x1c5d8, + 0x1e2ee, 0x18b90, 0x1c5cc, 0x18b88, 0x1c5c6, 0x18b84, + 0x18b82, 0x117a0, 0x18bd8, 0x1c5ee, 0x11790, 0x18bcc, + 0x11788, 0x18bc6, 0x11784, 0x11782, 0x117d8, 0x18bee, + 0x117cc, 0x117c6, 0x117ee, 0x1f350, 0x1f9ac, 0x135f8, + 0x1f348, 0x1f9a6, 0x134fc, 0x1f344, 0x1347e, 0x1f342, + 0x1e250, 0x1f12c, 0x1e6d0, 0x1e248, 0x1f126, 0x1e6c8, + 0x1f366, 0x1e6c4, 0x1e242, 0x1e6c2, 0x1c4d0, 0x1e26c, + 0x1cdd0, 0x1c4c8, 0x1e266, 0x1cdc8, 0x1e6e6, 0x1cdc4, + 0x1c4c2, 0x1cdc2, 0x189d0, 0x1c4ec, 0x19bd0, 0x189c8, + 0x1c4e6, 0x19bc8, 0x1cde6, 0x19bc4, 0x189c2, 0x19bc2, + 0x113d0, 0x189ec, 0x137d0, 0x113c8, 0x189e6, 0x137c8, + 0x19be6, 0x137c4, 0x113c2, 0x137c2, 0x113ec, 0x137ec, + 0x113e6, 0x137e6, 0x1fba8, 0x175f0, 0x1bafc, 0x1fba4, + 0x174f8, 0x1ba7e, 0x1fba2, 0x1747c, 0x1743e, 0x1f328, + 0x1f996, 0x132fc, 0x1f768, 0x1fbb6, 0x176fc, 0x1327e, + 0x1f764, 0x1f322, 0x1767e, 0x1f762, 0x1e228, 0x1f116, + 0x1e668, 0x1e224, 0x1eee8, 0x1f776, 0x1e222, 0x1eee4, + 0x1e662, 0x1eee2, 0x1c468, 0x1e236, 0x1cce8, 0x1c464, + 0x1dde8, 0x1cce4, 0x1c462, 0x1dde4, 0x1cce2, 0x1dde2, + 0x188e8, 0x1c476, 0x199e8, 0x188e4, 0x1bbe8, 0x199e4, + 0x188e2, 0x1bbe4, 0x199e2, 0x1bbe2, 0x111e8, 0x188f6, + 0x133e8, 0x111e4, 0x177e8, 0x133e4, 0x111e2, 0x177e4, + 0x133e2, 0x177e2, 0x111f6, 0x133f6, 0x1fb94, 0x172f8, + 0x1b97e, 0x1fb92, 0x1727c, 0x1723e, 0x1f314, 0x1317e, + 0x1f734, 0x1f312, 0x1737e, 0x1f732, 0x1e214, 0x1e634, + 0x1e212, 0x1ee74, 0x1e632, 0x1ee72, 0x1c434, 0x1cc74, + 0x1c432, 0x1dcf4, 0x1cc72, 0x1dcf2, 0x18874, 0x198f4, + 0x18872, 0x1b9f4, 0x198f2, 0x1b9f2, 0x110f4, 0x131f4, + 0x110f2, 0x173f4, 0x131f2, 0x173f2, 0x1fb8a, 0x1717c, + 0x1713e, 0x1f30a, 0x1f71a, 0x1e20a, 0x1e61a, 0x1ee3a, + 0x1c41a, 0x1cc3a, 0x1dc7a, 0x1883a, 0x1987a, 0x1b8fa, + 0x1107a, 0x130fa, 0x171fa, 0x170be, 0x1e150, 0x1f0ac, + 0x1e148, 0x1f0a6, 0x1e144, 0x1e142, 0x1c2d0, 0x1e16c, + 0x1c2c8, 0x1e166, 0x1c2c4, 0x1c2c2, 0x185d0, 0x1c2ec, + 0x185c8, 0x1c2e6, 0x185c4, 0x185c2, 0x10bd0, 0x185ec, + 0x10bc8, 0x185e6, 0x10bc4, 0x10bc2, 0x10bec, 0x10be6, + 0x1f1a8, 0x1f8d6, 0x11afc, 0x1f1a4, 0x11a7e, 0x1f1a2, + 0x1e128, 0x1f096, 0x1e368, 0x1e124, 0x1e364, 0x1e122, + 0x1e362, 0x1c268, 0x1e136, 0x1c6e8, 0x1c264, 0x1c6e4, + 0x1c262, 0x1c6e2, 0x184e8, 0x1c276, 0x18de8, 0x184e4, + 0x18de4, 0x184e2, 0x18de2, 0x109e8, 0x184f6, 0x11be8, + 0x109e4, 0x11be4, 0x109e2, 0x11be2, 0x109f6, 0x11bf6, + 0x1f9d4, 0x13af8, 0x19d7e, 0x1f9d2, 0x13a7c, 0x13a3e, + 0x1f194, 0x1197e, 0x1f3b4, 0x1f192, 0x13b7e, 0x1f3b2, + 0x1e114, 0x1e334, 0x1e112, 0x1e774, 0x1e332, 0x1e772, + 0x1c234, 0x1c674, 0x1c232, 0x1cef4, 0x1c672, 0x1cef2, + 0x18474, 0x18cf4, 0x18472, 0x19df4, 0x18cf2, 0x19df2, + 0x108f4, 0x119f4, 0x108f2, 0x13bf4, 0x119f2, 0x13bf2, + 0x17af0, 0x1bd7c, 0x17a78, 0x1bd3e, 0x17a3c, 0x17a1e, + 0x1f9ca, 0x1397c, 0x1fbda, 0x17b7c, 0x1393e, 0x17b3e, + 0x1f18a, 0x1f39a, 0x1f7ba, 0x1e10a, 0x1e31a, 0x1e73a, + 0x1ef7a, 0x1c21a, 0x1c63a, 0x1ce7a, 0x1defa, 0x1843a, + 0x18c7a, 0x19cfa, 0x1bdfa, 0x1087a, 0x118fa, 0x139fa, + 0x17978, 0x1bcbe, 0x1793c, 0x1791e, 0x138be, 0x179be, + 0x178bc, 0x1789e, 0x1785e, 0x1e0a8, 0x1e0a4, 0x1e0a2, + 0x1c168, 0x1e0b6, 0x1c164, 0x1c162, 0x182e8, 0x1c176, + 0x182e4, 0x182e2, 0x105e8, 0x182f6, 0x105e4, 0x105e2, + 0x105f6, 0x1f0d4, 0x10d7e, 0x1f0d2, 0x1e094, 0x1e1b4, + 0x1e092, 0x1e1b2, 0x1c134, 0x1c374, 0x1c132, 0x1c372, + 0x18274, 0x186f4, 0x18272, 0x186f2, 0x104f4, 0x10df4, + 0x104f2, 0x10df2, 0x1f8ea, 0x11d7c, 0x11d3e, 0x1f0ca, + 0x1f1da, 0x1e08a, 0x1e19a, 0x1e3ba, 0x1c11a, 0x1c33a, + 0x1c77a, 0x1823a, 0x1867a, 0x18efa, 0x1047a, 0x10cfa, + 0x11dfa, 0x13d78, 0x19ebe, 0x13d3c, 0x13d1e, 0x11cbe, + 0x13dbe, 0x17d70, 0x1bebc, 0x17d38, 0x1be9e, 0x17d1c, + 0x17d0e, 0x13cbc, 0x17dbc, 0x13c9e, 0x17d9e, 0x17cb8, + 0x1be5e, 0x17c9c, 0x17c8e, 0x13c5e, 0x17cde, 0x17c5c, + 0x17c4e, 0x17c2e, 0x1c0b4, 0x1c0b2, 0x18174, 0x18172, + 0x102f4, 0x102f2, 0x1e0da, 0x1c09a, 0x1c1ba, 0x1813a, + 0x1837a, 0x1027a, 0x106fa, 0x10ebe, 0x11ebc, 0x11e9e, + 0x13eb8, 0x19f5e, 0x13e9c, 0x13e8e, 0x11e5e, 0x13ede, + 0x17eb0, 0x1bf5c, 0x17e98, 0x1bf4e, 0x17e8c, 0x17e86, + 0x13e5c, 0x17edc, 0x13e4e, 0x17ece, 0x17e58, 0x1bf2e, + 0x17e4c, 0x17e46, 0x13e2e, 0x17e6e, 0x17e2c, 0x17e26, + 0x10f5e, 0x11f5c, 0x11f4e, 0x13f58, 0x19fae, 0x13f4c, + 0x13f46, 0x11f2e, 0x13f6e, 0x13f2c, 0x13f26}, + {0x1abe0, 0x1d5f8, 0x153c0, 0x1a9f0, 0x1d4fc, 0x151e0, + 0x1a8f8, 0x1d47e, 0x150f0, 0x1a87c, 0x15078, 0x1fad0, + 0x15be0, 0x1adf8, 0x1fac8, 0x159f0, 0x1acfc, 0x1fac4, + 0x158f8, 0x1ac7e, 0x1fac2, 0x1587c, 0x1f5d0, 0x1faec, + 0x15df8, 0x1f5c8, 0x1fae6, 0x15cfc, 0x1f5c4, 0x15c7e, + 0x1f5c2, 0x1ebd0, 0x1f5ec, 0x1ebc8, 0x1f5e6, 0x1ebc4, + 0x1ebc2, 0x1d7d0, 0x1ebec, 0x1d7c8, 0x1ebe6, 0x1d7c4, + 0x1d7c2, 0x1afd0, 0x1d7ec, 0x1afc8, 0x1d7e6, 0x1afc4, + 0x14bc0, 0x1a5f0, 0x1d2fc, 0x149e0, 0x1a4f8, 0x1d27e, + 0x148f0, 0x1a47c, 0x14878, 0x1a43e, 0x1483c, 0x1fa68, + 0x14df0, 0x1a6fc, 0x1fa64, 0x14cf8, 0x1a67e, 0x1fa62, + 0x14c7c, 0x14c3e, 0x1f4e8, 0x1fa76, 0x14efc, 0x1f4e4, + 0x14e7e, 0x1f4e2, 0x1e9e8, 0x1f4f6, 0x1e9e4, 0x1e9e2, + 0x1d3e8, 0x1e9f6, 0x1d3e4, 0x1d3e2, 0x1a7e8, 0x1d3f6, + 0x1a7e4, 0x1a7e2, 0x145e0, 0x1a2f8, 0x1d17e, 0x144f0, + 0x1a27c, 0x14478, 0x1a23e, 0x1443c, 0x1441e, 0x1fa34, + 0x146f8, 0x1a37e, 0x1fa32, 0x1467c, 0x1463e, 0x1f474, + 0x1477e, 0x1f472, 0x1e8f4, 0x1e8f2, 0x1d1f4, 0x1d1f2, + 0x1a3f4, 0x1a3f2, 0x142f0, 0x1a17c, 0x14278, 0x1a13e, + 0x1423c, 0x1421e, 0x1fa1a, 0x1437c, 0x1433e, 0x1f43a, + 0x1e87a, 0x1d0fa, 0x14178, 0x1a0be, 0x1413c, 0x1411e, + 0x141be, 0x140bc, 0x1409e, 0x12bc0, 0x195f0, 0x1cafc, + 0x129e0, 0x194f8, 0x1ca7e, 0x128f0, 0x1947c, 0x12878, + 0x1943e, 0x1283c, 0x1f968, 0x12df0, 0x196fc, 0x1f964, + 0x12cf8, 0x1967e, 0x1f962, 0x12c7c, 0x12c3e, 0x1f2e8, + 0x1f976, 0x12efc, 0x1f2e4, 0x12e7e, 0x1f2e2, 0x1e5e8, + 0x1f2f6, 0x1e5e4, 0x1e5e2, 0x1cbe8, 0x1e5f6, 0x1cbe4, + 0x1cbe2, 0x197e8, 0x1cbf6, 0x197e4, 0x197e2, 0x1b5e0, + 0x1daf8, 0x1ed7e, 0x169c0, 0x1b4f0, 0x1da7c, 0x168e0, + 0x1b478, 0x1da3e, 0x16870, 0x1b43c, 0x16838, 0x1b41e, + 0x1681c, 0x125e0, 0x192f8, 0x1c97e, 0x16de0, 0x124f0, + 0x1927c, 0x16cf0, 0x1b67c, 0x1923e, 0x16c78, 0x1243c, + 0x16c3c, 0x1241e, 0x16c1e, 0x1f934, 0x126f8, 0x1937e, + 0x1fb74, 0x1f932, 0x16ef8, 0x1267c, 0x1fb72, 0x16e7c, + 0x1263e, 0x16e3e, 0x1f274, 0x1277e, 0x1f6f4, 0x1f272, + 0x16f7e, 0x1f6f2, 0x1e4f4, 0x1edf4, 0x1e4f2, 0x1edf2, + 0x1c9f4, 0x1dbf4, 0x1c9f2, 0x1dbf2, 0x193f4, 0x193f2, + 0x165c0, 0x1b2f0, 0x1d97c, 0x164e0, 0x1b278, 0x1d93e, + 0x16470, 0x1b23c, 0x16438, 0x1b21e, 0x1641c, 0x1640e, + 0x122f0, 0x1917c, 0x166f0, 0x12278, 0x1913e, 0x16678, + 0x1b33e, 0x1663c, 0x1221e, 0x1661e, 0x1f91a, 0x1237c, + 0x1fb3a, 0x1677c, 0x1233e, 0x1673e, 0x1f23a, 0x1f67a, + 0x1e47a, 0x1ecfa, 0x1c8fa, 0x1d9fa, 0x191fa, 0x162e0, + 0x1b178, 0x1d8be, 0x16270, 0x1b13c, 0x16238, 0x1b11e, + 0x1621c, 0x1620e, 0x12178, 0x190be, 0x16378, 0x1213c, + 0x1633c, 0x1211e, 0x1631e, 0x121be, 0x163be, 0x16170, + 0x1b0bc, 0x16138, 0x1b09e, 0x1611c, 0x1610e, 0x120bc, + 0x161bc, 0x1209e, 0x1619e, 0x160b8, 0x1b05e, 0x1609c, + 0x1608e, 0x1205e, 0x160de, 0x1605c, 0x1604e, 0x115e0, + 0x18af8, 0x1c57e, 0x114f0, 0x18a7c, 0x11478, 0x18a3e, + 0x1143c, 0x1141e, 0x1f8b4, 0x116f8, 0x18b7e, 0x1f8b2, + 0x1167c, 0x1163e, 0x1f174, 0x1177e, 0x1f172, 0x1e2f4, + 0x1e2f2, 0x1c5f4, 0x1c5f2, 0x18bf4, 0x18bf2, 0x135c0, + 0x19af0, 0x1cd7c, 0x134e0, 0x19a78, 0x1cd3e, 0x13470, + 0x19a3c, 0x13438, 0x19a1e, 0x1341c, 0x1340e, 0x112f0, + 0x1897c, 0x136f0, 0x11278, 0x1893e, 0x13678, 0x19b3e, + 0x1363c, 0x1121e, 0x1361e, 0x1f89a, 0x1137c, 0x1f9ba, + 0x1377c, 0x1133e, 0x1373e, 0x1f13a, 0x1f37a, 0x1e27a, + 0x1e6fa, 0x1c4fa, 0x1cdfa, 0x189fa, 0x1bae0, 0x1dd78, + 0x1eebe, 0x174c0, 0x1ba70, 0x1dd3c, 0x17460, 0x1ba38, + 0x1dd1e, 0x17430, 0x1ba1c, 0x17418, 0x1ba0e, 0x1740c, + 0x132e0, 0x19978, 0x1ccbe, 0x176e0, 0x13270, 0x1993c, + 0x17670, 0x1bb3c, 0x1991e, 0x17638, 0x1321c, 0x1761c, + 0x1320e, 0x1760e, 0x11178, 0x188be, 0x13378, 0x1113c, + 0x17778, 0x1333c, 0x1111e, 0x1773c, 0x1331e, 0x1771e, + 0x111be, 0x133be, 0x177be, 0x172c0, 0x1b970, 0x1dcbc, + 0x17260, 0x1b938, 0x1dc9e, 0x17230, 0x1b91c, 0x17218, + 0x1b90e, 0x1720c, 0x17206, 0x13170, 0x198bc, 0x17370, + 0x13138, 0x1989e, 0x17338, 0x1b99e, 0x1731c, 0x1310e, + 0x1730e, 0x110bc, 0x131bc, 0x1109e, 0x173bc, 0x1319e, + 0x1739e, 0x17160, 0x1b8b8, 0x1dc5e, 0x17130, 0x1b89c, + 0x17118, 0x1b88e, 0x1710c, 0x17106, 0x130b8, 0x1985e, + 0x171b8, 0x1309c, 0x1719c, 0x1308e, 0x1718e, 0x1105e, + 0x130de, 0x171de, 0x170b0, 0x1b85c, 0x17098, 0x1b84e, + 0x1708c, 0x17086, 0x1305c, 0x170dc, 0x1304e, 0x170ce, + 0x17058, 0x1b82e, 0x1704c, 0x17046, 0x1302e, 0x1706e, + 0x1702c, 0x17026, 0x10af0, 0x1857c, 0x10a78, 0x1853e, + 0x10a3c, 0x10a1e, 0x10b7c, 0x10b3e, 0x1f0ba, 0x1e17a, + 0x1c2fa, 0x185fa, 0x11ae0, 0x18d78, 0x1c6be, 0x11a70, + 0x18d3c, 0x11a38, 0x18d1e, 0x11a1c, 0x11a0e, 0x10978, + 0x184be, 0x11b78, 0x1093c, 0x11b3c, 0x1091e, 0x11b1e, + 0x109be, 0x11bbe, 0x13ac0, 0x19d70, 0x1cebc, 0x13a60, + 0x19d38, 0x1ce9e, 0x13a30, 0x19d1c, 0x13a18, 0x19d0e, + 0x13a0c, 0x13a06, 0x11970, 0x18cbc, 0x13b70, 0x11938, + 0x18c9e, 0x13b38, 0x1191c, 0x13b1c, 0x1190e, 0x13b0e, + 0x108bc, 0x119bc, 0x1089e, 0x13bbc, 0x1199e, 0x13b9e, + 0x1bd60, 0x1deb8, 0x1ef5e, 0x17a40, 0x1bd30, 0x1de9c, + 0x17a20, 0x1bd18, 0x1de8e, 0x17a10, 0x1bd0c, 0x17a08, + 0x1bd06, 0x17a04, 0x13960, 0x19cb8, 0x1ce5e, 0x17b60, + 0x13930, 0x19c9c, 0x17b30, 0x1bd9c, 0x19c8e, 0x17b18, + 0x1390c, 0x17b0c, 0x13906, 0x17b06, 0x118b8, 0x18c5e, + 0x139b8, 0x1189c, 0x17bb8, 0x1399c, 0x1188e, 0x17b9c, + 0x1398e, 0x17b8e, 0x1085e, 0x118de, 0x139de, 0x17bde, + 0x17940, 0x1bcb0, 0x1de5c, 0x17920, 0x1bc98, 0x1de4e, + 0x17910, 0x1bc8c, 0x17908, 0x1bc86, 0x17904, 0x17902, + 0x138b0, 0x19c5c, 0x179b0, 0x13898, 0x19c4e, 0x17998, + 0x1bcce, 0x1798c, 0x13886, 0x17986, 0x1185c, 0x138dc, + 0x1184e, 0x179dc, 0x138ce, 0x179ce, 0x178a0, 0x1bc58, + 0x1de2e, 0x17890, 0x1bc4c, 0x17888, 0x1bc46, 0x17884, + 0x17882, 0x13858, 0x19c2e, 0x178d8, 0x1384c, 0x178cc, + 0x13846, 0x178c6, 0x1182e, 0x1386e, 0x178ee, 0x17850, + 0x1bc2c, 0x17848, 0x1bc26, 0x17844, 0x17842, 0x1382c, + 0x1786c, 0x13826, 0x17866, 0x17828, 0x1bc16, 0x17824, + 0x17822, 0x13816, 0x17836, 0x10578, 0x182be, 0x1053c, + 0x1051e, 0x105be, 0x10d70, 0x186bc, 0x10d38, 0x1869e, + 0x10d1c, 0x10d0e, 0x104bc, 0x10dbc, 0x1049e, 0x10d9e, + 0x11d60, 0x18eb8, 0x1c75e, 0x11d30, 0x18e9c, 0x11d18, + 0x18e8e, 0x11d0c, 0x11d06, 0x10cb8, 0x1865e, 0x11db8, + 0x10c9c, 0x11d9c, 0x10c8e, 0x11d8e, 0x1045e, 0x10cde, + 0x11dde, 0x13d40, 0x19eb0, 0x1cf5c, 0x13d20, 0x19e98, + 0x1cf4e, 0x13d10, 0x19e8c, 0x13d08, 0x19e86, 0x13d04, + 0x13d02, 0x11cb0, 0x18e5c, 0x13db0, 0x11c98, 0x18e4e, + 0x13d98, 0x19ece, 0x13d8c, 0x11c86, 0x13d86, 0x10c5c, + 0x11cdc, 0x10c4e, 0x13ddc, 0x11cce, 0x13dce, 0x1bea0, + 0x1df58, 0x1efae, 0x1be90, 0x1df4c, 0x1be88, 0x1df46, + 0x1be84, 0x1be82, 0x13ca0, 0x19e58, 0x1cf2e, 0x17da0, + 0x13c90, 0x19e4c, 0x17d90, 0x1becc, 0x19e46, 0x17d88, + 0x13c84, 0x17d84, 0x13c82, 0x17d82, 0x11c58, 0x18e2e, + 0x13cd8, 0x11c4c, 0x17dd8, 0x13ccc, 0x11c46, 0x17dcc, + 0x13cc6, 0x17dc6, 0x10c2e, 0x11c6e, 0x13cee, 0x17dee, + 0x1be50, 0x1df2c, 0x1be48, 0x1df26, 0x1be44, 0x1be42, + 0x13c50, 0x19e2c, 0x17cd0, 0x13c48, 0x19e26, 0x17cc8, + 0x1be66, 0x17cc4, 0x13c42, 0x17cc2, 0x11c2c, 0x13c6c, + 0x11c26, 0x17cec, 0x13c66, 0x17ce6, 0x1be28, 0x1df16, + 0x1be24, 0x1be22, 0x13c28, 0x19e16, 0x17c68, 0x13c24, + 0x17c64, 0x13c22, 0x17c62, 0x11c16, 0x13c36, 0x17c76, + 0x1be14, 0x1be12, 0x13c14, 0x17c34, 0x13c12, 0x17c32, + 0x102bc, 0x1029e, 0x106b8, 0x1835e, 0x1069c, 0x1068e, + 0x1025e, 0x106de, 0x10eb0, 0x1875c, 0x10e98, 0x1874e, + 0x10e8c, 0x10e86, 0x1065c, 0x10edc, 0x1064e, 0x10ece, + 0x11ea0, 0x18f58, 0x1c7ae, 0x11e90, 0x18f4c, 0x11e88, + 0x18f46, 0x11e84, 0x11e82, 0x10e58, 0x1872e, 0x11ed8, + 0x18f6e, 0x11ecc, 0x10e46, 0x11ec6, 0x1062e, 0x10e6e, + 0x11eee, 0x19f50, 0x1cfac, 0x19f48, 0x1cfa6, 0x19f44, + 0x19f42, 0x11e50, 0x18f2c, 0x13ed0, 0x19f6c, 0x18f26, + 0x13ec8, 0x11e44, 0x13ec4, 0x11e42, 0x13ec2, 0x10e2c, + 0x11e6c, 0x10e26, 0x13eec, 0x11e66, 0x13ee6, 0x1dfa8, + 0x1efd6, 0x1dfa4, 0x1dfa2, 0x19f28, 0x1cf96, 0x1bf68, + 0x19f24, 0x1bf64, 0x19f22, 0x1bf62, 0x11e28, 0x18f16, + 0x13e68, 0x11e24, 0x17ee8, 0x13e64, 0x11e22, 0x17ee4, + 0x13e62, 0x17ee2, 0x10e16, 0x11e36, 0x13e76, 0x17ef6, + 0x1df94, 0x1df92, 0x19f14, 0x1bf34, 0x19f12, 0x1bf32, + 0x11e14, 0x13e34, 0x11e12, 0x17e74, 0x13e32, 0x17e72, + 0x1df8a, 0x19f0a, 0x1bf1a, 0x11e0a, 0x13e1a, 0x17e3a, + 0x1035c, 0x1034e, 0x10758, 0x183ae, 0x1074c, 0x10746, + 0x1032e, 0x1076e, 0x10f50, 0x187ac, 0x10f48, 0x187a6, + 0x10f44, 0x10f42, 0x1072c, 0x10f6c, 0x10726, 0x10f66, + 0x18fa8, 0x1c7d6, 0x18fa4, 0x18fa2, 0x10f28, 0x18796, + 0x11f68, 0x18fb6, 0x11f64, 0x10f22, 0x11f62, 0x10716, + 0x10f36, 0x11f76, 0x1cfd4, 0x1cfd2, 0x18f94, 0x19fb4, + 0x18f92, 0x19fb2, 0x10f14, 0x11f34, 0x10f12, 0x13f74, + 0x11f32, 0x13f72, 0x1cfca, 0x18f8a, 0x19f9a, 0x10f0a, + 0x11f1a, 0x13f3a, 0x103ac, 0x103a6, 0x107a8, 0x183d6, + 0x107a4, 0x107a2, 0x10396, 0x107b6, 0x187d4, 0x187d2, + 0x10794, 0x10fb4, 0x10792, 0x10fb2, 0x1c7ea}}; + + private static final float PREFERRED_RATIO = 3.0f; + private static final float DEFAULT_MODULE_WIDTH = 0.357f; //1px in mm + private static final float HEIGHT = 2.0f; //mm + + private BarcodeMatrix barcodeMatrix; + private boolean compact; + private Compaction compaction; + private Charset encoding; + private int minCols; + private int maxCols; + private int maxRows; + private int minRows; + + public PDF417() { + this(false); + } + + public PDF417(boolean compact) { + this.compact = compact; + compaction = Compaction.AUTO; + encoding = null; // Use default + minCols = 2; + maxCols = 30; + maxRows = 30; + minRows = 2; + } + + public BarcodeMatrix getBarcodeMatrix() { + return barcodeMatrix; + } + + /** + * Calculates the necessary number of rows as described in annex Q of ISO/IEC 15438:2001(E). + * + * @param m the number of source codewords prior to the additional of the Symbol Length + * Descriptor and any pad codewords + * @param k the number of error correction codewords + * @param c the number of columns in the symbol in the data region (excluding start, stop and + * row indicator codewords) + * @return the number of rows in the symbol (r) + */ + private static int calculateNumberOfRows(int m, int k, int c) { + int r = ((m + 1 + k) / c) + 1; + if (c * r >= (m + 1 + k + c)) { + r--; + } + return r; + } + + /** + * Calculates the number of pad codewords as described in 4.9.2 of ISO/IEC 15438:2001(E). + * + * @param m the number of source codewords prior to the additional of the Symbol Length + * Descriptor and any pad codewords + * @param k the number of error correction codewords + * @param c the number of columns in the symbol in the data region (excluding start, stop and + * row indicator codewords) + * @param r the number of rows in the symbol + * @return the number of pad codewords + */ + private static int getNumberOfPadCodewords(int m, int k, int c, int r) { + int n = c * r - k; + return n > m + 1 ? n - m - 1 : 0; + } + + private static void encodeChar(int pattern, int len, BarcodeRow logic) { + int map = 1 << len - 1; + boolean last = (pattern & map) != 0; //Initialize to inverse of first bit + int width = 0; + for (int i = 0; i < len; i++) { + boolean black = (pattern & map) != 0; + if (last == black) { + width++; + } else { + logic.addBar(last, width); + + last = black; + width = 1; + } + map >>= 1; + } + logic.addBar(last, width); + } + + private void encodeLowLevel(CharSequence fullCodewords, + int c, + int r, + int errorCorrectionLevel, + BarcodeMatrix logic) { + + int idx = 0; + for (int y = 0; y < r; y++) { + int cluster = y % 3; + logic.startRow(); + encodeChar(START_PATTERN, 17, logic.getCurrentRow()); + + int left; + int right; + if (cluster == 0) { + left = (30 * (y / 3)) + ((r - 1) / 3); + right = (30 * (y / 3)) + (c - 1); + } else if (cluster == 1) { + left = (30 * (y / 3)) + (errorCorrectionLevel * 3) + ((r - 1) % 3); + right = (30 * (y / 3)) + ((r - 1) / 3); + } else { + left = (30 * (y / 3)) + (c - 1); + right = (30 * (y / 3)) + (errorCorrectionLevel * 3) + ((r - 1) % 3); + } + + int pattern = CODEWORD_TABLE[cluster][left]; + encodeChar(pattern, 17, logic.getCurrentRow()); + + for (int x = 0; x < c; x++) { + pattern = CODEWORD_TABLE[cluster][fullCodewords.charAt(idx)]; + encodeChar(pattern, 17, logic.getCurrentRow()); + idx++; + } + + if (compact) { + encodeChar(STOP_PATTERN, 1, logic.getCurrentRow()); // encodes stop line for compact pdf417 + } else { + pattern = CODEWORD_TABLE[cluster][right]; + encodeChar(pattern, 17, logic.getCurrentRow()); + + encodeChar(STOP_PATTERN, 18, logic.getCurrentRow()); + } + } + } + + /** + * @param msg message to encode + * @param errorCorrectionLevel PDF417 error correction level to use + * @throws WriterException if the contents cannot be encoded in this format + */ + public void generateBarcodeLogic(String msg, int errorCorrectionLevel) throws WriterException { + + //1. step: High-level encoding + int errorCorrectionCodeWords = PDF417ErrorCorrection.getErrorCorrectionCodewordCount(errorCorrectionLevel); + String highLevel = PDF417HighLevelEncoder.encodeHighLevel(msg, compaction, encoding); + int sourceCodeWords = highLevel.length(); + + int[] dimension = determineDimensions(sourceCodeWords, errorCorrectionCodeWords); + + int cols = dimension[0]; + int rows = dimension[1]; + + int pad = getNumberOfPadCodewords(sourceCodeWords, errorCorrectionCodeWords, cols, rows); + + //2. step: construct data codewords + if (sourceCodeWords + errorCorrectionCodeWords + 1 > 929) { // +1 for symbol length CW + throw new WriterException( + "Encoded message contains too many code words, message too big (" + msg.length() + " bytes)"); + } + int n = sourceCodeWords + pad + 1; + StringBuilder sb = new StringBuilder(n); + sb.append((char) n); + sb.append(highLevel); + for (int i = 0; i < pad; i++) { + sb.append((char) 900); //PAD characters + } + String dataCodewords = sb.toString(); + + //3. step: Error correction + String ec = PDF417ErrorCorrection.generateErrorCorrection(dataCodewords, errorCorrectionLevel); + + //4. step: low-level encoding + barcodeMatrix = new BarcodeMatrix(rows, cols); + encodeLowLevel(dataCodewords + ec, cols, rows, errorCorrectionLevel, barcodeMatrix); + } + + /** + * Determine optimal nr of columns and rows for the specified number of + * codewords. + * + * @param sourceCodeWords number of code words + * @param errorCorrectionCodeWords number of error correction code words + * @return dimension object containing cols as width and rows as height + */ + private int[] determineDimensions(int sourceCodeWords, int errorCorrectionCodeWords) throws WriterException { + float ratio = 0.0f; + int[] dimension = null; + + for (int cols = minCols; cols <= maxCols; cols++) { + + int rows = calculateNumberOfRows(sourceCodeWords, errorCorrectionCodeWords, cols); + + if (rows < minRows) { + break; + } + + if (rows > maxRows) { + continue; + } + + float newRatio = ((17 * cols + 69) * DEFAULT_MODULE_WIDTH) / (rows * HEIGHT); + + // ignore if previous ratio is closer to preferred ratio + if (dimension != null && Math.abs(newRatio - PREFERRED_RATIO) > Math.abs(ratio - PREFERRED_RATIO)) { + continue; + } + + ratio = newRatio; + dimension = new int[] {cols, rows}; + } + + // Handle case when min values were larger than necessary + if (dimension == null) { + int rows = calculateNumberOfRows(sourceCodeWords, errorCorrectionCodeWords, minCols); + if (rows < minRows) { + dimension = new int[]{minCols, minRows}; + } + } + + if (dimension == null) { + throw new WriterException("Unable to fit message in columns"); + } + + return dimension; + } + + /** + * Sets max/min row/col values + * + * @param maxCols maximum allowed columns + * @param minCols minimum allowed columns + * @param maxRows maximum allowed rows + * @param minRows minimum allowed rows + */ + public void setDimensions(int maxCols, int minCols, int maxRows, int minRows) { + this.maxCols = maxCols; + this.minCols = minCols; + this.maxRows = maxRows; + this.minRows = minRows; + } + + /** + * @param compaction compaction mode to use + */ + public void setCompaction(Compaction compaction) { + this.compaction = compaction; + } + + /** + * @param compact if true, enables compaction + */ + public void setCompact(boolean compact) { + this.compact = compact; + } + + /** + * @param encoding sets character encoding to use + */ + public void setEncoding(Charset encoding) { + this.encoding = encoding; + } + +} + diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417ErrorCorrection.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417ErrorCorrection.java new file mode 100644 index 0000000..6528172 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417ErrorCorrection.java @@ -0,0 +1,204 @@ +/* + * Copyright 2006 Jeremias Maerki in part, and ZXing Authors in part + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file has been modified from its original form in Barcode4J. + */ + +package com.google.zxing.pdf417.encoder; + +import com.google.zxing.WriterException; + +/** + * PDF417 error correction code following the algorithm described in ISO/IEC 15438:2001(E) in + * chapter 4.10. + */ +final class PDF417ErrorCorrection { + + /** + * Tables of coefficients for calculating error correction words + * (see annex F, ISO/IEC 15438:2001(E)) + */ + private static final int[][] EC_COEFFICIENTS = { + {27, 917}, + {522, 568, 723, 809}, + {237, 308, 436, 284, 646, 653, 428, 379}, + {274, 562, 232, 755, 599, 524, 801, 132, 295, 116, 442, 428, 295, + 42, 176, 65}, + {361, 575, 922, 525, 176, 586, 640, 321, 536, 742, 677, 742, 687, + 284, 193, 517, 273, 494, 263, 147, 593, 800, 571, 320, 803, + 133, 231, 390, 685, 330, 63, 410}, + {539, 422, 6, 93, 862, 771, 453, 106, 610, 287, 107, 505, 733, + 877, 381, 612, 723, 476, 462, 172, 430, 609, 858, 822, 543, + 376, 511, 400, 672, 762, 283, 184, 440, 35, 519, 31, 460, + 594, 225, 535, 517, 352, 605, 158, 651, 201, 488, 502, 648, + 733, 717, 83, 404, 97, 280, 771, 840, 629, 4, 381, 843, + 623, 264, 543}, + {521, 310, 864, 547, 858, 580, 296, 379, 53, 779, 897, 444, 400, + 925, 749, 415, 822, 93, 217, 208, 928, 244, 583, 620, 246, + 148, 447, 631, 292, 908, 490, 704, 516, 258, 457, 907, 594, + 723, 674, 292, 272, 96, 684, 432, 686, 606, 860, 569, 193, + 219, 129, 186, 236, 287, 192, 775, 278, 173, 40, 379, 712, + 463, 646, 776, 171, 491, 297, 763, 156, 732, 95, 270, 447, + 90, 507, 48, 228, 821, 808, 898, 784, 663, 627, 378, 382, + 262, 380, 602, 754, 336, 89, 614, 87, 432, 670, 616, 157, + 374, 242, 726, 600, 269, 375, 898, 845, 454, 354, 130, 814, + 587, 804, 34, 211, 330, 539, 297, 827, 865, 37, 517, 834, + 315, 550, 86, 801, 4, 108, 539}, + {524, 894, 75, 766, 882, 857, 74, 204, 82, 586, 708, 250, 905, + 786, 138, 720, 858, 194, 311, 913, 275, 190, 375, 850, 438, + 733, 194, 280, 201, 280, 828, 757, 710, 814, 919, 89, 68, + 569, 11, 204, 796, 605, 540, 913, 801, 700, 799, 137, 439, + 418, 592, 668, 353, 859, 370, 694, 325, 240, 216, 257, 284, + 549, 209, 884, 315, 70, 329, 793, 490, 274, 877, 162, 749, + 812, 684, 461, 334, 376, 849, 521, 307, 291, 803, 712, 19, + 358, 399, 908, 103, 511, 51, 8, 517, 225, 289, 470, 637, + 731, 66, 255, 917, 269, 463, 830, 730, 433, 848, 585, 136, + 538, 906, 90, 2, 290, 743, 199, 655, 903, 329, 49, 802, + 580, 355, 588, 188, 462, 10, 134, 628, 320, 479, 130, 739, + 71, 263, 318, 374, 601, 192, 605, 142, 673, 687, 234, 722, + 384, 177, 752, 607, 640, 455, 193, 689, 707, 805, 641, 48, + 60, 732, 621, 895, 544, 261, 852, 655, 309, 697, 755, 756, + 60, 231, 773, 434, 421, 726, 528, 503, 118, 49, 795, 32, + 144, 500, 238, 836, 394, 280, 566, 319, 9, 647, 550, 73, + 914, 342, 126, 32, 681, 331, 792, 620, 60, 609, 441, 180, + 791, 893, 754, 605, 383, 228, 749, 760, 213, 54, 297, 134, + 54, 834, 299, 922, 191, 910, 532, 609, 829, 189, 20, 167, + 29, 872, 449, 83, 402, 41, 656, 505, 579, 481, 173, 404, + 251, 688, 95, 497, 555, 642, 543, 307, 159, 924, 558, 648, + 55, 497, 10}, + {352, 77, 373, 504, 35, 599, 428, 207, 409, 574, 118, 498, 285, + 380, 350, 492, 197, 265, 920, 155, 914, 299, 229, 643, 294, + 871, 306, 88, 87, 193, 352, 781, 846, 75, 327, 520, 435, + 543, 203, 666, 249, 346, 781, 621, 640, 268, 794, 534, 539, + 781, 408, 390, 644, 102, 476, 499, 290, 632, 545, 37, 858, + 916, 552, 41, 542, 289, 122, 272, 383, 800, 485, 98, 752, + 472, 761, 107, 784, 860, 658, 741, 290, 204, 681, 407, 855, + 85, 99, 62, 482, 180, 20, 297, 451, 593, 913, 142, 808, + 684, 287, 536, 561, 76, 653, 899, 729, 567, 744, 390, 513, + 192, 516, 258, 240, 518, 794, 395, 768, 848, 51, 610, 384, + 168, 190, 826, 328, 596, 786, 303, 570, 381, 415, 641, 156, + 237, 151, 429, 531, 207, 676, 710, 89, 168, 304, 402, 40, + 708, 575, 162, 864, 229, 65, 861, 841, 512, 164, 477, 221, + 92, 358, 785, 288, 357, 850, 836, 827, 736, 707, 94, 8, + 494, 114, 521, 2, 499, 851, 543, 152, 729, 771, 95, 248, + 361, 578, 323, 856, 797, 289, 51, 684, 466, 533, 820, 669, + 45, 902, 452, 167, 342, 244, 173, 35, 463, 651, 51, 699, + 591, 452, 578, 37, 124, 298, 332, 552, 43, 427, 119, 662, + 777, 475, 850, 764, 364, 578, 911, 283, 711, 472, 420, 245, + 288, 594, 394, 511, 327, 589, 777, 699, 688, 43, 408, 842, + 383, 721, 521, 560, 644, 714, 559, 62, 145, 873, 663, 713, + 159, 672, 729, 624, 59, 193, 417, 158, 209, 563, 564, 343, + 693, 109, 608, 563, 365, 181, 772, 677, 310, 248, 353, 708, + 410, 579, 870, 617, 841, 632, 860, 289, 536, 35, 777, 618, + 586, 424, 833, 77, 597, 346, 269, 757, 632, 695, 751, 331, + 247, 184, 45, 787, 680, 18, 66, 407, 369, 54, 492, 228, + 613, 830, 922, 437, 519, 644, 905, 789, 420, 305, 441, 207, + 300, 892, 827, 141, 537, 381, 662, 513, 56, 252, 341, 242, + 797, 838, 837, 720, 224, 307, 631, 61, 87, 560, 310, 756, + 665, 397, 808, 851, 309, 473, 795, 378, 31, 647, 915, 459, + 806, 590, 731, 425, 216, 548, 249, 321, 881, 699, 535, 673, + 782, 210, 815, 905, 303, 843, 922, 281, 73, 469, 791, 660, + 162, 498, 308, 155, 422, 907, 817, 187, 62, 16, 425, 535, + 336, 286, 437, 375, 273, 610, 296, 183, 923, 116, 667, 751, + 353, 62, 366, 691, 379, 687, 842, 37, 357, 720, 742, 330, + 5, 39, 923, 311, 424, 242, 749, 321, 54, 669, 316, 342, + 299, 534, 105, 667, 488, 640, 672, 576, 540, 316, 486, 721, + 610, 46, 656, 447, 171, 616, 464, 190, 531, 297, 321, 762, + 752, 533, 175, 134, 14, 381, 433, 717, 45, 111, 20, 596, + 284, 736, 138, 646, 411, 877, 669, 141, 919, 45, 780, 407, + 164, 332, 899, 165, 726, 600, 325, 498, 655, 357, 752, 768, + 223, 849, 647, 63, 310, 863, 251, 366, 304, 282, 738, 675, + 410, 389, 244, 31, 121, 303, 263}}; + + private PDF417ErrorCorrection() { + } + + /** + * Determines the number of error correction codewords for a specified error correction + * level. + * + * @param errorCorrectionLevel the error correction level (0-8) + * @return the number of codewords generated for error correction + */ + static int getErrorCorrectionCodewordCount(int errorCorrectionLevel) { + if (errorCorrectionLevel < 0 || errorCorrectionLevel > 8) { + throw new IllegalArgumentException("Error correction level must be between 0 and 8!"); + } + return 1 << (errorCorrectionLevel + 1); + } + + /** + * Returns the recommended minimum error correction level as described in annex E of + * ISO/IEC 15438:2001(E). + * + * @param n the number of data codewords + * @return the recommended minimum error correction level + */ + static int getRecommendedMinimumErrorCorrectionLevel(int n) throws WriterException { + if (n <= 0) { + throw new IllegalArgumentException("n must be > 0"); + } + if (n <= 40) { + return 2; + } + if (n <= 160) { + return 3; + } + if (n <= 320) { + return 4; + } + if (n <= 863) { + return 5; + } + throw new WriterException("No recommendation possible"); + } + + /** + * Generates the error correction codewords according to 4.10 in ISO/IEC 15438:2001(E). + * + * @param dataCodewords the data codewords + * @param errorCorrectionLevel the error correction level (0-8) + * @return the String representing the error correction codewords + */ + static String generateErrorCorrection(CharSequence dataCodewords, int errorCorrectionLevel) { + int k = getErrorCorrectionCodewordCount(errorCorrectionLevel); + char[] e = new char[k]; + int sld = dataCodewords.length(); + for (int i = 0; i < sld; i++) { + int t1 = (dataCodewords.charAt(i) + e[e.length - 1]) % 929; + int t2; + int t3; + for (int j = k - 1; j >= 1; j--) { + t2 = (t1 * EC_COEFFICIENTS[errorCorrectionLevel][j]) % 929; + t3 = 929 - t2; + e[j] = (char) ((e[j - 1] + t3) % 929); + } + t2 = (t1 * EC_COEFFICIENTS[errorCorrectionLevel][0]) % 929; + t3 = 929 - t2; + e[0] = (char) (t3 % 929); + } + StringBuilder sb = new StringBuilder(k); + for (int j = k - 1; j >= 0; j--) { + if (e[j] != 0) { + e[j] = (char) (929 - e[j]); + } + sb.append(e[j]); + } + return sb.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java new file mode 100644 index 0000000..9c8847b --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/pdf417/encoder/PDF417HighLevelEncoder.java @@ -0,0 +1,583 @@ +/* + * Copyright 2006 Jeremias Maerki in part, and ZXing Authors in part + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * This file has been modified from its original form in Barcode4J. + */ + +package com.google.zxing.pdf417.encoder; + +import com.google.zxing.WriterException; +import com.google.zxing.common.CharacterSetECI; + +import java.math.BigInteger; +import java.nio.charset.Charset; +import java.nio.charset.CharsetEncoder; +import java.util.Arrays; + +/** + * PDF417 high-level encoder following the algorithm described in ISO/IEC 15438:2001(E) in + * annex P. + */ +final class PDF417HighLevelEncoder { + + /** + * code for Text compaction + */ + private static final int TEXT_COMPACTION = 0; + + /** + * code for Byte compaction + */ + private static final int BYTE_COMPACTION = 1; + + /** + * code for Numeric compaction + */ + private static final int NUMERIC_COMPACTION = 2; + + /** + * Text compaction submode Alpha + */ + private static final int SUBMODE_ALPHA = 0; + + /** + * Text compaction submode Lower + */ + private static final int SUBMODE_LOWER = 1; + + /** + * Text compaction submode Mixed + */ + private static final int SUBMODE_MIXED = 2; + + /** + * Text compaction submode Punctuation + */ + private static final int SUBMODE_PUNCTUATION = 3; + + /** + * mode latch to Text Compaction mode + */ + private static final int LATCH_TO_TEXT = 900; + + /** + * mode latch to Byte Compaction mode (number of characters NOT a multiple of 6) + */ + private static final int LATCH_TO_BYTE_PADDED = 901; + + /** + * mode latch to Numeric Compaction mode + */ + private static final int LATCH_TO_NUMERIC = 902; + + /** + * mode shift to Byte Compaction mode + */ + private static final int SHIFT_TO_BYTE = 913; + + /** + * mode latch to Byte Compaction mode (number of characters a multiple of 6) + */ + private static final int LATCH_TO_BYTE = 924; + + /** + * identifier for a user defined Extended Channel Interpretation (ECI) + */ + private static final int ECI_USER_DEFINED = 925; + + /** + * identifier for a general purpose ECO format + */ + private static final int ECI_GENERAL_PURPOSE = 926; + + /** + * identifier for an ECI of a character set of code page + */ + private static final int ECI_CHARSET = 927; + + /** + * Raw code table for text compaction Mixed sub-mode + */ + private static final byte[] TEXT_MIXED_RAW = { + 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 38, 13, 9, 44, 58, + 35, 45, 46, 36, 47, 43, 37, 42, 61, 94, 0, 32, 0, 0, 0}; + + /** + * Raw code table for text compaction: Punctuation sub-mode + */ + private static final byte[] TEXT_PUNCTUATION_RAW = { + 59, 60, 62, 64, 91, 92, 93, 95, 96, 126, 33, 13, 9, 44, 58, + 10, 45, 46, 36, 47, 34, 124, 42, 40, 41, 63, 123, 125, 39, 0}; + + private static final byte[] MIXED = new byte[128]; + private static final byte[] PUNCTUATION = new byte[128]; + + private static final Charset DEFAULT_ENCODING = Charset.forName("ISO-8859-1"); + + private PDF417HighLevelEncoder() { + } + + static { + //Construct inverse lookups + Arrays.fill(MIXED, (byte) -1); + for (int i = 0; i < TEXT_MIXED_RAW.length; i++) { + byte b = TEXT_MIXED_RAW[i]; + if (b > 0) { + MIXED[b] = (byte) i; + } + } + Arrays.fill(PUNCTUATION, (byte) -1); + for (int i = 0; i < TEXT_PUNCTUATION_RAW.length; i++) { + byte b = TEXT_PUNCTUATION_RAW[i]; + if (b > 0) { + PUNCTUATION[b] = (byte) i; + } + } + } + + /** + * Performs high-level encoding of a PDF417 message using the algorithm described in annex P + * of ISO/IEC 15438:2001(E). If byte compaction has been selected, then only byte compaction + * is used. + * + * @param msg the message + * @param compaction compaction mode to use + * @param encoding character encoding used to encode in default or byte compaction + * or {@code null} for default / not applicable + * @return the encoded message (the char values range from 0 to 928) + */ + static String encodeHighLevel(String msg, Compaction compaction, Charset encoding) throws WriterException { + + //the codewords 0..928 are encoded as Unicode characters + StringBuilder sb = new StringBuilder(msg.length()); + + if (encoding == null) { + encoding = DEFAULT_ENCODING; + } else if (!DEFAULT_ENCODING.equals(encoding)) { + CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding.name()); + if (eci != null) { + encodingECI(eci.getValue(), sb); + } + } + + int len = msg.length(); + int p = 0; + int textSubMode = SUBMODE_ALPHA; + + // User selected encoding mode + switch (compaction) { + case TEXT: + encodeText(msg, p, len, sb, textSubMode); + break; + case BYTE: + byte[] msgBytes = msg.getBytes(encoding); + encodeBinary(msgBytes, p, msgBytes.length, BYTE_COMPACTION, sb); + break; + case NUMERIC: + sb.append((char) LATCH_TO_NUMERIC); + encodeNumeric(msg, p, len, sb); + break; + default: + int encodingMode = TEXT_COMPACTION; //Default mode, see 4.4.2.1 + while (p < len) { + int n = determineConsecutiveDigitCount(msg, p); + if (n >= 13) { + sb.append((char) LATCH_TO_NUMERIC); + encodingMode = NUMERIC_COMPACTION; + textSubMode = SUBMODE_ALPHA; //Reset after latch + encodeNumeric(msg, p, n, sb); + p += n; + } else { + int t = determineConsecutiveTextCount(msg, p); + if (t >= 5 || n == len) { + if (encodingMode != TEXT_COMPACTION) { + sb.append((char) LATCH_TO_TEXT); + encodingMode = TEXT_COMPACTION; + textSubMode = SUBMODE_ALPHA; //start with submode alpha after latch + } + textSubMode = encodeText(msg, p, t, sb, textSubMode); + p += t; + } else { + int b = determineConsecutiveBinaryCount(msg, p, encoding); + if (b == 0) { + b = 1; + } + byte[] bytes = msg.substring(p, p + b).getBytes(encoding); + if (bytes.length == 1 && encodingMode == TEXT_COMPACTION) { + //Switch for one byte (instead of latch) + encodeBinary(bytes, 0, 1, TEXT_COMPACTION, sb); + } else { + //Mode latch performed by encodeBinary() + encodeBinary(bytes, 0, bytes.length, encodingMode, sb); + encodingMode = BYTE_COMPACTION; + textSubMode = SUBMODE_ALPHA; //Reset after latch + } + p += b; + } + } + } + break; + } + + return sb.toString(); + } + + /** + * Encode parts of the message using Text Compaction as described in ISO/IEC 15438:2001(E), + * chapter 4.4.2. + * + * @param msg the message + * @param startpos the start position within the message + * @param count the number of characters to encode + * @param sb receives the encoded codewords + * @param initialSubmode should normally be SUBMODE_ALPHA + * @return the text submode in which this method ends + */ + private static int encodeText(CharSequence msg, + int startpos, + int count, + StringBuilder sb, + int initialSubmode) { + StringBuilder tmp = new StringBuilder(count); + int submode = initialSubmode; + int idx = 0; + while (true) { + char ch = msg.charAt(startpos + idx); + switch (submode) { + case SUBMODE_ALPHA: + if (isAlphaUpper(ch)) { + if (ch == ' ') { + tmp.append((char) 26); //space + } else { + tmp.append((char) (ch - 65)); + } + } else { + if (isAlphaLower(ch)) { + submode = SUBMODE_LOWER; + tmp.append((char) 27); //ll + continue; + } else if (isMixed(ch)) { + submode = SUBMODE_MIXED; + tmp.append((char) 28); //ml + continue; + } else { + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + break; + } + } + break; + case SUBMODE_LOWER: + if (isAlphaLower(ch)) { + if (ch == ' ') { + tmp.append((char) 26); //space + } else { + tmp.append((char) (ch - 97)); + } + } else { + if (isAlphaUpper(ch)) { + tmp.append((char) 27); //as + tmp.append((char) (ch - 65)); + //space cannot happen here, it is also in "Lower" + break; + } else if (isMixed(ch)) { + submode = SUBMODE_MIXED; + tmp.append((char) 28); //ml + continue; + } else { + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + break; + } + } + break; + case SUBMODE_MIXED: + if (isMixed(ch)) { + tmp.append((char) MIXED[ch]); + } else { + if (isAlphaUpper(ch)) { + submode = SUBMODE_ALPHA; + tmp.append((char) 28); //al + continue; + } else if (isAlphaLower(ch)) { + submode = SUBMODE_LOWER; + tmp.append((char) 27); //ll + continue; + } else { + if (startpos + idx + 1 < count) { + char next = msg.charAt(startpos + idx + 1); + if (isPunctuation(next)) { + submode = SUBMODE_PUNCTUATION; + tmp.append((char) 25); //pl + continue; + } + } + tmp.append((char) 29); //ps + tmp.append((char) PUNCTUATION[ch]); + } + } + break; + default: //SUBMODE_PUNCTUATION + if (isPunctuation(ch)) { + tmp.append((char) PUNCTUATION[ch]); + } else { + submode = SUBMODE_ALPHA; + tmp.append((char) 29); //al + continue; + } + } + idx++; + if (idx >= count) { + break; + } + } + char h = 0; + int len = tmp.length(); + for (int i = 0; i < len; i++) { + boolean odd = (i % 2) != 0; + if (odd) { + h = (char) ((h * 30) + tmp.charAt(i)); + sb.append(h); + } else { + h = tmp.charAt(i); + } + } + if ((len % 2) != 0) { + sb.append((char) ((h * 30) + 29)); //ps + } + return submode; + } + + /** + * Encode parts of the message using Byte Compaction as described in ISO/IEC 15438:2001(E), + * chapter 4.4.3. The Unicode characters will be converted to binary using the cp437 + * codepage. + * + * @param bytes the message converted to a byte array + * @param startpos the start position within the message + * @param count the number of bytes to encode + * @param startmode the mode from which this method starts + * @param sb receives the encoded codewords + */ + private static void encodeBinary(byte[] bytes, + int startpos, + int count, + int startmode, + StringBuilder sb) { + if (count == 1 && startmode == TEXT_COMPACTION) { + sb.append((char) SHIFT_TO_BYTE); + } else { + if ((count % 6) == 0) { + sb.append((char) LATCH_TO_BYTE); + } else { + sb.append((char) LATCH_TO_BYTE_PADDED); + } + } + + int idx = startpos; + // Encode sixpacks + if (count >= 6) { + char[] chars = new char[5]; + while ((startpos + count - idx) >= 6) { + long t = 0; + for (int i = 0; i < 6; i++) { + t <<= 8; + t += bytes[idx + i] & 0xff; + } + for (int i = 0; i < 5; i++) { + chars[i] = (char) (t % 900); + t /= 900; + } + for (int i = chars.length - 1; i >= 0; i--) { + sb.append(chars[i]); + } + idx += 6; + } + } + //Encode rest (remaining n<5 bytes if any) + for (int i = idx; i < startpos + count; i++) { + int ch = bytes[i] & 0xff; + sb.append((char) ch); + } + } + + private static void encodeNumeric(String msg, int startpos, int count, StringBuilder sb) { + int idx = 0; + StringBuilder tmp = new StringBuilder(count / 3 + 1); + BigInteger num900 = BigInteger.valueOf(900); + BigInteger num0 = BigInteger.valueOf(0); + while (idx < count) { + tmp.setLength(0); + int len = Math.min(44, count - idx); + String part = '1' + msg.substring(startpos + idx, startpos + idx + len); + BigInteger bigint = new BigInteger(part); + do { + tmp.append((char) bigint.mod(num900).intValue()); + bigint = bigint.divide(num900); + } while (!bigint.equals(num0)); + + //Reverse temporary string + for (int i = tmp.length() - 1; i >= 0; i--) { + sb.append(tmp.charAt(i)); + } + idx += len; + } + } + + + private static boolean isDigit(char ch) { + return ch >= '0' && ch <= '9'; + } + + private static boolean isAlphaUpper(char ch) { + return ch == ' ' || (ch >= 'A' && ch <= 'Z'); + } + + private static boolean isAlphaLower(char ch) { + return ch == ' ' || (ch >= 'a' && ch <= 'z'); + } + + private static boolean isMixed(char ch) { + return MIXED[ch] != -1; + } + + private static boolean isPunctuation(char ch) { + return PUNCTUATION[ch] != -1; + } + + private static boolean isText(char ch) { + return ch == '\t' || ch == '\n' || ch == '\r' || (ch >= 32 && ch <= 126); + } + + /** + * Determines the number of consecutive characters that are encodable using numeric compaction. + * + * @param msg the message + * @param startpos the start position within the message + * @return the requested character count + */ + private static int determineConsecutiveDigitCount(CharSequence msg, int startpos) { + int count = 0; + int len = msg.length(); + int idx = startpos; + if (idx < len) { + char ch = msg.charAt(idx); + while (isDigit(ch) && idx < len) { + count++; + idx++; + if (idx < len) { + ch = msg.charAt(idx); + } + } + } + return count; + } + + /** + * Determines the number of consecutive characters that are encodable using text compaction. + * + * @param msg the message + * @param startpos the start position within the message + * @return the requested character count + */ + private static int determineConsecutiveTextCount(CharSequence msg, int startpos) { + int len = msg.length(); + int idx = startpos; + while (idx < len) { + char ch = msg.charAt(idx); + int numericCount = 0; + while (numericCount < 13 && isDigit(ch) && idx < len) { + numericCount++; + idx++; + if (idx < len) { + ch = msg.charAt(idx); + } + } + if (numericCount >= 13) { + return idx - startpos - numericCount; + } + if (numericCount > 0) { + //Heuristic: All text-encodable chars or digits are binary encodable + continue; + } + ch = msg.charAt(idx); + + //Check if character is encodable + if (!isText(ch)) { + break; + } + idx++; + } + return idx - startpos; + } + + /** + * Determines the number of consecutive characters that are encodable using binary compaction. + * + * @param msg the message + * @param startpos the start position within the message + * @param encoding the charset used to convert the message to a byte array + * @return the requested character count + */ + private static int determineConsecutiveBinaryCount(String msg, int startpos, Charset encoding) + throws WriterException { + CharsetEncoder encoder = encoding.newEncoder(); + int len = msg.length(); + int idx = startpos; + while (idx < len) { + char ch = msg.charAt(idx); + int numericCount = 0; + + while (numericCount < 13 && isDigit(ch)) { + numericCount++; + //textCount++; + int i = idx + numericCount; + if (i >= len) { + break; + } + ch = msg.charAt(i); + } + if (numericCount >= 13) { + return idx - startpos; + } + ch = msg.charAt(idx); + + if (!encoder.canEncode(ch)) { + throw new WriterException("Non-encodable character detected: " + ch + " (Unicode: " + (int) ch + ')'); + } + idx++; + } + return idx - startpos; + } + + private static void encodingECI(int eci, StringBuilder sb) throws WriterException { + if (eci >= 0 && eci < 900) { + sb.append((char) ECI_CHARSET); + sb.append((char) eci); + } else if (eci < 810900) { + sb.append((char) ECI_GENERAL_PURPOSE); + sb.append((char) (eci / 900 - 1)); + sb.append((char) (eci % 900)); + } else if (eci < 811800) { + sb.append((char) ECI_USER_DEFINED); + sb.append((char) (810900 - eci)); + } else { + throw new WriterException("ECI number not in valid range from 0..811799, but was " + eci); + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeReader.java b/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeReader.java new file mode 100644 index 0000000..6b0116d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeReader.java @@ -0,0 +1,222 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.BinaryBitmap; +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.Reader; +import com.google.zxing.Result; +import com.google.zxing.ResultMetadataType; +import com.google.zxing.ResultPoint; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.qrcode.decoder.Decoder; +import com.google.zxing.qrcode.decoder.QRCodeDecoderMetaData; +import com.google.zxing.qrcode.detector.Detector; + +import java.util.List; +import java.util.Map; + +/** + * This implementation can detect and decode QR Codes in an image. + * + * @author Sean Owen + */ +public class QRCodeReader implements Reader { + + private static final ResultPoint[] NO_POINTS = new ResultPoint[0]; + + private final Decoder decoder = new Decoder(); + + protected final Decoder getDecoder() { + return decoder; + } + + /** + * Locates and decodes a QR code in an image. + * + * @return a String representing the content encoded by the QR code + * @throws NotFoundException if a QR code cannot be found + * @throws FormatException if a QR code cannot be decoded + * @throws ChecksumException if error correction fails + */ + @Override + public Result decode(BinaryBitmap image) throws NotFoundException, ChecksumException, FormatException { + return decode(image, null); + } + + @Override + public final Result decode(BinaryBitmap image, Map hints) + throws NotFoundException, ChecksumException, FormatException { + DecoderResult decoderResult; + ResultPoint[] points; + if (hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE)) { + BitMatrix bits = extractPureBits(image.getBlackMatrix()); + decoderResult = decoder.decode(bits, hints); + points = NO_POINTS; + } else { + DetectorResult detectorResult = new Detector(image.getBlackMatrix()).detect(hints); + decoderResult = decoder.decode(detectorResult.getBits(), hints); + points = detectorResult.getPoints(); + } + + // If the code was mirrored: swap the bottom-left and the top-right points. + if (decoderResult.getOther() instanceof QRCodeDecoderMetaData) { + ((QRCodeDecoderMetaData) decoderResult.getOther()).applyMirroredCorrection(points); + } + + Result result = new Result(decoderResult.getText(), decoderResult.getRawBytes(), points, BarcodeFormat.QR_CODE); + List byteSegments = decoderResult.getByteSegments(); + if (byteSegments != null) { + result.putMetadata(ResultMetadataType.BYTE_SEGMENTS, byteSegments); + } + String ecLevel = decoderResult.getECLevel(); + if (ecLevel != null) { + result.putMetadata(ResultMetadataType.ERROR_CORRECTION_LEVEL, ecLevel); + } + if (decoderResult.hasStructuredAppend()) { + result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_SEQUENCE, + decoderResult.getStructuredAppendSequenceNumber()); + result.putMetadata(ResultMetadataType.STRUCTURED_APPEND_PARITY, + decoderResult.getStructuredAppendParity()); + } + return result; + } + + @Override + public void reset() { + // do nothing + } + + /** + * This method detects a code in a "pure" image -- that is, pure monochrome image + * which contains only an unrotated, unskewed, image of a code, with some white border + * around it. This is a specialized method that works exceptionally fast in this special + * case. + * + * @see com.google.zxing.datamatrix.DataMatrixReader#extractPureBits(BitMatrix) + */ + private static BitMatrix extractPureBits(BitMatrix image) throws NotFoundException { + + int[] leftTopBlack = image.getTopLeftOnBit(); + int[] rightBottomBlack = image.getBottomRightOnBit(); + if (leftTopBlack == null || rightBottomBlack == null) { + throw NotFoundException.getNotFoundInstance(); + } + + float moduleSize = moduleSize(leftTopBlack, image); + + int top = leftTopBlack[1]; + int bottom = rightBottomBlack[1]; + int left = leftTopBlack[0]; + int right = rightBottomBlack[0]; + + // Sanity check! + if (left >= right || top >= bottom) { + throw NotFoundException.getNotFoundInstance(); + } + + if (bottom - top != right - left) { + // Special case, where bottom-right module wasn't black so we found something else in the last row + // Assume it's a square, so use height as the width + right = left + (bottom - top); + if (right >= image.getWidth()) { + // Abort if that would not make sense -- off image + throw NotFoundException.getNotFoundInstance(); + } + } + + int matrixWidth = Math.round((right - left + 1) / moduleSize); + int matrixHeight = Math.round((bottom - top + 1) / moduleSize); + if (matrixWidth <= 0 || matrixHeight <= 0) { + throw NotFoundException.getNotFoundInstance(); + } + if (matrixHeight != matrixWidth) { + // Only possibly decode square regions + throw NotFoundException.getNotFoundInstance(); + } + + // Push in the "border" by half the module width so that we start + // sampling in the middle of the module. Just in case the image is a + // little off, this will help recover. + int nudge = (int) (moduleSize / 2.0f); + top += nudge; + left += nudge; + + // But careful that this does not sample off the edge + // "right" is the farthest-right valid pixel location -- right+1 is not necessarily + // This is positive by how much the inner x loop below would be too large + int nudgedTooFarRight = left + (int) ((matrixWidth - 1) * moduleSize) - right; + if (nudgedTooFarRight > 0) { + if (nudgedTooFarRight > nudge) { + // Neither way fits; abort + throw NotFoundException.getNotFoundInstance(); + } + left -= nudgedTooFarRight; + } + // See logic above + int nudgedTooFarDown = top + (int) ((matrixHeight - 1) * moduleSize) - bottom; + if (nudgedTooFarDown > 0) { + if (nudgedTooFarDown > nudge) { + // Neither way fits; abort + throw NotFoundException.getNotFoundInstance(); + } + top -= nudgedTooFarDown; + } + + // Now just read off the bits + BitMatrix bits = new BitMatrix(matrixWidth, matrixHeight); + for (int y = 0; y < matrixHeight; y++) { + int iOffset = top + (int) (y * moduleSize); + for (int x = 0; x < matrixWidth; x++) { + if (image.get(left + (int) (x * moduleSize), iOffset)) { + bits.set(x, y); + } + } + } + return bits; + } + + private static float moduleSize(int[] leftTopBlack, BitMatrix image) throws NotFoundException { + int height = image.getHeight(); + int width = image.getWidth(); + int x = leftTopBlack[0]; + int y = leftTopBlack[1]; + boolean inBlack = true; + int transitions = 0; + while (x < width && y < height) { + if (inBlack != image.get(x, y)) { + if (++transitions == 5) { + break; + } + inBlack = !inBlack; + } + x++; + y++; + } + if (x == width || y == height) { + throw NotFoundException.getNotFoundInstance(); + } + return (x - leftTopBlack[0]) / 7.0f; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeWriter.java b/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeWriter.java new file mode 100644 index 0000000..ce7ee07 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/QRCodeWriter.java @@ -0,0 +1,118 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode; + +import com.google.zxing.BarcodeFormat; +import com.google.zxing.EncodeHintType; +import com.google.zxing.Writer; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.qrcode.encoder.ByteMatrix; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.encoder.Encoder; +import com.google.zxing.qrcode.encoder.QRCode; + +import java.util.Map; + +/** + * This object renders a QR Code as a BitMatrix 2D array of greyscale values. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class QRCodeWriter implements Writer { + + private static final int QUIET_ZONE_SIZE = 4; + + @Override + public BitMatrix encode(String contents, BarcodeFormat format, int width, int height) + throws WriterException { + + return encode(contents, format, width, height, null); + } + + @Override + public BitMatrix encode(String contents, + BarcodeFormat format, + int width, + int height, + Map hints) throws WriterException { + + if (contents.isEmpty()) { + throw new IllegalArgumentException("Found empty contents"); + } + + if (format != BarcodeFormat.QR_CODE) { + throw new IllegalArgumentException("Can only encode QR_CODE, but got " + format); + } + + if (width < 0 || height < 0) { + throw new IllegalArgumentException("Requested dimensions are too small: " + width + 'x' + + height); + } + + ErrorCorrectionLevel errorCorrectionLevel = ErrorCorrectionLevel.L; + int quietZone = QUIET_ZONE_SIZE; + if (hints != null) { + if (hints.containsKey(EncodeHintType.ERROR_CORRECTION)) { + errorCorrectionLevel = ErrorCorrectionLevel.valueOf(hints.get(EncodeHintType.ERROR_CORRECTION).toString()); + } + if (hints.containsKey(EncodeHintType.MARGIN)) { + quietZone = Integer.parseInt(hints.get(EncodeHintType.MARGIN).toString()); + } + } + + QRCode code = Encoder.encode(contents, errorCorrectionLevel, hints); + return renderResult(code, width, height, quietZone); + } + + // Note that the input matrix uses 0 == white, 1 == black, while the output matrix uses + // 0 == black, 255 == white (i.e. an 8 bit greyscale bitmap). + private static BitMatrix renderResult(QRCode code, int width, int height, int quietZone) { + ByteMatrix input = code.getMatrix(); + if (input == null) { + throw new IllegalStateException(); + } + int inputWidth = input.getWidth(); + int inputHeight = input.getHeight(); + int qrWidth = inputWidth + (quietZone * 2); + int qrHeight = inputHeight + (quietZone * 2); + int outputWidth = Math.max(width, qrWidth); + int outputHeight = Math.max(height, qrHeight); + + int multiple = Math.min(outputWidth / qrWidth, outputHeight / qrHeight); + // Padding includes both the quiet zone and the extra white pixels to accommodate the requested + // dimensions. For example, if input is 25x25 the QR will be 33x33 including the quiet zone. + // If the requested size is 200x160, the multiple will be 4, for a QR of 132x132. These will + // handle all the padding from 100x100 (the actual QR) up to 200x160. + int leftPadding = (outputWidth - (inputWidth * multiple)) / 2; + int topPadding = (outputHeight - (inputHeight * multiple)) / 2; + + BitMatrix output = new BitMatrix(outputWidth, outputHeight); + + for (int inputY = 0, outputY = topPadding; inputY < inputHeight; inputY++, outputY += multiple) { + // Write the contents of this row of the barcode + for (int inputX = 0, outputX = leftPadding; inputX < inputWidth; inputX++, outputX += multiple) { + if (input.get(inputX, inputY) == 1) { + output.setRegion(outputX, outputY, multiple, multiple); + } + } + } + + return output; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/BitMatrixParser.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/BitMatrixParser.java new file mode 100644 index 0000000..2298695 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/BitMatrixParser.java @@ -0,0 +1,245 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * @author Sean Owen + */ +final class BitMatrixParser { + + private final BitMatrix bitMatrix; + private Version parsedVersion; + private FormatInformation parsedFormatInfo; + private boolean mirror; + + /** + * @param bitMatrix {@link BitMatrix} to parse + * @throws FormatException if dimension is not >= 21 and 1 mod 4 + */ + BitMatrixParser(BitMatrix bitMatrix) throws FormatException { + int dimension = bitMatrix.getHeight(); + if (dimension < 21 || (dimension & 0x03) != 1) { + throw FormatException.getFormatInstance(); + } + this.bitMatrix = bitMatrix; + } + + /** + *

Reads format information from one of its two locations within the QR Code.

+ * + * @return {@link FormatInformation} encapsulating the QR Code's format info + * @throws FormatException if both format information locations cannot be parsed as + * the valid encoding of format information + */ + FormatInformation readFormatInformation() throws FormatException { + + if (parsedFormatInfo != null) { + return parsedFormatInfo; + } + + // Read top-left format info bits + int formatInfoBits1 = 0; + for (int i = 0; i < 6; i++) { + formatInfoBits1 = copyBit(i, 8, formatInfoBits1); + } + // .. and skip a bit in the timing pattern ... + formatInfoBits1 = copyBit(7, 8, formatInfoBits1); + formatInfoBits1 = copyBit(8, 8, formatInfoBits1); + formatInfoBits1 = copyBit(8, 7, formatInfoBits1); + // .. and skip a bit in the timing pattern ... + for (int j = 5; j >= 0; j--) { + formatInfoBits1 = copyBit(8, j, formatInfoBits1); + } + + // Read the top-right/bottom-left pattern too + int dimension = bitMatrix.getHeight(); + int formatInfoBits2 = 0; + int jMin = dimension - 7; + for (int j = dimension - 1; j >= jMin; j--) { + formatInfoBits2 = copyBit(8, j, formatInfoBits2); + } + for (int i = dimension - 8; i < dimension; i++) { + formatInfoBits2 = copyBit(i, 8, formatInfoBits2); + } + + parsedFormatInfo = FormatInformation.decodeFormatInformation(formatInfoBits1, formatInfoBits2); + if (parsedFormatInfo != null) { + return parsedFormatInfo; + } + throw FormatException.getFormatInstance(); + } + + /** + *

Reads version information from one of its two locations within the QR Code.

+ * + * @return {@link Version} encapsulating the QR Code's version + * @throws FormatException if both version information locations cannot be parsed as + * the valid encoding of version information + */ + Version readVersion() throws FormatException { + + if (parsedVersion != null) { + return parsedVersion; + } + + int dimension = bitMatrix.getHeight(); + + int provisionalVersion = (dimension - 17) / 4; + if (provisionalVersion <= 6) { + return Version.getVersionForNumber(provisionalVersion); + } + + // Read top-right version info: 3 wide by 6 tall + int versionBits = 0; + int ijMin = dimension - 11; + for (int j = 5; j >= 0; j--) { + for (int i = dimension - 9; i >= ijMin; i--) { + versionBits = copyBit(i, j, versionBits); + } + } + + Version theParsedVersion = Version.decodeVersionInformation(versionBits); + if (theParsedVersion != null && theParsedVersion.getDimensionForVersion() == dimension) { + parsedVersion = theParsedVersion; + return theParsedVersion; + } + + // Hmm, failed. Try bottom left: 6 wide by 3 tall + versionBits = 0; + for (int i = 5; i >= 0; i--) { + for (int j = dimension - 9; j >= ijMin; j--) { + versionBits = copyBit(i, j, versionBits); + } + } + + theParsedVersion = Version.decodeVersionInformation(versionBits); + if (theParsedVersion != null && theParsedVersion.getDimensionForVersion() == dimension) { + parsedVersion = theParsedVersion; + return theParsedVersion; + } + throw FormatException.getFormatInstance(); + } + + private int copyBit(int i, int j, int versionBits) { + boolean bit = mirror ? bitMatrix.get(j, i) : bitMatrix.get(i, j); + return bit ? (versionBits << 1) | 0x1 : versionBits << 1; + } + + /** + *

Reads the bits in the {@link BitMatrix} representing the finder pattern in the + * correct order in order to reconstruct the codewords bytes contained within the + * QR Code.

+ * + * @return bytes encoded within the QR Code + * @throws FormatException if the exact number of bytes expected is not read + */ + byte[] readCodewords() throws FormatException { + + FormatInformation formatInfo = readFormatInformation(); + Version version = readVersion(); + + // Get the data mask for the format used in this QR Code. This will exclude + // some bits from reading as we wind through the bit matrix. + DataMask dataMask = DataMask.values()[formatInfo.getDataMask()]; + int dimension = bitMatrix.getHeight(); + dataMask.unmaskBitMatrix(bitMatrix, dimension); + + BitMatrix functionPattern = version.buildFunctionPattern(); + + boolean readingUp = true; + byte[] result = new byte[version.getTotalCodewords()]; + int resultOffset = 0; + int currentByte = 0; + int bitsRead = 0; + // Read columns in pairs, from right to left + for (int j = dimension - 1; j > 0; j -= 2) { + if (j == 6) { + // Skip whole column with vertical alignment pattern; + // saves time and makes the other code proceed more cleanly + j--; + } + // Read alternatingly from bottom to top then top to bottom + for (int count = 0; count < dimension; count++) { + int i = readingUp ? dimension - 1 - count : count; + for (int col = 0; col < 2; col++) { + // Ignore bits covered by the function pattern + if (!functionPattern.get(j - col, i)) { + // Read a bit + bitsRead++; + currentByte <<= 1; + if (bitMatrix.get(j - col, i)) { + currentByte |= 1; + } + // If we've made a whole byte, save it off + if (bitsRead == 8) { + result[resultOffset++] = (byte) currentByte; + bitsRead = 0; + currentByte = 0; + } + } + } + } + readingUp ^= true; // readingUp = !readingUp; // switch directions + } + if (resultOffset != version.getTotalCodewords()) { + throw FormatException.getFormatInstance(); + } + return result; + } + + /** + * Revert the mask removal done while reading the code words. The bit matrix should revert to its original state. + */ + void remask() { + if (parsedFormatInfo == null) { + return; // We have no format information, and have no data mask + } + DataMask dataMask = DataMask.values()[parsedFormatInfo.getDataMask()]; + int dimension = bitMatrix.getHeight(); + dataMask.unmaskBitMatrix(bitMatrix, dimension); + } + + /** + * Prepare the parser for a mirrored operation. + * This flag has effect only on the {@link #readFormatInformation()} and the + * {@link #readVersion()}. Before proceeding with {@link #readCodewords()} the + * {@link #mirror()} method should be called. + * + * @param mirror Whether to read version and format information mirrored. + */ + void setMirror(boolean mirror) { + parsedVersion = null; + parsedFormatInfo = null; + this.mirror = mirror; + } + + /** Mirror the bit matrix in order to attempt a second reading. */ + void mirror() { + for (int x = 0; x < bitMatrix.getWidth(); x++) { + for (int y = x + 1; y < bitMatrix.getHeight(); y++) { + if (bitMatrix.get(x, y) != bitMatrix.get(y, x)) { + bitMatrix.flip(y, x); + bitMatrix.flip(x, y); + } + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataBlock.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataBlock.java new file mode 100644 index 0000000..8f5cdcb --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataBlock.java @@ -0,0 +1,122 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +/** + *

Encapsulates a block of data within a QR Code. QR Codes may split their data into + * multiple blocks, each of which is a unit of data and error-correction codewords. Each + * is represented by an instance of this class.

+ * + * @author Sean Owen + */ +final class DataBlock { + + private final int numDataCodewords; + private final byte[] codewords; + + private DataBlock(int numDataCodewords, byte[] codewords) { + this.numDataCodewords = numDataCodewords; + this.codewords = codewords; + } + + /** + *

When QR Codes use multiple data blocks, they are actually interleaved. + * That is, the first byte of data block 1 to n is written, then the second bytes, and so on. This + * method will separate the data into original blocks.

+ * + * @param rawCodewords bytes as read directly from the QR Code + * @param version version of the QR Code + * @param ecLevel error-correction level of the QR Code + * @return DataBlocks containing original bytes, "de-interleaved" from representation in the + * QR Code + */ + static DataBlock[] getDataBlocks(byte[] rawCodewords, + Version version, + ErrorCorrectionLevel ecLevel) { + + if (rawCodewords.length != version.getTotalCodewords()) { + throw new IllegalArgumentException(); + } + + // Figure out the number and size of data blocks used by this version and + // error correction level + Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); + + // First count the total number of data blocks + int totalBlocks = 0; + Version.ECB[] ecBlockArray = ecBlocks.getECBlocks(); + for (Version.ECB ecBlock : ecBlockArray) { + totalBlocks += ecBlock.getCount(); + } + + // Now establish DataBlocks of the appropriate size and number of data codewords + DataBlock[] result = new DataBlock[totalBlocks]; + int numResultBlocks = 0; + for (Version.ECB ecBlock : ecBlockArray) { + for (int i = 0; i < ecBlock.getCount(); i++) { + int numDataCodewords = ecBlock.getDataCodewords(); + int numBlockCodewords = ecBlocks.getECCodewordsPerBlock() + numDataCodewords; + result[numResultBlocks++] = new DataBlock(numDataCodewords, new byte[numBlockCodewords]); + } + } + + // All blocks have the same amount of data, except that the last n + // (where n may be 0) have 1 more byte. Figure out where these start. + int shorterBlocksTotalCodewords = result[0].codewords.length; + int longerBlocksStartAt = result.length - 1; + while (longerBlocksStartAt >= 0) { + int numCodewords = result[longerBlocksStartAt].codewords.length; + if (numCodewords == shorterBlocksTotalCodewords) { + break; + } + longerBlocksStartAt--; + } + longerBlocksStartAt++; + + int shorterBlocksNumDataCodewords = shorterBlocksTotalCodewords - ecBlocks.getECCodewordsPerBlock(); + // The last elements of result may be 1 element longer; + // first fill out as many elements as all of them have + int rawCodewordsOffset = 0; + for (int i = 0; i < shorterBlocksNumDataCodewords; i++) { + for (int j = 0; j < numResultBlocks; j++) { + result[j].codewords[i] = rawCodewords[rawCodewordsOffset++]; + } + } + // Fill out the last data block in the longer ones + for (int j = longerBlocksStartAt; j < numResultBlocks; j++) { + result[j].codewords[shorterBlocksNumDataCodewords] = rawCodewords[rawCodewordsOffset++]; + } + // Now add in error correction blocks + int max = result[0].codewords.length; + for (int i = shorterBlocksNumDataCodewords; i < max; i++) { + for (int j = 0; j < numResultBlocks; j++) { + int iOffset = j < longerBlocksStartAt ? i : i + 1; + result[j].codewords[iOffset] = rawCodewords[rawCodewordsOffset++]; + } + } + return result; + } + + int getNumDataCodewords() { + return numDataCodewords; + } + + byte[] getCodewords() { + return codewords; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java new file mode 100644 index 0000000..e60868d --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DataMask.java @@ -0,0 +1,141 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.common.BitMatrix; + +/** + *

Encapsulates data masks for the data bits in a QR code, per ISO 18004:2006 6.8. Implementations + * of this class can un-mask a raw BitMatrix. For simplicity, they will unmask the entire BitMatrix, + * including areas used for finder patterns, timing patterns, etc. These areas should be unused + * after the point they are unmasked anyway.

+ * + *

Note that the diagram in section 6.8.1 is misleading since it indicates that i is column position + * and j is row position. In fact, as the text says, i is row position and j is column position.

+ * + * @author Sean Owen + */ +enum DataMask { + + // See ISO 18004:2006 6.8.1 + + /** + * 000: mask bits for which (x + y) mod 2 == 0 + */ + DATA_MASK_000() { + @Override + boolean isMasked(int i, int j) { + return ((i + j) & 0x01) == 0; + } + }, + + /** + * 001: mask bits for which x mod 2 == 0 + */ + DATA_MASK_001() { + @Override + boolean isMasked(int i, int j) { + return (i & 0x01) == 0; + } + }, + + /** + * 010: mask bits for which y mod 3 == 0 + */ + DATA_MASK_010() { + @Override + boolean isMasked(int i, int j) { + return j % 3 == 0; + } + }, + + /** + * 011: mask bits for which (x + y) mod 3 == 0 + */ + DATA_MASK_011() { + @Override + boolean isMasked(int i, int j) { + return (i + j) % 3 == 0; + } + }, + + /** + * 100: mask bits for which (x/2 + y/3) mod 2 == 0 + */ + DATA_MASK_100() { + @Override + boolean isMasked(int i, int j) { + return (((i / 2) + (j / 3)) & 0x01) == 0; + } + }, + + /** + * 101: mask bits for which xy mod 2 + xy mod 3 == 0 + * equivalently, such that xy mod 6 == 0 + */ + DATA_MASK_101() { + @Override + boolean isMasked(int i, int j) { + return (i * j) % 6 == 0; + } + }, + + /** + * 110: mask bits for which (xy mod 2 + xy mod 3) mod 2 == 0 + * equivalently, such that xy mod 6 < 3 + */ + DATA_MASK_110() { + @Override + boolean isMasked(int i, int j) { + return ((i * j) % 6) < 3; + } + }, + + /** + * 111: mask bits for which ((x+y)mod 2 + xy mod 3) mod 2 == 0 + * equivalently, such that (x + y + xy mod 3) mod 2 == 0 + */ + DATA_MASK_111() { + @Override + boolean isMasked(int i, int j) { + return ((i + j + ((i * j) % 3)) & 0x01) == 0; + } + }; + + // End of enum constants. + + + /** + *

Implementations of this method reverse the data masking process applied to a QR Code and + * make its bits ready to read.

+ * + * @param bits representation of QR Code bits + * @param dimension dimension of QR Code, represented by bits, being unmasked + */ + final void unmaskBitMatrix(BitMatrix bits, int dimension) { + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (isMasked(i, j)) { + bits.flip(j, i); + } + } + } + } + + abstract boolean isMasked(int i, int j); + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java new file mode 100644 index 0000000..cf3db15 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/DecodedBitStreamParser.java @@ -0,0 +1,360 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.common.BitSource; +import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.StringUtils; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + *

QR Codes can encode text as bits in one of several modes, and can use multiple modes + * in one QR Code. This class decodes the bits back into text.

+ * + *

See ISO 18004:2006, 6.4.3 - 6.4.7

+ * + * @author Sean Owen + */ +final class DecodedBitStreamParser { + + /** + * See ISO 18004:2006, 6.4.4 Table 5 + */ + private static final char[] ALPHANUMERIC_CHARS = + "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".toCharArray(); + private static final int GB2312_SUBSET = 1; + + private DecodedBitStreamParser() { + } + + static DecoderResult decode(byte[] bytes, + Version version, + ErrorCorrectionLevel ecLevel, + Map hints) throws FormatException { + BitSource bits = new BitSource(bytes); + StringBuilder result = new StringBuilder(50); + List byteSegments = new ArrayList<>(1); + int symbolSequence = -1; + int parityData = -1; + + try { + CharacterSetECI currentCharacterSetECI = null; + boolean fc1InEffect = false; + Mode mode; + do { + // While still another segment to read... + if (bits.available() < 4) { + // OK, assume we're done. Really, a TERMINATOR mode should have been recorded here + mode = Mode.TERMINATOR; + } else { + mode = Mode.forBits(bits.readBits(4)); // mode is encoded by 4 bits + } + switch (mode) { + case TERMINATOR: + break; + case FNC1_FIRST_POSITION: + case FNC1_SECOND_POSITION: + // We do little with FNC1 except alter the parsed result a bit according to the spec + fc1InEffect = true; + break; + case STRUCTURED_APPEND: + if (bits.available() < 16) { + throw FormatException.getFormatInstance(); + } + // sequence number and parity is added later to the result metadata + // Read next 8 bits (symbol sequence #) and 8 bits (parity data), then continue + symbolSequence = bits.readBits(8); + parityData = bits.readBits(8); + break; + case ECI: + // Count doesn't apply to ECI + int value = parseECIValue(bits); + currentCharacterSetECI = CharacterSetECI.getCharacterSetECIByValue(value); + if (currentCharacterSetECI == null) { + throw FormatException.getFormatInstance(); + } + break; + case HANZI: + // First handle Hanzi mode which does not start with character count + // Chinese mode contains a sub set indicator right after mode indicator + int subset = bits.readBits(4); + int countHanzi = bits.readBits(mode.getCharacterCountBits(version)); + if (subset == GB2312_SUBSET) { + decodeHanziSegment(bits, result, countHanzi); + } + break; + default: + // "Normal" QR code modes: + // How many characters will follow, encoded in this mode? + int count = bits.readBits(mode.getCharacterCountBits(version)); + switch (mode) { + case NUMERIC: + decodeNumericSegment(bits, result, count); + break; + case ALPHANUMERIC: + decodeAlphanumericSegment(bits, result, count, fc1InEffect); + break; + case BYTE: + decodeByteSegment(bits, result, count, currentCharacterSetECI, byteSegments, hints); + break; + case KANJI: + decodeKanjiSegment(bits, result, count); + break; + default: + throw FormatException.getFormatInstance(); + } + break; + } + } while (mode != Mode.TERMINATOR); + } catch (IllegalArgumentException iae) { + // from readBits() calls + throw FormatException.getFormatInstance(); + } + + return new DecoderResult(bytes, + result.toString(), + byteSegments.isEmpty() ? null : byteSegments, + ecLevel == null ? null : ecLevel.toString(), + symbolSequence, + parityData); + } + + /** + * See specification GBT 18284-2000 + */ + private static void decodeHanziSegment(BitSource bits, + StringBuilder result, + int count) throws FormatException { + // Don't crash trying to read more bits than we have available. + if (count * 13 > bits.available()) { + throw FormatException.getFormatInstance(); + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs + // and decode as GB2312 afterwards + byte[] buffer = new byte[2 * count]; + int offset = 0; + while (count > 0) { + // Each 13 bits encodes a 2-byte character + int twoBytes = bits.readBits(13); + int assembledTwoBytes = ((twoBytes / 0x060) << 8) | (twoBytes % 0x060); + if (assembledTwoBytes < 0x003BF) { + // In the 0xA1A1 to 0xAAFE range + assembledTwoBytes += 0x0A1A1; + } else { + // In the 0xB0A1 to 0xFAFE range + assembledTwoBytes += 0x0A6A1; + } + buffer[offset] = (byte) ((assembledTwoBytes >> 8) & 0xFF); + buffer[offset + 1] = (byte) (assembledTwoBytes & 0xFF); + offset += 2; + count--; + } + + try { + result.append(new String(buffer, StringUtils.GB2312)); + } catch (UnsupportedEncodingException ignored) { + throw FormatException.getFormatInstance(); + } + } + + private static void decodeKanjiSegment(BitSource bits, + StringBuilder result, + int count) throws FormatException { + // Don't crash trying to read more bits than we have available. + if (count * 13 > bits.available()) { + throw FormatException.getFormatInstance(); + } + + // Each character will require 2 bytes. Read the characters as 2-byte pairs + // and decode as Shift_JIS afterwards + byte[] buffer = new byte[2 * count]; + int offset = 0; + while (count > 0) { + // Each 13 bits encodes a 2-byte character + int twoBytes = bits.readBits(13); + int assembledTwoBytes = ((twoBytes / 0x0C0) << 8) | (twoBytes % 0x0C0); + if (assembledTwoBytes < 0x01F00) { + // In the 0x8140 to 0x9FFC range + assembledTwoBytes += 0x08140; + } else { + // In the 0xE040 to 0xEBBF range + assembledTwoBytes += 0x0C140; + } + buffer[offset] = (byte) (assembledTwoBytes >> 8); + buffer[offset + 1] = (byte) assembledTwoBytes; + offset += 2; + count--; + } + // Shift_JIS may not be supported in some environments: + try { + result.append(new String(buffer, StringUtils.SHIFT_JIS)); + } catch (UnsupportedEncodingException ignored) { + throw FormatException.getFormatInstance(); + } + } + + private static void decodeByteSegment(BitSource bits, + StringBuilder result, + int count, + CharacterSetECI currentCharacterSetECI, + Collection byteSegments, + Map hints) throws FormatException { + // Don't crash trying to read more bits than we have available. + if (8 * count > bits.available()) { + throw FormatException.getFormatInstance(); + } + + byte[] readBytes = new byte[count]; + for (int i = 0; i < count; i++) { + readBytes[i] = (byte) bits.readBits(8); + } + String encoding; + if (currentCharacterSetECI == null) { + // The spec isn't clear on this mode; see + // section 6.4.5: t does not say which encoding to assuming + // upon decoding. I have seen ISO-8859-1 used as well as + // Shift_JIS -- without anything like an ECI designator to + // give a hint. + encoding = StringUtils.guessEncoding(readBytes, hints); + } else { + encoding = currentCharacterSetECI.name(); + } + try { + result.append(new String(readBytes, encoding)); + } catch (UnsupportedEncodingException ignored) { + throw FormatException.getFormatInstance(); + } + byteSegments.add(readBytes); + } + + private static char toAlphaNumericChar(int value) throws FormatException { + if (value >= ALPHANUMERIC_CHARS.length) { + throw FormatException.getFormatInstance(); + } + return ALPHANUMERIC_CHARS[value]; + } + + private static void decodeAlphanumericSegment(BitSource bits, + StringBuilder result, + int count, + boolean fc1InEffect) throws FormatException { + // Read two characters at a time + int start = result.length(); + while (count > 1) { + if (bits.available() < 11) { + throw FormatException.getFormatInstance(); + } + int nextTwoCharsBits = bits.readBits(11); + result.append(toAlphaNumericChar(nextTwoCharsBits / 45)); + result.append(toAlphaNumericChar(nextTwoCharsBits % 45)); + count -= 2; + } + if (count == 1) { + // special case: one character left + if (bits.available() < 6) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(bits.readBits(6))); + } + // See section 6.4.8.1, 6.4.8.2 + if (fc1InEffect) { + // We need to massage the result a bit if in an FNC1 mode: + for (int i = start; i < result.length(); i++) { + if (result.charAt(i) == '%') { + if (i < result.length() - 1 && result.charAt(i + 1) == '%') { + // %% is rendered as % + result.deleteCharAt(i + 1); + } else { + // In alpha mode, % should be converted to FNC1 separator 0x1D + result.setCharAt(i, (char) 0x1D); + } + } + } + } + } + + private static void decodeNumericSegment(BitSource bits, + StringBuilder result, + int count) throws FormatException { + // Read three digits at a time + while (count >= 3) { + // Each 10 bits encodes three digits + if (bits.available() < 10) { + throw FormatException.getFormatInstance(); + } + int threeDigitsBits = bits.readBits(10); + if (threeDigitsBits >= 1000) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(threeDigitsBits / 100)); + result.append(toAlphaNumericChar((threeDigitsBits / 10) % 10)); + result.append(toAlphaNumericChar(threeDigitsBits % 10)); + count -= 3; + } + if (count == 2) { + // Two digits left over to read, encoded in 7 bits + if (bits.available() < 7) { + throw FormatException.getFormatInstance(); + } + int twoDigitsBits = bits.readBits(7); + if (twoDigitsBits >= 100) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(twoDigitsBits / 10)); + result.append(toAlphaNumericChar(twoDigitsBits % 10)); + } else if (count == 1) { + // One digit left over to read + if (bits.available() < 4) { + throw FormatException.getFormatInstance(); + } + int digitBits = bits.readBits(4); + if (digitBits >= 10) { + throw FormatException.getFormatInstance(); + } + result.append(toAlphaNumericChar(digitBits)); + } + } + + private static int parseECIValue(BitSource bits) throws FormatException { + int firstByte = bits.readBits(8); + if ((firstByte & 0x80) == 0) { + // just one byte + return firstByte & 0x7F; + } + if ((firstByte & 0xC0) == 0x80) { + // two bytes + int secondByte = bits.readBits(8); + return ((firstByte & 0x3F) << 8) | secondByte; + } + if ((firstByte & 0xE0) == 0xC0) { + // three bytes + int secondThirdBytes = bits.readBits(16); + return ((firstByte & 0x1F) << 16) | secondThirdBytes; + } + throw FormatException.getFormatInstance(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Decoder.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Decoder.java new file mode 100644 index 0000000..9519687 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Decoder.java @@ -0,0 +1,193 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.ChecksumException; +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DecoderResult; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonDecoder; +import com.google.zxing.common.reedsolomon.ReedSolomonException; + +import java.util.Map; + +/** + *

The main class which implements QR Code decoding -- as opposed to locating and extracting + * the QR Code from an image.

+ * + * @author Sean Owen + */ +public final class Decoder { + + private final ReedSolomonDecoder rsDecoder; + + public Decoder() { + rsDecoder = new ReedSolomonDecoder(GenericGF.QR_CODE_FIELD_256); + } + + public DecoderResult decode(boolean[][] image) throws ChecksumException, FormatException { + return decode(image, null); + } + + /** + *

Convenience method that can decode a QR Code represented as a 2D array of booleans. + * "true" is taken to mean a black module.

+ * + * @param image booleans representing white/black QR Code modules + * @param hints decoding hints that should be used to influence decoding + * @return text and bytes encoded within the QR Code + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(boolean[][] image, Map hints) + throws ChecksumException, FormatException { + return decode(BitMatrix.parse(image), hints); + } + + public DecoderResult decode(BitMatrix bits) throws ChecksumException, FormatException { + return decode(bits, null); + } + + /** + *

Decodes a QR Code represented as a {@link BitMatrix}. A 1 or "true" is taken to mean a black module.

+ * + * @param bits booleans representing white/black QR Code modules + * @param hints decoding hints that should be used to influence decoding + * @return text and bytes encoded within the QR Code + * @throws FormatException if the QR Code cannot be decoded + * @throws ChecksumException if error correction fails + */ + public DecoderResult decode(BitMatrix bits, Map hints) + throws FormatException, ChecksumException { + + // Construct a parser and read version, error-correction level + BitMatrixParser parser = new BitMatrixParser(bits); + FormatException fe = null; + ChecksumException ce = null; + try { + return decode(parser, hints); + } catch (FormatException e) { + fe = e; + } catch (ChecksumException e) { + ce = e; + } + + try { + + // Revert the bit matrix + parser.remask(); + + // Will be attempting a mirrored reading of the version and format info. + parser.setMirror(true); + + // Preemptively read the version. + parser.readVersion(); + + // Preemptively read the format information. + parser.readFormatInformation(); + + /* + * Since we're here, this means we have successfully detected some kind + * of version and format information when mirrored. This is a good sign, + * that the QR code may be mirrored, and we should try once more with a + * mirrored content. + */ + // Prepare for a mirrored reading. + parser.mirror(); + + DecoderResult result = decode(parser, hints); + + // Success! Notify the caller that the code was mirrored. + result.setOther(new QRCodeDecoderMetaData(true)); + + return result; + + } catch (FormatException | ChecksumException e) { + // Throw the exception from the original reading + if (fe != null) { + throw fe; + } + if (ce != null) { + throw ce; + } + throw e; + + } + } + + private DecoderResult decode(BitMatrixParser parser, Map hints) + throws FormatException, ChecksumException { + Version version = parser.readVersion(); + ErrorCorrectionLevel ecLevel = parser.readFormatInformation().getErrorCorrectionLevel(); + + // Read codewords + byte[] codewords = parser.readCodewords(); + // Separate into data blocks + DataBlock[] dataBlocks = DataBlock.getDataBlocks(codewords, version, ecLevel); + + // Count total number of data bytes + int totalBytes = 0; + for (DataBlock dataBlock : dataBlocks) { + totalBytes += dataBlock.getNumDataCodewords(); + } + byte[] resultBytes = new byte[totalBytes]; + int resultOffset = 0; + + // Error-correct and copy data blocks together into a stream of bytes + for (DataBlock dataBlock : dataBlocks) { + byte[] codewordBytes = dataBlock.getCodewords(); + int numDataCodewords = dataBlock.getNumDataCodewords(); + correctErrors(codewordBytes, numDataCodewords); + for (int i = 0; i < numDataCodewords; i++) { + resultBytes[resultOffset++] = codewordBytes[i]; + } + } + + // Decode the contents of that stream of bytes + return DecodedBitStreamParser.decode(resultBytes, version, ecLevel, hints); + } + + /** + *

Given data and error-correction codewords received, possibly corrupted by errors, attempts to + * correct the errors in-place using Reed-Solomon error correction.

+ * + * @param codewordBytes data and error correction codewords + * @param numDataCodewords number of codewords that are data bytes + * @throws ChecksumException if error correction fails + */ + private void correctErrors(byte[] codewordBytes, int numDataCodewords) throws ChecksumException { + int numCodewords = codewordBytes.length; + // First read into an array of ints + int[] codewordsInts = new int[numCodewords]; + for (int i = 0; i < numCodewords; i++) { + codewordsInts[i] = codewordBytes[i] & 0xFF; + } + try { + rsDecoder.decode(codewordsInts, codewordBytes.length - numDataCodewords); + } catch (ReedSolomonException ignored) { + throw ChecksumException.getChecksumInstance(); + } + // Copy back into array of bytes -- only need to worry about the bytes that were data + // We don't care about errors in the error-correction codewords + for (int i = 0; i < numDataCodewords; i++) { + codewordBytes[i] = (byte) codewordsInts[i]; + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java new file mode 100644 index 0000000..0f1e113 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/ErrorCorrectionLevel.java @@ -0,0 +1,60 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +/** + *

See ISO 18004:2006, 6.5.1. This enum encapsulates the four error correction levels + * defined by the QR code standard.

+ * + * @author Sean Owen + */ +public enum ErrorCorrectionLevel { + + /** L = ~7% correction */ + L(0x01), + /** M = ~15% correction */ + M(0x00), + /** Q = ~25% correction */ + Q(0x03), + /** H = ~30% correction */ + H(0x02); + + private static final ErrorCorrectionLevel[] FOR_BITS = {M, L, H, Q}; + + private final int bits; + + ErrorCorrectionLevel(int bits) { + this.bits = bits; + } + + public int getBits() { + return bits; + } + + /** + * @param bits int containing the two bits encoding a QR Code's error correction level + * @return ErrorCorrectionLevel representing the encoded error correction level + */ + public static ErrorCorrectionLevel forBits(int bits) { + if (bits < 0 || bits >= FOR_BITS.length) { + throw new IllegalArgumentException(); + } + return FOR_BITS[bits]; + } + + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/FormatInformation.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/FormatInformation.java new file mode 100644 index 0000000..95ee701 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/FormatInformation.java @@ -0,0 +1,157 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +/** + *

Encapsulates a QR Code's format information, including the data mask used and + * error correction level.

+ * + * @author Sean Owen + * @see DataMask + * @see ErrorCorrectionLevel + */ +final class FormatInformation { + + private static final int FORMAT_INFO_MASK_QR = 0x5412; + + /** + * See ISO 18004:2006, Annex C, Table C.1 + */ + private static final int[][] FORMAT_INFO_DECODE_LOOKUP = { + {0x5412, 0x00}, + {0x5125, 0x01}, + {0x5E7C, 0x02}, + {0x5B4B, 0x03}, + {0x45F9, 0x04}, + {0x40CE, 0x05}, + {0x4F97, 0x06}, + {0x4AA0, 0x07}, + {0x77C4, 0x08}, + {0x72F3, 0x09}, + {0x7DAA, 0x0A}, + {0x789D, 0x0B}, + {0x662F, 0x0C}, + {0x6318, 0x0D}, + {0x6C41, 0x0E}, + {0x6976, 0x0F}, + {0x1689, 0x10}, + {0x13BE, 0x11}, + {0x1CE7, 0x12}, + {0x19D0, 0x13}, + {0x0762, 0x14}, + {0x0255, 0x15}, + {0x0D0C, 0x16}, + {0x083B, 0x17}, + {0x355F, 0x18}, + {0x3068, 0x19}, + {0x3F31, 0x1A}, + {0x3A06, 0x1B}, + {0x24B4, 0x1C}, + {0x2183, 0x1D}, + {0x2EDA, 0x1E}, + {0x2BED, 0x1F}, + }; + + private final ErrorCorrectionLevel errorCorrectionLevel; + private final byte dataMask; + + private FormatInformation(int formatInfo) { + // Bits 3,4 + errorCorrectionLevel = ErrorCorrectionLevel.forBits((formatInfo >> 3) & 0x03); + // Bottom 3 bits + dataMask = (byte) (formatInfo & 0x07); + } + + static int numBitsDiffering(int a, int b) { + return Integer.bitCount(a ^ b); + } + + /** + * @param maskedFormatInfo1 format info indicator, with mask still applied + * @param maskedFormatInfo2 second copy of same info; both are checked at the same time + * to establish best match + * @return information about the format it specifies, or {@code null} + * if doesn't seem to match any known pattern + */ + static FormatInformation decodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) { + FormatInformation formatInfo = doDecodeFormatInformation(maskedFormatInfo1, maskedFormatInfo2); + if (formatInfo != null) { + return formatInfo; + } + // Should return null, but, some QR codes apparently + // do not mask this info. Try again by actually masking the pattern + // first + return doDecodeFormatInformation(maskedFormatInfo1 ^ FORMAT_INFO_MASK_QR, + maskedFormatInfo2 ^ FORMAT_INFO_MASK_QR); + } + + private static FormatInformation doDecodeFormatInformation(int maskedFormatInfo1, int maskedFormatInfo2) { + // Find the int in FORMAT_INFO_DECODE_LOOKUP with fewest bits differing + int bestDifference = Integer.MAX_VALUE; + int bestFormatInfo = 0; + for (int[] decodeInfo : FORMAT_INFO_DECODE_LOOKUP) { + int targetInfo = decodeInfo[0]; + if (targetInfo == maskedFormatInfo1 || targetInfo == maskedFormatInfo2) { + // Found an exact match + return new FormatInformation(decodeInfo[1]); + } + int bitsDifference = numBitsDiffering(maskedFormatInfo1, targetInfo); + if (bitsDifference < bestDifference) { + bestFormatInfo = decodeInfo[1]; + bestDifference = bitsDifference; + } + if (maskedFormatInfo1 != maskedFormatInfo2) { + // also try the other option + bitsDifference = numBitsDiffering(maskedFormatInfo2, targetInfo); + if (bitsDifference < bestDifference) { + bestFormatInfo = decodeInfo[1]; + bestDifference = bitsDifference; + } + } + } + // Hamming distance of the 32 masked codes is 7, by construction, so <= 3 bits + // differing means we found a match + if (bestDifference <= 3) { + return new FormatInformation(bestFormatInfo); + } + return null; + } + + ErrorCorrectionLevel getErrorCorrectionLevel() { + return errorCorrectionLevel; + } + + byte getDataMask() { + return dataMask; + } + + @Override + public int hashCode() { + return (errorCorrectionLevel.ordinal() << 3) | dataMask; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FormatInformation)) { + return false; + } + FormatInformation other = (FormatInformation) o; + return this.errorCorrectionLevel == other.errorCorrectionLevel && + this.dataMask == other.dataMask; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Mode.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Mode.java new file mode 100644 index 0000000..b7e9ab3 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Mode.java @@ -0,0 +1,102 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +/** + *

See ISO 18004:2006, 6.4.1, Tables 2 and 3. This enum encapsulates the various modes in which + * data can be encoded to bits in the QR code standard.

+ * + * @author Sean Owen + */ +public enum Mode { + + TERMINATOR(new int[]{0, 0, 0}, 0x00), // Not really a mode... + NUMERIC(new int[]{10, 12, 14}, 0x01), + ALPHANUMERIC(new int[]{9, 11, 13}, 0x02), + STRUCTURED_APPEND(new int[]{0, 0, 0}, 0x03), // Not supported + BYTE(new int[]{8, 16, 16}, 0x04), + ECI(new int[]{0, 0, 0}, 0x07), // character counts don't apply + KANJI(new int[]{8, 10, 12}, 0x08), + FNC1_FIRST_POSITION(new int[]{0, 0, 0}, 0x05), + FNC1_SECOND_POSITION(new int[]{0, 0, 0}, 0x09), + /** See GBT 18284-2000; "Hanzi" is a transliteration of this mode name. */ + HANZI(new int[]{8, 10, 12}, 0x0D); + + private final int[] characterCountBitsForVersions; + private final int bits; + + Mode(int[] characterCountBitsForVersions, int bits) { + this.characterCountBitsForVersions = characterCountBitsForVersions; + this.bits = bits; + } + + /** + * @param bits four bits encoding a QR Code data mode + * @return Mode encoded by these bits + * @throws IllegalArgumentException if bits do not correspond to a known mode + */ + public static Mode forBits(int bits) { + switch (bits) { + case 0x0: + return TERMINATOR; + case 0x1: + return NUMERIC; + case 0x2: + return ALPHANUMERIC; + case 0x3: + return STRUCTURED_APPEND; + case 0x4: + return BYTE; + case 0x5: + return FNC1_FIRST_POSITION; + case 0x7: + return ECI; + case 0x8: + return KANJI; + case 0x9: + return FNC1_SECOND_POSITION; + case 0xD: + // 0xD is defined in GBT 18284-2000, may not be supported in foreign country + return HANZI; + default: + throw new IllegalArgumentException(); + } + } + + /** + * @param version version in question + * @return number of bits used, in this QR Code symbol {@link Version}, to encode the + * count of characters that will follow encoded in this Mode + */ + public int getCharacterCountBits(Version version) { + int number = version.getVersionNumber(); + int offset; + if (number <= 9) { + offset = 0; + } else if (number <= 26) { + offset = 1; + } else { + offset = 2; + } + return characterCountBitsForVersions[offset]; + } + + public int getBits() { + return bits; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/QRCodeDecoderMetaData.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/QRCodeDecoderMetaData.java new file mode 100644 index 0000000..299bb65 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/QRCodeDecoderMetaData.java @@ -0,0 +1,57 @@ +/* + * Copyright 2013 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.ResultPoint; + +/** + * Meta-data container for QR Code decoding. Instances of this class may be used to convey information back to the + * decoding caller. Callers are expected to process this. + * + * @see com.google.zxing.common.DecoderResult#getOther() + */ +public final class QRCodeDecoderMetaData { + + private final boolean mirrored; + + QRCodeDecoderMetaData(boolean mirrored) { + this.mirrored = mirrored; + } + + /** + * @return true if the QR Code was mirrored. + */ + public boolean isMirrored() { + return mirrored; + } + + /** + * Apply the result points' order correction due to mirroring. + * + * @param points Array of points to apply mirror correction to. + */ + public void applyMirroredCorrection(ResultPoint[] points) { + if (!mirrored || points == null || points.length < 3) { + return; + } + ResultPoint bottomLeft = points[0]; + points[0] = points[2]; + points[2] = bottomLeft; + // No need to 'fix' top-left and alignment pattern. + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Version.java b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Version.java new file mode 100644 index 0000000..98a6850 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/decoder/Version.java @@ -0,0 +1,578 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.decoder; + +import com.google.zxing.FormatException; +import com.google.zxing.common.BitMatrix; + +/** + * See ISO 18004:2006 Annex D + * + * @author Sean Owen + */ +public final class Version { + + /** + * See ISO 18004:2006 Annex D. + * Element i represents the raw version bits that specify version i + 7 + */ + private static final int[] VERSION_DECODE_INFO = { + 0x07C94, 0x085BC, 0x09A99, 0x0A4D3, 0x0BBF6, + 0x0C762, 0x0D847, 0x0E60D, 0x0F928, 0x10B78, + 0x1145D, 0x12A17, 0x13532, 0x149A6, 0x15683, + 0x168C9, 0x177EC, 0x18EC4, 0x191E1, 0x1AFAB, + 0x1B08E, 0x1CC1A, 0x1D33F, 0x1ED75, 0x1F250, + 0x209D5, 0x216F0, 0x228BA, 0x2379F, 0x24B0B, + 0x2542E, 0x26A64, 0x27541, 0x28C69 + }; + + private static final Version[] VERSIONS = buildVersions(); + + private final int versionNumber; + private final int[] alignmentPatternCenters; + private final ECBlocks[] ecBlocks; + private final int totalCodewords; + + private Version(int versionNumber, + int[] alignmentPatternCenters, + ECBlocks... ecBlocks) { + this.versionNumber = versionNumber; + this.alignmentPatternCenters = alignmentPatternCenters; + this.ecBlocks = ecBlocks; + int total = 0; + int ecCodewords = ecBlocks[0].getECCodewordsPerBlock(); + ECB[] ecbArray = ecBlocks[0].getECBlocks(); + for (ECB ecBlock : ecbArray) { + total += ecBlock.getCount() * (ecBlock.getDataCodewords() + ecCodewords); + } + this.totalCodewords = total; + } + + public int getVersionNumber() { + return versionNumber; + } + + public int[] getAlignmentPatternCenters() { + return alignmentPatternCenters; + } + + public int getTotalCodewords() { + return totalCodewords; + } + + public int getDimensionForVersion() { + return 17 + 4 * versionNumber; + } + + public ECBlocks getECBlocksForLevel(ErrorCorrectionLevel ecLevel) { + return ecBlocks[ecLevel.ordinal()]; + } + + /** + *

Deduces version information purely from QR Code dimensions.

+ * + * @param dimension dimension in modules + * @return Version for a QR Code of that dimension + * @throws FormatException if dimension is not 1 mod 4 + */ + public static Version getProvisionalVersionForDimension(int dimension) throws FormatException { + if (dimension % 4 != 1) { + throw FormatException.getFormatInstance(); + } + try { + return getVersionForNumber((dimension - 17) / 4); + } catch (IllegalArgumentException ignored) { + throw FormatException.getFormatInstance(); + } + } + + public static Version getVersionForNumber(int versionNumber) { + if (versionNumber < 1 || versionNumber > 40) { + throw new IllegalArgumentException(); + } + return VERSIONS[versionNumber - 1]; + } + + static Version decodeVersionInformation(int versionBits) { + int bestDifference = Integer.MAX_VALUE; + int bestVersion = 0; + for (int i = 0; i < VERSION_DECODE_INFO.length; i++) { + int targetVersion = VERSION_DECODE_INFO[i]; + // Do the version info bits match exactly? done. + if (targetVersion == versionBits) { + return getVersionForNumber(i + 7); + } + // Otherwise see if this is the closest to a real version info bit string + // we have seen so far + int bitsDifference = FormatInformation.numBitsDiffering(versionBits, targetVersion); + if (bitsDifference < bestDifference) { + bestVersion = i + 7; + bestDifference = bitsDifference; + } + } + // We can tolerate up to 3 bits of error since no two version info codewords will + // differ in less than 8 bits. + if (bestDifference <= 3) { + return getVersionForNumber(bestVersion); + } + // If we didn't find a close enough match, fail + return null; + } + + /** + * See ISO 18004:2006 Annex E + */ + BitMatrix buildFunctionPattern() { + int dimension = getDimensionForVersion(); + BitMatrix bitMatrix = new BitMatrix(dimension); + + // Top left finder pattern + separator + format + bitMatrix.setRegion(0, 0, 9, 9); + // Top right finder pattern + separator + format + bitMatrix.setRegion(dimension - 8, 0, 8, 9); + // Bottom left finder pattern + separator + format + bitMatrix.setRegion(0, dimension - 8, 9, 8); + + // Alignment patterns + int max = alignmentPatternCenters.length; + for (int x = 0; x < max; x++) { + int i = alignmentPatternCenters[x] - 2; + for (int y = 0; y < max; y++) { + if ((x == 0 && (y == 0 || y == max - 1)) || (x == max - 1 && y == 0)) { + // No alignment patterns near the three finder patterns + continue; + } + bitMatrix.setRegion(alignmentPatternCenters[y] - 2, i, 5, 5); + } + } + + // Vertical timing pattern + bitMatrix.setRegion(6, 9, 1, dimension - 17); + // Horizontal timing pattern + bitMatrix.setRegion(9, 6, dimension - 17, 1); + + if (versionNumber > 6) { + // Version info, top right + bitMatrix.setRegion(dimension - 11, 0, 3, 6); + // Version info, bottom left + bitMatrix.setRegion(0, dimension - 11, 6, 3); + } + + return bitMatrix; + } + + /** + *

Encapsulates a set of error-correction blocks in one symbol version. Most versions will + * use blocks of differing sizes within one version, so, this encapsulates the parameters for + * each set of blocks. It also holds the number of error-correction codewords per block since it + * will be the same across all blocks within one version.

+ */ + public static final class ECBlocks { + private final int ecCodewordsPerBlock; + private final ECB[] ecBlocks; + + ECBlocks(int ecCodewordsPerBlock, ECB... ecBlocks) { + this.ecCodewordsPerBlock = ecCodewordsPerBlock; + this.ecBlocks = ecBlocks; + } + + public int getECCodewordsPerBlock() { + return ecCodewordsPerBlock; + } + + public int getNumBlocks() { + int total = 0; + for (ECB ecBlock : ecBlocks) { + total += ecBlock.getCount(); + } + return total; + } + + public int getTotalECCodewords() { + return ecCodewordsPerBlock * getNumBlocks(); + } + + public ECB[] getECBlocks() { + return ecBlocks; + } + } + + /** + *

Encapsulates the parameters for one error-correction block in one symbol version. + * This includes the number of data codewords, and the number of times a block with these + * parameters is used consecutively in the QR code version's format.

+ */ + public static final class ECB { + private final int count; + private final int dataCodewords; + + ECB(int count, int dataCodewords) { + this.count = count; + this.dataCodewords = dataCodewords; + } + + public int getCount() { + return count; + } + + public int getDataCodewords() { + return dataCodewords; + } + } + + @Override + public String toString() { + return String.valueOf(versionNumber); + } + + /** + * See ISO 18004:2006 6.5.1 Table 9 + */ + private static Version[] buildVersions() { + return new Version[]{ + new Version(1, new int[]{}, + new ECBlocks(7, new ECB(1, 19)), + new ECBlocks(10, new ECB(1, 16)), + new ECBlocks(13, new ECB(1, 13)), + new ECBlocks(17, new ECB(1, 9))), + new Version(2, new int[]{6, 18}, + new ECBlocks(10, new ECB(1, 34)), + new ECBlocks(16, new ECB(1, 28)), + new ECBlocks(22, new ECB(1, 22)), + new ECBlocks(28, new ECB(1, 16))), + new Version(3, new int[]{6, 22}, + new ECBlocks(15, new ECB(1, 55)), + new ECBlocks(26, new ECB(1, 44)), + new ECBlocks(18, new ECB(2, 17)), + new ECBlocks(22, new ECB(2, 13))), + new Version(4, new int[]{6, 26}, + new ECBlocks(20, new ECB(1, 80)), + new ECBlocks(18, new ECB(2, 32)), + new ECBlocks(26, new ECB(2, 24)), + new ECBlocks(16, new ECB(4, 9))), + new Version(5, new int[]{6, 30}, + new ECBlocks(26, new ECB(1, 108)), + new ECBlocks(24, new ECB(2, 43)), + new ECBlocks(18, new ECB(2, 15), + new ECB(2, 16)), + new ECBlocks(22, new ECB(2, 11), + new ECB(2, 12))), + new Version(6, new int[]{6, 34}, + new ECBlocks(18, new ECB(2, 68)), + new ECBlocks(16, new ECB(4, 27)), + new ECBlocks(24, new ECB(4, 19)), + new ECBlocks(28, new ECB(4, 15))), + new Version(7, new int[]{6, 22, 38}, + new ECBlocks(20, new ECB(2, 78)), + new ECBlocks(18, new ECB(4, 31)), + new ECBlocks(18, new ECB(2, 14), + new ECB(4, 15)), + new ECBlocks(26, new ECB(4, 13), + new ECB(1, 14))), + new Version(8, new int[]{6, 24, 42}, + new ECBlocks(24, new ECB(2, 97)), + new ECBlocks(22, new ECB(2, 38), + new ECB(2, 39)), + new ECBlocks(22, new ECB(4, 18), + new ECB(2, 19)), + new ECBlocks(26, new ECB(4, 14), + new ECB(2, 15))), + new Version(9, new int[]{6, 26, 46}, + new ECBlocks(30, new ECB(2, 116)), + new ECBlocks(22, new ECB(3, 36), + new ECB(2, 37)), + new ECBlocks(20, new ECB(4, 16), + new ECB(4, 17)), + new ECBlocks(24, new ECB(4, 12), + new ECB(4, 13))), + new Version(10, new int[]{6, 28, 50}, + new ECBlocks(18, new ECB(2, 68), + new ECB(2, 69)), + new ECBlocks(26, new ECB(4, 43), + new ECB(1, 44)), + new ECBlocks(24, new ECB(6, 19), + new ECB(2, 20)), + new ECBlocks(28, new ECB(6, 15), + new ECB(2, 16))), + new Version(11, new int[]{6, 30, 54}, + new ECBlocks(20, new ECB(4, 81)), + new ECBlocks(30, new ECB(1, 50), + new ECB(4, 51)), + new ECBlocks(28, new ECB(4, 22), + new ECB(4, 23)), + new ECBlocks(24, new ECB(3, 12), + new ECB(8, 13))), + new Version(12, new int[]{6, 32, 58}, + new ECBlocks(24, new ECB(2, 92), + new ECB(2, 93)), + new ECBlocks(22, new ECB(6, 36), + new ECB(2, 37)), + new ECBlocks(26, new ECB(4, 20), + new ECB(6, 21)), + new ECBlocks(28, new ECB(7, 14), + new ECB(4, 15))), + new Version(13, new int[]{6, 34, 62}, + new ECBlocks(26, new ECB(4, 107)), + new ECBlocks(22, new ECB(8, 37), + new ECB(1, 38)), + new ECBlocks(24, new ECB(8, 20), + new ECB(4, 21)), + new ECBlocks(22, new ECB(12, 11), + new ECB(4, 12))), + new Version(14, new int[]{6, 26, 46, 66}, + new ECBlocks(30, new ECB(3, 115), + new ECB(1, 116)), + new ECBlocks(24, new ECB(4, 40), + new ECB(5, 41)), + new ECBlocks(20, new ECB(11, 16), + new ECB(5, 17)), + new ECBlocks(24, new ECB(11, 12), + new ECB(5, 13))), + new Version(15, new int[]{6, 26, 48, 70}, + new ECBlocks(22, new ECB(5, 87), + new ECB(1, 88)), + new ECBlocks(24, new ECB(5, 41), + new ECB(5, 42)), + new ECBlocks(30, new ECB(5, 24), + new ECB(7, 25)), + new ECBlocks(24, new ECB(11, 12), + new ECB(7, 13))), + new Version(16, new int[]{6, 26, 50, 74}, + new ECBlocks(24, new ECB(5, 98), + new ECB(1, 99)), + new ECBlocks(28, new ECB(7, 45), + new ECB(3, 46)), + new ECBlocks(24, new ECB(15, 19), + new ECB(2, 20)), + new ECBlocks(30, new ECB(3, 15), + new ECB(13, 16))), + new Version(17, new int[]{6, 30, 54, 78}, + new ECBlocks(28, new ECB(1, 107), + new ECB(5, 108)), + new ECBlocks(28, new ECB(10, 46), + new ECB(1, 47)), + new ECBlocks(28, new ECB(1, 22), + new ECB(15, 23)), + new ECBlocks(28, new ECB(2, 14), + new ECB(17, 15))), + new Version(18, new int[]{6, 30, 56, 82}, + new ECBlocks(30, new ECB(5, 120), + new ECB(1, 121)), + new ECBlocks(26, new ECB(9, 43), + new ECB(4, 44)), + new ECBlocks(28, new ECB(17, 22), + new ECB(1, 23)), + new ECBlocks(28, new ECB(2, 14), + new ECB(19, 15))), + new Version(19, new int[]{6, 30, 58, 86}, + new ECBlocks(28, new ECB(3, 113), + new ECB(4, 114)), + new ECBlocks(26, new ECB(3, 44), + new ECB(11, 45)), + new ECBlocks(26, new ECB(17, 21), + new ECB(4, 22)), + new ECBlocks(26, new ECB(9, 13), + new ECB(16, 14))), + new Version(20, new int[]{6, 34, 62, 90}, + new ECBlocks(28, new ECB(3, 107), + new ECB(5, 108)), + new ECBlocks(26, new ECB(3, 41), + new ECB(13, 42)), + new ECBlocks(30, new ECB(15, 24), + new ECB(5, 25)), + new ECBlocks(28, new ECB(15, 15), + new ECB(10, 16))), + new Version(21, new int[]{6, 28, 50, 72, 94}, + new ECBlocks(28, new ECB(4, 116), + new ECB(4, 117)), + new ECBlocks(26, new ECB(17, 42)), + new ECBlocks(28, new ECB(17, 22), + new ECB(6, 23)), + new ECBlocks(30, new ECB(19, 16), + new ECB(6, 17))), + new Version(22, new int[]{6, 26, 50, 74, 98}, + new ECBlocks(28, new ECB(2, 111), + new ECB(7, 112)), + new ECBlocks(28, new ECB(17, 46)), + new ECBlocks(30, new ECB(7, 24), + new ECB(16, 25)), + new ECBlocks(24, new ECB(34, 13))), + new Version(23, new int[]{6, 30, 54, 78, 102}, + new ECBlocks(30, new ECB(4, 121), + new ECB(5, 122)), + new ECBlocks(28, new ECB(4, 47), + new ECB(14, 48)), + new ECBlocks(30, new ECB(11, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(16, 15), + new ECB(14, 16))), + new Version(24, new int[]{6, 28, 54, 80, 106}, + new ECBlocks(30, new ECB(6, 117), + new ECB(4, 118)), + new ECBlocks(28, new ECB(6, 45), + new ECB(14, 46)), + new ECBlocks(30, new ECB(11, 24), + new ECB(16, 25)), + new ECBlocks(30, new ECB(30, 16), + new ECB(2, 17))), + new Version(25, new int[]{6, 32, 58, 84, 110}, + new ECBlocks(26, new ECB(8, 106), + new ECB(4, 107)), + new ECBlocks(28, new ECB(8, 47), + new ECB(13, 48)), + new ECBlocks(30, new ECB(7, 24), + new ECB(22, 25)), + new ECBlocks(30, new ECB(22, 15), + new ECB(13, 16))), + new Version(26, new int[]{6, 30, 58, 86, 114}, + new ECBlocks(28, new ECB(10, 114), + new ECB(2, 115)), + new ECBlocks(28, new ECB(19, 46), + new ECB(4, 47)), + new ECBlocks(28, new ECB(28, 22), + new ECB(6, 23)), + new ECBlocks(30, new ECB(33, 16), + new ECB(4, 17))), + new Version(27, new int[]{6, 34, 62, 90, 118}, + new ECBlocks(30, new ECB(8, 122), + new ECB(4, 123)), + new ECBlocks(28, new ECB(22, 45), + new ECB(3, 46)), + new ECBlocks(30, new ECB(8, 23), + new ECB(26, 24)), + new ECBlocks(30, new ECB(12, 15), + new ECB(28, 16))), + new Version(28, new int[]{6, 26, 50, 74, 98, 122}, + new ECBlocks(30, new ECB(3, 117), + new ECB(10, 118)), + new ECBlocks(28, new ECB(3, 45), + new ECB(23, 46)), + new ECBlocks(30, new ECB(4, 24), + new ECB(31, 25)), + new ECBlocks(30, new ECB(11, 15), + new ECB(31, 16))), + new Version(29, new int[]{6, 30, 54, 78, 102, 126}, + new ECBlocks(30, new ECB(7, 116), + new ECB(7, 117)), + new ECBlocks(28, new ECB(21, 45), + new ECB(7, 46)), + new ECBlocks(30, new ECB(1, 23), + new ECB(37, 24)), + new ECBlocks(30, new ECB(19, 15), + new ECB(26, 16))), + new Version(30, new int[]{6, 26, 52, 78, 104, 130}, + new ECBlocks(30, new ECB(5, 115), + new ECB(10, 116)), + new ECBlocks(28, new ECB(19, 47), + new ECB(10, 48)), + new ECBlocks(30, new ECB(15, 24), + new ECB(25, 25)), + new ECBlocks(30, new ECB(23, 15), + new ECB(25, 16))), + new Version(31, new int[]{6, 30, 56, 82, 108, 134}, + new ECBlocks(30, new ECB(13, 115), + new ECB(3, 116)), + new ECBlocks(28, new ECB(2, 46), + new ECB(29, 47)), + new ECBlocks(30, new ECB(42, 24), + new ECB(1, 25)), + new ECBlocks(30, new ECB(23, 15), + new ECB(28, 16))), + new Version(32, new int[]{6, 34, 60, 86, 112, 138}, + new ECBlocks(30, new ECB(17, 115)), + new ECBlocks(28, new ECB(10, 46), + new ECB(23, 47)), + new ECBlocks(30, new ECB(10, 24), + new ECB(35, 25)), + new ECBlocks(30, new ECB(19, 15), + new ECB(35, 16))), + new Version(33, new int[]{6, 30, 58, 86, 114, 142}, + new ECBlocks(30, new ECB(17, 115), + new ECB(1, 116)), + new ECBlocks(28, new ECB(14, 46), + new ECB(21, 47)), + new ECBlocks(30, new ECB(29, 24), + new ECB(19, 25)), + new ECBlocks(30, new ECB(11, 15), + new ECB(46, 16))), + new Version(34, new int[]{6, 34, 62, 90, 118, 146}, + new ECBlocks(30, new ECB(13, 115), + new ECB(6, 116)), + new ECBlocks(28, new ECB(14, 46), + new ECB(23, 47)), + new ECBlocks(30, new ECB(44, 24), + new ECB(7, 25)), + new ECBlocks(30, new ECB(59, 16), + new ECB(1, 17))), + new Version(35, new int[]{6, 30, 54, 78, 102, 126, 150}, + new ECBlocks(30, new ECB(12, 121), + new ECB(7, 122)), + new ECBlocks(28, new ECB(12, 47), + new ECB(26, 48)), + new ECBlocks(30, new ECB(39, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(22, 15), + new ECB(41, 16))), + new Version(36, new int[]{6, 24, 50, 76, 102, 128, 154}, + new ECBlocks(30, new ECB(6, 121), + new ECB(14, 122)), + new ECBlocks(28, new ECB(6, 47), + new ECB(34, 48)), + new ECBlocks(30, new ECB(46, 24), + new ECB(10, 25)), + new ECBlocks(30, new ECB(2, 15), + new ECB(64, 16))), + new Version(37, new int[]{6, 28, 54, 80, 106, 132, 158}, + new ECBlocks(30, new ECB(17, 122), + new ECB(4, 123)), + new ECBlocks(28, new ECB(29, 46), + new ECB(14, 47)), + new ECBlocks(30, new ECB(49, 24), + new ECB(10, 25)), + new ECBlocks(30, new ECB(24, 15), + new ECB(46, 16))), + new Version(38, new int[]{6, 32, 58, 84, 110, 136, 162}, + new ECBlocks(30, new ECB(4, 122), + new ECB(18, 123)), + new ECBlocks(28, new ECB(13, 46), + new ECB(32, 47)), + new ECBlocks(30, new ECB(48, 24), + new ECB(14, 25)), + new ECBlocks(30, new ECB(42, 15), + new ECB(32, 16))), + new Version(39, new int[]{6, 26, 54, 82, 110, 138, 166}, + new ECBlocks(30, new ECB(20, 117), + new ECB(4, 118)), + new ECBlocks(28, new ECB(40, 47), + new ECB(7, 48)), + new ECBlocks(30, new ECB(43, 24), + new ECB(22, 25)), + new ECBlocks(30, new ECB(10, 15), + new ECB(67, 16))), + new Version(40, new int[]{6, 30, 58, 86, 114, 142, 170}, + new ECBlocks(30, new ECB(19, 118), + new ECB(6, 119)), + new ECBlocks(28, new ECB(18, 47), + new ECB(31, 48)), + new ECBlocks(30, new ECB(34, 24), + new ECB(34, 25)), + new ECBlocks(30, new ECB(20, 15), + new ECB(61, 16))) + }; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPattern.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPattern.java new file mode 100644 index 0000000..96d9194 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPattern.java @@ -0,0 +1,59 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +import com.google.zxing.ResultPoint; + +/** + *

Encapsulates an alignment pattern, which are the smaller square patterns found in + * all but the simplest QR Codes.

+ * + * @author Sean Owen + */ +public final class AlignmentPattern extends ResultPoint { + + private final float estimatedModuleSize; + + AlignmentPattern(float posX, float posY, float estimatedModuleSize) { + super(posX, posY); + this.estimatedModuleSize = estimatedModuleSize; + } + + /** + *

Determines if this alignment pattern "about equals" an alignment pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.

+ */ + boolean aboutEquals(float moduleSize, float i, float j) { + if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) { + float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize); + return moduleSizeDiff <= 1.0f || moduleSizeDiff <= estimatedModuleSize; + } + return false; + } + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing an average of the two. + */ + AlignmentPattern combineEstimate(float i, float j, float newModuleSize) { + float combinedX = (getX() + j) / 2.0f; + float combinedY = (getY() + i) / 2.0f; + float combinedModuleSize = (estimatedModuleSize + newModuleSize) / 2.0f; + return new AlignmentPattern(combinedX, combinedY, combinedModuleSize); + } + +} \ No newline at end of file diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java new file mode 100644 index 0000000..c4f9aa0 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/AlignmentPatternFinder.java @@ -0,0 +1,277 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; + +import java.util.ArrayList; +import java.util.List; + +/** + *

This class attempts to find alignment patterns in a QR Code. Alignment patterns look like finder + * patterns but are smaller and appear at regular intervals throughout the image.

+ * + *

At the moment this only looks for the bottom-right alignment pattern.

+ * + *

This is mostly a simplified copy of {@link FinderPatternFinder}. It is copied, + * pasted and stripped down here for maximum performance but does unfortunately duplicate + * some code.

+ * + *

This class is thread-safe but not reentrant. Each thread must allocate its own object.

+ * + * @author Sean Owen + */ +final class AlignmentPatternFinder { + + private final BitMatrix image; + private final List possibleCenters; + private final int startX; + private final int startY; + private final int width; + private final int height; + private final float moduleSize; + private final int[] crossCheckStateCount; + private final ResultPointCallback resultPointCallback; + + /** + *

Creates a finder that will look in a portion of the whole image.

+ * + * @param image image to search + * @param startX left column from which to start searching + * @param startY top row from which to start searching + * @param width width of region to search + * @param height height of region to search + * @param moduleSize estimated module size so far + */ + AlignmentPatternFinder(BitMatrix image, + int startX, + int startY, + int width, + int height, + float moduleSize, + ResultPointCallback resultPointCallback) { + this.image = image; + this.possibleCenters = new ArrayList<>(5); + this.startX = startX; + this.startY = startY; + this.width = width; + this.height = height; + this.moduleSize = moduleSize; + this.crossCheckStateCount = new int[3]; + this.resultPointCallback = resultPointCallback; + } + + /** + *

This method attempts to find the bottom-right alignment pattern in the image. It is a bit messy since + * it's pretty performance-critical and so is written to be fast foremost.

+ * + * @return {@link AlignmentPattern} if found + * @throws NotFoundException if not found + */ + AlignmentPattern find() throws NotFoundException { + int startX = this.startX; + int height = this.height; + int maxJ = startX + width; + int middleI = startY + (height / 2); + // We are looking for black/white/black modules in 1:1:1 ratio; + // this tracks the number of black/white/black modules seen so far + int[] stateCount = new int[3]; + for (int iGen = 0; iGen < height; iGen++) { + // Search from middle outwards + int i = middleI + ((iGen & 0x01) == 0 ? (iGen + 1) / 2 : -((iGen + 1) / 2)); + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + int j = startX; + // Burn off leading white pixels before anything else; if we start in the middle of + // a white run, it doesn't make sense to count its length, since we don't know if the + // white run continued to the left of the start point + while (j < maxJ && !image.get(j, i)) { + j++; + } + int currentState = 0; + while (j < maxJ) { + if (image.get(j, i)) { + // Black pixel + if (currentState == 1) { // Counting black pixels + stateCount[1]++; + } else { // Counting white pixels + if (currentState == 2) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, j); + if (confirmed != null) { + return confirmed; + } + } + stateCount[0] = stateCount[2]; + stateCount[1] = 1; + stateCount[2] = 0; + currentState = 1; + } else { + stateCount[++currentState]++; + } + } + } else { // White pixel + if (currentState == 1) { // Counting black pixels + currentState++; + } + stateCount[currentState]++; + } + j++; + } + if (foundPatternCross(stateCount)) { + AlignmentPattern confirmed = handlePossibleCenter(stateCount, i, maxJ); + if (confirmed != null) { + return confirmed; + } + } + + } + + // Hmm, nothing we saw was observed and confirmed twice. If we had + // any guess at all, return it. + if (!possibleCenters.isEmpty()) { + return possibleCenters.get(0); + } + + throw NotFoundException.getNotFoundInstance(); + } + + /** + * Given a count of black/white/black pixels just seen and an end position, + * figures the location of the center of this black/white/black run. + */ + private static float centerFromEnd(int[] stateCount, int end) { + return (end - stateCount[2]) - stateCount[1] / 2.0f; + } + + /** + * @param stateCount count of black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/1 ratios + * used by alignment patterns to be considered a match + */ + private boolean foundPatternCross(int[] stateCount) { + float moduleSize = this.moduleSize; + float maxVariance = moduleSize / 2.0f; + for (int i = 0; i < 3; i++) { + if (Math.abs(moduleSize - stateCount[i]) >= maxVariance) { + return false; + } + } + return true; + } + + /** + *

After a horizontal scan finds a potential alignment pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * alignment pattern to see if the same proportion is detected.

+ * + * @param startI row where an alignment pattern was detected + * @param centerJ center of the section that appears to cross an alignment pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of alignment pattern, or {@link Float#NaN} if not found + */ + private float crossCheckVertical(int startI, int centerJ, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxI = image.getHeight(); + int[] stateCount = crossCheckStateCount; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + + // Start counting up from center + int i = startI; + while (i >= 0 && image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i >= 0 && !image.get(centerJ, i) && stateCount[0] <= maxCount) { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i++; + } + if (i == maxI || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i < maxI && !image.get(centerJ, i) && stateCount[2] <= maxCount) { + stateCount[2]++; + i++; + } + if (stateCount[2] > maxCount) { + return Float.NaN; + } + + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN; + } + + /** + *

This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will see if this pattern had been + * found on a previous horizontal scan. If so, we consider it confirmed and conclude we have + * found the alignment pattern.

+ * + * @param stateCount reading state module counts from horizontal scan + * @param i row where alignment pattern may be found + * @param j end of possible alignment pattern in row + * @return {@link AlignmentPattern} if we have found the same pattern twice, or null if not + */ + private AlignmentPattern handlePossibleCenter(int[] stateCount, int i, int j) { + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2]; + float centerJ = centerFromEnd(stateCount, j); + float centerI = crossCheckVertical(i, (int) centerJ, 2 * stateCount[1], stateCountTotal); + if (!Float.isNaN(centerI)) { + float estimatedModuleSize = (stateCount[0] + stateCount[1] + stateCount[2]) / 3.0f; + for (AlignmentPattern center : possibleCenters) { + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) { + return center.combineEstimate(centerI, centerJ, estimatedModuleSize); + } + } + // Hadn't found this before; save it + AlignmentPattern point = new AlignmentPattern(centerJ, centerI, estimatedModuleSize); + possibleCenters.add(point); + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(point); + } + } + return null; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/Detector.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/Detector.java new file mode 100644 index 0000000..9648883 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/Detector.java @@ -0,0 +1,405 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.FormatException; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; +import com.google.zxing.common.DetectorResult; +import com.google.zxing.common.GridSampler; +import com.google.zxing.common.PerspectiveTransform; +import com.google.zxing.common.detector.MathUtils; +import com.google.zxing.qrcode.decoder.Version; + +import java.util.Map; + +/** + *

Encapsulates logic that can detect a QR Code in an image, even if the QR Code + * is rotated or skewed, or partially obscured.

+ * + * @author Sean Owen + */ +public class Detector { + + private final BitMatrix image; + private ResultPointCallback resultPointCallback; + + public Detector(BitMatrix image) { + this.image = image; + } + + protected final BitMatrix getImage() { + return image; + } + + protected final ResultPointCallback getResultPointCallback() { + return resultPointCallback; + } + + /** + *

Detects a QR Code in an image.

+ * + * @return {@link DetectorResult} encapsulating results of detecting a QR Code + * @throws NotFoundException if QR Code cannot be found + * @throws FormatException if a QR Code cannot be decoded + */ + public DetectorResult detect() throws NotFoundException, FormatException { + return detect(null); + } + + /** + *

Detects a QR Code in an image.

+ * + * @param hints optional hints to detector + * @return {@link DetectorResult} encapsulating results of detecting a QR Code + * @throws NotFoundException if QR Code cannot be found + * @throws FormatException if a QR Code cannot be decoded + */ + public final DetectorResult detect(Map hints) throws NotFoundException, FormatException { + + resultPointCallback = hints == null ? null : + (ResultPointCallback) hints.get(DecodeHintType.NEED_RESULT_POINT_CALLBACK); + + FinderPatternFinder finder = new FinderPatternFinder(image, resultPointCallback); + FinderPatternInfo info = finder.find(hints); + + return processFinderPatternInfo(info); + } + + protected final DetectorResult processFinderPatternInfo(FinderPatternInfo info) + throws NotFoundException, FormatException { + + FinderPattern topLeft = info.getTopLeft(); + FinderPattern topRight = info.getTopRight(); + FinderPattern bottomLeft = info.getBottomLeft(); + + float moduleSize = calculateModuleSize(topLeft, topRight, bottomLeft); + if (moduleSize < 1.0f) { + throw NotFoundException.getNotFoundInstance(); + } + int dimension = computeDimension(topLeft, topRight, bottomLeft, moduleSize); + Version provisionalVersion = Version.getProvisionalVersionForDimension(dimension); + int modulesBetweenFPCenters = provisionalVersion.getDimensionForVersion() - 7; + + AlignmentPattern alignmentPattern = null; + // Anything above version 1 has an alignment pattern + if (provisionalVersion.getAlignmentPatternCenters().length > 0) { + + // Guess where a "bottom right" finder pattern would have been + float bottomRightX = topRight.getX() - topLeft.getX() + bottomLeft.getX(); + float bottomRightY = topRight.getY() - topLeft.getY() + bottomLeft.getY(); + + // Estimate that alignment pattern is closer by 3 modules + // from "bottom right" to known top left location + float correctionToTopLeft = 1.0f - 3.0f / modulesBetweenFPCenters; + int estAlignmentX = (int) (topLeft.getX() + correctionToTopLeft * (bottomRightX - topLeft.getX())); + int estAlignmentY = (int) (topLeft.getY() + correctionToTopLeft * (bottomRightY - topLeft.getY())); + + // Kind of arbitrary -- expand search radius before giving up + for (int i = 4; i <= 16; i <<= 1) { + try { + alignmentPattern = findAlignmentInRegion(moduleSize, + estAlignmentX, + estAlignmentY, + i); + break; + } catch (NotFoundException re) { + // try next round + } + } + // If we didn't find alignment pattern... well try anyway without it + } + + PerspectiveTransform transform = + createTransform(topLeft, topRight, bottomLeft, alignmentPattern, dimension); + + BitMatrix bits = sampleGrid(image, transform, dimension); + + ResultPoint[] points; + if (alignmentPattern == null) { + points = new ResultPoint[]{bottomLeft, topLeft, topRight}; + } else { + points = new ResultPoint[]{bottomLeft, topLeft, topRight, alignmentPattern}; + } + return new DetectorResult(bits, points); + } + + private static PerspectiveTransform createTransform(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft, + ResultPoint alignmentPattern, + int dimension) { + float dimMinusThree = dimension - 3.5f; + float bottomRightX; + float bottomRightY; + float sourceBottomRightX; + float sourceBottomRightY; + if (alignmentPattern != null) { + bottomRightX = alignmentPattern.getX(); + bottomRightY = alignmentPattern.getY(); + sourceBottomRightX = dimMinusThree - 3.0f; + sourceBottomRightY = sourceBottomRightX; + } else { + // Don't have an alignment pattern, just make up the bottom-right point + bottomRightX = (topRight.getX() - topLeft.getX()) + bottomLeft.getX(); + bottomRightY = (topRight.getY() - topLeft.getY()) + bottomLeft.getY(); + sourceBottomRightX = dimMinusThree; + sourceBottomRightY = dimMinusThree; + } + + return PerspectiveTransform.quadrilateralToQuadrilateral( + 3.5f, + 3.5f, + dimMinusThree, + 3.5f, + sourceBottomRightX, + sourceBottomRightY, + 3.5f, + dimMinusThree, + topLeft.getX(), + topLeft.getY(), + topRight.getX(), + topRight.getY(), + bottomRightX, + bottomRightY, + bottomLeft.getX(), + bottomLeft.getY()); + } + + private static BitMatrix sampleGrid(BitMatrix image, + PerspectiveTransform transform, + int dimension) throws NotFoundException { + + GridSampler sampler = GridSampler.getInstance(); + return sampler.sampleGrid(image, dimension, dimension, transform); + } + + /** + *

Computes the dimension (number of modules on a size) of the QR Code based on the position + * of the finder patterns and estimated module size.

+ */ + private static int computeDimension(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft, + float moduleSize) throws NotFoundException { + int tltrCentersDimension = MathUtils.round(ResultPoint.distance(topLeft, topRight) / moduleSize); + int tlblCentersDimension = MathUtils.round(ResultPoint.distance(topLeft, bottomLeft) / moduleSize); + int dimension = ((tltrCentersDimension + tlblCentersDimension) / 2) + 7; + switch (dimension & 0x03) { // mod 4 + case 0: + dimension++; + break; + // 1? do nothing + case 2: + dimension--; + break; + case 3: + throw NotFoundException.getNotFoundInstance(); + } + return dimension; + } + + /** + *

Computes an average estimated module size based on estimated derived from the positions + * of the three finder patterns.

+ * + * @param topLeft detected top-left finder pattern center + * @param topRight detected top-right finder pattern center + * @param bottomLeft detected bottom-left finder pattern center + * @return estimated module size + */ + protected final float calculateModuleSize(ResultPoint topLeft, + ResultPoint topRight, + ResultPoint bottomLeft) { + // Take the average + return (calculateModuleSizeOneWay(topLeft, topRight) + + calculateModuleSizeOneWay(topLeft, bottomLeft)) / 2.0f; + } + + /** + *

Estimates module size based on two finder patterns -- it uses + * {@link #sizeOfBlackWhiteBlackRunBothWays(int, int, int, int)} to figure the + * width of each, measuring along the axis between their centers.

+ */ + private float calculateModuleSizeOneWay(ResultPoint pattern, ResultPoint otherPattern) { + float moduleSizeEst1 = sizeOfBlackWhiteBlackRunBothWays((int) pattern.getX(), + (int) pattern.getY(), + (int) otherPattern.getX(), + (int) otherPattern.getY()); + float moduleSizeEst2 = sizeOfBlackWhiteBlackRunBothWays((int) otherPattern.getX(), + (int) otherPattern.getY(), + (int) pattern.getX(), + (int) pattern.getY()); + if (Float.isNaN(moduleSizeEst1)) { + return moduleSizeEst2 / 7.0f; + } + if (Float.isNaN(moduleSizeEst2)) { + return moduleSizeEst1 / 7.0f; + } + // Average them, and divide by 7 since we've counted the width of 3 black modules, + // and 1 white and 1 black module on either side. Ergo, divide sum by 14. + return (moduleSizeEst1 + moduleSizeEst2) / 14.0f; + } + + /** + * See {@link #sizeOfBlackWhiteBlackRun(int, int, int, int)}; computes the total width of + * a finder pattern by looking for a black-white-black run from the center in the direction + * of another point (another finder pattern center), and in the opposite direction too. + */ + private float sizeOfBlackWhiteBlackRunBothWays(int fromX, int fromY, int toX, int toY) { + + float result = sizeOfBlackWhiteBlackRun(fromX, fromY, toX, toY); + + // Now count other way -- don't run off image though of course + float scale = 1.0f; + int otherToX = fromX - (toX - fromX); + if (otherToX < 0) { + scale = fromX / (float) (fromX - otherToX); + otherToX = 0; + } else if (otherToX >= image.getWidth()) { + scale = (image.getWidth() - 1 - fromX) / (float) (otherToX - fromX); + otherToX = image.getWidth() - 1; + } + int otherToY = (int) (fromY - (toY - fromY) * scale); + + scale = 1.0f; + if (otherToY < 0) { + scale = fromY / (float) (fromY - otherToY); + otherToY = 0; + } else if (otherToY >= image.getHeight()) { + scale = (image.getHeight() - 1 - fromY) / (float) (otherToY - fromY); + otherToY = image.getHeight() - 1; + } + otherToX = (int) (fromX + (otherToX - fromX) * scale); + + result += sizeOfBlackWhiteBlackRun(fromX, fromY, otherToX, otherToY); + + // Middle pixel is double-counted this way; subtract 1 + return result - 1.0f; + } + + /** + *

This method traces a line from a point in the image, in the direction towards another point. + * It begins in a black region, and keeps going until it finds white, then black, then white again. + * It reports the distance from the start to this point.

+ * + *

This is used when figuring out how wide a finder pattern is, when the finder pattern + * may be skewed or rotated.

+ */ + private float sizeOfBlackWhiteBlackRun(int fromX, int fromY, int toX, int toY) { + // Mild variant of Bresenham's algorithm; + // see http://en.wikipedia.org/wiki/Bresenham's_line_algorithm + boolean steep = Math.abs(toY - fromY) > Math.abs(toX - fromX); + if (steep) { + int temp = fromX; + fromX = fromY; + fromY = temp; + temp = toX; + toX = toY; + toY = temp; + } + + int dx = Math.abs(toX - fromX); + int dy = Math.abs(toY - fromY); + int error = -dx / 2; + int xstep = fromX < toX ? 1 : -1; + int ystep = fromY < toY ? 1 : -1; + + // In black pixels, looking for white, first or second time. + int state = 0; + // Loop up until x == toX, but not beyond + int xLimit = toX + xstep; + for (int x = fromX, y = fromY; x != xLimit; x += xstep) { + int realX = steep ? y : x; + int realY = steep ? x : y; + + // Does current pixel mean we have moved white to black or vice versa? + // Scanning black in state 0,2 and white in state 1, so if we find the wrong + // color, advance to next state or end if we are in state 2 already + if ((state == 1) == image.get(realX, realY)) { + if (state == 2) { + return MathUtils.distance(x, y, fromX, fromY); + } + state++; + } + + error += dy; + if (error > 0) { + if (y == toY) { + break; + } + y += ystep; + error -= dx; + } + } + // Found black-white-black; give the benefit of the doubt that the next pixel outside the image + // is "white" so this last point at (toX+xStep,toY) is the right ending. This is really a + // small approximation; (toX+xStep,toY+yStep) might be really correct. Ignore this. + if (state == 2) { + return MathUtils.distance(toX + xstep, toY, fromX, fromY); + } + // else we didn't find even black-white-black; no estimate is really possible + return Float.NaN; + } + + /** + *

Attempts to locate an alignment pattern in a limited region of the image, which is + * guessed to contain it. This method uses {@link AlignmentPattern}.

+ * + * @param overallEstModuleSize estimated module size so far + * @param estAlignmentX x coordinate of center of area probably containing alignment pattern + * @param estAlignmentY y coordinate of above + * @param allowanceFactor number of pixels in all directions to search from the center + * @return {@link AlignmentPattern} if found, or null otherwise + * @throws NotFoundException if an unexpected error occurs during detection + */ + protected final AlignmentPattern findAlignmentInRegion(float overallEstModuleSize, + int estAlignmentX, + int estAlignmentY, + float allowanceFactor) + throws NotFoundException { + // Look for an alignment pattern (3 modules in size) around where it + // should be + int allowance = (int) (allowanceFactor * overallEstModuleSize); + int alignmentAreaLeftX = Math.max(0, estAlignmentX - allowance); + int alignmentAreaRightX = Math.min(image.getWidth() - 1, estAlignmentX + allowance); + if (alignmentAreaRightX - alignmentAreaLeftX < overallEstModuleSize * 3) { + throw NotFoundException.getNotFoundInstance(); + } + + int alignmentAreaTopY = Math.max(0, estAlignmentY - allowance); + int alignmentAreaBottomY = Math.min(image.getHeight() - 1, estAlignmentY + allowance); + if (alignmentAreaBottomY - alignmentAreaTopY < overallEstModuleSize * 3) { + throw NotFoundException.getNotFoundInstance(); + } + + AlignmentPatternFinder alignmentFinder = + new AlignmentPatternFinder( + image, + alignmentAreaLeftX, + alignmentAreaTopY, + alignmentAreaRightX - alignmentAreaLeftX, + alignmentAreaBottomY - alignmentAreaTopY, + overallEstModuleSize, + resultPointCallback); + return alignmentFinder.find(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPattern.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPattern.java new file mode 100644 index 0000000..a64e7c2 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPattern.java @@ -0,0 +1,82 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +import com.google.zxing.ResultPoint; + +/** + *

Encapsulates a finder pattern, which are the three square patterns found in + * the corners of QR Codes. It also encapsulates a count of similar finder patterns, + * as a convenience to the finder's bookkeeping.

+ * + * @author Sean Owen + */ +public final class FinderPattern extends ResultPoint { + + private final float estimatedModuleSize; + private final int count; + + FinderPattern(float posX, float posY, float estimatedModuleSize) { + this(posX, posY, estimatedModuleSize, 1); + } + + private FinderPattern(float posX, float posY, float estimatedModuleSize, int count) { + super(posX, posY); + this.estimatedModuleSize = estimatedModuleSize; + this.count = count; + } + + public float getEstimatedModuleSize() { + return estimatedModuleSize; + } + + int getCount() { + return count; + } + + /* + void incrementCount() { + this.count++; + } + */ + + /** + *

Determines if this finder pattern "about equals" a finder pattern at the stated + * position and size -- meaning, it is at nearly the same center with nearly the same size.

+ */ + boolean aboutEquals(float moduleSize, float i, float j) { + if (Math.abs(i - getY()) <= moduleSize && Math.abs(j - getX()) <= moduleSize) { + float moduleSizeDiff = Math.abs(moduleSize - estimatedModuleSize); + return moduleSizeDiff <= 1.0f || moduleSizeDiff <= estimatedModuleSize; + } + return false; + } + + /** + * Combines this object's current estimate of a finder pattern position and module size + * with a new estimate. It returns a new {@code FinderPattern} containing a weighted average + * based on count. + */ + FinderPattern combineEstimate(float i, float j, float newModuleSize) { + int combinedCount = count + 1; + float combinedX = (count * getX() + j) / combinedCount; + float combinedY = (count * getY() + i) / combinedCount; + float combinedModuleSize = (count * estimatedModuleSize + newModuleSize) / combinedCount; + return new FinderPattern(combinedX, combinedY, combinedModuleSize, combinedCount); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternFinder.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternFinder.java new file mode 100644 index 0000000..61fe3c1 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternFinder.java @@ -0,0 +1,680 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +import com.google.zxing.DecodeHintType; +import com.google.zxing.NotFoundException; +import com.google.zxing.ResultPoint; +import com.google.zxing.ResultPointCallback; +import com.google.zxing.common.BitMatrix; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +/** + *

This class attempts to find finder patterns in a QR Code. Finder patterns are the square + * markers at three corners of a QR Code.

+ * + *

This class is thread-safe but not reentrant. Each thread must allocate its own object. + * + * @author Sean Owen + */ +public class FinderPatternFinder { + + private static final int CENTER_QUORUM = 2; + protected static final int MIN_SKIP = 3; // 1 pixel/module times 3 modules/center + protected static final int MAX_MODULES = 57; // support up to version 10 for mobile clients + + private final BitMatrix image; + private final List possibleCenters; + private boolean hasSkipped; + private final int[] crossCheckStateCount; + private final ResultPointCallback resultPointCallback; + + /** + *

Creates a finder that will search the image for three finder patterns.

+ * + * @param image image to search + */ + public FinderPatternFinder(BitMatrix image) { + this(image, null); + } + + public FinderPatternFinder(BitMatrix image, ResultPointCallback resultPointCallback) { + this.image = image; + this.possibleCenters = new ArrayList<>(); + this.crossCheckStateCount = new int[5]; + this.resultPointCallback = resultPointCallback; + } + + protected final BitMatrix getImage() { + return image; + } + + protected final List getPossibleCenters() { + return possibleCenters; + } + + final FinderPatternInfo find(Map hints) throws NotFoundException { + boolean tryHarder = hints != null && hints.containsKey(DecodeHintType.TRY_HARDER); + boolean pureBarcode = hints != null && hints.containsKey(DecodeHintType.PURE_BARCODE); + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + // We are looking for black/white/black/white/black modules in + // 1:1:3:1:1 ratio; this tracks the number of such modules seen so far + + // Let's assume that the maximum version QR Code we support takes up 1/4 the height of the + // image, and then account for the center being 3 modules in size. This gives the smallest + // number of pixels the center could be, so skip this often. When trying harder, look for all + // QR versions regardless of how dense they are. + int iSkip = (3 * maxI) / (4 * MAX_MODULES); + if (iSkip < MIN_SKIP || tryHarder) { + iSkip = MIN_SKIP; + } + + boolean done = false; + int[] stateCount = new int[5]; + for (int i = iSkip - 1; i < maxI && !done; i += iSkip) { + // Get a row of black/white values + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + int currentState = 0; + for (int j = 0; j < maxJ; j++) { + if (image.get(j, i)) { + // Black pixel + if ((currentState & 1) == 1) { // Counting white pixels + currentState++; + } + stateCount[currentState]++; + } else { // White pixel + if ((currentState & 1) == 0) { // Counting black pixels + if (currentState == 4) { // A winner? + if (foundPatternCross(stateCount)) { // Yes + boolean confirmed = handlePossibleCenter(stateCount, i, j, pureBarcode); + if (confirmed) { + // Start examining every other line. Checking each line turned out to be too + // expensive and didn't improve performance. + iSkip = 2; + if (hasSkipped) { + done = haveMultiplyConfirmedCenters(); + } else { + int rowSkip = findRowSkip(); + if (rowSkip > stateCount[2]) { + // Skip rows between row of lower confirmed center + // and top of presumed third confirmed center + // but back up a bit to get a full chance of detecting + // it, entire width of center of finder pattern + + // Skip by rowSkip, but back off by stateCount[2] (size of last center + // of pattern we saw) to be conservative, and also back off by iSkip which + // is about to be re-added + i += rowSkip - stateCount[2] - iSkip; + j = maxJ - 1; + } + } + } else { + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + continue; + } + // Clear state to start looking again + currentState = 0; + stateCount[0] = 0; + stateCount[1] = 0; + stateCount[2] = 0; + stateCount[3] = 0; + stateCount[4] = 0; + } else { // No, shift counts back by two + stateCount[0] = stateCount[2]; + stateCount[1] = stateCount[3]; + stateCount[2] = stateCount[4]; + stateCount[3] = 1; + stateCount[4] = 0; + currentState = 3; + } + } else { + stateCount[++currentState]++; + } + } else { // Counting white pixels + stateCount[currentState]++; + } + } + } + if (foundPatternCross(stateCount)) { + boolean confirmed = handlePossibleCenter(stateCount, i, maxJ, pureBarcode); + if (confirmed) { + iSkip = stateCount[0]; + if (hasSkipped) { + // Found a third one + done = haveMultiplyConfirmedCenters(); + } + } + } + } + + FinderPattern[] patternInfo = selectBestPatterns(); + ResultPoint.orderBestPatterns(patternInfo); + + return new FinderPatternInfo(patternInfo); + } + + /** + * Given a count of black/white/black/white/black pixels just seen and an end position, + * figures the location of the center of this run. + */ + private static float centerFromEnd(int[] stateCount, int end) { + return (end - stateCount[4] - stateCount[3]) - stateCount[2] / 2.0f; + } + + /** + * @param stateCount count of black/white/black/white/black pixels just read + * @return true iff the proportions of the counts is close enough to the 1/1/3/1/1 ratios + * used by finder patterns to be considered a match + */ + protected static boolean foundPatternCross(int[] stateCount) { + int totalModuleSize = 0; + for (int i = 0; i < 5; i++) { + int count = stateCount[i]; + if (count == 0) { + return false; + } + totalModuleSize += count; + } + if (totalModuleSize < 7) { + return false; + } + float moduleSize = totalModuleSize / 7.0f; + float maxVariance = moduleSize / 2.0f; + // Allow less than 50% variance from 1-1-3-1-1 proportions + return + Math.abs(moduleSize - stateCount[0]) < maxVariance && + Math.abs(moduleSize - stateCount[1]) < maxVariance && + Math.abs(3.0f * moduleSize - stateCount[2]) < 3 * maxVariance && + Math.abs(moduleSize - stateCount[3]) < maxVariance && + Math.abs(moduleSize - stateCount[4]) < maxVariance; + } + + private int[] getCrossCheckStateCount() { + crossCheckStateCount[0] = 0; + crossCheckStateCount[1] = 0; + crossCheckStateCount[2] = 0; + crossCheckStateCount[3] = 0; + crossCheckStateCount[4] = 0; + return crossCheckStateCount; + } + + /** + * After a vertical and horizontal scan finds a potential finder pattern, this method + * "cross-cross-cross-checks" by scanning down diagonally through the center of the possible + * finder pattern to see if the same proportion is detected. + * + * @param startI row where a finder pattern was detected + * @param centerJ center of the section that appears to cross a finder pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @param originalStateCountTotal The original state count total. + * @return true if proportions are withing expected limits + */ + private boolean crossCheckDiagonal(int startI, int centerJ, int maxCount, int originalStateCountTotal) { + int[] stateCount = getCrossCheckStateCount(); + + // Start counting up, left from center finding black center mass + int i = 0; + while (startI >= i && centerJ >= i && image.get(centerJ - i, startI - i)) { + stateCount[2]++; + i++; + } + + if (startI < i || centerJ < i) { + return false; + } + + // Continue up, left finding white space + while (startI >= i && centerJ >= i && !image.get(centerJ - i, startI - i) && + stateCount[1] <= maxCount) { + stateCount[1]++; + i++; + } + + // If already too many modules in this state or ran off the edge: + if (startI < i || centerJ < i || stateCount[1] > maxCount) { + return false; + } + + // Continue up, left finding black border + while (startI >= i && centerJ >= i && image.get(centerJ - i, startI - i) && + stateCount[0] <= maxCount) { + stateCount[0]++; + i++; + } + if (stateCount[0] > maxCount) { + return false; + } + + int maxI = image.getHeight(); + int maxJ = image.getWidth(); + + // Now also count down, right from center + i = 1; + while (startI + i < maxI && centerJ + i < maxJ && image.get(centerJ + i, startI + i)) { + stateCount[2]++; + i++; + } + + // Ran off the edge? + if (startI + i >= maxI || centerJ + i >= maxJ) { + return false; + } + + while (startI + i < maxI && centerJ + i < maxJ && !image.get(centerJ + i, startI + i) && + stateCount[3] < maxCount) { + stateCount[3]++; + i++; + } + + if (startI + i >= maxI || centerJ + i >= maxJ || stateCount[3] >= maxCount) { + return false; + } + + while (startI + i < maxI && centerJ + i < maxJ && image.get(centerJ + i, startI + i) && + stateCount[4] < maxCount) { + stateCount[4]++; + i++; + } + + if (stateCount[4] >= maxCount) { + return false; + } + + // If we found a finder-pattern-like section, but its size is more than 100% different than + // the original, assume it's a false positive + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + stateCount[4]; + return + Math.abs(stateCountTotal - originalStateCountTotal) < 2 * originalStateCountTotal && + foundPatternCross(stateCount); + } + + /** + *

After a horizontal scan finds a potential finder pattern, this method + * "cross-checks" by scanning down vertically through the center of the possible + * finder pattern to see if the same proportion is detected.

+ * + * @param startI row where a finder pattern was detected + * @param centerJ center of the section that appears to cross a finder pattern + * @param maxCount maximum reasonable number of modules that should be + * observed in any reading state, based on the results of the horizontal scan + * @return vertical center of finder pattern, or {@link Float#NaN} if not found + */ + private float crossCheckVertical(int startI, int centerJ, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxI = image.getHeight(); + int[] stateCount = getCrossCheckStateCount(); + + // Start counting up from center + int i = startI; + while (i >= 0 && image.get(centerJ, i)) { + stateCount[2]++; + i--; + } + if (i < 0) { + return Float.NaN; + } + while (i >= 0 && !image.get(centerJ, i) && stateCount[1] <= maxCount) { + stateCount[1]++; + i--; + } + // If already too many modules in this state or ran off the edge: + if (i < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (i >= 0 && image.get(centerJ, i) && stateCount[0] <= maxCount) { + stateCount[0]++; + i--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + // Now also count down from center + i = startI + 1; + while (i < maxI && image.get(centerJ, i)) { + stateCount[2]++; + i++; + } + if (i == maxI) { + return Float.NaN; + } + while (i < maxI && !image.get(centerJ, i) && stateCount[3] < maxCount) { + stateCount[3]++; + i++; + } + if (i == maxI || stateCount[3] >= maxCount) { + return Float.NaN; + } + while (i < maxI && image.get(centerJ, i) && stateCount[4] < maxCount) { + stateCount[4]++; + i++; + } + if (stateCount[4] >= maxCount) { + return Float.NaN; + } + + // If we found a finder-pattern-like section, but its size is more than 40% different than + // the original, assume it's a false positive + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= 2 * originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, i) : Float.NaN; + } + + /** + *

Like {@link #crossCheckVertical(int, int, int, int)}, and in fact is basically identical, + * except it reads horizontally instead of vertically. This is used to cross-cross + * check a vertical cross check and locate the real center of the alignment pattern.

+ */ + private float crossCheckHorizontal(int startJ, int centerI, int maxCount, + int originalStateCountTotal) { + BitMatrix image = this.image; + + int maxJ = image.getWidth(); + int[] stateCount = getCrossCheckStateCount(); + + int j = startJ; + while (j >= 0 && image.get(j, centerI)) { + stateCount[2]++; + j--; + } + if (j < 0) { + return Float.NaN; + } + while (j >= 0 && !image.get(j, centerI) && stateCount[1] <= maxCount) { + stateCount[1]++; + j--; + } + if (j < 0 || stateCount[1] > maxCount) { + return Float.NaN; + } + while (j >= 0 && image.get(j, centerI) && stateCount[0] <= maxCount) { + stateCount[0]++; + j--; + } + if (stateCount[0] > maxCount) { + return Float.NaN; + } + + j = startJ + 1; + while (j < maxJ && image.get(j, centerI)) { + stateCount[2]++; + j++; + } + if (j == maxJ) { + return Float.NaN; + } + while (j < maxJ && !image.get(j, centerI) && stateCount[3] < maxCount) { + stateCount[3]++; + j++; + } + if (j == maxJ || stateCount[3] >= maxCount) { + return Float.NaN; + } + while (j < maxJ && image.get(j, centerI) && stateCount[4] < maxCount) { + stateCount[4]++; + j++; + } + if (stateCount[4] >= maxCount) { + return Float.NaN; + } + + // If we found a finder-pattern-like section, but its size is significantly different than + // the original, assume it's a false positive + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + if (5 * Math.abs(stateCountTotal - originalStateCountTotal) >= originalStateCountTotal) { + return Float.NaN; + } + + return foundPatternCross(stateCount) ? centerFromEnd(stateCount, j) : Float.NaN; + } + + /** + *

This is called when a horizontal scan finds a possible alignment pattern. It will + * cross check with a vertical scan, and if successful, will, ah, cross-cross-check + * with another horizontal scan. This is needed primarily to locate the real horizontal + * center of the pattern in cases of extreme skew. + * And then we cross-cross-cross check with another diagonal scan.

+ * + *

If that succeeds the finder pattern location is added to a list that tracks + * the number of times each location has been nearly-matched as a finder pattern. + * Each additional find is more evidence that the location is in fact a finder + * pattern center + * + * @param stateCount reading state module counts from horizontal scan + * @param i row where finder pattern may be found + * @param j end of possible finder pattern in row + * @param pureBarcode true if in "pure barcode" mode + * @return true if a finder pattern candidate was found this time + */ + protected final boolean handlePossibleCenter(int[] stateCount, int i, int j, boolean pureBarcode) { + int stateCountTotal = stateCount[0] + stateCount[1] + stateCount[2] + stateCount[3] + + stateCount[4]; + float centerJ = centerFromEnd(stateCount, j); + float centerI = crossCheckVertical(i, (int) centerJ, stateCount[2], stateCountTotal); + if (!Float.isNaN(centerI)) { + // Re-cross check + centerJ = crossCheckHorizontal((int) centerJ, (int) centerI, stateCount[2], stateCountTotal); + if (!Float.isNaN(centerJ) && + (!pureBarcode || crossCheckDiagonal((int) centerI, (int) centerJ, stateCount[2], stateCountTotal))) { + float estimatedModuleSize = stateCountTotal / 7.0f; + boolean found = false; + for (int index = 0; index < possibleCenters.size(); index++) { + FinderPattern center = possibleCenters.get(index); + // Look for about the same center and module size: + if (center.aboutEquals(estimatedModuleSize, centerI, centerJ)) { + possibleCenters.set(index, center.combineEstimate(centerI, centerJ, estimatedModuleSize)); + found = true; + break; + } + } + if (!found) { + FinderPattern point = new FinderPattern(centerJ, centerI, estimatedModuleSize); + possibleCenters.add(point); + if (resultPointCallback != null) { + resultPointCallback.foundPossibleResultPoint(point); + } + } + return true; + } + } + return false; + } + + /** + * @return number of rows we could safely skip during scanning, based on the first + * two finder patterns that have been located. In some cases their position will + * allow us to infer that the third pattern must lie below a certain point farther + * down in the image. + */ + private int findRowSkip() { + int max = possibleCenters.size(); + if (max <= 1) { + return 0; + } + ResultPoint firstConfirmedCenter = null; + for (FinderPattern center : possibleCenters) { + if (center.getCount() >= CENTER_QUORUM) { + if (firstConfirmedCenter == null) { + firstConfirmedCenter = center; + } else { + // We have two confirmed centers + // How far down can we skip before resuming looking for the next + // pattern? In the worst case, only the difference between the + // difference in the x / y coordinates of the two centers. + // This is the case where you find top left last. + hasSkipped = true; + return (int) (Math.abs(firstConfirmedCenter.getX() - center.getX()) - + Math.abs(firstConfirmedCenter.getY() - center.getY())) / 2; + } + } + } + return 0; + } + + /** + * @return true iff we have found at least 3 finder patterns that have been detected + * at least {@link #CENTER_QUORUM} times each, and, the estimated module size of the + * candidates is "pretty similar" + */ + private boolean haveMultiplyConfirmedCenters() { + int confirmedCount = 0; + float totalModuleSize = 0.0f; + int max = possibleCenters.size(); + for (FinderPattern pattern : possibleCenters) { + if (pattern.getCount() >= CENTER_QUORUM) { + confirmedCount++; + totalModuleSize += pattern.getEstimatedModuleSize(); + } + } + if (confirmedCount < 3) { + return false; + } + // OK, we have at least 3 confirmed centers, but, it's possible that one is a "false positive" + // and that we need to keep looking. We detect this by asking if the estimated module sizes + // vary too much. We arbitrarily say that when the total deviation from average exceeds + // 5% of the total module size estimates, it's too much. + float average = totalModuleSize / max; + float totalDeviation = 0.0f; + for (FinderPattern pattern : possibleCenters) { + totalDeviation += Math.abs(pattern.getEstimatedModuleSize() - average); + } + return totalDeviation <= 0.05f * totalModuleSize; + } + + /** + * @return the 3 best {@link FinderPattern}s from our list of candidates. The "best" are + * those that have been detected at least {@link #CENTER_QUORUM} times, and whose module + * size differs from the average among those patterns the least + * @throws NotFoundException if 3 such finder patterns do not exist + */ + private FinderPattern[] selectBestPatterns() throws NotFoundException { + + int startSize = possibleCenters.size(); + if (startSize < 3) { + // Couldn't find enough finder patterns + throw NotFoundException.getNotFoundInstance(); + } + + // Filter outlier possibilities whose module size is too different + if (startSize > 3) { + // But we can only afford to do so if we have at least 4 possibilities to choose from + float totalModuleSize = 0.0f; + float square = 0.0f; + for (FinderPattern center : possibleCenters) { + float size = center.getEstimatedModuleSize(); + totalModuleSize += size; + square += size * size; + } + float average = totalModuleSize / startSize; + float stdDev = (float) Math.sqrt(square / startSize - average * average); + + Collections.sort(possibleCenters, new FurthestFromAverageComparator(average)); + + float limit = Math.max(0.2f * average, stdDev); + + for (int i = 0; i < possibleCenters.size() && possibleCenters.size() > 3; i++) { + FinderPattern pattern = possibleCenters.get(i); + if (Math.abs(pattern.getEstimatedModuleSize() - average) > limit) { + possibleCenters.remove(i); + i--; + } + } + } + + if (possibleCenters.size() > 3) { + // Throw away all but those first size candidate points we found. + + float totalModuleSize = 0.0f; + for (FinderPattern possibleCenter : possibleCenters) { + totalModuleSize += possibleCenter.getEstimatedModuleSize(); + } + + float average = totalModuleSize / possibleCenters.size(); + + Collections.sort(possibleCenters, new CenterComparator(average)); + + possibleCenters.subList(3, possibleCenters.size()).clear(); + } + + return new FinderPattern[]{ + possibleCenters.get(0), + possibleCenters.get(1), + possibleCenters.get(2) + }; + } + + /** + *

Orders by furthest from average

+ */ + private static final class FurthestFromAverageComparator implements Comparator, Serializable { + private final float average; + private FurthestFromAverageComparator(float f) { + average = f; + } + @Override + public int compare(FinderPattern center1, FinderPattern center2) { + float dA = Math.abs(center2.getEstimatedModuleSize() - average); + float dB = Math.abs(center1.getEstimatedModuleSize() - average); + return dA < dB ? -1 : dA > dB ? 1 : 0; + } + } + + /** + *

Orders by {@link FinderPattern#getCount()}, descending.

+ */ + private static final class CenterComparator implements Comparator, Serializable { + private final float average; + private CenterComparator(float f) { + average = f; + } + @Override + public int compare(FinderPattern center1, FinderPattern center2) { + if (center2.getCount() == center1.getCount()) { + float dA = Math.abs(center2.getEstimatedModuleSize() - average); + float dB = Math.abs(center1.getEstimatedModuleSize() - average); + return dA < dB ? 1 : dA > dB ? -1 : 0; + } else { + return center2.getCount() - center1.getCount(); + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternInfo.java b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternInfo.java new file mode 100644 index 0000000..3c34010 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/detector/FinderPatternInfo.java @@ -0,0 +1,49 @@ +/* + * Copyright 2007 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.detector; + +/** + *

Encapsulates information about finder patterns in an image, including the location of + * the three finder patterns, and their estimated module size.

+ * + * @author Sean Owen + */ +public final class FinderPatternInfo { + + private final FinderPattern bottomLeft; + private final FinderPattern topLeft; + private final FinderPattern topRight; + + public FinderPatternInfo(FinderPattern[] patternCenters) { + this.bottomLeft = patternCenters[0]; + this.topLeft = patternCenters[1]; + this.topRight = patternCenters[2]; + } + + public FinderPattern getBottomLeft() { + return bottomLeft; + } + + public FinderPattern getTopLeft() { + return topLeft; + } + + public FinderPattern getTopRight() { + return topRight; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/BlockPair.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/BlockPair.java new file mode 100644 index 0000000..5714d9c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/BlockPair.java @@ -0,0 +1,37 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +final class BlockPair { + + private final byte[] dataBytes; + private final byte[] errorCorrectionBytes; + + BlockPair(byte[] data, byte[] errorCorrection) { + dataBytes = data; + errorCorrectionBytes = errorCorrection; + } + + public byte[] getDataBytes() { + return dataBytes; + } + + public byte[] getErrorCorrectionBytes() { + return errorCorrectionBytes; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/ByteMatrix.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/ByteMatrix.java new file mode 100644 index 0000000..35f344c --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/ByteMatrix.java @@ -0,0 +1,99 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +import java.util.Arrays; + +/** + * JAVAPORT: The original code was a 2D array of ints, but since it only ever gets assigned + * -1, 0, and 1, I'm going to use less memory and go with bytes. + * + * @author dswitkin@google.com (Daniel Switkin) + */ +public final class ByteMatrix { + + private final byte[][] bytes; + private final int width; + private final int height; + + public ByteMatrix(int width, int height) { + bytes = new byte[height][width]; + this.width = width; + this.height = height; + } + + public int getHeight() { + return height; + } + + public int getWidth() { + return width; + } + + public byte get(int x, int y) { + return bytes[y][x]; + } + + /** + * @return an internal representation as bytes, in row-major order. array[y][x] represents point (x,y) + */ + public byte[][] getArray() { + return bytes; + } + + public void set(int x, int y, byte value) { + bytes[y][x] = value; + } + + public void set(int x, int y, int value) { + bytes[y][x] = (byte) value; + } + + public void set(int x, int y, boolean value) { + bytes[y][x] = (byte) (value ? 1 : 0); + } + + public void clear(byte value) { + for (byte[] aByte : bytes) { + Arrays.fill(aByte, value); + } + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(2 * width * height + 2); + for (int y = 0; y < height; ++y) { + byte[] bytesY = bytes[y]; + for (int x = 0; x < width; ++x) { + switch (bytesY[x]) { + case 0: + result.append(" 0"); + break; + case 1: + result.append(" 1"); + break; + default: + result.append(" "); + break; + } + } + result.append('\n'); + } + return result.toString(); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java new file mode 100644 index 0000000..9532857 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/Encoder.java @@ -0,0 +1,610 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +import com.google.zxing.EncodeHintType; +import com.google.zxing.WriterException; +import com.google.zxing.common.BitArray; +import com.google.zxing.common.CharacterSetECI; +import com.google.zxing.common.reedsolomon.GenericGF; +import com.google.zxing.common.reedsolomon.ReedSolomonEncoder; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.decoder.Mode; +import com.google.zxing.qrcode.decoder.Version; + +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Map; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class Encoder { + + // The original table is defined in the table 5 of JISX0510:2004 (p.19). + private static final int[] ALPHANUMERIC_TABLE = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x00-0x0f + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, // 0x10-0x1f + 36, -1, -1, -1, 37, 38, -1, -1, -1, -1, 39, 40, -1, 41, 42, 43, // 0x20-0x2f + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 44, -1, -1, -1, -1, -1, // 0x30-0x3f + -1, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, // 0x40-0x4f + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, -1, -1, -1, -1, -1, // 0x50-0x5f + }; + + static final String DEFAULT_BYTE_MODE_ENCODING = "ISO-8859-1"; + + private Encoder() { + } + + // The mask penalty calculation is complicated. See Table 21 of JISX0510:2004 (p.45) for details. + // Basically it applies four rules and summate all penalties. + private static int calculateMaskPenalty(ByteMatrix matrix) { + return MaskUtil.applyMaskPenaltyRule1(matrix) + + MaskUtil.applyMaskPenaltyRule2(matrix) + + MaskUtil.applyMaskPenaltyRule3(matrix) + + MaskUtil.applyMaskPenaltyRule4(matrix); + } + + /** + * @param content text to encode + * @param ecLevel error correction level to use + * @return {@link QRCode} representing the encoded QR code + * @throws WriterException if encoding can't succeed, because of for example invalid content + * or configuration + */ + public static QRCode encode(String content, ErrorCorrectionLevel ecLevel) throws WriterException { + return encode(content, ecLevel, null); + } + + public static QRCode encode(String content, + ErrorCorrectionLevel ecLevel, + Map hints) throws WriterException { + + // Determine what character encoding has been specified by the caller, if any + String encoding = DEFAULT_BYTE_MODE_ENCODING; + boolean hasEncodingHint = hints != null && hints.containsKey(EncodeHintType.CHARACTER_SET); + if (hasEncodingHint) { + encoding = hints.get(EncodeHintType.CHARACTER_SET).toString(); + } + + // Pick an encoding mode appropriate for the content. Note that this will not attempt to use + // multiple modes / segments even if that were more efficient. Twould be nice. + Mode mode = chooseMode(content, encoding); + + // This will store the header information, like mode and + // length, as well as "header" segments like an ECI segment. + BitArray headerBits = new BitArray(); + + // Append ECI segment if applicable + if (mode == Mode.BYTE && (hasEncodingHint || !DEFAULT_BYTE_MODE_ENCODING.equals(encoding))) { + CharacterSetECI eci = CharacterSetECI.getCharacterSetECIByName(encoding); + if (eci != null) { + appendECI(eci, headerBits); + } + } + + // (With ECI in place,) Write the mode marker + appendModeInfo(mode, headerBits); + + // Collect data within the main segment, separately, to count its size if needed. Don't add it to + // main payload yet. + BitArray dataBits = new BitArray(); + appendBytes(content, mode, dataBits, encoding); + + Version version; + if (hints != null && hints.containsKey(EncodeHintType.QR_VERSION)) { + int versionNumber = Integer.parseInt(hints.get(EncodeHintType.QR_VERSION).toString()); + version = Version.getVersionForNumber(versionNumber); + int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, version); + if (!willFit(bitsNeeded, version, ecLevel)) { + throw new WriterException("Data too big for requested version"); + } + } else { + version = recommendVersion(ecLevel, mode, headerBits, dataBits); + } + + BitArray headerAndDataBits = new BitArray(); + headerAndDataBits.appendBitArray(headerBits); + // Find "length" of main segment and write it + int numLetters = mode == Mode.BYTE ? dataBits.getSizeInBytes() : content.length(); + appendLengthInfo(numLetters, version, mode, headerAndDataBits); + // Put data together into the overall payload + headerAndDataBits.appendBitArray(dataBits); + + Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); + int numDataBytes = version.getTotalCodewords() - ecBlocks.getTotalECCodewords(); + + // Terminate the bits properly. + terminateBits(numDataBytes, headerAndDataBits); + + // Interleave data bits with error correction code. + BitArray finalBits = interleaveWithECBytes(headerAndDataBits, + version.getTotalCodewords(), + numDataBytes, + ecBlocks.getNumBlocks()); + + QRCode qrCode = new QRCode(); + + qrCode.setECLevel(ecLevel); + qrCode.setMode(mode); + qrCode.setVersion(version); + + // Choose the mask pattern and set to "qrCode". + int dimension = version.getDimensionForVersion(); + ByteMatrix matrix = new ByteMatrix(dimension, dimension); + int maskPattern = chooseMaskPattern(finalBits, ecLevel, version, matrix); + qrCode.setMaskPattern(maskPattern); + + // Build the matrix and set it to "qrCode". + MatrixUtil.buildMatrix(finalBits, ecLevel, version, maskPattern, matrix); + qrCode.setMatrix(matrix); + + return qrCode; + } + + /** + * Decides the smallest version of QR code that will contain all of the provided data. + * + * @throws WriterException if the data cannot fit in any version + */ + private static Version recommendVersion(ErrorCorrectionLevel ecLevel, + Mode mode, + BitArray headerBits, + BitArray dataBits) throws WriterException { + // Hard part: need to know version to know how many bits length takes. But need to know how many + // bits it takes to know version. First we take a guess at version by assuming version will be + // the minimum, 1: + int provisionalBitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, Version.getVersionForNumber(1)); + Version provisionalVersion = chooseVersion(provisionalBitsNeeded, ecLevel); + + // Use that guess to calculate the right version. I am still not sure this works in 100% of cases. + int bitsNeeded = calculateBitsNeeded(mode, headerBits, dataBits, provisionalVersion); + return chooseVersion(bitsNeeded, ecLevel); + } + + private static int calculateBitsNeeded(Mode mode, + BitArray headerBits, + BitArray dataBits, + Version version) { + return headerBits.getSize() + mode.getCharacterCountBits(version) + dataBits.getSize(); + } + + /** + * @return the code point of the table used in alphanumeric mode or + * -1 if there is no corresponding code in the table. + */ + static int getAlphanumericCode(int code) { + if (code < ALPHANUMERIC_TABLE.length) { + return ALPHANUMERIC_TABLE[code]; + } + return -1; + } + + public static Mode chooseMode(String content) { + return chooseMode(content, null); + } + + /** + * Choose the best mode by examining the content. Note that 'encoding' is used as a hint; + * if it is Shift_JIS, and the input is only double-byte Kanji, then we return {@link Mode#KANJI}. + */ + private static Mode chooseMode(String content, String encoding) { + if ("Shift_JIS".equals(encoding) && isOnlyDoubleByteKanji(content)) { + // Choose Kanji mode if all input are double-byte characters + return Mode.KANJI; + } + boolean hasNumeric = false; + boolean hasAlphanumeric = false; + for (int i = 0; i < content.length(); ++i) { + char c = content.charAt(i); + if (c >= '0' && c <= '9') { + hasNumeric = true; + } else if (getAlphanumericCode(c) != -1) { + hasAlphanumeric = true; + } else { + return Mode.BYTE; + } + } + if (hasAlphanumeric) { + return Mode.ALPHANUMERIC; + } + if (hasNumeric) { + return Mode.NUMERIC; + } + return Mode.BYTE; + } + + private static boolean isOnlyDoubleByteKanji(String content) { + byte[] bytes; + try { + bytes = content.getBytes("Shift_JIS"); + } catch (UnsupportedEncodingException ignored) { + return false; + } + int length = bytes.length; + if (length % 2 != 0) { + return false; + } + for (int i = 0; i < length; i += 2) { + int byte1 = bytes[i] & 0xFF; + if ((byte1 < 0x81 || byte1 > 0x9F) && (byte1 < 0xE0 || byte1 > 0xEB)) { + return false; + } + } + return true; + } + + private static int chooseMaskPattern(BitArray bits, + ErrorCorrectionLevel ecLevel, + Version version, + ByteMatrix matrix) throws WriterException { + + int minPenalty = Integer.MAX_VALUE; // Lower penalty is better. + int bestMaskPattern = -1; + // We try all mask patterns to choose the best one. + for (int maskPattern = 0; maskPattern < QRCode.NUM_MASK_PATTERNS; maskPattern++) { + MatrixUtil.buildMatrix(bits, ecLevel, version, maskPattern, matrix); + int penalty = calculateMaskPenalty(matrix); + if (penalty < minPenalty) { + minPenalty = penalty; + bestMaskPattern = maskPattern; + } + } + return bestMaskPattern; + } + + private static Version chooseVersion(int numInputBits, ErrorCorrectionLevel ecLevel) throws WriterException { + for (int versionNum = 1; versionNum <= 40; versionNum++) { + Version version = Version.getVersionForNumber(versionNum); + if (willFit(numInputBits, version, ecLevel)) { + return version; + } + } + throw new WriterException("Data too big"); + } + + /** + * @return true if the number of input bits will fit in a code with the specified version and + * error correction level. + */ + private static boolean willFit(int numInputBits, Version version, ErrorCorrectionLevel ecLevel) { + // In the following comments, we use numbers of Version 7-H. + // numBytes = 196 + int numBytes = version.getTotalCodewords(); + // getNumECBytes = 130 + Version.ECBlocks ecBlocks = version.getECBlocksForLevel(ecLevel); + int numEcBytes = ecBlocks.getTotalECCodewords(); + // getNumDataBytes = 196 - 130 = 66 + int numDataBytes = numBytes - numEcBytes; + int totalInputBytes = (numInputBits + 7) / 8; + return numDataBytes >= totalInputBytes; + } + + /** + * Terminate bits as described in 8.4.8 and 8.4.9 of JISX0510:2004 (p.24). + */ + static void terminateBits(int numDataBytes, BitArray bits) throws WriterException { + int capacity = numDataBytes * 8; + if (bits.getSize() > capacity) { + throw new WriterException("data bits cannot fit in the QR Code" + bits.getSize() + " > " + + capacity); + } + for (int i = 0; i < 4 && bits.getSize() < capacity; ++i) { + bits.appendBit(false); + } + // Append termination bits. See 8.4.8 of JISX0510:2004 (p.24) for details. + // If the last byte isn't 8-bit aligned, we'll add padding bits. + int numBitsInLastByte = bits.getSize() & 0x07; + if (numBitsInLastByte > 0) { + for (int i = numBitsInLastByte; i < 8; i++) { + bits.appendBit(false); + } + } + // If we have more space, we'll fill the space with padding patterns defined in 8.4.9 (p.24). + int numPaddingBytes = numDataBytes - bits.getSizeInBytes(); + for (int i = 0; i < numPaddingBytes; ++i) { + bits.appendBits((i & 0x01) == 0 ? 0xEC : 0x11, 8); + } + if (bits.getSize() != capacity) { + throw new WriterException("Bits size does not equal capacity"); + } + } + + /** + * Get number of data bytes and number of error correction bytes for block id "blockID". Store + * the result in "numDataBytesInBlock", and "numECBytesInBlock". See table 12 in 8.5.1 of + * JISX0510:2004 (p.30) + */ + static void getNumDataBytesAndNumECBytesForBlockID(int numTotalBytes, + int numDataBytes, + int numRSBlocks, + int blockID, + int[] numDataBytesInBlock, + int[] numECBytesInBlock) throws WriterException { + if (blockID >= numRSBlocks) { + throw new WriterException("Block ID too large"); + } + // numRsBlocksInGroup2 = 196 % 5 = 1 + int numRsBlocksInGroup2 = numTotalBytes % numRSBlocks; + // numRsBlocksInGroup1 = 5 - 1 = 4 + int numRsBlocksInGroup1 = numRSBlocks - numRsBlocksInGroup2; + // numTotalBytesInGroup1 = 196 / 5 = 39 + int numTotalBytesInGroup1 = numTotalBytes / numRSBlocks; + // numTotalBytesInGroup2 = 39 + 1 = 40 + int numTotalBytesInGroup2 = numTotalBytesInGroup1 + 1; + // numDataBytesInGroup1 = 66 / 5 = 13 + int numDataBytesInGroup1 = numDataBytes / numRSBlocks; + // numDataBytesInGroup2 = 13 + 1 = 14 + int numDataBytesInGroup2 = numDataBytesInGroup1 + 1; + // numEcBytesInGroup1 = 39 - 13 = 26 + int numEcBytesInGroup1 = numTotalBytesInGroup1 - numDataBytesInGroup1; + // numEcBytesInGroup2 = 40 - 14 = 26 + int numEcBytesInGroup2 = numTotalBytesInGroup2 - numDataBytesInGroup2; + // Sanity checks. + // 26 = 26 + if (numEcBytesInGroup1 != numEcBytesInGroup2) { + throw new WriterException("EC bytes mismatch"); + } + // 5 = 4 + 1. + if (numRSBlocks != numRsBlocksInGroup1 + numRsBlocksInGroup2) { + throw new WriterException("RS blocks mismatch"); + } + // 196 = (13 + 26) * 4 + (14 + 26) * 1 + if (numTotalBytes != + ((numDataBytesInGroup1 + numEcBytesInGroup1) * + numRsBlocksInGroup1) + + ((numDataBytesInGroup2 + numEcBytesInGroup2) * + numRsBlocksInGroup2)) { + throw new WriterException("Total bytes mismatch"); + } + + if (blockID < numRsBlocksInGroup1) { + numDataBytesInBlock[0] = numDataBytesInGroup1; + numECBytesInBlock[0] = numEcBytesInGroup1; + } else { + numDataBytesInBlock[0] = numDataBytesInGroup2; + numECBytesInBlock[0] = numEcBytesInGroup2; + } + } + + /** + * Interleave "bits" with corresponding error correction bytes. On success, store the result in + * "result". The interleave rule is complicated. See 8.6 of JISX0510:2004 (p.37) for details. + */ + static BitArray interleaveWithECBytes(BitArray bits, + int numTotalBytes, + int numDataBytes, + int numRSBlocks) throws WriterException { + + // "bits" must have "getNumDataBytes" bytes of data. + if (bits.getSizeInBytes() != numDataBytes) { + throw new WriterException("Number of bits and data bytes does not match"); + } + + // Step 1. Divide data bytes into blocks and generate error correction bytes for them. We'll + // store the divided data bytes blocks and error correction bytes blocks into "blocks". + int dataBytesOffset = 0; + int maxNumDataBytes = 0; + int maxNumEcBytes = 0; + + // Since, we know the number of reedsolmon blocks, we can initialize the vector with the number. + Collection blocks = new ArrayList<>(numRSBlocks); + + for (int i = 0; i < numRSBlocks; ++i) { + int[] numDataBytesInBlock = new int[1]; + int[] numEcBytesInBlock = new int[1]; + getNumDataBytesAndNumECBytesForBlockID( + numTotalBytes, numDataBytes, numRSBlocks, i, + numDataBytesInBlock, numEcBytesInBlock); + + int size = numDataBytesInBlock[0]; + byte[] dataBytes = new byte[size]; + bits.toBytes(8 * dataBytesOffset, dataBytes, 0, size); + byte[] ecBytes = generateECBytes(dataBytes, numEcBytesInBlock[0]); + blocks.add(new BlockPair(dataBytes, ecBytes)); + + maxNumDataBytes = Math.max(maxNumDataBytes, size); + maxNumEcBytes = Math.max(maxNumEcBytes, ecBytes.length); + dataBytesOffset += numDataBytesInBlock[0]; + } + if (numDataBytes != dataBytesOffset) { + throw new WriterException("Data bytes does not match offset"); + } + + BitArray result = new BitArray(); + + // First, place data blocks. + for (int i = 0; i < maxNumDataBytes; ++i) { + for (BlockPair block : blocks) { + byte[] dataBytes = block.getDataBytes(); + if (i < dataBytes.length) { + result.appendBits(dataBytes[i], 8); + } + } + } + // Then, place error correction blocks. + for (int i = 0; i < maxNumEcBytes; ++i) { + for (BlockPair block : blocks) { + byte[] ecBytes = block.getErrorCorrectionBytes(); + if (i < ecBytes.length) { + result.appendBits(ecBytes[i], 8); + } + } + } + if (numTotalBytes != result.getSizeInBytes()) { // Should be same. + throw new WriterException("Interleaving error: " + numTotalBytes + " and " + + result.getSizeInBytes() + " differ."); + } + + return result; + } + + static byte[] generateECBytes(byte[] dataBytes, int numEcBytesInBlock) { + int numDataBytes = dataBytes.length; + int[] toEncode = new int[numDataBytes + numEcBytesInBlock]; + for (int i = 0; i < numDataBytes; i++) { + toEncode[i] = dataBytes[i] & 0xFF; + } + new ReedSolomonEncoder(GenericGF.QR_CODE_FIELD_256).encode(toEncode, numEcBytesInBlock); + + byte[] ecBytes = new byte[numEcBytesInBlock]; + for (int i = 0; i < numEcBytesInBlock; i++) { + ecBytes[i] = (byte) toEncode[numDataBytes + i]; + } + return ecBytes; + } + + /** + * Append mode info. On success, store the result in "bits". + */ + static void appendModeInfo(Mode mode, BitArray bits) { + bits.appendBits(mode.getBits(), 4); + } + + + /** + * Append length info. On success, store the result in "bits". + */ + static void appendLengthInfo(int numLetters, Version version, Mode mode, BitArray bits) throws WriterException { + int numBits = mode.getCharacterCountBits(version); + if (numLetters >= (1 << numBits)) { + throw new WriterException(numLetters + " is bigger than " + ((1 << numBits) - 1)); + } + bits.appendBits(numLetters, numBits); + } + + /** + * Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". + */ + static void appendBytes(String content, + Mode mode, + BitArray bits, + String encoding) throws WriterException { + switch (mode) { + case NUMERIC: + appendNumericBytes(content, bits); + break; + case ALPHANUMERIC: + appendAlphanumericBytes(content, bits); + break; + case BYTE: + append8BitBytes(content, bits, encoding); + break; + case KANJI: + appendKanjiBytes(content, bits); + break; + default: + throw new WriterException("Invalid mode: " + mode); + } + } + + static void appendNumericBytes(CharSequence content, BitArray bits) { + int length = content.length(); + int i = 0; + while (i < length) { + int num1 = content.charAt(i) - '0'; + if (i + 2 < length) { + // Encode three numeric letters in ten bits. + int num2 = content.charAt(i + 1) - '0'; + int num3 = content.charAt(i + 2) - '0'; + bits.appendBits(num1 * 100 + num2 * 10 + num3, 10); + i += 3; + } else if (i + 1 < length) { + // Encode two numeric letters in seven bits. + int num2 = content.charAt(i + 1) - '0'; + bits.appendBits(num1 * 10 + num2, 7); + i += 2; + } else { + // Encode one numeric letter in four bits. + bits.appendBits(num1, 4); + i++; + } + } + } + + static void appendAlphanumericBytes(CharSequence content, BitArray bits) throws WriterException { + int length = content.length(); + int i = 0; + while (i < length) { + int code1 = getAlphanumericCode(content.charAt(i)); + if (code1 == -1) { + throw new WriterException(); + } + if (i + 1 < length) { + int code2 = getAlphanumericCode(content.charAt(i + 1)); + if (code2 == -1) { + throw new WriterException(); + } + // Encode two alphanumeric letters in 11 bits. + bits.appendBits(code1 * 45 + code2, 11); + i += 2; + } else { + // Encode one alphanumeric letter in six bits. + bits.appendBits(code1, 6); + i++; + } + } + } + + static void append8BitBytes(String content, BitArray bits, String encoding) + throws WriterException { + byte[] bytes; + try { + bytes = content.getBytes(encoding); + } catch (UnsupportedEncodingException uee) { + throw new WriterException(uee); + } + for (byte b : bytes) { + bits.appendBits(b, 8); + } + } + + static void appendKanjiBytes(String content, BitArray bits) throws WriterException { + byte[] bytes; + try { + bytes = content.getBytes("Shift_JIS"); + } catch (UnsupportedEncodingException uee) { + throw new WriterException(uee); + } + int length = bytes.length; + for (int i = 0; i < length; i += 2) { + int byte1 = bytes[i] & 0xFF; + int byte2 = bytes[i + 1] & 0xFF; + int code = (byte1 << 8) | byte2; + int subtracted = -1; + if (code >= 0x8140 && code <= 0x9ffc) { + subtracted = code - 0x8140; + } else if (code >= 0xe040 && code <= 0xebbf) { + subtracted = code - 0xc140; + } + if (subtracted == -1) { + throw new WriterException("Invalid byte sequence"); + } + int encoded = ((subtracted >> 8) * 0xc0) + (subtracted & 0xff); + bits.appendBits(encoded, 13); + } + } + + private static void appendECI(CharacterSetECI eci, BitArray bits) { + bits.appendBits(Mode.ECI.getBits(), 4); + // This is correct for values up to 127, which is all we need now. + bits.appendBits(eci.getValue(), 8); + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java new file mode 100644 index 0000000..11d7804 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MaskUtil.java @@ -0,0 +1,222 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +/** + * @author Satoru Takabayashi + * @author Daniel Switkin + * @author Sean Owen + */ +final class MaskUtil { + + // Penalty weights from section 6.8.2.1 + private static final int N1 = 3; + private static final int N2 = 3; + private static final int N3 = 40; + private static final int N4 = 10; + + private MaskUtil() { + // do nothing + } + + /** + * Apply mask penalty rule 1 and return the penalty. Find repetitive cells with the same color and + * give penalty to them. Example: 00000 or 11111. + */ + static int applyMaskPenaltyRule1(ByteMatrix matrix) { + return applyMaskPenaltyRule1Internal(matrix, true) + applyMaskPenaltyRule1Internal(matrix, false); + } + + /** + * Apply mask penalty rule 2 and return the penalty. Find 2x2 blocks with the same color and give + * penalty to them. This is actually equivalent to the spec's rule, which is to find MxN blocks and give a + * penalty proportional to (M-1)x(N-1), because this is the number of 2x2 blocks inside such a block. + */ + static int applyMaskPenaltyRule2(ByteMatrix matrix) { + int penalty = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height - 1; y++) { + byte[] arrayY = array[y]; + for (int x = 0; x < width - 1; x++) { + int value = arrayY[x]; + if (value == arrayY[x + 1] && value == array[y + 1][x] && value == array[y + 1][x + 1]) { + penalty++; + } + } + } + return N2 * penalty; + } + + /** + * Apply mask penalty rule 3 and return the penalty. Find consecutive runs of 1:1:3:1:1:4 + * starting with black, or 4:1:1:3:1:1 starting with white, and give penalty to them. If we + * find patterns like 000010111010000, we give penalty once. + */ + static int applyMaskPenaltyRule3(ByteMatrix matrix) { + int numPenalties = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + byte[] arrayY = array[y]; // We can at least optimize this access + if (x + 6 < width && + arrayY[x] == 1 && + arrayY[x + 1] == 0 && + arrayY[x + 2] == 1 && + arrayY[x + 3] == 1 && + arrayY[x + 4] == 1 && + arrayY[x + 5] == 0 && + arrayY[x + 6] == 1 && + (isWhiteHorizontal(arrayY, x - 4, x) || isWhiteHorizontal(arrayY, x + 7, x + 11))) { + numPenalties++; + } + if (y + 6 < height && + array[y][x] == 1 && + array[y + 1][x] == 0 && + array[y + 2][x] == 1 && + array[y + 3][x] == 1 && + array[y + 4][x] == 1 && + array[y + 5][x] == 0 && + array[y + 6][x] == 1 && + (isWhiteVertical(array, x, y - 4, y) || isWhiteVertical(array, x, y + 7, y + 11))) { + numPenalties++; + } + } + } + return numPenalties * N3; + } + + private static boolean isWhiteHorizontal(byte[] rowArray, int from, int to) { + from = Math.max(from, 0); + to = Math.min(to, rowArray.length); + for (int i = from; i < to; i++) { + if (rowArray[i] == 1) { + return false; + } + } + return true; + } + + private static boolean isWhiteVertical(byte[][] array, int col, int from, int to) { + from = Math.max(from, 0); + to = Math.min(to, array.length); + for (int i = from; i < to; i++) { + if (array[i][col] == 1) { + return false; + } + } + return true; + } + + /** + * Apply mask penalty rule 4 and return the penalty. Calculate the ratio of dark cells and give + * penalty if the ratio is far from 50%. It gives 10 penalty for 5% distance. + */ + static int applyMaskPenaltyRule4(ByteMatrix matrix) { + int numDarkCells = 0; + byte[][] array = matrix.getArray(); + int width = matrix.getWidth(); + int height = matrix.getHeight(); + for (int y = 0; y < height; y++) { + byte[] arrayY = array[y]; + for (int x = 0; x < width; x++) { + if (arrayY[x] == 1) { + numDarkCells++; + } + } + } + int numTotalCells = matrix.getHeight() * matrix.getWidth(); + int fivePercentVariances = Math.abs(numDarkCells * 2 - numTotalCells) * 10 / numTotalCells; + return fivePercentVariances * N4; + } + + /** + * Return the mask bit for "getMaskPattern" at "x" and "y". See 8.8 of JISX0510:2004 for mask + * pattern conditions. + */ + static boolean getDataMaskBit(int maskPattern, int x, int y) { + int intermediate; + int temp; + switch (maskPattern) { + case 0: + intermediate = (y + x) & 0x1; + break; + case 1: + intermediate = y & 0x1; + break; + case 2: + intermediate = x % 3; + break; + case 3: + intermediate = (y + x) % 3; + break; + case 4: + intermediate = ((y / 2) + (x / 3)) & 0x1; + break; + case 5: + temp = y * x; + intermediate = (temp & 0x1) + (temp % 3); + break; + case 6: + temp = y * x; + intermediate = ((temp & 0x1) + (temp % 3)) & 0x1; + break; + case 7: + temp = y * x; + intermediate = ((temp % 3) + ((y + x) & 0x1)) & 0x1; + break; + default: + throw new IllegalArgumentException("Invalid mask pattern: " + maskPattern); + } + return intermediate == 0; + } + + /** + * Helper function for applyMaskPenaltyRule1. We need this for doing this calculation in both + * vertical and horizontal orders respectively. + */ + private static int applyMaskPenaltyRule1Internal(ByteMatrix matrix, boolean isHorizontal) { + int penalty = 0; + int iLimit = isHorizontal ? matrix.getHeight() : matrix.getWidth(); + int jLimit = isHorizontal ? matrix.getWidth() : matrix.getHeight(); + byte[][] array = matrix.getArray(); + for (int i = 0; i < iLimit; i++) { + int numSameBitCells = 0; + int prevBit = -1; + for (int j = 0; j < jLimit; j++) { + int bit = isHorizontal ? array[i][j] : array[j][i]; + if (bit == prevBit) { + numSameBitCells++; + } else { + if (numSameBitCells >= 5) { + penalty += N1 + (numSameBitCells - 5); + } + numSameBitCells = 1; // Include the cell itself. + prevBit = bit; + } + } + if (numSameBitCells >= 5) { + penalty += N1 + (numSameBitCells - 5); + } + } + return penalty; + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MatrixUtil.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MatrixUtil.java new file mode 100644 index 0000000..561284a --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/MatrixUtil.java @@ -0,0 +1,476 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +import com.google.zxing.WriterException; +import com.google.zxing.common.BitArray; +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.decoder.Version; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +final class MatrixUtil { + + private static final int[][] POSITION_DETECTION_PATTERN = { + {1, 1, 1, 1, 1, 1, 1}, + {1, 0, 0, 0, 0, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 1, 1, 1, 0, 1}, + {1, 0, 0, 0, 0, 0, 1}, + {1, 1, 1, 1, 1, 1, 1}, + }; + + private static final int[][] POSITION_ADJUSTMENT_PATTERN = { + {1, 1, 1, 1, 1}, + {1, 0, 0, 0, 1}, + {1, 0, 1, 0, 1}, + {1, 0, 0, 0, 1}, + {1, 1, 1, 1, 1}, + }; + + // From Appendix E. Table 1, JIS0510X:2004 (p 71). The table was double-checked by komatsu. + private static final int[][] POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE = { + {-1, -1, -1, -1, -1, -1, -1}, // Version 1 + { 6, 18, -1, -1, -1, -1, -1}, // Version 2 + { 6, 22, -1, -1, -1, -1, -1}, // Version 3 + { 6, 26, -1, -1, -1, -1, -1}, // Version 4 + { 6, 30, -1, -1, -1, -1, -1}, // Version 5 + { 6, 34, -1, -1, -1, -1, -1}, // Version 6 + { 6, 22, 38, -1, -1, -1, -1}, // Version 7 + { 6, 24, 42, -1, -1, -1, -1}, // Version 8 + { 6, 26, 46, -1, -1, -1, -1}, // Version 9 + { 6, 28, 50, -1, -1, -1, -1}, // Version 10 + { 6, 30, 54, -1, -1, -1, -1}, // Version 11 + { 6, 32, 58, -1, -1, -1, -1}, // Version 12 + { 6, 34, 62, -1, -1, -1, -1}, // Version 13 + { 6, 26, 46, 66, -1, -1, -1}, // Version 14 + { 6, 26, 48, 70, -1, -1, -1}, // Version 15 + { 6, 26, 50, 74, -1, -1, -1}, // Version 16 + { 6, 30, 54, 78, -1, -1, -1}, // Version 17 + { 6, 30, 56, 82, -1, -1, -1}, // Version 18 + { 6, 30, 58, 86, -1, -1, -1}, // Version 19 + { 6, 34, 62, 90, -1, -1, -1}, // Version 20 + { 6, 28, 50, 72, 94, -1, -1}, // Version 21 + { 6, 26, 50, 74, 98, -1, -1}, // Version 22 + { 6, 30, 54, 78, 102, -1, -1}, // Version 23 + { 6, 28, 54, 80, 106, -1, -1}, // Version 24 + { 6, 32, 58, 84, 110, -1, -1}, // Version 25 + { 6, 30, 58, 86, 114, -1, -1}, // Version 26 + { 6, 34, 62, 90, 118, -1, -1}, // Version 27 + { 6, 26, 50, 74, 98, 122, -1}, // Version 28 + { 6, 30, 54, 78, 102, 126, -1}, // Version 29 + { 6, 26, 52, 78, 104, 130, -1}, // Version 30 + { 6, 30, 56, 82, 108, 134, -1}, // Version 31 + { 6, 34, 60, 86, 112, 138, -1}, // Version 32 + { 6, 30, 58, 86, 114, 142, -1}, // Version 33 + { 6, 34, 62, 90, 118, 146, -1}, // Version 34 + { 6, 30, 54, 78, 102, 126, 150}, // Version 35 + { 6, 24, 50, 76, 102, 128, 154}, // Version 36 + { 6, 28, 54, 80, 106, 132, 158}, // Version 37 + { 6, 32, 58, 84, 110, 136, 162}, // Version 38 + { 6, 26, 54, 82, 110, 138, 166}, // Version 39 + { 6, 30, 58, 86, 114, 142, 170}, // Version 40 + }; + + // Type info cells at the left top corner. + private static final int[][] TYPE_INFO_COORDINATES = { + {8, 0}, + {8, 1}, + {8, 2}, + {8, 3}, + {8, 4}, + {8, 5}, + {8, 7}, + {8, 8}, + {7, 8}, + {5, 8}, + {4, 8}, + {3, 8}, + {2, 8}, + {1, 8}, + {0, 8}, + }; + + // From Appendix D in JISX0510:2004 (p. 67) + private static final int VERSION_INFO_POLY = 0x1f25; // 1 1111 0010 0101 + + // From Appendix C in JISX0510:2004 (p.65). + private static final int TYPE_INFO_POLY = 0x537; + private static final int TYPE_INFO_MASK_PATTERN = 0x5412; + + private MatrixUtil() { + // do nothing + } + + // Set all cells to -1. -1 means that the cell is empty (not set yet). + // + // JAVAPORT: We shouldn't need to do this at all. The code should be rewritten to begin encoding + // with the ByteMatrix initialized all to zero. + static void clearMatrix(ByteMatrix matrix) { + matrix.clear((byte) -1); + } + + // Build 2D matrix of QR Code from "dataBits" with "ecLevel", "version" and "getMaskPattern". On + // success, store the result in "matrix" and return true. + static void buildMatrix(BitArray dataBits, + ErrorCorrectionLevel ecLevel, + Version version, + int maskPattern, + ByteMatrix matrix) throws WriterException { + clearMatrix(matrix); + embedBasicPatterns(version, matrix); + // Type information appear with any version. + embedTypeInfo(ecLevel, maskPattern, matrix); + // Version info appear if version >= 7. + maybeEmbedVersionInfo(version, matrix); + // Data should be embedded at end. + embedDataBits(dataBits, maskPattern, matrix); + } + + // Embed basic patterns. On success, modify the matrix and return true. + // The basic patterns are: + // - Position detection patterns + // - Timing patterns + // - Dark dot at the left bottom corner + // - Position adjustment patterns, if need be + static void embedBasicPatterns(Version version, ByteMatrix matrix) throws WriterException { + // Let's get started with embedding big squares at corners. + embedPositionDetectionPatternsAndSeparators(matrix); + // Then, embed the dark dot at the left bottom corner. + embedDarkDotAtLeftBottomCorner(matrix); + + // Position adjustment patterns appear if version >= 2. + maybeEmbedPositionAdjustmentPatterns(version, matrix); + // Timing patterns should be embedded after position adj. patterns. + embedTimingPatterns(matrix); + } + + // Embed type information. On success, modify the matrix. + static void embedTypeInfo(ErrorCorrectionLevel ecLevel, int maskPattern, ByteMatrix matrix) + throws WriterException { + BitArray typeInfoBits = new BitArray(); + makeTypeInfoBits(ecLevel, maskPattern, typeInfoBits); + + for (int i = 0; i < typeInfoBits.getSize(); ++i) { + // Place bits in LSB to MSB order. LSB (least significant bit) is the last value in + // "typeInfoBits". + boolean bit = typeInfoBits.get(typeInfoBits.getSize() - 1 - i); + + // Type info bits at the left top corner. See 8.9 of JISX0510:2004 (p.46). + int[] coordinates = TYPE_INFO_COORDINATES[i]; + int x1 = coordinates[0]; + int y1 = coordinates[1]; + matrix.set(x1, y1, bit); + + if (i < 8) { + // Right top corner. + int x2 = matrix.getWidth() - i - 1; + int y2 = 8; + matrix.set(x2, y2, bit); + } else { + // Left bottom corner. + int x2 = 8; + int y2 = matrix.getHeight() - 7 + (i - 8); + matrix.set(x2, y2, bit); + } + } + } + + // Embed version information if need be. On success, modify the matrix and return true. + // See 8.10 of JISX0510:2004 (p.47) for how to embed version information. + static void maybeEmbedVersionInfo(Version version, ByteMatrix matrix) throws WriterException { + if (version.getVersionNumber() < 7) { // Version info is necessary if version >= 7. + return; // Don't need version info. + } + BitArray versionInfoBits = new BitArray(); + makeVersionInfoBits(version, versionInfoBits); + + int bitIndex = 6 * 3 - 1; // It will decrease from 17 to 0. + for (int i = 0; i < 6; ++i) { + for (int j = 0; j < 3; ++j) { + // Place bits in LSB (least significant bit) to MSB order. + boolean bit = versionInfoBits.get(bitIndex); + bitIndex--; + // Left bottom corner. + matrix.set(i, matrix.getHeight() - 11 + j, bit); + // Right bottom corner. + matrix.set(matrix.getHeight() - 11 + j, i, bit); + } + } + } + + // Embed "dataBits" using "getMaskPattern". On success, modify the matrix and return true. + // For debugging purposes, it skips masking process if "getMaskPattern" is -1. + // See 8.7 of JISX0510:2004 (p.38) for how to embed data bits. + static void embedDataBits(BitArray dataBits, int maskPattern, ByteMatrix matrix) + throws WriterException { + int bitIndex = 0; + int direction = -1; + // Start from the right bottom cell. + int x = matrix.getWidth() - 1; + int y = matrix.getHeight() - 1; + while (x > 0) { + // Skip the vertical timing pattern. + if (x == 6) { + x -= 1; + } + while (y >= 0 && y < matrix.getHeight()) { + for (int i = 0; i < 2; ++i) { + int xx = x - i; + // Skip the cell if it's not empty. + if (!isEmpty(matrix.get(xx, y))) { + continue; + } + boolean bit; + if (bitIndex < dataBits.getSize()) { + bit = dataBits.get(bitIndex); + ++bitIndex; + } else { + // Padding bit. If there is no bit left, we'll fill the left cells with 0, as described + // in 8.4.9 of JISX0510:2004 (p. 24). + bit = false; + } + + // Skip masking if mask_pattern is -1. + if (maskPattern != -1 && MaskUtil.getDataMaskBit(maskPattern, xx, y)) { + bit = !bit; + } + matrix.set(xx, y, bit); + } + y += direction; + } + direction = -direction; // Reverse the direction. + y += direction; + x -= 2; // Move to the left. + } + // All bits should be consumed. + if (bitIndex != dataBits.getSize()) { + throw new WriterException("Not all bits consumed: " + bitIndex + '/' + dataBits.getSize()); + } + } + + // Return the position of the most significant bit set (to one) in the "value". The most + // significant bit is position 32. If there is no bit set, return 0. Examples: + // - findMSBSet(0) => 0 + // - findMSBSet(1) => 1 + // - findMSBSet(255) => 8 + static int findMSBSet(int value) { + return 32 - Integer.numberOfLeadingZeros(value); + } + + // Calculate BCH (Bose-Chaudhuri-Hocquenghem) code for "value" using polynomial "poly". The BCH + // code is used for encoding type information and version information. + // Example: Calculation of version information of 7. + // f(x) is created from 7. + // - 7 = 000111 in 6 bits + // - f(x) = x^2 + x^1 + x^0 + // g(x) is given by the standard (p. 67) + // - g(x) = x^12 + x^11 + x^10 + x^9 + x^8 + x^5 + x^2 + 1 + // Multiply f(x) by x^(18 - 6) + // - f'(x) = f(x) * x^(18 - 6) + // - f'(x) = x^14 + x^13 + x^12 + // Calculate the remainder of f'(x) / g(x) + // x^2 + // __________________________________________________ + // g(x) )x^14 + x^13 + x^12 + // x^14 + x^13 + x^12 + x^11 + x^10 + x^7 + x^4 + x^2 + // -------------------------------------------------- + // x^11 + x^10 + x^7 + x^4 + x^2 + // + // The remainder is x^11 + x^10 + x^7 + x^4 + x^2 + // Encode it in binary: 110010010100 + // The return value is 0xc94 (1100 1001 0100) + // + // Since all coefficients in the polynomials are 1 or 0, we can do the calculation by bit + // operations. We don't care if coefficients are positive or negative. + static int calculateBCHCode(int value, int poly) { + if (poly == 0) { + throw new IllegalArgumentException("0 polynomial"); + } + // If poly is "1 1111 0010 0101" (version info poly), msbSetInPoly is 13. We'll subtract 1 + // from 13 to make it 12. + int msbSetInPoly = findMSBSet(poly); + value <<= msbSetInPoly - 1; + // Do the division business using exclusive-or operations. + while (findMSBSet(value) >= msbSetInPoly) { + value ^= poly << (findMSBSet(value) - msbSetInPoly); + } + // Now the "value" is the remainder (i.e. the BCH code) + return value; + } + + // Make bit vector of type information. On success, store the result in "bits" and return true. + // Encode error correction level and mask pattern. See 8.9 of + // JISX0510:2004 (p.45) for details. + static void makeTypeInfoBits(ErrorCorrectionLevel ecLevel, int maskPattern, BitArray bits) + throws WriterException { + if (!QRCode.isValidMaskPattern(maskPattern)) { + throw new WriterException("Invalid mask pattern"); + } + int typeInfo = (ecLevel.getBits() << 3) | maskPattern; + bits.appendBits(typeInfo, 5); + + int bchCode = calculateBCHCode(typeInfo, TYPE_INFO_POLY); + bits.appendBits(bchCode, 10); + + BitArray maskBits = new BitArray(); + maskBits.appendBits(TYPE_INFO_MASK_PATTERN, 15); + bits.xor(maskBits); + + if (bits.getSize() != 15) { // Just in case. + throw new WriterException("should not happen but we got: " + bits.getSize()); + } + } + + // Make bit vector of version information. On success, store the result in "bits" and return true. + // See 8.10 of JISX0510:2004 (p.45) for details. + static void makeVersionInfoBits(Version version, BitArray bits) throws WriterException { + bits.appendBits(version.getVersionNumber(), 6); + int bchCode = calculateBCHCode(version.getVersionNumber(), VERSION_INFO_POLY); + bits.appendBits(bchCode, 12); + + if (bits.getSize() != 18) { // Just in case. + throw new WriterException("should not happen but we got: " + bits.getSize()); + } + } + + // Check if "value" is empty. + private static boolean isEmpty(int value) { + return value == -1; + } + + private static void embedTimingPatterns(ByteMatrix matrix) { + // -8 is for skipping position detection patterns (size 7), and two horizontal/vertical + // separation patterns (size 1). Thus, 8 = 7 + 1. + for (int i = 8; i < matrix.getWidth() - 8; ++i) { + int bit = (i + 1) % 2; + // Horizontal line. + if (isEmpty(matrix.get(i, 6))) { + matrix.set(i, 6, bit); + } + // Vertical line. + if (isEmpty(matrix.get(6, i))) { + matrix.set(6, i, bit); + } + } + } + + // Embed the lonely dark dot at left bottom corner. JISX0510:2004 (p.46) + private static void embedDarkDotAtLeftBottomCorner(ByteMatrix matrix) throws WriterException { + if (matrix.get(8, matrix.getHeight() - 8) == 0) { + throw new WriterException(); + } + matrix.set(8, matrix.getHeight() - 8, 1); + } + + private static void embedHorizontalSeparationPattern(int xStart, + int yStart, + ByteMatrix matrix) throws WriterException { + for (int x = 0; x < 8; ++x) { + if (!isEmpty(matrix.get(xStart + x, yStart))) { + throw new WriterException(); + } + matrix.set(xStart + x, yStart, 0); + } + } + + private static void embedVerticalSeparationPattern(int xStart, + int yStart, + ByteMatrix matrix) throws WriterException { + for (int y = 0; y < 7; ++y) { + if (!isEmpty(matrix.get(xStart, yStart + y))) { + throw new WriterException(); + } + matrix.set(xStart, yStart + y, 0); + } + } + + private static void embedPositionAdjustmentPattern(int xStart, int yStart, ByteMatrix matrix) { + for (int y = 0; y < 5; ++y) { + int[] patternY = POSITION_ADJUSTMENT_PATTERN[y]; + for (int x = 0; x < 5; ++x) { + matrix.set(xStart + x, yStart + y, patternY[x]); + } + } + } + + private static void embedPositionDetectionPattern(int xStart, int yStart, ByteMatrix matrix) { + for (int y = 0; y < 7; ++y) { + int[] patternY = POSITION_DETECTION_PATTERN[y]; + for (int x = 0; x < 7; ++x) { + matrix.set(xStart + x, yStart + y, patternY[x]); + } + } + } + + // Embed position detection patterns and surrounding vertical/horizontal separators. + private static void embedPositionDetectionPatternsAndSeparators(ByteMatrix matrix) throws WriterException { + // Embed three big squares at corners. + int pdpWidth = POSITION_DETECTION_PATTERN[0].length; + // Left top corner. + embedPositionDetectionPattern(0, 0, matrix); + // Right top corner. + embedPositionDetectionPattern(matrix.getWidth() - pdpWidth, 0, matrix); + // Left bottom corner. + embedPositionDetectionPattern(0, matrix.getWidth() - pdpWidth, matrix); + + // Embed horizontal separation patterns around the squares. + int hspWidth = 8; + // Left top corner. + embedHorizontalSeparationPattern(0, hspWidth - 1, matrix); + // Right top corner. + embedHorizontalSeparationPattern(matrix.getWidth() - hspWidth, + hspWidth - 1, matrix); + // Left bottom corner. + embedHorizontalSeparationPattern(0, matrix.getWidth() - hspWidth, matrix); + + // Embed vertical separation patterns around the squares. + int vspSize = 7; + // Left top corner. + embedVerticalSeparationPattern(vspSize, 0, matrix); + // Right top corner. + embedVerticalSeparationPattern(matrix.getHeight() - vspSize - 1, 0, matrix); + // Left bottom corner. + embedVerticalSeparationPattern(vspSize, matrix.getHeight() - vspSize, + matrix); + } + + // Embed position adjustment patterns if need be. + private static void maybeEmbedPositionAdjustmentPatterns(Version version, ByteMatrix matrix) { + if (version.getVersionNumber() < 2) { // The patterns appear if version >= 2 + return; + } + int index = version.getVersionNumber() - 1; + int[] coordinates = POSITION_ADJUSTMENT_PATTERN_COORDINATE_TABLE[index]; + for (int y : coordinates) { + if (y >= 0) { + for (int x : coordinates) { + if (x >= 0 && isEmpty(matrix.get(x, y))) { + // If the cell is unset, we embed the position adjustment pattern here. + // -2 is necessary since the x/y coordinates point to the center of the pattern, not the + // left top corner. + embedPositionAdjustmentPattern(x - 2, y - 2, matrix); + } + } + } + } + } + +} diff --git a/rubylib/src/main/java/com/google/zxing/qrcode/encoder/QRCode.java b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/QRCode.java new file mode 100644 index 0000000..b560de9 --- /dev/null +++ b/rubylib/src/main/java/com/google/zxing/qrcode/encoder/QRCode.java @@ -0,0 +1,108 @@ +/* + * Copyright 2008 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.zxing.qrcode.encoder; + +import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; +import com.google.zxing.qrcode.decoder.Mode; +import com.google.zxing.qrcode.decoder.Version; + +/** + * @author satorux@google.com (Satoru Takabayashi) - creator + * @author dswitkin@google.com (Daniel Switkin) - ported from C++ + */ +public final class QRCode { + + public static final int NUM_MASK_PATTERNS = 8; + + private Mode mode; + private ErrorCorrectionLevel ecLevel; + private Version version; + private int maskPattern; + private ByteMatrix matrix; + + public QRCode() { + maskPattern = -1; + } + + public Mode getMode() { + return mode; + } + + public ErrorCorrectionLevel getECLevel() { + return ecLevel; + } + + public Version getVersion() { + return version; + } + + public int getMaskPattern() { + return maskPattern; + } + + public ByteMatrix getMatrix() { + return matrix; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(200); + result.append("<<\n"); + result.append(" mode: "); + result.append(mode); + result.append("\n ecLevel: "); + result.append(ecLevel); + result.append("\n version: "); + result.append(version); + result.append("\n maskPattern: "); + result.append(maskPattern); + if (matrix == null) { + result.append("\n matrix: null\n"); + } else { + result.append("\n matrix:\n"); + result.append(matrix); + } + result.append(">>\n"); + return result.toString(); + } + + public void setMode(Mode value) { + mode = value; + } + + public void setECLevel(ErrorCorrectionLevel value) { + ecLevel = value; + } + + public void setVersion(Version version) { + this.version = version; + } + + public void setMaskPattern(int value) { + maskPattern = value; + } + + public void setMatrix(ByteMatrix value) { + matrix = value; + } + + // Check if "mask_pattern" is valid. + public static boolean isValidMaskPattern(int maskPattern) { + return maskPattern >= 0 && maskPattern < NUM_MASK_PATTERNS; + } + +} -- GitLab