Skip to content

Commit 24fe1c1

Browse files
committed
rMQR: add SizeStrategy (Balanced/MinHeight/MaxHeight) for shape control
rMQR spans flat-and-wide to tall-and-narrow form factors; RmqrEncoder::build_with / build_text_with let the caller pick the shape (min-height 77x7, balanced 43x11, max-height 43x17 for the same data) instead of always minimizing area.
1 parent 22bde9e commit 24fe1c1

3 files changed

Lines changed: 90 additions & 11 deletions

File tree

‎src/codes/rmqr/encode.rs‎

Lines changed: 50 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
77
use super::matrix::Canvas;
88
use super::tables::{char_count_bits, info, mode_value};
9-
use super::{RmqrEcLevel, RmqrMeta, RmqrSize};
9+
use super::{RmqrEcLevel, RmqrMeta, RmqrSize, SizeStrategy};
1010
use crate::codes::qr::gf;
1111
use crate::error::{Error, Result};
1212
use crate::output::Encoding;
@@ -26,9 +26,20 @@ impl RmqrEncoder {
2626
}
2727

2828
/// Build a reproducible [`Symbol`] from `segments` at EC `level`, choosing the
29-
/// smallest-area fitting size.
29+
/// smallest-area fitting size ([`SizeStrategy::Balanced`]).
3030
pub fn build(&self, segments: Vec<Segment>, level: RmqrEcLevel) -> Result<Symbol> {
31-
let size = choose_size(&segments, level)?;
31+
self.build_with(segments, level, SizeStrategy::Balanced)
32+
}
33+
34+
/// Build a reproducible [`Symbol`], choosing the fitting size per `strategy`
35+
/// (flat-and-wide, tall-and-narrow, or most-compact).
36+
pub fn build_with(
37+
&self,
38+
segments: Vec<Segment>,
39+
level: RmqrEcLevel,
40+
strategy: SizeStrategy,
41+
) -> Result<Symbol> {
42+
let size = choose_size(&segments, level, strategy)?;
3243
let meta = RmqrMeta {
3344
size,
3445
ec_level: level,
@@ -44,6 +55,20 @@ impl RmqrEncoder {
4455
pub fn build_text(&self, text: &str, level: RmqrEcLevel) -> Result<Symbol> {
4556
self.build(vec![Segment::byte(text.as_bytes().to_vec())], level)
4657
}
58+
59+
/// Convenience: [`build_with`](Self::build_with) from UTF-8 `text`.
60+
pub fn build_text_with(
61+
&self,
62+
text: &str,
63+
level: RmqrEcLevel,
64+
strategy: SizeStrategy,
65+
) -> Result<Symbol> {
66+
self.build_with(
67+
vec![Segment::byte(text.as_bytes().to_vec())],
68+
level,
69+
strategy,
70+
)
71+
}
4772
}
4873

4974
impl Encode for RmqrEncoder {
@@ -186,21 +211,35 @@ fn segments_bit_len(segments: &[Segment], size: RmqrSize) -> Option<usize> {
186211
}
187212

188213
/// The smallest-area size (at `level`) whose data capacity holds `segments`.
189-
fn choose_size(segments: &[Segment], level: RmqrEcLevel) -> Result<RmqrSize> {
190-
let mut best: Option<(RmqrSize, usize)> = None;
214+
fn choose_size(
215+
segments: &[Segment],
216+
level: RmqrEcLevel,
217+
strategy: SizeStrategy,
218+
) -> Result<RmqrSize> {
219+
let mut best: Option<RmqrSize> = None;
191220
for size in RmqrSize::all() {
192221
let cap = info(size).data_bit_capacity(level);
193222
if let Some(len) = segments_bit_len(segments, size)
194223
&& len <= cap
224+
&& best.is_none_or(|b| prefers(size, b, strategy))
195225
{
196-
let area = size.width() * size.height();
197-
if best.as_ref().is_none_or(|&(_, a)| area < a) {
198-
best = Some((size, area));
199-
}
226+
best = Some(size);
227+
}
228+
}
229+
best.ok_or_else(|| Error::capacity("data does not fit any rMQR size at this EC level"))
230+
}
231+
232+
/// Whether `a` is preferred over the current best `b` under `strategy`. Every
233+
/// strategy uses smaller area as the tie-breaker so the result stays compact.
234+
fn prefers(a: RmqrSize, b: RmqrSize, strategy: SizeStrategy) -> bool {
235+
let area = |s: RmqrSize| s.width() * s.height();
236+
match strategy {
237+
SizeStrategy::Balanced => area(a) < area(b),
238+
SizeStrategy::MinHeight => (a.height(), area(a)) < (b.height(), area(b)),
239+
SizeStrategy::MaxHeight => {
240+
a.height() > b.height() || (a.height() == b.height() && area(a) < area(b))
200241
}
201242
}
202-
best.map(|(s, _)| s)
203-
.ok_or_else(|| Error::capacity("data does not fit any rMQR size at this EC level"))
204243
}
205244

206245
/// Build the full interleaved codeword bit stream (data + EC + remainder).

‎src/codes/rmqr/mod.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,20 @@ pub enum RmqrEcLevel {
7979
H,
8080
}
8181

82+
/// How [`RmqrEncoder::build_with`](crate::codes::rmqr::RmqrEncoder::build_with)
83+
/// chooses among the rMQR sizes that fit the data. rMQR spans flat-and-wide to
84+
/// tall-and-narrow form factors, so the caller can pick the shape for the space.
85+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
86+
pub enum SizeStrategy {
87+
/// Smallest total module area — the most compact fitting symbol (default).
88+
#[default]
89+
Balanced,
90+
/// Prefer the shortest (flattest, widest) fitting symbol.
91+
MinHeight,
92+
/// Prefer the tallest (narrowest) fitting symbol.
93+
MaxHeight,
94+
}
95+
8296
/// rMQR-specific parameters needed to re-encode a symbol identically.
8397
#[derive(Debug, Clone, PartialEq, Eq)]
8498
pub struct RmqrMeta {

‎tests/rmqr.rs‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,3 +238,29 @@ fn wrong_size_matrix_is_undecodable() {
238238
let m = BitMatrix::new(40, 8, 2);
239239
assert!(RmqrDecoder::new().decode(&Encoding::Matrix(m)).is_err());
240240
}
241+
242+
#[test]
243+
fn size_strategy_picks_shape() {
244+
use anyd::codes::rmqr::{RmqrEcLevel, RmqrEncoder, SizeStrategy};
245+
use anyd::output::Encoding;
246+
use anyd::traits::Encode;
247+
let e = RmqrEncoder::new();
248+
let dims = |s| {
249+
let sym = e
250+
.build_text_with("HELLO WORLD 12345", RmqrEcLevel::M, s)
251+
.unwrap();
252+
match e.encode(&sym).unwrap() {
253+
Encoding::Matrix(m) => (m.width(), m.height()),
254+
_ => unreachable!(),
255+
}
256+
};
257+
let (_, bh) = dims(SizeStrategy::Balanced);
258+
let (_, minh) = dims(SizeStrategy::MinHeight);
259+
let (_, maxh) = dims(SizeStrategy::MaxHeight);
260+
// min-height must be the shortest, max-height the tallest, all valid rMQR heights.
261+
assert!(minh <= bh && bh <= maxh);
262+
assert!(minh < maxh, "strategies should differ ({minh} vs {maxh})");
263+
for h in [minh, bh, maxh] {
264+
assert!([7, 9, 11, 13, 15, 17].contains(&h));
265+
}
266+
}

0 commit comments

Comments
 (0)