• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dynobo / normcap / 18953976799

30 Oct 2025 08:21PM UTC coverage: 80.119% (-1.4%) from 81.509%
18953976799

Pull #798

github

web-flow
Merge a4477b844 into 50be529a6
Pull Request #798: Refactoring

858 of 1175 new or added lines in 51 files covered. (73.02%)

23 existing lines in 5 files now uncovered.

2974 of 3712 relevant lines covered (80.12%)

4.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

92.65
/normcap/detection/codes/detector.py
1
"""Barcode and QR Code detection."""
2

3
import logging
6✔
4
import os
6✔
5
from collections.abc import Generator
6✔
6

7
import zxingcpp
6✔
8
from PySide6 import QtGui
6✔
9

10
from normcap.detection.codes.models import CodeType
6✔
11
from normcap.detection.models import DetectionResult, TextDetector, TextType
6✔
12

13
logger = logging.getLogger(__name__)
6✔
14

15

16
def _image_to_memoryview(image: QtGui.QImage) -> memoryview:
6✔
17
    """Transform Qimage to a bytearray without padding.
18

19
    Args:
20
        image: Input image.
21

22
    Returns:
23
        Memoryview of the image data as 3D array: width x height x RGB.
24
    """
25
    image = image.convertToFormat(QtGui.QImage.Format.Format_RGB888)
6✔
26
    ptr = image.constBits()
6✔
27
    bytes_per_line = image.bytesPerLine()  # Includes padding
6✔
28
    width = image.width()
6✔
29
    height = image.height()
6✔
30
    channels = 3  # RGB888 has 3 channels
6✔
31

32
    # Create a new bytearray to exclude padding
33
    raw_data = bytearray()
6✔
34
    for y in range(height):
6✔
35
        row_start = y * bytes_per_line
6✔
36
        row_end = row_start + (width * channels)
6✔
37
        raw_data.extend(ptr[row_start:row_end])  # Exclude padding
6✔
38

39
    return memoryview(raw_data).cast(
6✔
40
        "B", shape=(image.height(), image.width(), channels)
41
    )
42

43

44
def _get_text_type_and_transform(text: str) -> tuple[str, TextType]:
6✔
45
    """Estimate the type of text based on the content."""
46
    if text.startswith("https://") or text.startswith("http://"):
6✔
47
        text_type = TextType.URL
6✔
48
    elif text.startswith("tel:"):
6✔
49
        text = text.removeprefix("tel:")
6✔
50
        text_type = TextType.PHONE_NUMBER
6✔
51
    elif text.startswith("mailto:"):
6✔
52
        text = text.removeprefix("mailto:")
6✔
53
        text_type = TextType.MAIL
6✔
54
    elif text.startswith("BEGIN:VCARD") and text.endswith("END:VCARD"):
6✔
55
        text_type = TextType.VCARD
6✔
56
    elif text.startswith("BEGIN:VEVENT") and text.endswith("END:VEVENT"):
6✔
57
        text_type = TextType.VEVENT
6✔
58
    elif os.linesep + os.linesep in text:
6✔
59
        text_type = TextType.PARAGRAPH
×
60
    elif os.linesep in text:
6✔
61
        text_type = TextType.MULTI_LINE
×
62
    else:
63
        text_type = TextType.SINGLE_LINE
6✔
64

65
    return text, text_type
6✔
66

67

68
def _detect_codes_via_zxing(
6✔
69
    image: memoryview,
70
) -> Generator[tuple[str, TextType, CodeType], None, None]:
71
    """Decode QR and barcodes from image.
72

73
    Args:
74
        image: Input image.
75

76
    Returns:
77
        URL(s), separated bye newline
78
    """
79
    logger.info("Detect Barcodes and QR Codes")
6✔
80

81
    results = zxingcpp.read_barcodes(image)
6✔
82

83
    if not results:
6✔
84
        return None
6✔
85

86
    codes = [result.text for result in results if result.text]
6✔
87
    code_types = [r.format for r in results]
6✔
88

89
    qr_formats = {
6✔
90
        zxingcpp.BarcodeFormat.QRCode,
91
        zxingcpp.BarcodeFormat.RMQRCode,
92
        zxingcpp.BarcodeFormat.MicroQRCode,
93
    }
94

95
    logger.info("Found %s codes", len(codes))
6✔
96

97
    for code, code_format in zip(codes, code_types, strict=True):
6✔
98
        if code_format in qr_formats:
6✔
99
            code_type = CodeType.QR
6✔
NEW
100
        elif code_format not in (qr_formats):
×
NEW
101
            code_type = CodeType.BARCODE
×
102
        else:
NEW
103
            raise ValueError()
×
104

105
        text = code.strip()
6✔
106
        text, text_type = _get_text_type_and_transform(text)
6✔
107
        yield text, text_type, code_type
6✔
108

109

110
def detect_codes(image: QtGui.QImage) -> list[DetectionResult]:
6✔
111
    """Decode & decode QR and barcodes from image.
112

113
    Args:
114
        image: Input image with potentially one or more barcocdes / QR Codes.
115

116
    Returns:
117
        Result of the detection. If more than one code is detected, the detected values
118
            are separated by newlines. If no code is detected, None is returned.
119
    """
120
    logger.debug("Start QR/Barcode detection")
6✔
121
    # ONHOLD: Switch to using QImage directly, when a new zxingcpp is released
122
    image_buffer = _image_to_memoryview(image)
6✔
123

124
    results = []
6✔
125
    for text, text_type, code_type in _detect_codes_via_zxing(image=image_buffer):
6✔
126
        text_detector = TextDetector[code_type.value]
6✔
127
        results.append(
6✔
128
            DetectionResult(text=text, text_type=text_type, detector=text_detector)
129
        )
130

131
    if not results:
6✔
132
        logger.debug("No codes found")
6✔
133

134
    return results
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc