Comparing version 0.6.11 to 0.6.14
{ | ||
"name": "quagga", | ||
"version": "0.6.11", | ||
"version": "0.6.14", | ||
"description": "An advanced barcode-scanner written in JavaScript", | ||
@@ -45,2 +45,5 @@ "main": "dist/quagga.js", | ||
"code39", | ||
"codabar", | ||
"i2of5", | ||
"upc", | ||
"getusermedia", | ||
@@ -47,0 +50,0 @@ "imageprocessing" |
quaggaJS | ||
======== | ||
- [Changelog](#changelog) (2015-06-21) | ||
- [Changelog](#changelog) (2015-07-29) | ||
@@ -10,7 +10,7 @@ ## What is QuaggaJS? | ||
time localization and decoding of various types of barcodes such as __EAN__, | ||
__CODE 128__, __CODE 39__, __EAN 8__, __UPC-A__, __UPC-C__ and __CODABAR__. | ||
The library is also capable of using `getUserMedia` to get direct access to | ||
the user's camera stream. Although the code relies on heavy image-processing | ||
even recent smartphones are capable of locating and decoding barcodes in | ||
real-time. | ||
__CODE 128__, __CODE 39__, __EAN 8__, __UPC-A__, __UPC-C__, __I2of5__ and | ||
__CODABAR__. The library is also capable of using `getUserMedia` to get direct | ||
access to the user's camera stream. Although the code relies on heavy image- | ||
processing even recent smartphones are capable of locating and decoding | ||
barcodes in real-time. | ||
@@ -84,8 +84,12 @@ Try some [examples](http://serratus.github.io/quaggaJS/examples) and check out | ||
### Quagga.init(config, callback) | ||
### <a name="quaggainit">Quagga.init(config, callback)</a> | ||
This method initializes the library for a given configuration `config` (see | ||
below) and invokes the `callback` when Quagga is ready to start. The | ||
initialization process also requests for camera access if real-time detection is | ||
configured. | ||
below) and invokes the `callback(err)` when Quagga has finished its | ||
bootstrapping phase. The initialization process also requests for camera | ||
access if real-time detection is configured. In case of an error, the `err` | ||
parameter is set and contains information about the cause. A potential cause | ||
may be the `inputStream.type` is set to `LiveStream`, but the browser does | ||
not support this API, or simply if the user denies the permission to use the | ||
camera. | ||
@@ -101,3 +105,7 @@ ```javascript | ||
} | ||
}, function() { | ||
}, function(err) { | ||
if (err) { | ||
console.log(err); | ||
return | ||
} | ||
console.log("Initialization finished. Ready to start"); | ||
@@ -277,7 +285,9 @@ Quagga.start(); | ||
Quagga.decodeSingle({ | ||
readers: ['code_128_reader'], | ||
locate: true, // try to locate the barcode in the image | ||
src: '/test/fixtures/code_128/image-001.jpg' // or 'data:image/jpg;base64,' + data | ||
decoder: { | ||
readers: ["code_128_reader"] // List of active readers | ||
}, | ||
locate: true, // try to locate the barcode in the image | ||
src: '/test/fixtures/code_128/image-001.jpg' // or 'data:image/jpg;base64,' + data | ||
}, function(result){ | ||
console.log(result); | ||
console.log(result); | ||
}); | ||
@@ -364,2 +374,18 @@ ``` | ||
### 2015-07-29 | ||
- Features | ||
- Added basic support for [ITF][i2of5_wiki] barcodes (`i2of5_reader`) | ||
### 2015-07-08 | ||
- Improvements | ||
- Parameter tweaking to reduce false-positives significantly (for the | ||
entire EAN and UPC family) | ||
- Fixing bug in parity check for UPC-E codes | ||
- Fixing bug in alignment for EAN-8 codes | ||
### 2015-07-06 | ||
- Improvements | ||
- Added `err` parameter to [Quagga.init()](#quaggainit) callback | ||
function | ||
### 2015-06-21 | ||
@@ -465,1 +491,2 @@ - Features | ||
[github_examples]: http://serratus.github.io/quaggaJS/examples | ||
[i2of5_wiki]: https://en.wikipedia.org/wiki/Interleaved_2_of_5 |
define(['camera_access'], function(CameraAccess){ | ||
var originalURL, | ||
originalUserMedia, | ||
originalMediaStreamTrack, | ||
video, | ||
stream; | ||
beforeEach(function() { | ||
@@ -14,3 +13,2 @@ var tracks = [{ | ||
originalURL = window.URL; | ||
originalUserMedia = window.getUserMedia; | ||
originalMediaStreamTrack = window.MediaStreamTrack; | ||
@@ -27,6 +25,3 @@ window.MediaStreamTrack = {}; | ||
sinon.spy(tracks[0], "stop"); | ||
navigator.getUserMedia = function(constraints, cb) { | ||
cb(stream); | ||
}; | ||
sinon.spy(navigator, "getUserMedia"); | ||
video = { | ||
@@ -53,3 +48,2 @@ src: null, | ||
afterEach(function() { | ||
navigator.getUserMedia = originalUserMedia; | ||
window.URL = originalURL; | ||
@@ -59,23 +53,75 @@ window.MediaStreamTrack = originalMediaStreamTrack; | ||
describe('request', function() { | ||
it('should request the camera', function(done) { | ||
CameraAccess.request(video, {}, function() { | ||
expect(navigator.getUserMedia.calledOnce).to.equal(true); | ||
expect(video.src).to.deep.equal(stream); | ||
done(); | ||
describe('success', function() { | ||
beforeEach(function() { | ||
sinon.stub(navigator, "getUserMedia", function(constraints, success) { | ||
success(stream); | ||
}); | ||
}); | ||
afterEach(function() { | ||
navigator.getUserMedia.restore(); | ||
}); | ||
describe('request', function () { | ||
it('should request the camera', function (done) { | ||
CameraAccess.request(video, {}, function () { | ||
expect(navigator.getUserMedia.calledOnce).to.equal(true); | ||
expect(video.src).to.deep.equal(stream); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe('release', function () { | ||
it('should release the camera', function (done) { | ||
CameraAccess.request(video, {}, function () { | ||
expect(video.src).to.deep.equal(stream); | ||
CameraAccess.release(); | ||
expect(video.src.getVideoTracks()).to.have.length(1); | ||
expect(video.src.getVideoTracks()[0].stop.calledOnce).to.equal(true); | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
describe('release', function() { | ||
it('should release the camera', function(done) { | ||
CameraAccess.request(video, {}, function() { | ||
expect(video.src).to.deep.equal(stream); | ||
CameraAccess.release(); | ||
expect(video.src.getVideoTracks()).to.have.length(1); | ||
expect(video.src.getVideoTracks()[0].stop.calledOnce).to.equal(true); | ||
done(); | ||
describe('failure', function() { | ||
describe("permission denied", function(){ | ||
before(function() { | ||
sinon.stub(navigator, "getUserMedia", function(constraints, success, failure) { | ||
failure(new Error()); | ||
}); | ||
}); | ||
after(function() { | ||
navigator.getUserMedia.restore(); | ||
}); | ||
it('should throw if getUserMedia not available', function(done) { | ||
CameraAccess.request(video, {}, function(err) { | ||
expect(err).to.be.defined; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
describe("not available", function(){ | ||
var originalGetUserMedia; | ||
before(function() { | ||
originalGetUserMedia = navigator.getUserMedia; | ||
navigator.getUserMedia = undefined; | ||
}); | ||
after(function() { | ||
navigator.getUserMedia = originalGetUserMedia; | ||
}); | ||
it('should throw if getUserMedia not available', function(done) { | ||
CameraAccess.request(video, {}, function(err) { | ||
expect(err).to.be.defined; | ||
done(); | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -29,4 +29,13 @@ | ||
var readers = config.decoder.readers.slice(), | ||
folder = baseFolder + readers[0].split('_').slice(0, -1).join('_') + "/"; | ||
format, | ||
folder; | ||
if (typeof readers[0] === 'string'){ | ||
format = readers[0]; | ||
} else { | ||
format = readers[0].format; | ||
} | ||
folder = baseFolder + format.split('_').slice(0, -1).join('_') + "/"; | ||
it('should decode ' + folder + " correctly", function(done) { | ||
@@ -75,3 +84,3 @@ async.eachSeries(testSet, function (sample, callback) { | ||
{"name": "image-001.jpg", "result": "0001285112001000040801"}, | ||
{"name": "image-002.jpg", "result": "FANAVF1461710"}, | ||
// {"name": "image-002.jpg", "result": "FANAVF1461710"}, | ||
// {"name": "image-003.jpg", "result": "673023"}, | ||
@@ -99,3 +108,2 @@ // {"name": "image-004.jpg", "result": "010210150301625334"}, | ||
{"name": "image-001.jpg", "result": "B3% $DAD$"}, | ||
/*{"name": "image-002.jpg", "result": "QUAGGAJS"},*/ | ||
{"name": "image-003.jpg", "result": "CODE39"}, | ||
@@ -129,3 +137,3 @@ {"name": "image-004.jpg", "result": "QUAGGAJS"}, | ||
{"name": "image-007.jpg", "result": "42176817"}, | ||
/*{"name": "image-008.jpg", "result": "42191605"},*/ | ||
{"name": "image-008.jpg", "result": "42191605"}, | ||
{"name": "image-009.jpg", "result": "42242215"}, | ||
@@ -151,3 +159,3 @@ {"name": "image-010.jpg", "result": "42184799"} | ||
{"name": "image-005.jpg", "result": "882428015343"}, | ||
{"name": "image-006.jpg", "result": "882428015046"}, | ||
/* {"name": "image-006.jpg", "result": "882428015046"}, */ | ||
{"name": "image-007.jpg", "result": "882428015084"}, | ||
@@ -200,3 +208,3 @@ {"name": "image-008.jpg", "result": "882428015046"}, | ||
{"name": "image-007.jpg", "result": "C$399.95A"}, | ||
/* {"name": "image-008.jpg", "result": "01264904"}, */ | ||
{"name": "image-008.jpg", "result": "A16:9/4:3/3:2D"}, | ||
{"name": "image-009.jpg", "result": "C$399.95A"}, | ||
@@ -213,3 +221,35 @@ {"name": "image-010.jpg", "result": "C$399.95A"} | ||
}); | ||
describe("I2of5 with localization", function() { | ||
var config = { | ||
inputStream: { | ||
size: 800, | ||
singleChannel: false | ||
}, | ||
locator: { | ||
patchSize: "small", | ||
halfSample: false | ||
}, | ||
numOfWorkers: 0, | ||
decoder: { | ||
readers: ["i2of5_reader"], | ||
}, | ||
locate: true, | ||
src: null | ||
}, testSet = [ | ||
{"name": "image-001.jpg", "result": "2167361334"}, | ||
{"name": "image-002.jpg", "result": "2167361334"}, | ||
{"name": "image-003.jpg", "result": "2167361334"}, | ||
{"name": "image-004.jpg", "result": "2167361334"}, | ||
{"name": "image-005.jpg", "result": "2167361334"} | ||
]; | ||
testSet.forEach(function(sample) { | ||
sample.format = "i2of5"; | ||
}); | ||
_runTestSet(testSet, config); | ||
}); | ||
}); | ||
}); |
@@ -14,3 +14,4 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
'ean_8_reader', | ||
'upc_e_reader' | ||
'upc_e_reader', | ||
'i2of5_reader' | ||
], function( | ||
@@ -26,3 +27,4 @@ Bresenham, | ||
EAN8Reader, | ||
UPCEReader) { | ||
UPCEReader, | ||
I2of5Reader) { | ||
"use strict"; | ||
@@ -38,3 +40,4 @@ | ||
upc_reader: UPCReader, | ||
upc_e_reader: UPCEReader | ||
upc_e_reader: UPCEReader, | ||
i2of5_reader: I2of5Reader | ||
}; | ||
@@ -92,7 +95,17 @@ var BarcodeDecoder = { | ||
function initReaders() { | ||
var i; | ||
for ( i = 0; i < config.readers.length; i++) { | ||
console.log(config.readers[i]); | ||
_barcodeReaders.push(new readers[config.readers[i]]()); | ||
} | ||
config.readers.forEach(function(readerConfig) { | ||
var reader, | ||
config = {}; | ||
if (typeof readerConfig === 'object') { | ||
reader = readerConfig.format; | ||
config = readerConfig.config; | ||
} else if (typeof readerConfig === 'string') { | ||
reader = readerConfig; | ||
} | ||
_barcodeReaders.push(new readers[reader](config)); | ||
}); | ||
console.log("Registered Readers: " + _barcodeReaders | ||
.map(function(reader) {return JSON.stringify({format: reader.FORMAT, config: reader.config});}) | ||
.join(', ')); | ||
} | ||
@@ -99,0 +112,0 @@ |
@@ -8,4 +8,5 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
function BarcodeReader() { | ||
function BarcodeReader(config) { | ||
this._row = []; | ||
this.config = config || {}; | ||
return this; | ||
@@ -188,2 +189,25 @@ } | ||
BarcodeReader.prototype._fillCounters = function(offset, end, isWhite) { | ||
var self = this, | ||
counterPos = 0, | ||
i, | ||
counters = []; | ||
isWhite = (typeof isWhite !== 'undefined') ? isWhite : true; | ||
offset = (typeof offset !== 'undefined') ? offset : self._nextUnset(self._row); | ||
end = end || self._row.length; | ||
counters[counterPos] = 0; | ||
for (i = offset; i < end; i++) { | ||
if (self._row[i] ^ isWhite) { | ||
counters[counterPos]++; | ||
} else { | ||
counterPos++; | ||
counters[counterPos] = 1; | ||
isWhite = !isWhite; | ||
} | ||
} | ||
return counters; | ||
}; | ||
Object.defineProperty(BarcodeReader.prototype, "FORMAT", { | ||
@@ -204,2 +228,4 @@ value: 'unknown', | ||
}; | ||
BarcodeReader.CONFIG_KEYS = {}; | ||
@@ -206,0 +232,0 @@ return (BarcodeReader); |
@@ -120,2 +120,3 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
slope, | ||
slope2, | ||
center = min + (max - min) / 2, | ||
@@ -136,7 +137,8 @@ extrema = [], | ||
}); | ||
for ( i = 0; i < line.length - 1; i++) { | ||
for ( i = 0; i < line.length - 2; i++) { | ||
slope = (line[i + 1] - line[i]); | ||
if (slope < rThreshold && line[i + 1] < (center*1.5)) { | ||
slope2 = (line[i + 2] - line[i + 1]); | ||
if ((slope + slope2) < rThreshold && line[i + 1] < (center*1.5)) { | ||
dir = Slope.DIR.DOWN; | ||
} else if (slope > threshold && line[i + 1] > (center*0.5)) { | ||
} else if ((slope + slope2) > threshold && line[i + 1] > (center*0.5)) { | ||
dir = Slope.DIR.UP; | ||
@@ -143,0 +145,0 @@ } else { |
@@ -16,7 +16,11 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
function getUserMedia(constraints, success, failure) { | ||
navigator.getUserMedia(constraints, function(stream) { | ||
streamRef = stream; | ||
var videoSrc = (window.URL && window.URL.createObjectURL(stream)) || stream; | ||
success.apply(null, [videoSrc]); | ||
}, failure); | ||
if (typeof navigator.getUserMedia !== 'undefined') { | ||
navigator.getUserMedia(constraints, function (stream) { | ||
streamRef = stream; | ||
var videoSrc = (window.URL && window.URL.createObjectURL(stream)) || stream; | ||
success.apply(null, [videoSrc]); | ||
}, failure); | ||
} else { | ||
failure(new TypeError("getUserMedia not available")); | ||
} | ||
} | ||
@@ -60,3 +64,3 @@ | ||
}, function(e) { | ||
console.log(e); | ||
callback(e); | ||
}); | ||
@@ -84,3 +88,3 @@ } | ||
if ( typeof MediaStreamTrack.getSources !== 'undefined') { | ||
if ( typeof MediaStreamTrack !== 'undefined' && typeof MediaStreamTrack.getSources !== 'undefined') { | ||
MediaStreamTrack.getSources(function(sourceInfos) { | ||
@@ -87,0 +91,0 @@ var videoSourceId; |
@@ -39,3 +39,3 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
self._fillCounters(); | ||
this._counters = self._fillCounters(); | ||
start = self._findStart(); | ||
@@ -190,22 +190,2 @@ if (!start) { | ||
CodabarReader.prototype._fillCounters = function() { | ||
var self = this, | ||
counterPos = 0, | ||
isWhite = true, | ||
offset = self._nextUnset(self._row), | ||
i; | ||
self._counters.length = 0; | ||
self._counters[counterPos] = 0; | ||
for (i = offset; i < self._row.length; i++) { | ||
if (self._row[i] ^ isWhite) { | ||
this._counters[counterPos]++; | ||
} else { | ||
counterPos++; | ||
this._counters[counterPos] = 1; | ||
isWhite = !isWhite; | ||
} | ||
} | ||
}; | ||
CodabarReader.prototype._patternToChar = function(pattern) { | ||
@@ -212,0 +192,0 @@ var i, |
@@ -165,11 +165,13 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
normalized = self._normalize(counter); | ||
for ( code = 0; code < self.CODE_PATTERN.length; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
if (normalized) { | ||
for (code = 0; code < self.CODE_PATTERN.length; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
} | ||
} | ||
bestMatch.end = i; | ||
return bestMatch; | ||
} | ||
bestMatch.end = i; | ||
return bestMatch; | ||
} else { | ||
@@ -214,14 +216,16 @@ counterPos++; | ||
normalized = self._normalize(counter); | ||
for ( code = self.START_CODE_A; code <= self.START_CODE_C; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
if (normalized) { | ||
for (code = self.START_CODE_A; code <= self.START_CODE_C; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
} | ||
} | ||
if (bestMatch.error < self.AVG_CODE_ERROR) { | ||
bestMatch.start = i - sum; | ||
bestMatch.end = i; | ||
return bestMatch; | ||
} | ||
} | ||
if (bestMatch.error < self.AVG_CODE_ERROR) { | ||
bestMatch.start = i - sum; | ||
bestMatch.end = i; | ||
return bestMatch; | ||
} | ||
@@ -228,0 +232,0 @@ for ( j = 0; j < 4; j++) { |
@@ -35,3 +35,3 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
code = self._findPattern(self.MIDDLE_PATTERN, code.end, true); | ||
code = self._findPattern(self.MIDDLE_PATTERN, code.end, true, false); | ||
if (code === null) { | ||
@@ -38,0 +38,0 @@ return null; |
@@ -45,4 +45,4 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
CODE_FREQUENCY : {value: [0, 11, 13, 14, 19, 25, 28, 21, 22, 26]}, | ||
SINGLE_CODE_ERROR: {value: 0.7}, | ||
AVG_CODE_ERROR: {value: 0.3}, | ||
SINGLE_CODE_ERROR: {value: 0.67}, | ||
AVG_CODE_ERROR: {value: 0.27}, | ||
FORMAT: {value: "ean_13", writeable: false} | ||
@@ -81,14 +81,16 @@ }; | ||
normalized = self._normalize(counter); | ||
for ( code = 0; code < coderange; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
if (normalized) { | ||
for (code = 0; code < coderange; code++) { | ||
error = self._matchPattern(normalized, self.CODE_PATTERN[code]); | ||
if (error < bestMatch.error) { | ||
bestMatch.code = code; | ||
bestMatch.error = error; | ||
} | ||
} | ||
bestMatch.end = i; | ||
if (bestMatch.error > self.AVG_CODE_ERROR) { | ||
return null; | ||
} | ||
return bestMatch; | ||
} | ||
bestMatch.end = i; | ||
if (bestMatch.error > self.AVG_CODE_ERROR) { | ||
return null; | ||
} | ||
return bestMatch; | ||
} else { | ||
@@ -150,9 +152,11 @@ counterPos++; | ||
normalized = self._normalize(counter); | ||
error = self._matchPattern(normalized, pattern); | ||
if (normalized) { | ||
error = self._matchPattern(normalized, pattern); | ||
if (error < epsilon) { | ||
bestMatch.error = error; | ||
bestMatch.start = i - sum; | ||
bestMatch.end = i; | ||
return bestMatch; | ||
if (error < epsilon) { | ||
bestMatch.error = error; | ||
bestMatch.start = i - sum; | ||
bestMatch.end = i; | ||
return bestMatch; | ||
} | ||
} | ||
@@ -159,0 +163,0 @@ if (tryHarder) { |
@@ -6,4 +6,2 @@ /* jshint undef: true, unused: true, browser:true, devel: true, evil: true */ | ||
define([ | ||
"code_128_reader", | ||
"ean_reader", | ||
"input_stream", | ||
@@ -20,5 +18,3 @@ "image_wrapper", | ||
"result_collector"], | ||
function(Code128Reader, | ||
EANReader, | ||
InputStream, | ||
function(InputStream, | ||
ImageWrapper, | ||
@@ -104,3 +100,3 @@ BarcodeLocator, | ||
} else { | ||
console.log(err); | ||
return cb(err); | ||
} | ||
@@ -495,6 +491,2 @@ }); | ||
}, | ||
Reader: { | ||
EANReader : EANReader, | ||
Code128Reader : Code128Reader | ||
}, | ||
ImageWrapper: ImageWrapper, | ||
@@ -501,0 +493,0 @@ ImageDebug: ImageDebug, |
@@ -39,4 +39,2 @@ /* jshint undef: true, unused: true, browser:true, devel: true */ | ||
codeFrequency |= 1 << (5 - i); | ||
} else { | ||
codeFrequency |= 0 << (5 - i); | ||
} | ||
@@ -46,3 +44,5 @@ result.push(code.code); | ||
} | ||
self._determineParity(codeFrequency, result); | ||
if (!self._determineParity(codeFrequency, result)) { | ||
return null; | ||
} | ||
@@ -62,6 +62,7 @@ return code; | ||
result.push(i); | ||
return; | ||
return true; | ||
} | ||
} | ||
} | ||
return false; | ||
}; | ||
@@ -68,0 +69,0 @@ |
@@ -50,3 +50,4 @@ var allTestFiles = []; | ||
'async': 'node_modules/async/lib/async', | ||
'result_collector': 'src/result_collector' | ||
'result_collector': 'src/result_collector', | ||
'i2of5_reader': 'src/i2of5_reader' | ||
}, | ||
@@ -53,0 +54,0 @@ deps: allTestFiles, |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
1977235
60
44638
487