Commit f5e0fc56 authored by Travis Skare's avatar Travis Skare Committed by Commit Bot

Add support for selectable versions in QR encoder.

Bug: 1088012
Change-Id: I5f09de4fb703864e13af8b67f6f4f130735f82fe
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2238491
Commit-Queue: Travis Skare <skare@chromium.org>
Reviewed-by: default avatarAdam Langley <agl@chromium.org>
Cr-Commit-Position: refs/heads/master@{#786083}
parent 00d1fa84
...@@ -29,7 +29,7 @@ class QRView : public views::View { ...@@ -29,7 +29,7 @@ class QRView : public views::View {
static constexpr int kDinoTilePixels = 3; static constexpr int kDinoTilePixels = 3;
// kMid is the pixel offset from the top (or left) to the middle of the // kMid is the pixel offset from the top (or left) to the middle of the
// displayed QR code. // displayed QR code.
static constexpr int kMid = (kTilePixels * (2 + QRCode::kSize + 2)) / 2; static constexpr int kMid = (kTilePixels * (2 + QRCode::V5::kSize + 2)) / 2;
// kDinoX is the x-coordinate of the dino image. // kDinoX is the x-coordinate of the dino image.
static constexpr int kDinoX = static constexpr int kDinoX =
kMid - (dino_image::kDinoWidth * kDinoTilePixels) / 2; kMid - (dino_image::kDinoWidth * kDinoTilePixels) / 2;
...@@ -37,21 +37,30 @@ class QRView : public views::View { ...@@ -37,21 +37,30 @@ class QRView : public views::View {
static constexpr int kDinoY = static constexpr int kDinoY =
kMid - (dino_image::kDinoHeight * kDinoTilePixels) / 2; kMid - (dino_image::kDinoHeight * kDinoTilePixels) / 2;
explicit QRView(base::span<const uint8_t> qr_data) explicit QRView(base::span<const uint8_t> qr_data) {
: qr_tiles_(qr_.Generate(qr_data)) {} base::Optional<QRCode::GeneratedCode> code = qr_.Generate(qr_data);
DCHECK(code);
// The QR Encoder supports dynamic sizing but we expect our data to fit in
// a version five code.
DCHECK(code->qr_size == QRCode::V5::kSize);
qr_tiles_ = code->data;
}
~QRView() override = default; ~QRView() override = default;
void RefreshQRCode(base::span<const uint8_t> new_qr_data) { void RefreshQRCode(base::span<const uint8_t> new_qr_data) {
state_ = (state_ + 1) % 6; state_ = (state_ + 1) % 6;
qr_tiles_ = qr_.Generate(new_qr_data); base::Optional<QRCode::GeneratedCode> code = qr_.Generate(new_qr_data);
DCHECK(code);
qr_tiles_ = code->data;
SchedulePaint(); SchedulePaint();
} }
// View: // View:
gfx::Size CalculatePreferredSize() const override { gfx::Size CalculatePreferredSize() const override {
// A two-tile border is required around the QR code. // A two-tile border is required around the QR code.
return gfx::Size((2 + QRCode::kSize + 2) * kTilePixels, return gfx::Size((2 + QRCode::V5::kSize + 2) * kTilePixels,
(2 + QRCode::kSize + 2) * kTilePixels); (2 + QRCode::V5::kSize + 2) * kTilePixels);
} }
void OnPaint(gfx::Canvas* canvas) override { void OnPaint(gfx::Canvas* canvas) override {
...@@ -83,28 +92,28 @@ class QRView : public views::View { ...@@ -83,28 +92,28 @@ class QRView : public views::View {
// Draw the two-tile border around the edge. // Draw the two-tile border around the edge.
// Top. // Top.
canvas->FillRect( canvas->FillRect(gfx::Rect(0, 0, (2 + QRCode::V5::kSize + 2) * kTilePixels,
gfx::Rect(0, 0, (2 + QRCode::kSize + 2) * kTilePixels, 2 * kTilePixels), 2 * kTilePixels),
off); off);
// Bottom. // Bottom.
canvas->FillRect( canvas->FillRect(
gfx::Rect(0, (2 + QRCode::kSize) * kTilePixels, gfx::Rect(0, (2 + QRCode::V5::kSize) * kTilePixels,
(2 + QRCode::kSize + 2) * kTilePixels, 2 * kTilePixels), (2 + QRCode::V5::kSize + 2) * kTilePixels, 2 * kTilePixels),
off); off);
// Left // Left
canvas->FillRect(gfx::Rect(0, 2 * kTilePixels, 2 * kTilePixels, canvas->FillRect(gfx::Rect(0, 2 * kTilePixels, 2 * kTilePixels,
QRCode::kSize * kTilePixels), QRCode::V5::kSize * kTilePixels),
off); off);
// Right // Right
canvas->FillRect( canvas->FillRect(
gfx::Rect((2 + QRCode::kSize) * kTilePixels, 2 * kTilePixels, gfx::Rect((2 + QRCode::V5::kSize) * kTilePixels, 2 * kTilePixels,
2 * kTilePixels, QRCode::kSize * kTilePixels), 2 * kTilePixels, QRCode::V5::kSize * kTilePixels),
off); off);
// Paint the QR code. // Paint the QR code.
int index = 0; int index = 0;
for (int y = 0; y < QRCode::kSize; y++) { for (int y = 0; y < QRCode::V5::kSize; y++) {
for (int x = 0; x < QRCode::kSize; x++) { for (int x = 0; x < QRCode::V5::kSize; x++) {
SkColor tile_color = qr_tiles_[index++] & 1 ? on : off; SkColor tile_color = qr_tiles_[index++] & 1 ? on : off;
canvas->FillRect(gfx::Rect((x + 2) * kTilePixels, (y + 2) * kTilePixels, canvas->FillRect(gfx::Rect((x + 2) * kTilePixels, (y + 2) * kTilePixels,
kTilePixels, kTilePixels), kTilePixels, kTilePixels),
...@@ -152,7 +161,7 @@ class QRView : public views::View { ...@@ -152,7 +161,7 @@ class QRView : public views::View {
} }
QRCode qr_; QRCode qr_;
base::span<const uint8_t, QRCode::kTotalSize> qr_tiles_; base::span<const uint8_t> qr_tiles_;
int state_ = 0; int state_ = 0;
DISALLOW_COPY_AND_ASSIGN(QRView); DISALLOW_COPY_AND_ASSIGN(QRView);
...@@ -169,7 +178,7 @@ constexpr size_t Base64EncodedSize(size_t input_length) { ...@@ -169,7 +178,7 @@ constexpr size_t Base64EncodedSize(size_t input_length) {
// |qr_generator_key| and the current time such that the caBLE discovery code // |qr_generator_key| and the current time such that the caBLE discovery code
// can recognise the URL as valid. // can recognise the URL as valid.
base::span<uint8_t> QRDataForCurrentTime( base::span<uint8_t> QRDataForCurrentTime(
uint8_t out_buf[QRCode::kInputBytes], uint8_t out_buf[QRCode::V5::kInputBytes],
base::span<const uint8_t, 32> qr_generator_key) { base::span<const uint8_t, 32> qr_generator_key) {
const int64_t current_tick = device::CableDiscoveryData::CurrentTimeTick(); const int64_t current_tick = device::CableDiscoveryData::CurrentTimeTick();
const device::CableQRData qr_data = const device::CableQRData qr_data =
...@@ -187,7 +196,7 @@ base::span<uint8_t> QRDataForCurrentTime( ...@@ -187,7 +196,7 @@ base::span<uint8_t> QRDataForCurrentTime(
static constexpr char kPrefix[] = "fido://c1/"; static constexpr char kPrefix[] = "fido://c1/";
static constexpr size_t kPrefixLength = sizeof(kPrefix) - 1; static constexpr size_t kPrefixLength = sizeof(kPrefix) - 1;
static_assert(QRCode::kInputBytes >= kPrefixLength + kEncodedDataLength, static_assert(QRCode::V5::kInputBytes >= kPrefixLength + kEncodedDataLength,
"unexpected QR input length"); "unexpected QR input length");
memcpy(out_buf, kPrefix, kPrefixLength); memcpy(out_buf, kPrefix, kPrefixLength);
memcpy(&out_buf[kPrefixLength], base64_qr_data.data(), kEncodedDataLength); memcpy(&out_buf[kPrefixLength], base64_qr_data.data(), kEncodedDataLength);
...@@ -228,7 +237,7 @@ AuthenticatorQRSheetView::~AuthenticatorQRSheetView() = default; ...@@ -228,7 +237,7 @@ AuthenticatorQRSheetView::~AuthenticatorQRSheetView() = default;
std::unique_ptr<views::View> std::unique_ptr<views::View>
AuthenticatorQRSheetView::BuildStepSpecificContent() { AuthenticatorQRSheetView::BuildStepSpecificContent() {
uint8_t qr_data_buf[QRCode::kInputBytes]; uint8_t qr_data_buf[QRCode::V5::kInputBytes];
auto qr_view = std::make_unique<AuthenticatorQRViewCentered>( auto qr_view = std::make_unique<AuthenticatorQRViewCentered>(
QRDataForCurrentTime(qr_data_buf, qr_generator_key_)); QRDataForCurrentTime(qr_data_buf, qr_generator_key_));
qr_view_ = qr_view.get(); qr_view_ = qr_view.get();
...@@ -239,6 +248,6 @@ AuthenticatorQRSheetView::BuildStepSpecificContent() { ...@@ -239,6 +248,6 @@ AuthenticatorQRSheetView::BuildStepSpecificContent() {
} }
void AuthenticatorQRSheetView::Update() { void AuthenticatorQRSheetView::Update() {
uint8_t qr_data_buf[QRCode::kInputBytes]; uint8_t qr_data_buf[QRCode::V5::kInputBytes];
qr_view_->RefreshQRCode(QRDataForCurrentTime(qr_data_buf, qr_generator_key_)); qr_view_->RefreshQRCode(QRDataForCurrentTime(qr_data_buf, qr_generator_key_));
} }
...@@ -9,38 +9,134 @@ ...@@ -9,38 +9,134 @@
#include <stdint.h> #include <stdint.h>
#include "base/containers/span.h" #include "base/containers/span.h"
#include "base/optional.h"
// QRCodeGenerator generates version five, class M QR codes that carry 84 // QRCodeGenerator generates class M QR codes of various versions.
// bytes of raw data. References in the following comments refer to ISO 18004 // References in the following comments refer to ISO 18004 (3rd edition).
// (3rd edition). // Supports versions up to 26 by adding constants.
class QRCodeGenerator { class QRCodeGenerator {
public: public:
// kSize is the number of "tiles" in each dimension for a v5 QR code. See // A structure containing QR version-specific constants and data.
// table 1. (The colored squares in in QR codes are called tiles in the // All versions currently use error correction at level M.
// spec.) struct QRVersionInfo {
static constexpr int kSize = 37; constexpr QRVersionInfo(const int version,
// kTotalSize is the total number of tiles for a v5 QR code, in both const int size,
// directions. const size_t total_bytes,
static constexpr int kTotalSize = kSize * kSize; const size_t group_bytes,
// These values are taken from table 9 (page 38) for a version five, class M const size_t num_segments,
// QR code. (That table is very badly formatted for version five, the rows in const size_t segment_data_bytes,
// the last column don't line up correctly. The correct value for 5M is const size_t group_bytes_1,
// (67,43,12).) const size_t num_segments_1,
static constexpr size_t kTotalBytes = 134; const size_t segment_data_bytes_1)
static constexpr size_t kNumSegments = 2; : version(version),
static constexpr size_t kSegmentDataBytes = 43; size(size),
total_bytes(total_bytes),
static constexpr size_t kSegmentBytes = kTotalBytes / kNumSegments; group_bytes(group_bytes),
static constexpr size_t kSegmentECBytes = kSegmentBytes - kSegmentDataBytes; num_segments(num_segments),
static constexpr size_t kDataBytes = kSegmentDataBytes * kNumSegments; segment_data_bytes(segment_data_bytes),
// Two bytes of overhead are needed for QR framing. group_bytes_1(group_bytes_1),
static constexpr size_t kInputBytes = kDataBytes - 2; num_segments_1(num_segments_1),
segment_data_bytes_1(segment_data_bytes_1) {}
// Generate generates a QR code containing the given data and returns a
// pointer to an array of kTotalSize bytes where the least-significant bit of // The version of the QR code.
// each byte is set if that tile should be "black". The length of |in| must be const int version;
// less than, or equal to, |kInputBytes|.
base::span<uint8_t, kTotalSize> Generate(base::span<const uint8_t> in); // The number of "tiles" in each dimension for a QR code of |version|. See
// table 1. (The colored squares in in QR codes are called tiles in the
// spec.)
const int size;
// Values taken from Table 9, page 38, for a QR code of version |version|.
const size_t total_bytes; // Sum of group_bytes for each group.
const size_t group_bytes;
const size_t num_segments;
const size_t segment_data_bytes;
const size_t group_bytes_1;
const size_t num_segments_1;
const size_t segment_data_bytes_1;
// Total number of tiles for the QR code, size*size.
constexpr int total_size() const { return size * size; }
constexpr size_t segment_bytes() const {
return group_bytes / num_segments;
}
constexpr size_t segment_ec_bytes() const {
return segment_bytes() - segment_data_bytes;
}
constexpr size_t data_bytes() const {
return segment_data_bytes * num_segments;
}
constexpr size_t segment_bytes_1() const {
if (num_segments_1 == 0)
return 0;
return group_bytes_1 / num_segments_1;
}
constexpr size_t segment_ec_bytes_1() const {
return segment_bytes_1() - segment_data_bytes_1;
}
constexpr size_t data_bytes_1() const {
return segment_data_bytes_1 * num_segments_1;
}
// Two bytes of overhead are needed for QR framing.
// If extending beyond version 26, framing would need to be updated.
size_t input_bytes() const {
int framing_bytes = version <= 9 ? 2 : 3;
return data_bytes() + data_bytes_1() - framing_bytes;
}
DISALLOW_COPY_AND_ASSIGN(QRVersionInfo);
};
// Contains output data for Generate().
// The default state contains no data.
struct GeneratedCode {
public:
GeneratedCode();
GeneratedCode(GeneratedCode&&);
~GeneratedCode();
// Pixel data; pointer to an array of bytes, where the least-significant
// bit of each byte is set if that tile should be "black".
// Clients should ensure four modules of padding when rendering the code.
// On error, will not be populated, and will evaluate to false.
base::span<uint8_t> data;
// Width and height (which are equal) of the generated data, in tiles.
int qr_size = 0;
DISALLOW_COPY_AND_ASSIGN(GeneratedCode);
};
// Static parameters for V5 QR codes.
// These exist while migrating clients to dynamic sizes.
struct V5 {
static constexpr int kSize = 37;
static constexpr int kTotalSize = kSize * kSize;
static constexpr size_t kNumSegments = 2;
static constexpr size_t kSegmentDataBytes = 43;
static constexpr size_t kDataBytes = kSegmentDataBytes * kNumSegments;
static constexpr size_t kInputBytes = kDataBytes - 2;
};
QRCodeGenerator();
~QRCodeGenerator();
// Returns parameters for different QR code versions, or nullptr if the
// version is not supported (you may provide support in the cc file).
static const QRVersionInfo* GetVersionInfo(int version);
// Generates a QR code containing the given data.
// The generator will attempt to choose a version that fits the data. The
// returned span's length is input-dependent and not known at compile-time in
// this case.
base::Optional<GeneratedCode> Generate(base::span<const uint8_t> in);
private: private:
// MaskFunction3 implements one of the data-masking functions. See figure 21. // MaskFunction3 implements one of the data-masking functions. See figure 21.
...@@ -88,14 +184,27 @@ class QRCodeGenerator { ...@@ -88,14 +184,27 @@ class QRCodeGenerator {
// AddErrorCorrection writes the Reed-Solomon expanded version of |in| to // AddErrorCorrection writes the Reed-Solomon expanded version of |in| to
// |out|. // |out|.
static void AddErrorCorrection(uint8_t out[kSegmentBytes], // |out| should have length segment_bytes for the code's version.
const uint8_t in[kSegmentDataBytes]); // |in| should have length segment_data_bytes for the code's version.
// |segment_bytes| and |segment_ec_bytes| must be provided for the current
// version/level/group.
void AddErrorCorrection(uint8_t out[],
const uint8_t in[],
size_t segment_bytes,
size_t segment_ec_bytes);
// Parameters for the currently-selected version of the QR code.
// Generate() will pick a version that can contain enough data.
// Unowned; nullptr until initialized in Generate().
const QRVersionInfo* version_info_ = nullptr;
// d_ represents a QR code with one byte per pixel. Each byte is one pixel. // d_ represents a QR code with one byte per pixel. Each byte is one pixel.
// The LSB is set if the pixel is "black". The second bit is set if the pixel // The LSB is set if the pixel is "black". The second bit is set if the pixel
// is part of the structure of the QR code, i.e. finder or alignment symbols, // is part of the structure of the QR code, i.e. finder or alignment symbols,
// timing patterns, or format data. // timing patterns, or format data.
uint8_t d_[kSize * kSize]; // Initialized and possibly reinitialized in Generate().
std::unique_ptr<uint8_t[]> d_;
// clip_dump_ is the target of paints that would otherwise fall outside of the // clip_dump_ is the target of paints that would otherwise fall outside of the
// QR code. // QR code.
uint8_t clip_dump_; uint8_t clip_dump_;
......
...@@ -13,14 +13,43 @@ TEST(QRCodeGenerator, Generate) { ...@@ -13,14 +13,43 @@ TEST(QRCodeGenerator, Generate) {
// run under ASan, this will also check that every byte of the output has been // run under ASan, this will also check that every byte of the output has been
// written to. // written to.
QRCodeGenerator qr; QRCodeGenerator qr;
uint8_t input[QRCodeGenerator::kInputBytes]; uint8_t input[QRCodeGenerator::V5::kInputBytes];
memset(input, 'a', sizeof(input)); memset(input, 'a', sizeof(input));
auto qr_data = qr.Generate(input); base::Optional<QRCodeGenerator::GeneratedCode> qr_code = qr.Generate(input);
ASSERT_NE(qr_code, base::nullopt);
auto& qr_data = qr_code->data;
int index = 0; int index = 0;
for (int y = 0; y < QRCodeGenerator::kSize; y++) { ASSERT_EQ(qr_data.size(),
for (int x = 0; x < QRCodeGenerator::kSize; x++) { static_cast<size_t>(QRCodeGenerator::V5::kTotalSize));
for (int y = 0; y < QRCodeGenerator::V5::kSize; y++) {
for (int x = 0; x < QRCodeGenerator::V5::kSize; x++) {
ASSERT_EQ(0, qr_data[index++] & 0b11111100); ASSERT_EQ(0, qr_data[index++] & 0b11111100);
} }
} }
} }
TEST(QRCodeGenerator, GenerateVersionSelection) {
// Ensure that dynamic version selection works,
// even when reusing the same QR Generator object.
// Longer inputs produce longer codes.
QRCodeGenerator qr;
uint8_t input_small[10];
memset(input_small, 'a', sizeof(input_small));
base::Optional<QRCodeGenerator::GeneratedCode> qr_code_small(
qr.Generate(input_small));
ASSERT_NE(qr_code_small, base::nullopt);
int size_small = qr_code_small->data.size();
uint8_t input_large[100];
memset(input_large, 'a', sizeof(input_large));
base::Optional<QRCodeGenerator::GeneratedCode> qr_code_large(
qr.Generate(input_large));
ASSERT_NE(qr_code_large, base::nullopt);
int size_large = qr_code_large->data.size();
ASSERT_GT(size_small, 0);
ASSERT_GT(size_large, 0);
ASSERT_GT(size_large, size_small);
}
...@@ -226,34 +226,52 @@ void QRCodeGeneratorServiceImpl::GenerateQRCode( ...@@ -226,34 +226,52 @@ void QRCodeGeneratorServiceImpl::GenerateQRCode(
mojom::GenerateQRCodeResponsePtr response = mojom::GenerateQRCodeResponsePtr response =
mojom::GenerateQRCodeResponse::New(); mojom::GenerateQRCodeResponse::New();
// TODO(skare): Use a higher QR code version to allow for more data. if (!request->data.data()) {
// TODO(skare): cap string length with message in the UI. response->error_code = mojom::QRCodeGeneratorError::UNKNOWN_ERROR;
if (request->data.length() > QRCodeGenerator::kInputBytes) { std::move(callback).Run(std::move(response));
return;
}
// These lengths account for the dino we superimpose, which
// cuts into error correction bits.
constexpr size_t kLengthSmall = 84;
constexpr size_t kLengthMedium = 122;
// TODO(skare): Test very large codes with readers:
// constexpr size_t kLengthLarge = 331;
constexpr size_t kLengthMax = kLengthMedium;
if (request->data.length() > kLengthMax) {
response->error_code = mojom::QRCodeGeneratorError::INPUT_TOO_LONG; response->error_code = mojom::QRCodeGeneratorError::INPUT_TOO_LONG;
std::move(callback).Run(std::move(response)); std::move(callback).Run(std::move(response));
return; return;
} }
// TODO(skare): Use a higher QR code vdersion. Until then, use the size
// from the common encoder.
const gfx::Size qr_output_data_size = {QRCodeGenerator::kSize,
QRCodeGenerator::kSize};
// TODO(skare): cap string length with message in the UI. // TODO(skare): cap string length with message in the UI.
uint8_t input[QRCodeGenerator::kInputBytes + 1] = {0}; uint8_t input[kLengthMax + 1] = {0};
base::strlcpy(reinterpret_cast<char*>(input), request->data.c_str(), base::strlcpy(reinterpret_cast<char*>(input), request->data.c_str(),
QRCodeGenerator::kInputBytes); kLengthMax);
size_t data_size =
request->data.length() <= kLengthSmall ? kLengthSmall : kLengthMedium;
// Pad with spaces rather than null for better reader compatibility. // Pad with spaces rather than null for better reader compatibility.
// This will go away when supporting multiple versions/data length. for (size_t i = request->data.length(); i < data_size; i++)
for (size_t i = request->data.length(); i < QRCodeGenerator::kInputBytes; i++)
input[i] = 0x20; input[i] = 0x20;
input[QRCodeGenerator::kInputBytes] = 0; input[data_size - 1] = 0;
QRCodeGenerator qr; QRCodeGenerator qr;
auto qr_data_span = qr.Generate(base::span<const uint8_t>( base::Optional<QRCodeGenerator::GeneratedCode> qr_data =
reinterpret_cast<const uint8_t*>(request->data.data()), qr.Generate(base::span<const uint8_t>(
request->data.size())); reinterpret_cast<const uint8_t*>(request->data.data()),
request->data.size()));
if (!qr_data) {
// The above check should have caught the too-long-URL case.
// Remaining errors can be treated as UNKNOWN.
response->error_code = mojom::QRCodeGeneratorError::UNKNOWN_ERROR;
std::move(callback).Run(std::move(response));
return;
}
auto& qr_data_span = qr_data->data;
// The least significant bit of each byte in |qr_data_span| is set if the tile // The least significant bit of each byte in |qr_data.span| is set if the tile
// should be black. // should be black.
for (size_t i = 0; i < qr_data_span.size(); i++) { for (size_t i = 0; i < qr_data_span.size(); i++) {
qr_data_span[i] &= 1; qr_data_span[i] &= 1;
...@@ -261,8 +279,9 @@ void QRCodeGeneratorServiceImpl::GenerateQRCode( ...@@ -261,8 +279,9 @@ void QRCodeGeneratorServiceImpl::GenerateQRCode(
response->data.insert(response->data.begin(), qr_data_span.begin(), response->data.insert(response->data.begin(), qr_data_span.begin(),
qr_data_span.end()); qr_data_span.end());
response->data_size = qr_output_data_size; response->data_size = {qr_data_span.size(), qr_data_span.size()};
response->error_code = mojom::QRCodeGeneratorError::NONE; response->error_code = mojom::QRCodeGeneratorError::NONE;
gfx::Size qr_output_data_size = {qr_data->qr_size, qr_data->qr_size};
RenderBitmap(qr_data_span.data(), qr_output_data_size, request, &response); RenderBitmap(qr_data_span.data(), qr_output_data_size, request, &response);
std::move(callback).Run(std::move(response)); std::move(callback).Run(std::move(response));
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment