Part-DB-server/src/Services/LabelSystem/BarcodeScanner/BarcodeScanHelper.php

281 lines
9.9 KiB
PHP
Raw Normal View History

2020-04-26 21:26:11 +02:00
<?php
2022-11-29 21:21:26 +01:00
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
2020-05-10 21:39:31 +02:00
declare(strict_types=1);
2020-04-26 21:26:11 +02:00
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
2022-11-29 22:28:53 +01:00
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
2020-04-26 21:26:11 +02:00
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Services\LabelSystem\BarcodeScanner;
2020-04-26 21:26:11 +02:00
use App\Entity\LabelSystem\LabelSupportedElement;
use App\Entity\Parts\Part;
use App\Entity\Parts\PartLot;
use Doctrine\ORM\EntityManagerInterface;
2022-08-14 19:32:53 +02:00
use InvalidArgumentException;
2023-06-11 15:02:59 +02:00
/**
2024-06-22 00:31:43 +02:00
* @see \App\Tests\Services\LabelSystem\Barcodes\BarcodeScanHelperTest
2023-06-11 15:02:59 +02:00
*/
final class BarcodeScanHelper
2020-04-26 21:26:11 +02:00
{
2020-05-09 18:17:23 +02:00
private const PREFIX_TYPE_MAP = [
'L' => LabelSupportedElement::PART_LOT,
'P' => LabelSupportedElement::PART,
'S' => LabelSupportedElement::STORELOCATION,
2020-04-26 21:26:11 +02:00
];
public const QR_TYPE_MAP = [
'lot' => LabelSupportedElement::PART_LOT,
'part' => LabelSupportedElement::PART,
'location' => LabelSupportedElement::STORELOCATION,
];
public function __construct(private readonly EntityManagerInterface $entityManager)
{
}
/**
* Parse the given barcode content and return the target type and ID.
* If the barcode could not be parsed, an exception is thrown.
* Using the $type parameter, you can specify how the barcode should be parsed. If set to null, the function
* will try to guess the type.
* @param string $input
* @param BarcodeSourceType|null $type
* @return BarcodeScanResultInterface
*/
public function scanBarcodeContent(string $input, ?BarcodeSourceType $type = null): BarcodeScanResultInterface
{
//Do specific parsing
if ($type === BarcodeSourceType::INTERNAL) {
return $this->parseInternalBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::USER_DEFINED) {
return $this->parseUserDefinedBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::IPN) {
return $this->parseIPNBarcode($input) ?? throw new InvalidArgumentException('Could not parse barcode');
}
if ($type === BarcodeSourceType::EIGP114) {
return $this->parseEIGP114Barcode($input);
}
Label Scanner Enhancements: LCSC barcode, create part, augmented scanning (#1194) * added handling of LCSC barcode decoding and part loading on Label Scanner * when a part is scanned and not found, the scanner did not redraw so scanning subsequent parts was not possible without reloading the browser page. fixed the barcode scanner initialization and shutdown so it redraws properly after part not found * added redirection to part page on successful scan of lcsc, digikey, and mouser barcodes. added create part button if part does not exist in database * added augmented mode to label scanner to use vendor labels for part lookup to see part storage location quickly * shrink camera height on mobile so augmented information can been viewed onscreen * handle momentarily bad reads from qrcode library * removed augmented checkbox and combined functionality into info mode checkbox. changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found. * fix scanning of part-db barcodes to redirect to storage location or part lots. made scan result messages conditional for parts or other non-part barcodes * fix static analysis errors * added unit tests for meeting code coverage report * fix @MayNiklas reported bug: when manually submitting the form (from a barcode scan or manual input) redirect to Create New part screen for the decoded information instead of showing 'Format Unknown' popup error message * fix @d-buchmann bug: clear 'scan-augmented-result' field upon rescan of new barcode * fix @d-buchmann bug: after scanning in Info mode, if Info mode is turned off when scanning a part that did not exist, it now redirects user to create part page * fix @d-buchmann bug: make barcode decode table 100% width of page * fix bug with manual form submission where a part does not exist but decodes properly which causes the camera to not redraw on page reload due to unclean shutdown. this is an existing bug in the scanner interface. steps to produce the issue: - have camera active - put in code in Input - info mode ticked - click submit button on page reload the camera does not reactivate * fixed translation messages * Use symfony native functions to generate the routes for part creation * Use native request functions for request param parsing * Refactored LCSCBarcocdeScanResult to be an value object like the other Barcode results * Added test for LCSCBarcodeScanResult * Fixed exception when submitting form for info mode * Made BarcodeSourceType a backed enum, so that it can be used in Request::getEnum() * Moved database queries from BarcodeRedirector to PartRepository * Fixed modeEnum parsing * Fixed test errors * Refactored BarcodeRedirector logic to be more universal * Fixed BarcodeScanResultHandler test * Refactored BarcodeScanResultHandler to be able to resolve arbitary entities from scans * Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service * Improved augmentented info styling and allow to use it with normal form submit too * Correctly handle nullable infoURL in ScanController * Replaced the custom controller for fragment replacements with symfony streams This does not require a complete new endpoint * Removed data-lookup-url attribute from scan read box * Removed unused translations * Added basic info block when an storage location was found for an barcode * Fixed phpstan issues * Fixed tests * Fixed part image for mobile view * Added more tests for BarcodeScanResultHandler service * Fixed tests --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-02-23 09:26:44 +13:00
if ($type === BarcodeSourceType::GTIN) {
return $this->parseGTINBarcode($input);
}
Label Scanner Enhancements: LCSC barcode, create part, augmented scanning (#1194) * added handling of LCSC barcode decoding and part loading on Label Scanner * when a part is scanned and not found, the scanner did not redraw so scanning subsequent parts was not possible without reloading the browser page. fixed the barcode scanner initialization and shutdown so it redraws properly after part not found * added redirection to part page on successful scan of lcsc, digikey, and mouser barcodes. added create part button if part does not exist in database * added augmented mode to label scanner to use vendor labels for part lookup to see part storage location quickly * shrink camera height on mobile so augmented information can been viewed onscreen * handle momentarily bad reads from qrcode library * removed augmented checkbox and combined functionality into info mode checkbox. changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found. * fix scanning of part-db barcodes to redirect to storage location or part lots. made scan result messages conditional for parts or other non-part barcodes * fix static analysis errors * added unit tests for meeting code coverage report * fix @MayNiklas reported bug: when manually submitting the form (from a barcode scan or manual input) redirect to Create New part screen for the decoded information instead of showing 'Format Unknown' popup error message * fix @d-buchmann bug: clear 'scan-augmented-result' field upon rescan of new barcode * fix @d-buchmann bug: after scanning in Info mode, if Info mode is turned off when scanning a part that did not exist, it now redirects user to create part page * fix @d-buchmann bug: make barcode decode table 100% width of page * fix bug with manual form submission where a part does not exist but decodes properly which causes the camera to not redraw on page reload due to unclean shutdown. this is an existing bug in the scanner interface. steps to produce the issue: - have camera active - put in code in Input - info mode ticked - click submit button on page reload the camera does not reactivate * fixed translation messages * Use symfony native functions to generate the routes for part creation * Use native request functions for request param parsing * Refactored LCSCBarcocdeScanResult to be an value object like the other Barcode results * Added test for LCSCBarcodeScanResult * Fixed exception when submitting form for info mode * Made BarcodeSourceType a backed enum, so that it can be used in Request::getEnum() * Moved database queries from BarcodeRedirector to PartRepository * Fixed modeEnum parsing * Fixed test errors * Refactored BarcodeRedirector logic to be more universal * Fixed BarcodeScanResultHandler test * Refactored BarcodeScanResultHandler to be able to resolve arbitary entities from scans * Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service * Improved augmentented info styling and allow to use it with normal form submit too * Correctly handle nullable infoURL in ScanController * Replaced the custom controller for fragment replacements with symfony streams This does not require a complete new endpoint * Removed data-lookup-url attribute from scan read box * Removed unused translations * Added basic info block when an storage location was found for an barcode * Fixed phpstan issues * Fixed tests * Fixed part image for mobile view * Added more tests for BarcodeScanResultHandler service * Fixed tests --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-02-23 09:26:44 +13:00
if ($type === BarcodeSourceType::LCSC) {
return $this->parseLCSCBarcode($input);
}
if ($type === BarcodeSourceType::AMAZON) {
return new AmazonBarcodeScanResult($input);
}
//Null means auto and we try the different formats
$result = $this->parseInternalBarcode($input);
if ($result !== null) {
return $result;
}
//Try to parse as User defined barcode
$result = $this->parseUserDefinedBarcode($input);
if ($result !== null) {
return $result;
}
//If the barcode is formatted as EIGP114, we can parse it directly
if (EIGP114BarcodeScanResult::isFormat06Code($input)) {
return $this->parseEIGP114Barcode($input);
}
//Try to parse as IPN barcode
$result = $this->parseIPNBarcode($input);
if ($result !== null) {
return $result;
}
//If the result is a valid GTIN barcode, we can parse it directly
if (GTINBarcodeScanResult::isValidGTIN($input)) {
return $this->parseGTINBarcode($input);
}
Label Scanner Enhancements: LCSC barcode, create part, augmented scanning (#1194) * added handling of LCSC barcode decoding and part loading on Label Scanner * when a part is scanned and not found, the scanner did not redraw so scanning subsequent parts was not possible without reloading the browser page. fixed the barcode scanner initialization and shutdown so it redraws properly after part not found * added redirection to part page on successful scan of lcsc, digikey, and mouser barcodes. added create part button if part does not exist in database * added augmented mode to label scanner to use vendor labels for part lookup to see part storage location quickly * shrink camera height on mobile so augmented information can been viewed onscreen * handle momentarily bad reads from qrcode library * removed augmented checkbox and combined functionality into info mode checkbox. changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found. * fix scanning of part-db barcodes to redirect to storage location or part lots. made scan result messages conditional for parts or other non-part barcodes * fix static analysis errors * added unit tests for meeting code coverage report * fix @MayNiklas reported bug: when manually submitting the form (from a barcode scan or manual input) redirect to Create New part screen for the decoded information instead of showing 'Format Unknown' popup error message * fix @d-buchmann bug: clear 'scan-augmented-result' field upon rescan of new barcode * fix @d-buchmann bug: after scanning in Info mode, if Info mode is turned off when scanning a part that did not exist, it now redirects user to create part page * fix @d-buchmann bug: make barcode decode table 100% width of page * fix bug with manual form submission where a part does not exist but decodes properly which causes the camera to not redraw on page reload due to unclean shutdown. this is an existing bug in the scanner interface. steps to produce the issue: - have camera active - put in code in Input - info mode ticked - click submit button on page reload the camera does not reactivate * fixed translation messages * Use symfony native functions to generate the routes for part creation * Use native request functions for request param parsing * Refactored LCSCBarcocdeScanResult to be an value object like the other Barcode results * Added test for LCSCBarcodeScanResult * Fixed exception when submitting form for info mode * Made BarcodeSourceType a backed enum, so that it can be used in Request::getEnum() * Moved database queries from BarcodeRedirector to PartRepository * Fixed modeEnum parsing * Fixed test errors * Refactored BarcodeRedirector logic to be more universal * Fixed BarcodeScanResultHandler test * Refactored BarcodeScanResultHandler to be able to resolve arbitary entities from scans * Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service * Improved augmentented info styling and allow to use it with normal form submit too * Correctly handle nullable infoURL in ScanController * Replaced the custom controller for fragment replacements with symfony streams This does not require a complete new endpoint * Removed data-lookup-url attribute from scan read box * Removed unused translations * Added basic info block when an storage location was found for an barcode * Fixed phpstan issues * Fixed tests * Fixed part image for mobile view * Added more tests for BarcodeScanResultHandler service * Fixed tests --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-02-23 09:26:44 +13:00
// Try LCSC barcode
if (LCSCBarcodeScanResult::isLCSCBarcode($input)) {
return $this->parseLCSCBarcode($input);
}
//Try amazon barcode
if (AmazonBarcodeScanResult::isAmazonBarcode($input)) {
return new AmazonBarcodeScanResult($input);
}
throw new InvalidArgumentException('Unknown barcode');
}
private function parseGTINBarcode(string $input): GTINBarcodeScanResult
{
return new GTINBarcodeScanResult($input);
}
private function parseEIGP114Barcode(string $input): EIGP114BarcodeScanResult
{
return EIGP114BarcodeScanResult::parseFormat06Code($input);
}
Label Scanner Enhancements: LCSC barcode, create part, augmented scanning (#1194) * added handling of LCSC barcode decoding and part loading on Label Scanner * when a part is scanned and not found, the scanner did not redraw so scanning subsequent parts was not possible without reloading the browser page. fixed the barcode scanner initialization and shutdown so it redraws properly after part not found * added redirection to part page on successful scan of lcsc, digikey, and mouser barcodes. added create part button if part does not exist in database * added augmented mode to label scanner to use vendor labels for part lookup to see part storage location quickly * shrink camera height on mobile so augmented information can been viewed onscreen * handle momentarily bad reads from qrcode library * removed augmented checkbox and combined functionality into info mode checkbox. changed barcode scanner to use XHR callback for barcode decoding to avoid problems with form submission and camera caused by page reloaded when part not found. * fix scanning of part-db barcodes to redirect to storage location or part lots. made scan result messages conditional for parts or other non-part barcodes * fix static analysis errors * added unit tests for meeting code coverage report * fix @MayNiklas reported bug: when manually submitting the form (from a barcode scan or manual input) redirect to Create New part screen for the decoded information instead of showing 'Format Unknown' popup error message * fix @d-buchmann bug: clear 'scan-augmented-result' field upon rescan of new barcode * fix @d-buchmann bug: after scanning in Info mode, if Info mode is turned off when scanning a part that did not exist, it now redirects user to create part page * fix @d-buchmann bug: make barcode decode table 100% width of page * fix bug with manual form submission where a part does not exist but decodes properly which causes the camera to not redraw on page reload due to unclean shutdown. this is an existing bug in the scanner interface. steps to produce the issue: - have camera active - put in code in Input - info mode ticked - click submit button on page reload the camera does not reactivate * fixed translation messages * Use symfony native functions to generate the routes for part creation * Use native request functions for request param parsing * Refactored LCSCBarcocdeScanResult to be an value object like the other Barcode results * Added test for LCSCBarcodeScanResult * Fixed exception when submitting form for info mode * Made BarcodeSourceType a backed enum, so that it can be used in Request::getEnum() * Moved database queries from BarcodeRedirector to PartRepository * Fixed modeEnum parsing * Fixed test errors * Refactored BarcodeRedirector logic to be more universal * Fixed BarcodeScanResultHandler test * Refactored BarcodeScanResultHandler to be able to resolve arbitary entities from scans * Moved barcode to info provider logic from Controller to BarcodeScanResultHandler service * Improved augmentented info styling and allow to use it with normal form submit too * Correctly handle nullable infoURL in ScanController * Replaced the custom controller for fragment replacements with symfony streams This does not require a complete new endpoint * Removed data-lookup-url attribute from scan read box * Removed unused translations * Added basic info block when an storage location was found for an barcode * Fixed phpstan issues * Fixed tests * Fixed part image for mobile view * Added more tests for BarcodeScanResultHandler service * Fixed tests --------- Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-02-23 09:26:44 +13:00
private function parseLCSCBarcode(string $input): LCSCBarcodeScanResult
{
return LCSCBarcodeScanResult::parse($input);
}
private function parseUserDefinedBarcode(string $input): ?LocalBarcodeScanResult
{
$lot_repo = $this->entityManager->getRepository(PartLot::class);
//Find only the first result
$results = $lot_repo->findBy(['user_barcode' => $input], limit: 1);
if (count($results) === 0) {
return null;
}
//We found a part, so use it to create the result
$lot = $results[0];
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART_LOT,
target_id: $lot->getID(),
source_type: BarcodeSourceType::USER_DEFINED
);
}
private function parseIPNBarcode(string $input): ?LocalBarcodeScanResult
{
$part_repo = $this->entityManager->getRepository(Part::class);
//Find only the first result
$results = $part_repo->findBy(['ipn' => $input], limit: 1);
if (count($results) === 0) {
return null;
}
//We found a part, so use it to create the result
$part = $results[0];
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART,
target_id: $part->getID(),
source_type: BarcodeSourceType::IPN
);
}
2020-04-26 21:26:11 +02:00
/**
* This function tries to interpret the given barcode content as an internal barcode.
* If the barcode could not be parsed at all, null is returned. If the barcode is a valid format, but could
* not be found in the database, an exception is thrown.
* @param string $input
* @return LocalBarcodeScanResult|null
2020-04-26 21:26:11 +02:00
*/
private function parseInternalBarcode(string $input): ?LocalBarcodeScanResult
2020-04-26 21:26:11 +02:00
{
$input = trim($input);
$matches = [];
//Some scanner output '-' as ß, so replace it (ß is never used, so we can replace it safely)
$input = str_replace('ß', '-', $input);
//Extract parts from QR code's URL
if (preg_match('#^https?://.*/scan/(\w+)/(\d+)/?$#', $input, $matches)) {
return new LocalBarcodeScanResult(
target_type: self::QR_TYPE_MAP[strtolower($matches[1])],
target_id: (int) $matches[2],
source_type: BarcodeSourceType::INTERNAL
);
2020-04-26 21:26:11 +02:00
}
//New Code39 barcode use L0001 format
if (preg_match('#^([A-Z])(\d{4,})$#', $input, $matches)) {
$prefix = $matches[1];
$id = (int) $matches[2];
2020-08-21 21:36:22 +02:00
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
2022-08-14 19:32:53 +02:00
throw new InvalidArgumentException('Unknown prefix '.$prefix);
}
2020-05-10 21:39:31 +02:00
return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id,
source_type: BarcodeSourceType::INTERNAL
);
}
//During development the L-000001 format was used
2020-04-26 21:26:11 +02:00
if (preg_match('#^(\w)-(\d{6,})$#', $input, $matches)) {
$prefix = $matches[1];
$id = (int) $matches[2];
2020-08-21 21:36:22 +02:00
if (!isset(self::PREFIX_TYPE_MAP[$prefix])) {
2022-08-14 19:32:53 +02:00
throw new InvalidArgumentException('Unknown prefix '.$prefix);
2020-04-26 21:26:11 +02:00
}
2020-05-10 21:39:31 +02:00
return new LocalBarcodeScanResult(
target_type: self::PREFIX_TYPE_MAP[$prefix],
target_id: $id,
source_type: BarcodeSourceType::INTERNAL
);
2020-04-26 21:26:11 +02:00
}
//Legacy Part-DB location labels used $L00336 format
if (preg_match('#^\$L(\d{5,})$#', $input, $matches)) {
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::STORELOCATION,
target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL
);
2020-04-26 21:26:11 +02:00
}
//Legacy Part-DB used EAN8 barcodes for part labels. Format 0000001(2) (note the optional 8th digit => checksum)
if (preg_match('#^(\d{7})\d?$#', $input, $matches)) {
return new LocalBarcodeScanResult(
target_type: LabelSupportedElement::PART,
target_id: (int) $matches[1],
source_type: BarcodeSourceType::INTERNAL
);
2020-04-26 21:26:11 +02:00
}
//This function abstain from further parsing
return null;
2020-04-26 21:26:11 +02:00
}
2020-05-10 21:39:31 +02:00
}