Skip to content

Commit d604869

Browse files
authored
Merge pull request #35 from OUDON/develop
release: v0.3.1
2 parents 0b61ade + d551f2d commit d604869

File tree

4 files changed

+130
-51
lines changed

4 files changed

+130
-51
lines changed

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[metadata]
22
name = rmqrcode
3-
version = 0.3.0
3+
version = 0.3.1
44
author = Takahiro Tomita
55
author_email = [email protected]
66
description = An rMQR Code Generetor

src/rmqrcode/rmqrcode.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -82,23 +82,24 @@ def fit(data, ecc=ErrorCorrectionLevel.M, fit_strategy=FitStrategy.BALANCED):
8282
logger.debug("Select rMQR Code version")
8383
for version_name, qr_version in DataCapacities.items():
8484
optimizer = qr_segments.SegmentOptimizer()
85-
optimized_segments = optimizer.compute(data, version_name)
86-
data_length = qr_segments.compute_length(optimized_segments, version_name)
87-
88-
if data_length <= qr_version["number_of_data_bits"][ecc]:
89-
width, height = qr_version["width"], qr_version["height"]
90-
if width not in determined_width and height not in determined_height:
91-
determined_width.add(width)
92-
determined_height.add(height)
93-
ok_versions.append(
94-
{
95-
"version": version_name,
96-
"width": width,
97-
"height": height,
98-
"segments": optimized_segments,
99-
}
100-
)
101-
logger.debug(f"ok: {version_name}")
85+
try:
86+
optimized_segments = optimizer.compute(data, version_name, ecc)
87+
except DataTooLongError:
88+
continue
89+
90+
width, height = qr_version["width"], qr_version["height"]
91+
if width not in determined_width and height not in determined_height:
92+
determined_width.add(width)
93+
determined_height.add(height)
94+
ok_versions.append(
95+
{
96+
"version": version_name,
97+
"width": width,
98+
"height": height,
99+
"segments": optimized_segments,
100+
}
101+
)
102+
logger.debug(f"ok: {version_name}")
102103

103104
if len(ok_versions) == 0:
104105
raise DataTooLongError("The data is too long.")
@@ -128,7 +129,7 @@ def sort_key(x):
128129

129130
def _optimized_segments(self, data):
130131
optimizer = qr_segments.SegmentOptimizer()
131-
return optimizer.compute(data, self.version_name())
132+
return optimizer.compute(data, self.version_name(), self._error_correction_level)
132133

133134
def __init__(self, version, ecc, with_quiet_zone=True, logger=None):
134135
self._logger = logger or rMQR._init_logger()

src/rmqrcode/segments.py

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from . import encoder
22
from .errors import DataTooLongError
3+
from .format.data_capacities import DataCapacities
34
from .format.rmqr_versions import rMQRVersions
45

