Skip to content

Commit 3f31653

Browse files
authored
Merge pull request #1500 from roboflow/feat-qr-code-block
Add QR Code Generator workflow block
2 parents 7749c19 + d5382c7 commit 3f31653

File tree

5 files changed

+592
-0
lines changed

5 files changed

+592
-0
lines changed

inference/core/workflows/core_steps/loader.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,9 @@
341341
from inference.core.workflows.core_steps.transformations.perspective_correction.v1 import (
342342
PerspectiveCorrectionBlockV1,
343343
)
344+
from inference.core.workflows.core_steps.transformations.qr_code_generator.v1 import (
345+
QRCodeGeneratorBlockV1,
346+
)
344347
from inference.core.workflows.core_steps.transformations.relative_static_crop.v1 import (
345348
RelativeStaticCropBlockV1,
346349
)
@@ -667,6 +670,7 @@ def load_blocks() -> List[Type[WorkflowBlock]]:
667670
Moondream2BlockV1,
668671
OverlapBlockV1,
669672
ONVIFSinkBlockV1,
673+
QRCodeGeneratorBlockV1,
670674
]
671675

672676

inference/core/workflows/core_steps/transformations/qr_code_generator/__init__.py

Whitespace-only changes.
Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
import threading
2+
import time
3+
from collections import OrderedDict
4+
from typing import List, Literal, Optional, Type, Union
5+
from uuid import uuid4
6+
7+
import numpy as np
8+
from pydantic import ConfigDict, Field
9+
10+
from inference.core.workflows.core_steps.visualizations.common.utils import str_to_color
11+
from inference.core.workflows.execution_engine.entities.base import (
12+
Batch,
13+
ImageParentMetadata,
14+
OutputDefinition,
15+
WorkflowImageData,
16+
)
17+
from inference.core.workflows.execution_engine.entities.types import (
18+
IMAGE_KIND,
19+
INTEGER_KIND,
20+
STRING_KIND,
21+
ImageInputField,
22+
Selector,
23+
)
24+
from inference.core.workflows.prototypes.block import (
25+
BlockResult,
26+
WorkflowBlock,
27+
WorkflowBlockManifest,
28+
)
29+
30+
LONG_DESCRIPTION = """
31+
Generate a QR code image from a string input (typically a URL).
32+
33+
This block creates a QR code PNG image from the provided text input. It supports
34+
various customization options including size control, error correction levels,
35+
and visual styling. The generated QR code can be used in workflows where you need
36+
to create QR codes for URLs, text content, or other data that needs to be encoded.
37+
38+
The output is a PNG image that can be passed to other workflow blocks such as
39+
visualizers or image processing blocks.
40+
"""
41+
42+
43+
class BlockManifest(WorkflowBlockManifest):
44+
model_config = ConfigDict(
45+
json_schema_extra={
46+
"name": "QR Code Generator",
47+
"version": "v1",
48+
"short_description": "Generate a QR code image from text input.",
49+
"long_description": LONG_DESCRIPTION,
50+
"license": "Apache-2.0",
51+
"block_type": "transformation",
52+
"ui_manifest": {
53+
"section": "transformation",
54+
"icon": "fas fa-qrcode",
55+
"blockPriority": 1,
56+
},
57+
}
58+
)
59+
type: Literal["roboflow_core/qr_code_generator@v1", "QRCodeGenerator"]
60+
61+
# Main input - the text/URL to encode
62+
text: Union[str, Selector(kind=[STRING_KIND])] = Field(
63+
description="Text or URL to encode in the QR code",
64+
examples=["https://roboflow.com", "$inputs.url", "Hello World"],
65+
)
66+
67+
# Note: version=None and box_size=10 are hardcoded defaults (non-editable per spec)
68+
69+
# Error correction level - Single-Select (no param linking per spec 9a)
70+
error_correct: Literal[
71+
"Low (~7% word recovery / highest data capacity)",
72+
"Medium (~15% word recovery)",
73+
"Quartile (~25% word recovery)",
74+
"High (~30% word recovery / lowest data capacity)",
75+
] = Field(
76+
default="Medium (~15% word recovery)",
77+
title="Error Correction",
78+
description="Increased error correction comes at the expense of data capacity (text length). Use higher error correction if the QR code is likely to be transformed or obscured, but use a lower error correction level if the URL is long and the QR code is clearly visible.",
79+
examples=[
80+
"Low (~7% word recovery / highest data capacity)",
81+
"Medium (~15% word recovery)",
82+
"Quartile (~25% word recovery)",
83+
"High (~30% word recovery / lowest data capacity)",
84+
],
85+
)
86+
87+
# Visual styling
88+
border: Union[int, Selector(kind=[INTEGER_KIND])] = Field(
89+
default=4,
90+
title="Border Width",
91+
description="Border thickness in modules (default: 4)",
92+
examples=[2, 4, 6, "$inputs.border"],
93+
)
94+
fill_color: Union[str, Selector(kind=[STRING_KIND])] = Field(
95+
default="BLACK",
96+
title="Fill Color",
97+
description="QR code block color. Supports hex (#FF0000), rgb(255, 0, 0), standard names (BLACK, WHITE, RED, etc.), or CSS3 color names.",
98+
examples=["BLACK", "#000000", "rgb(0, 0, 0)", "$inputs.fill_color"],
99+
)
100+
back_color: Union[str, Selector(kind=[STRING_KIND])] = Field(
101+
default="WHITE",
102+
title="Background Color",
103+
description="QR code background color. Supports hex (#FFFFFF), rgb(255, 255, 255), standard names (BLACK, WHITE, RED, etc.), or CSS3 color names.",
104+
examples=["WHITE", "#FFFFFF", "rgb(255, 255, 255)", "$inputs.back_color"],
105+
)
106+
107+
@classmethod
108+
def get_parameters_accepting_batches(cls) -> List[str]:
109+
return []
110+
111+
@classmethod
112+
def describe_outputs(cls) -> List[OutputDefinition]:
113+
return [
114+
OutputDefinition(name="qr_code", kind=[IMAGE_KIND]),
115+
]
116+
117+
@classmethod
118+
def get_execution_engine_compatibility(cls) -> Optional[str]:
119+
return ">=1.3.0,<2.0.0"
120+
121+
122+
class QRCodeGeneratorBlockV1(WorkflowBlock):
123+
124+
@classmethod
125+
def get_manifest(cls) -> Type[WorkflowBlockManifest]:
126+
return BlockManifest
127+
128+
def run(
129+
self,
130+
text: str,
131+
error_correct: Literal[
132+
"Low (~7% word recovery / highest data capacity)",
133+
"Medium (~15% word recovery)",
134+
"Quartile (~25% word recovery)",
135+
"High (~30% word recovery / lowest data capacity)",
136+
] = "Medium (~15% word recovery)",
137+
border: int = 4,
138+
fill_color: str = "BLACK",
139+
back_color: str = "WHITE",
140+
) -> BlockResult:
141+
142+
qr_image = generate_qr_code(
143+
text=text,
144+
version=None, # Auto-sizing as per spec requirement 8a
145+
box_size=10, # Fixed default as per spec requirement 8b
146+
error_correct=error_correct,
147+
border=border,
148+
fill_color=fill_color,
149+
back_color=back_color,
150+
)
151+
152+
return {"qr_code": qr_image}
153+
154+
155+
def generate_qr_code(
156+
text: str,
157+
version: Optional[int] = None,
158+
box_size: int = 10,
159+
error_correct: str = "M",
160+
border: int = 4,
161+
fill_color: str = "BLACK",
162+
back_color: str = "WHITE",
163+
) -> WorkflowImageData:
164+
"""Generate a QR code PNG image from text input."""
165+
global _ERROR_LEVELS, _QR_CACHE
166+
167+
# Check cache first
168+
cached_result = _QR_CACHE.get(
169+
text, version, box_size, error_correct, border, fill_color, back_color
170+
)
171+
if cached_result is not None:
172+
return cached_result
173+
174+
try:
175+
import qrcode
176+
except ImportError:
177+
raise ImportError(
178+
"qrcode library is required for QR code generation. "
179+
"Install it with: pip install qrcode"
180+
)
181+
if _ERROR_LEVELS is None:
182+
_ERROR_LEVELS = _get_error_levels()
183+
184+
# Parse colors using the common utility that handles hex, rgb, bgr, and standard names
185+
try:
186+
# Convert to supervision Color object, then to RGB tuple for qrcode library
187+
fill_sv_color = str_to_color(fill_color)
188+
fill = fill_sv_color.as_rgb() # Returns (R, G, B) tuple
189+
except (ValueError, AttributeError):
190+
# Fallback to original string if not a recognized format
191+
# This allows qrcode library to handle CSS3 color names directly
192+
fill = fill_color
193+
194+
try:
195+
back_sv_color = str_to_color(back_color)
196+
back = back_sv_color.as_rgb() # Returns (R, G, B) tuple
197+
except (ValueError, AttributeError):
198+
# Fallback to original string if not a recognized format
199+
back = back_color
200+
201+
error_level = _ERROR_LEVELS.get(
202+
error_correct.upper(), qrcode.constants.ERROR_CORRECT_M
203+
)
204+
205+
# Create QR code
206+
qr = qrcode.QRCode(
207+
version=version,
208+
error_correction=error_level,
209+
box_size=box_size,
210+
border=border,
211+
)
212+
213+
qr.add_data(text)
214+
qr.make(fit=(version is None))
215+
216+
# Generate image using default image factory
217+
img = qr.make_image(
218+
fill_color=fill,
219+
back_color=back,
220+
).convert(
221+
"RGB"
222+
) # Ensure always RGB
223+
224+
# Direct conversion from PIL.Image to numpy array (much faster than encode/decode)
225+
numpy_image = np.array(img)
226+
227+
# Convert from RGB (PIL format) to BGR (OpenCV/WorkflowImageData format)
228+
# PIL creates RGB images, but WorkflowImageData expects BGR format
229+
numpy_image = numpy_image[:, :, ::-1] # RGB -> BGR
230+
231+
# Defensive: numpy_image should never be None; original code checks for None on OpenCV decode failure
232+
if numpy_image is None or numpy_image.size == 0:
233+
raise ValueError("Failed to generate QR code image")
234+
235+
# Create WorkflowImageData
236+
parent_metadata = ImageParentMetadata(parent_id=f"qr_code.{uuid4()}")
237+
result = WorkflowImageData(
238+
parent_metadata=parent_metadata,
239+
numpy_image=numpy_image,
240+
)
241+
242+
# Store in cache
243+
_QR_CACHE.put(
244+
text, version, box_size, error_correct, border, fill_color, back_color, result
245+
)
246+
247+
return result
248+
249+
250+
class _QRCodeLRUCache:
251+
"""LRU Cache with TTL for QR code generation results."""
252+
253+
def __init__(self, max_size: int = 100, ttl_seconds: int = 3600):
254+
self.max_size = max_size
255+
self.ttl_seconds = ttl_seconds
256+
self._cache = OrderedDict()
257+
self._lock = threading.RLock()
258+
259+
def _make_key(
260+
self,
261+
text: str,
262+
version: Optional[int],
263+
box_size: int,
264+
error_correct: str,
265+
border: int,
266+
fill_color: str,
267+
back_color: str,
268+
) -> str:
269+
"""Create cache key from QR code parameters."""
270+
return f"{text}|{version}|{box_size}|{error_correct}|{border}|{fill_color}|{back_color}"
271+
272+
def _is_expired(self, timestamp: float) -> bool:
273+
"""Check if cache entry is expired."""
274+
return time.time() - timestamp > self.ttl_seconds
275+
276+
def _cleanup_expired(self):
277+
"""Remove expired entries from cache."""
278+
current_time = time.time()
279+
expired_keys = [
280+
key
281+
for key, (_, timestamp) in self._cache.items()
282+
if current_time - timestamp > self.ttl_seconds
283+
]
284+
for key in expired_keys:
285+
del self._cache[key]
286+
287+
def get(
288+
self,
289+
text: str,
290+
version: Optional[int],
291+
box_size: int,
292+
error_correct: str,
293+
border: int,
294+
fill_color: str,
295+
back_color: str,
296+
) -> Optional:
297+
"""Get cached QR code result if available and not expired."""
298+
key = self._make_key(
299+
text, version, box_size, error_correct, border, fill_color, back_color
300+
)
301+
302+
with self._lock:
303+
if key in self._cache:
304+
result, timestamp = self._cache[key]
305+
if not self._is_expired(timestamp):
306+
# Move to end (most recently used)
307+
self._cache.move_to_end(key)
308+
return result
309+
else:
310+
# Remove expired entry
311+
del self._cache[key]
312+
313+
return None
314+
315+
def put(
316+
self,
317+
text: str,
318+
version: Optional[int],
319+
box_size: int,
320+
error_correct: str,
321+
border: int,
322+
fill_color: str,
323+
back_color: str,
324+
result,
325+
) -> None:
326+
"""Store QR code result in cache."""
327+
key = self._make_key(
328+
text, version, box_size, error_correct, border, fill_color, back_color
329+
)
330+
331+
with self._lock:
332+
# Clean up expired entries periodically
333+
if len(self._cache) % 10 == 0: # Every 10th insertion
334+
self._cleanup_expired()
335+
336+
# Remove oldest entries if at capacity
337+
while len(self._cache) >= self.max_size:
338+
self._cache.popitem(last=False) # Remove oldest (FIFO when at capacity)
339+
340+
# Add new entry
341+
self._cache[key] = (result, time.time())
342+
343+
344+
def _get_error_levels():
345+
try:
346+
import qrcode
347+
348+
C = qrcode.constants
349+
return {
350+
"LOW (~7% WORD RECOVERY / HIGHEST DATA CAPACITY)": C.ERROR_CORRECT_L,
351+
"MEDIUM (~15% WORD RECOVERY)": C.ERROR_CORRECT_M,
352+
"QUARTILE (~25% WORD RECOVERY)": C.ERROR_CORRECT_Q,
353+
"HIGH (~30% WORD RECOVERY / LOWEST DATA CAPACITY)": C.ERROR_CORRECT_H,
354+
"ERROR_CORRECT_L": C.ERROR_CORRECT_L,
355+
"ERROR_CORRECT_M": C.ERROR_CORRECT_M,
356+
"ERROR_CORRECT_Q": C.ERROR_CORRECT_Q,
357+
"ERROR_CORRECT_H": C.ERROR_CORRECT_H,
358+
"L": C.ERROR_CORRECT_L,
359+
"M": C.ERROR_CORRECT_M,
360+
"Q": C.ERROR_CORRECT_Q,
361+
"H": C.ERROR_CORRECT_H,
362+
}
363+
except ImportError:
364+
raise ImportError(
365+
"qrcode library is required for QR code generation. "
366+
"Install it with: pip install qrcode"
367+
)
368+
369+
370+
_ERROR_LEVELS = None
371+
_QR_CACHE = _QRCodeLRUCache(max_size=100, ttl_seconds=3600)

requirements/_requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ backoff~=2.2.0
4646
filelock>=3.12.0,<=3.17.0
4747
onvif-zeep-async==2.0.0 # versions > 2.0.0 will not work with Python 3.9 despite docs
4848
simple-pid~=2.0.1
49+
qrcode~=8.0.0

0 commit comments

Comments
 (0)