56
encoders = [
@@ -47,12 +48,13 @@ def __init__(self):
4748
self.dp = [[[self.INF for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)]
4849
self.parents = [[[-1 for n in range(3)] for mode in range(4)] for length in range(self.MAX_CHARACTER + 1)]
4950

50-
def compute(self, data, version):
51+
def compute(self, data, version, ecc):
5152
"""Computes the optimize segmentation for the given data.
5253
5354
Args:
5455
data (str): The data to encode.
5556
version (str): The version name.
57+
ecc (rmqrcode.ErrorCorrectionLevel): The error correction level.
5658
5759
Returns:
5860
list: The list of segments.
@@ -66,8 +68,11 @@ def compute(self, data, version):
6668

6769
self.qr_version = rMQRVersions[version]
6870
self._compute_costs(data)
69-
best_index = self._find_best(data)
70-
path = self._reconstruct_path(best_index)
71+
best = self._find_best(data)
72+
if best["cost"] > DataCapacities[version]["number_of_data_bits"][ecc]:
73+
raise DataTooLongError
74+
75+
path = self._reconstruct_path(best["index"])
7176
segments = self._compute_segments(path, data)
7277
return segments
7378

@@ -102,44 +107,76 @@ def _compute_costs(self, data):
102107
if not encoders[new_mode].is_valid_characters(data[n]):
103108
continue
104109

105-
encoder_class = encoders[new_mode]
106-
character_count_indicator_length = self.qr_version["character_count_indicator_length"][
107-
encoder_class
108-
]
109110
if new_mode == mode:
110-
# Keep the mode
111-
if encoder_class == encoder.NumericEncoder:
112-
new_length = (unfilled_length + 1) % 3
113-
cost = 4 if unfilled_length == 0 else 3
114-
elif encoder_class == encoder.AlphanumericEncoder:
115-
new_length = (unfilled_length + 1) % 2
116-
cost = 6 if unfilled_length == 0 else 5
117-
elif encoder_class == encoder.ByteEncoder:
118-
new_length = 0
119-
cost = 8
120-
elif encoder_class == encoder.KanjiEncoder:
121-
new_length = 0
122-
cost = 13
111+
cost, new_length = self._compute_new_state_without_mode_changing(
112+
data[n], new_mode, unfilled_length
113+
)
123114
else:
124-
# Change the mode
125-
if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]:
126-
new_length = 1
127-
elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]:
128-
new_length = 0
129-
cost = encoders[new_mode].length(data[n], character_count_indicator_length)
115+
cost, new_length = self._compute_new_state_with_mode_changing(
116+
data[n], new_mode, unfilled_length
117+
)
130118

131119
if self.dp[n][mode][unfilled_length] + cost < self.dp[n + 1][new_mode][new_length]:
132120
self.dp[n + 1][new_mode][new_length] = self.dp[n][mode][unfilled_length] + cost
133121
self.parents[n + 1][new_mode][new_length] = (n, mode, unfilled_length)
134122

123+
def _compute_new_state_without_mode_changing(self, character, new_mode, unfilled_length):
124+
"""Computes the new state values without mode changing.
125+
126+
Args:
127+
character (str): The current character. Assume this as one length string.
128+
new_mode (int): The state of the new mode.
129+
unfilled_length (int): The state of the current unfilled_length.
130+
131+
Returns:
132+
tuple: (cost, new_length).
133+
134+
"""
135+
encoder_class = encoders[new_mode]
136+
if encoder_class == encoder.NumericEncoder:
137+
new_length = (unfilled_length + 1) % 3
138+
cost = 4 if unfilled_length == 0 else 3
139+
elif encoder_class == encoder.AlphanumericEncoder:
140+
new_length = (unfilled_length + 1) % 2
141+
cost = 6 if unfilled_length == 0 else 5
142+
elif encoder_class == encoder.ByteEncoder:
143+
new_length = 0
144+
cost = 8 * len(character.encode("utf-8"))
145+
elif encoder_class == encoder.KanjiEncoder:
146+
new_length = 0
147+
cost = 13
148+
return (cost, new_length)
149+
150+
def _compute_new_state_with_mode_changing(self, character, new_mode, unfilled_length):
151+
"""Computes the new state values with mode changing.
152+
153+
Args:
154+
character (str): The current character. Assume this as one length string.
155+
new_mode (int): The state of the new mode.
156+
unfilled_length (int): The state of the current unfilled_length.
157+
158+
Returns:
159+
tuple: (cost, new_length).
160+
161+
"""
162+
encoder_class = encoders[new_mode]
163+
character_count_indicator_length = self.qr_version["character_count_indicator_length"][encoder_class]
164+
if encoder_class in [encoder.NumericEncoder, encoder.AlphanumericEncoder]:
165+
new_length = 1
166+
elif encoder_class in [encoder.ByteEncoder, encoder.KanjiEncoder]:
167+
new_length = 0
168+
cost = encoder_class.length(character, character_count_indicator_length)
169+
return (cost, new_length)
170+
135171
def _find_best(self, data):
136172
"""Find the index which has the minimum costs.
137173
138174
Args:
139175
data (str): The data to encode.
140176
141177
Returns:
142-
tuple: The best index as tuple (n, mode, unfilled_length).
178+
dict: The dict object includes "cost" and "index". The "cost" is the value of minimum cost.
179+
The "index" is the index of the dp table as a tuple (n, mode, unfilled_length).
143180
144181
"""
145182
best = self.INF
@@ -149,7 +186,7 @@ def _find_best(self, data):
149186
if self.dp[len(data)][mode][unfilled_length] < best:
150187
best = self.dp[len(data)][mode][unfilled_length]
151188
best_index = (len(data), mode, unfilled_length)
152-
return best_index
189+
return {"cost": best, "index": best_index}
153190

154191
def _reconstruct_path(self, best_index):
155192
"""Reconstructs the path.

tests/segments_test.py

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,59 @@
11
from rmqrcode.segments import SegmentOptimizer, compute_length
2-
from rmqrcode import encoder
2+
from rmqrcode import encoder, ErrorCorrectionLevel, DataTooLongError
33
import pytest
44

55

66
class TestSegments:
7-
def test_can_optimize_segments(self):
7+
def test_can_optimize_segments_numeric_and_byte(self):
88
optimizer = SegmentOptimizer()
9-
segments = optimizer.compute("123Abc", "R7x43")
9+
segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M)
1010
assert segments == [
1111
{"data": "123", "encoder_class": encoder.NumericEncoder},
1212
{"data": "Abc", "encoder_class": encoder.ByteEncoder},
1313
]
1414

15+
def test_can_optimize_segments_alphanumeric_and_kanji(self):
16+
optimizer = SegmentOptimizer()
17+
segments = optimizer.compute("17:30集合", "R7x59", ErrorCorrectionLevel.M)
18+
assert segments == [
19+
{"data": "17:30", "encoder_class": encoder.AlphanumericEncoder},
20+
{"data": "集合", "encoder_class": encoder.KanjiEncoder},
21+
]
22+
23+
def test_can_optimize_segments_numeric_only(self):
24+
optimizer = SegmentOptimizer()
25+
segments = optimizer.compute("123456", "R7x59", ErrorCorrectionLevel.M)
26+
assert segments == [
27+
{"data": "123456", "encoder_class": encoder.NumericEncoder},
28+
]
29+
30+
def test_can_optimize_segments_alphanumeric_only(self):
31+
optimizer = SegmentOptimizer()
32+
segments = optimizer.compute("HTTPS://", "R7x59", ErrorCorrectionLevel.M)
33+
assert segments == [
34+
{"data": "HTTPS://", "encoder_class": encoder.AlphanumericEncoder},
35+
]
36+
37+
def test_can_optimize_segments_byte_only(self):
38+
optimizer = SegmentOptimizer()
39+
segments = optimizer.compute("1+zY!a:K", "R7x59", ErrorCorrectionLevel.M)
40+
assert segments == [
41+
{"data": "1+zY!a:K", "encoder_class": encoder.ByteEncoder},
42+
]
43+
44+
def test_can_optimize_segments_kanji_only(self):
45+
optimizer = SegmentOptimizer()
46+
segments = optimizer.compute("漢字", "R7x59", ErrorCorrectionLevel.M)
47+
assert segments == [
48+
{"data": "漢字", "encoder_class": encoder.KanjiEncoder},
49+
]
50+
51+
def test_optimize_segments_raises_data_too_long_error(self):
52+
optimizer = SegmentOptimizer()
53+
with pytest.raises(DataTooLongError) as e:
54+
segments = optimizer.compute("a" * 12, "R7x59", ErrorCorrectionLevel.M)
55+
1556
def test_compute_length(self):
1657
optimizer = SegmentOptimizer()
17-
segments = optimizer.compute("123Abc", "R7x43")
58+
segments = optimizer.compute("123Abc", "R7x43", ErrorCorrectionLevel.M)
1859
assert compute_length(segments, "R7x43") is 47

0 commit comments

Comments
 (0)