fastqr 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/BUILD.md +482 -0
- data/CHANGELOG.md +51 -0
- data/CMakeLists.txt +126 -0
- data/CONTRIBUTING.md +63 -0
- data/DISTRIBUTION.md +730 -0
- data/INSTALL.md +171 -0
- data/LICENSE +39 -0
- data/PREBUILT.md +171 -0
- data/README.md +312 -0
- data/VERSION +1 -0
- data/bindings/nodejs/binding.gyp +38 -0
- data/bindings/nodejs/fastqr_node.cpp +125 -0
- data/bindings/nodejs/index.d.ts +80 -0
- data/bindings/nodejs/index.js +187 -0
- data/bindings/nodejs/lib/platform.js +72 -0
- data/bindings/nodejs/package.json +45 -0
- data/bindings/nodejs/test/test.js +45 -0
- data/bindings/php/fastqr_php.cpp +85 -0
- data/bindings/php/src/FastQR.php +316 -0
- data/bindings/php/tests/FastQRTest.php +97 -0
- data/bindings/ruby/extconf.rb +29 -0
- data/bindings/ruby/fastqr_ruby.cpp +122 -0
- data/bindings/ruby/lib/fastqr/platform.rb +70 -0
- data/bindings/ruby/lib/fastqr/version.rb +6 -0
- data/bindings/ruby/lib/fastqr.rb +129 -0
- data/bindings/ruby/prebuilt/macos-arm64.tar.gz +1 -0
- data/bindings/ruby/prebuilt/macos-x86_64.tar.gz +1 -0
- data/build.sh +109 -0
- data/cmake/fastqrConfig.cmake.in +6 -0
- data/composer.json +36 -0
- data/docs/CLI_USAGE.md +478 -0
- data/docs/NODEJS_USAGE.md +694 -0
- data/docs/PHP_USAGE.md +815 -0
- data/docs/README.md +191 -0
- data/docs/RUBY_USAGE.md +537 -0
- data/examples/CMakeLists.txt +7 -0
- data/examples/basic.cpp +58 -0
- data/include/fastqr.h +97 -0
- data/include/stb_image.h +7988 -0
- data/include/stb_image_write.h +1724 -0
- data/phpunit.xml +18 -0
- data/prebuilt/README.md +131 -0
- data/scripts/README.md +248 -0
- data/scripts/build-binaries.sh +98 -0
- data/scripts/build-local.sh +18 -0
- data/scripts/install.sh +87 -0
- data/scripts/release.sh +78 -0
- data/scripts/update-version.sh +58 -0
- data/src/cli.cpp +316 -0
- data/src/fastqr.cpp +665 -0
- data/test.sh +86 -0
- metadata +155 -0
data/src/fastqr.cpp
ADDED
|
@@ -0,0 +1,665 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FastQR - Fast QR Code Generator Library
|
|
3
|
+
* Copyright (C) 2025 FastQR Project
|
|
4
|
+
*
|
|
5
|
+
* This library is free software; you can redistribute it and/or
|
|
6
|
+
* modify it under the terms of the GNU Lesser General Public
|
|
7
|
+
* License as published by the Free Software Foundation; either
|
|
8
|
+
* version 2.1 of the License, or (at your option) any later version.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
#include "fastqr.h"
|
|
12
|
+
#include <qrencode.h>
|
|
13
|
+
#include <png.h>
|
|
14
|
+
#include <zlib.h>
|
|
15
|
+
#define STB_IMAGE_IMPLEMENTATION
|
|
16
|
+
#include "stb_image.h"
|
|
17
|
+
#include <cstring>
|
|
18
|
+
#include <memory>
|
|
19
|
+
#include <iostream>
|
|
20
|
+
#include <vector>
|
|
21
|
+
#include <algorithm>
|
|
22
|
+
#include <cstdio>
|
|
23
|
+
#include <chrono>
|
|
24
|
+
|
|
25
|
+
// Enable benchmarking
|
|
26
|
+
// #define FASTQR_BENCHMARK
|
|
27
|
+
|
|
28
|
+
#define FASTQR_VERSION "1.0.1"
|
|
29
|
+
|
|
30
|
+
namespace fastqr {
|
|
31
|
+
|
|
32
|
+
// RAII wrapper for QRcode
|
|
33
|
+
struct QRCodeDeleter {
|
|
34
|
+
void operator()(QRcode* qr) const {
|
|
35
|
+
if (qr) QRcode_free(qr);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
using QRCodePtr = std::unique_ptr<QRcode, QRCodeDeleter>;
|
|
39
|
+
|
|
40
|
+
// Detect text encoding and return appropriate encoding mode
|
|
41
|
+
static QRecLevel to_qr_level(ErrorCorrectionLevel level) {
|
|
42
|
+
switch (level) {
|
|
43
|
+
case ErrorCorrectionLevel::LOW: return QR_ECLEVEL_L;
|
|
44
|
+
case ErrorCorrectionLevel::MEDIUM: return QR_ECLEVEL_M;
|
|
45
|
+
case ErrorCorrectionLevel::QUARTILE: return QR_ECLEVEL_Q;
|
|
46
|
+
case ErrorCorrectionLevel::HIGH: return QR_ECLEVEL_H;
|
|
47
|
+
default: return QR_ECLEVEL_M;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Detect if string contains only ASCII
|
|
52
|
+
static bool is_ascii(const std::string& str) {
|
|
53
|
+
for (unsigned char c : str) {
|
|
54
|
+
if (c > 127) return false;
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Generate QR code using libqrencode with proper UTF-8 support
|
|
60
|
+
static QRCodePtr generate_qr_code(const std::string& data, ErrorCorrectionLevel ec_level) {
|
|
61
|
+
QRecLevel level = to_qr_level(ec_level);
|
|
62
|
+
|
|
63
|
+
// Use appropriate encoding based on content
|
|
64
|
+
QRcode* qr = nullptr;
|
|
65
|
+
if (is_ascii(data)) {
|
|
66
|
+
// ASCII data - use standard encoding
|
|
67
|
+
qr = QRcode_encodeString(data.c_str(), 0, level, QR_MODE_8, 1);
|
|
68
|
+
} else {
|
|
69
|
+
// UTF-8 data - encode as 8-bit data
|
|
70
|
+
qr = QRcode_encodeData(data.length(),
|
|
71
|
+
reinterpret_cast<const unsigned char*>(data.c_str()),
|
|
72
|
+
0, level);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!qr) {
|
|
76
|
+
std::cerr << "Failed to generate QR code" << std::endl;
|
|
77
|
+
return nullptr;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return QRCodePtr(qr);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Write indexed PNG (1-bit, black and white) - fastest method
|
|
84
|
+
static bool write_indexed_png(const char* filename, const std::vector<unsigned char>& data,
|
|
85
|
+
int width, int height) {
|
|
86
|
+
FILE* fp = fopen(filename, "wb");
|
|
87
|
+
if (!fp) return false;
|
|
88
|
+
|
|
89
|
+
// Large buffer for faster I/O
|
|
90
|
+
setvbuf(fp, nullptr, _IOFBF, 65536);
|
|
91
|
+
|
|
92
|
+
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
93
|
+
if (!png) {
|
|
94
|
+
fclose(fp);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
png_infop info = png_create_info_struct(png);
|
|
99
|
+
if (!info) {
|
|
100
|
+
png_destroy_write_struct(&png, nullptr);
|
|
101
|
+
fclose(fp);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (setjmp(png_jmpbuf(png))) {
|
|
106
|
+
png_destroy_write_struct(&png, &info);
|
|
107
|
+
fclose(fp);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
png_init_io(png, fp);
|
|
112
|
+
|
|
113
|
+
// Set PNG parameters for 1-bit indexed (palette)
|
|
114
|
+
png_set_IHDR(png, info, width, height, 1, PNG_COLOR_TYPE_PALETTE,
|
|
115
|
+
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
116
|
+
|
|
117
|
+
// Create palette: 0=white, 1=black
|
|
118
|
+
png_color palette[2];
|
|
119
|
+
palette[0].red = palette[0].green = palette[0].blue = 255; // white
|
|
120
|
+
palette[1].red = palette[1].green = palette[1].blue = 0; // black
|
|
121
|
+
png_set_PLTE(png, info, palette, 2);
|
|
122
|
+
|
|
123
|
+
// Level 1 compression - good balance between speed and size
|
|
124
|
+
png_set_compression_level(png, 1);
|
|
125
|
+
png_set_filter(png, 0, PNG_FILTER_NONE);
|
|
126
|
+
|
|
127
|
+
png_write_info(png, info);
|
|
128
|
+
|
|
129
|
+
// Data is already packed (8 pixels per byte), no packing needed
|
|
130
|
+
|
|
131
|
+
// Write image data row by row
|
|
132
|
+
std::vector<png_bytep> row_pointers(height);
|
|
133
|
+
for (int y = 0; y < height; y++) {
|
|
134
|
+
row_pointers[y] = const_cast<png_bytep>(&data[y * ((width + 7) / 8)]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
png_write_image(png, row_pointers.data());
|
|
138
|
+
png_write_end(png, nullptr);
|
|
139
|
+
|
|
140
|
+
png_destroy_write_struct(&png, &info);
|
|
141
|
+
fclose(fp);
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Write grayscale PNG (8-bit)
|
|
146
|
+
static bool write_grayscale_png(const char* filename, const std::vector<unsigned char>& data,
|
|
147
|
+
int width, int height) {
|
|
148
|
+
FILE* fp = fopen(filename, "wb");
|
|
149
|
+
if (!fp) return false;
|
|
150
|
+
|
|
151
|
+
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
152
|
+
if (!png) {
|
|
153
|
+
fclose(fp);
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
png_infop info = png_create_info_struct(png);
|
|
158
|
+
if (!info) {
|
|
159
|
+
png_destroy_write_struct(&png, nullptr);
|
|
160
|
+
fclose(fp);
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (setjmp(png_jmpbuf(png))) {
|
|
165
|
+
png_destroy_write_struct(&png, &info);
|
|
166
|
+
fclose(fp);
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
png_init_io(png, fp);
|
|
171
|
+
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_GRAY,
|
|
172
|
+
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
173
|
+
|
|
174
|
+
png_set_compression_level(png, 1);
|
|
175
|
+
png_set_filter(png, 0, PNG_FILTER_NONE);
|
|
176
|
+
|
|
177
|
+
png_write_info(png, info);
|
|
178
|
+
|
|
179
|
+
std::vector<png_bytep> row_pointers(height);
|
|
180
|
+
for (int y = 0; y < height; y++) {
|
|
181
|
+
row_pointers[y] = const_cast<png_bytep>(&data[y * width]);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
png_write_image(png, row_pointers.data());
|
|
185
|
+
png_write_end(png, nullptr);
|
|
186
|
+
|
|
187
|
+
png_destroy_write_struct(&png, &info);
|
|
188
|
+
fclose(fp);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Write RGB PNG
|
|
193
|
+
static bool write_rgb_png(const char* filename, const std::vector<unsigned char>& data,
|
|
194
|
+
int width, int height) {
|
|
195
|
+
FILE* fp = fopen(filename, "wb");
|
|
196
|
+
if (!fp) return false;
|
|
197
|
+
|
|
198
|
+
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr);
|
|
199
|
+
if (!png) {
|
|
200
|
+
fclose(fp);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
png_infop info = png_create_info_struct(png);
|
|
205
|
+
if (!info) {
|
|
206
|
+
png_destroy_write_struct(&png, nullptr);
|
|
207
|
+
fclose(fp);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (setjmp(png_jmpbuf(png))) {
|
|
212
|
+
png_destroy_write_struct(&png, &info);
|
|
213
|
+
fclose(fp);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
png_init_io(png, fp);
|
|
218
|
+
png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_RGB,
|
|
219
|
+
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT);
|
|
220
|
+
|
|
221
|
+
png_set_compression_level(png, 1);
|
|
222
|
+
png_set_filter(png, 0, PNG_FILTER_NONE);
|
|
223
|
+
|
|
224
|
+
png_write_info(png, info);
|
|
225
|
+
|
|
226
|
+
std::vector<png_bytep> row_pointers(height);
|
|
227
|
+
for (int y = 0; y < height; y++) {
|
|
228
|
+
row_pointers[y] = const_cast<png_bytep>(&data[y * width * 3]);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
png_write_image(png, row_pointers.data());
|
|
232
|
+
png_write_end(png, nullptr);
|
|
233
|
+
|
|
234
|
+
png_destroy_write_struct(&png, &info);
|
|
235
|
+
fclose(fp);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Simple nearest-neighbor resize for logo
|
|
240
|
+
static void resize_logo(const std::vector<unsigned char>& src, int src_w, int src_h, int channels,
|
|
241
|
+
std::vector<unsigned char>& dst, int dst_w, int dst_h) {
|
|
242
|
+
dst.resize(dst_w * dst_h * channels);
|
|
243
|
+
double x_ratio = static_cast<double>(src_w) / dst_w;
|
|
244
|
+
double y_ratio = static_cast<double>(src_h) / dst_h;
|
|
245
|
+
|
|
246
|
+
for (int y = 0; y < dst_h; y++) {
|
|
247
|
+
int src_y = static_cast<int>(y * y_ratio);
|
|
248
|
+
for (int x = 0; x < dst_w; x++) {
|
|
249
|
+
int src_x = static_cast<int>(x * x_ratio);
|
|
250
|
+
for (int c = 0; c < channels; c++) {
|
|
251
|
+
dst[(y * dst_w + x) * channels + c] = src[(src_y * src_w + src_x) * channels + c];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Add logo to QR code (works with grayscale or RGB)
|
|
258
|
+
static void add_logo_to_image(std::vector<unsigned char>& qr_img, int qr_size, int qr_channels,
|
|
259
|
+
const std::string& logo_path, int logo_size_percent) {
|
|
260
|
+
// Load logo
|
|
261
|
+
int logo_w, logo_h, logo_channels;
|
|
262
|
+
unsigned char* logo_data = stbi_load(logo_path.c_str(), &logo_w, &logo_h, &logo_channels, 0);
|
|
263
|
+
|
|
264
|
+
if (!logo_data) {
|
|
265
|
+
std::cerr << "Warning: Failed to load logo: " << logo_path << std::endl;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Calculate logo size
|
|
270
|
+
int logo_target_size = (qr_size * logo_size_percent) / 100;
|
|
271
|
+
int logo_new_w, logo_new_h;
|
|
272
|
+
|
|
273
|
+
// Keep aspect ratio
|
|
274
|
+
if (logo_w > logo_h) {
|
|
275
|
+
logo_new_w = logo_target_size;
|
|
276
|
+
logo_new_h = (logo_h * logo_target_size) / logo_w;
|
|
277
|
+
} else {
|
|
278
|
+
logo_new_h = logo_target_size;
|
|
279
|
+
logo_new_w = (logo_w * logo_target_size) / logo_h;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Resize logo
|
|
283
|
+
std::vector<unsigned char> logo_src(logo_data, logo_data + logo_w * logo_h * logo_channels);
|
|
284
|
+
stbi_image_free(logo_data);
|
|
285
|
+
|
|
286
|
+
std::vector<unsigned char> logo_resized;
|
|
287
|
+
resize_logo(logo_src, logo_w, logo_h, logo_channels, logo_resized, logo_new_w, logo_new_h);
|
|
288
|
+
|
|
289
|
+
// Calculate position (center)
|
|
290
|
+
int start_x = (qr_size - logo_new_w) / 2;
|
|
291
|
+
int start_y = (qr_size - logo_new_h) / 2;
|
|
292
|
+
|
|
293
|
+
// Composite logo onto QR
|
|
294
|
+
for (int y = 0; y < logo_new_h; y++) {
|
|
295
|
+
for (int x = 0; x < logo_new_w; x++) {
|
|
296
|
+
int qr_x = start_x + x;
|
|
297
|
+
int qr_y = start_y + y;
|
|
298
|
+
|
|
299
|
+
if (qr_x >= 0 && qr_x < qr_size && qr_y >= 0 && qr_y < qr_size) {
|
|
300
|
+
int logo_idx = (y * logo_new_w + x) * logo_channels;
|
|
301
|
+
int qr_idx = (qr_y * qr_size + qr_x) * qr_channels;
|
|
302
|
+
|
|
303
|
+
if (logo_channels == 4) {
|
|
304
|
+
// Has alpha channel
|
|
305
|
+
float alpha = logo_resized[logo_idx + 3] / 255.0f;
|
|
306
|
+
|
|
307
|
+
if (qr_channels == 1) {
|
|
308
|
+
// Grayscale QR
|
|
309
|
+
unsigned char logo_gray = (logo_resized[logo_idx] +
|
|
310
|
+
logo_resized[logo_idx + 1] +
|
|
311
|
+
logo_resized[logo_idx + 2]) / 3;
|
|
312
|
+
qr_img[qr_idx] = static_cast<unsigned char>(
|
|
313
|
+
logo_gray * alpha + qr_img[qr_idx] * (1 - alpha)
|
|
314
|
+
);
|
|
315
|
+
} else {
|
|
316
|
+
// RGB QR
|
|
317
|
+
for (int c = 0; c < 3; c++) {
|
|
318
|
+
qr_img[qr_idx + c] = static_cast<unsigned char>(
|
|
319
|
+
logo_resized[logo_idx + c] * alpha + qr_img[qr_idx + c] * (1 - alpha)
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// No alpha, direct copy
|
|
325
|
+
if (qr_channels == 1 && logo_channels >= 3) {
|
|
326
|
+
// Convert RGB logo to grayscale
|
|
327
|
+
qr_img[qr_idx] = (logo_resized[logo_idx] +
|
|
328
|
+
logo_resized[logo_idx + 1] +
|
|
329
|
+
logo_resized[logo_idx + 2]) / 3;
|
|
330
|
+
} else if (qr_channels == 3 && logo_channels == 1) {
|
|
331
|
+
// Grayscale logo to RGB
|
|
332
|
+
qr_img[qr_idx] = qr_img[qr_idx + 1] = qr_img[qr_idx + 2] = logo_resized[logo_idx];
|
|
333
|
+
} else {
|
|
334
|
+
// Same channels
|
|
335
|
+
for (int c = 0; c < std::min(qr_channels, logo_channels); c++) {
|
|
336
|
+
qr_img[qr_idx + c] = logo_resized[logo_idx + c];
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
bool generate(const std::string& data, const std::string& output_path, const QROptions& options) {
|
|
346
|
+
#ifdef FASTQR_BENCHMARK
|
|
347
|
+
auto t_start = std::chrono::high_resolution_clock::now();
|
|
348
|
+
#endif
|
|
349
|
+
|
|
350
|
+
// Generate QR code
|
|
351
|
+
auto qr = generate_qr_code(data, options.ec_level);
|
|
352
|
+
if (!qr) {
|
|
353
|
+
return false;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#ifdef FASTQR_BENCHMARK
|
|
357
|
+
auto t_qr_gen = std::chrono::high_resolution_clock::now();
|
|
358
|
+
auto dur_qr = std::chrono::duration_cast<std::chrono::microseconds>(t_qr_gen - t_start).count();
|
|
359
|
+
#endif
|
|
360
|
+
|
|
361
|
+
int qr_size = qr->width;
|
|
362
|
+
unsigned char* qr_data = qr->data;
|
|
363
|
+
|
|
364
|
+
// Determine final output size
|
|
365
|
+
int final_size = options.size;
|
|
366
|
+
if (options.optimize_size) {
|
|
367
|
+
// Round up to nearest integer multiple for best performance
|
|
368
|
+
int scale = (options.size + qr_size - 1) / qr_size; // Ceiling division
|
|
369
|
+
final_size = scale * qr_size;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Check if using default black/white colors
|
|
373
|
+
bool is_bw = (options.foreground.r == 0 && options.foreground.g == 0 && options.foreground.b == 0 &&
|
|
374
|
+
options.background.r == 255 && options.background.g == 255 && options.background.b == 255);
|
|
375
|
+
|
|
376
|
+
// Check if we can use indexed PNG (only if no logo)
|
|
377
|
+
bool can_use_indexed = is_bw && options.logo_path.empty();
|
|
378
|
+
|
|
379
|
+
if (can_use_indexed) {
|
|
380
|
+
// FASTEST PATH: 1-bit indexed PNG (like qrencode)
|
|
381
|
+
// Pack 8 pixels into 1 byte
|
|
382
|
+
int scale = final_size / qr_size;
|
|
383
|
+
|
|
384
|
+
#ifdef FASTQR_BENCHMARK
|
|
385
|
+
FILE* debug_log = fopen("/tmp/fastqr_debug.log", "a");
|
|
386
|
+
if (debug_log) {
|
|
387
|
+
fprintf(debug_log, "DEBUG: is_bw=%d, qr_size=%d, final_size=%d, scale=%d, scale*qr=%d\n",
|
|
388
|
+
is_bw, qr_size, final_size, scale, scale*qr_size);
|
|
389
|
+
fclose(debug_log);
|
|
390
|
+
}
|
|
391
|
+
#endif
|
|
392
|
+
|
|
393
|
+
if (scale * qr_size == final_size) {
|
|
394
|
+
// Integer scaling - optimized bit packing
|
|
395
|
+
int bytes_per_row = (final_size + 7) / 8;
|
|
396
|
+
std::vector<unsigned char> packed_data(bytes_per_row * final_size, 0);
|
|
397
|
+
|
|
398
|
+
// Build one template row then replicate vertically
|
|
399
|
+
std::vector<unsigned char> template_row(bytes_per_row);
|
|
400
|
+
|
|
401
|
+
for (int src_y = 0; src_y < qr_size; src_y++) {
|
|
402
|
+
// Build template row for this QR row
|
|
403
|
+
std::memset(template_row.data(), 0, bytes_per_row);
|
|
404
|
+
|
|
405
|
+
for (int src_x = 0; src_x < qr_size; src_x++) {
|
|
406
|
+
if (qr_data[src_y * qr_size + src_x] & 1) {
|
|
407
|
+
// Black pixel - set all bits for scaled pixels
|
|
408
|
+
int dst_x_start = src_x * scale;
|
|
409
|
+
int dst_x_end = dst_x_start + scale;
|
|
410
|
+
|
|
411
|
+
for (int dst_x = dst_x_start; dst_x < dst_x_end; dst_x++) {
|
|
412
|
+
int byte_idx = dst_x >> 3;
|
|
413
|
+
int bit_idx = 7 - (dst_x & 7);
|
|
414
|
+
template_row[byte_idx] |= (1 << bit_idx);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Copy template row to all scaled rows
|
|
420
|
+
int dst_y_start = src_y * scale;
|
|
421
|
+
for (int dy = 0; dy < scale; dy++) {
|
|
422
|
+
std::memcpy(&packed_data[(dst_y_start + dy) * bytes_per_row],
|
|
423
|
+
template_row.data(), bytes_per_row);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#ifdef FASTQR_BENCHMARK
|
|
428
|
+
auto t_scale = std::chrono::high_resolution_clock::now();
|
|
429
|
+
auto dur_scale = std::chrono::duration_cast<std::chrono::microseconds>(t_scale - t_qr_gen).count();
|
|
430
|
+
#endif
|
|
431
|
+
|
|
432
|
+
bool result = write_indexed_png(output_path.c_str(), packed_data, final_size, final_size);
|
|
433
|
+
|
|
434
|
+
#ifdef FASTQR_BENCHMARK
|
|
435
|
+
auto t_write = std::chrono::high_resolution_clock::now();
|
|
436
|
+
auto dur_write = std::chrono::duration_cast<std::chrono::microseconds>(t_write - t_scale).count();
|
|
437
|
+
auto dur_total = std::chrono::duration_cast<std::chrono::microseconds>(t_write - t_start).count();
|
|
438
|
+
|
|
439
|
+
FILE* timing_log = fopen("/tmp/fastqr_timing.log", "a");
|
|
440
|
+
if (timing_log) {
|
|
441
|
+
fprintf(timing_log, "TIMING: QR=%ldus, Scale=%ldus, Write=%ldus, Total=%ldus\n",
|
|
442
|
+
dur_qr, dur_scale, dur_write, dur_total);
|
|
443
|
+
fclose(timing_log);
|
|
444
|
+
}
|
|
445
|
+
#endif
|
|
446
|
+
|
|
447
|
+
return result;
|
|
448
|
+
} else {
|
|
449
|
+
// Non-integer scaling - use grayscale
|
|
450
|
+
std::vector<unsigned char> final_image(final_size * final_size);
|
|
451
|
+
double x_ratio = static_cast<double>(qr_size) / final_size;
|
|
452
|
+
double y_ratio = static_cast<double>(qr_size) / final_size;
|
|
453
|
+
|
|
454
|
+
for (int y = 0; y < final_size; y++) {
|
|
455
|
+
int src_y = static_cast<int>(y * y_ratio);
|
|
456
|
+
int src_row_idx = src_y * qr_size;
|
|
457
|
+
for (int x = 0; x < final_size; x++) {
|
|
458
|
+
int src_x = static_cast<int>(x * x_ratio);
|
|
459
|
+
final_image[y * final_size + x] = (qr_data[src_row_idx + src_x] & 1) ? 0 : 255;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// Add logo if specified
|
|
464
|
+
if (!options.logo_path.empty()) {
|
|
465
|
+
add_logo_to_image(final_image, final_size, 1, options.logo_path, options.logo_size_percent);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return write_grayscale_png(output_path.c_str(), final_image, final_size, final_size);
|
|
469
|
+
}
|
|
470
|
+
} else if (is_bw) {
|
|
471
|
+
// Black/white but with logo - use RGB to preserve logo colors
|
|
472
|
+
std::vector<unsigned char> final_image(final_size * final_size * 3);
|
|
473
|
+
int scale = final_size / qr_size;
|
|
474
|
+
|
|
475
|
+
if (scale * qr_size == final_size) {
|
|
476
|
+
// Integer scaling
|
|
477
|
+
std::vector<unsigned char> scaled_row(final_size * 3);
|
|
478
|
+
|
|
479
|
+
for (int src_y = 0; src_y < qr_size; src_y++) {
|
|
480
|
+
for (int src_x = 0; src_x < qr_size; src_x++) {
|
|
481
|
+
unsigned char val = (qr_data[src_y * qr_size + src_x] & 1) ? 0 : 255;
|
|
482
|
+
int dst_x_start = src_x * scale;
|
|
483
|
+
for (int dx = 0; dx < scale; dx++) {
|
|
484
|
+
int idx = (dst_x_start + dx) * 3;
|
|
485
|
+
scaled_row[idx] = scaled_row[idx + 1] = scaled_row[idx + 2] = val;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
int dst_y_start = src_y * scale;
|
|
490
|
+
unsigned char* dst_ptr = &final_image[dst_y_start * final_size * 3];
|
|
491
|
+
std::memcpy(dst_ptr, scaled_row.data(), final_size * 3);
|
|
492
|
+
for (int dy = 1; dy < scale; dy++) {
|
|
493
|
+
std::memcpy(dst_ptr + dy * final_size * 3, dst_ptr, final_size * 3);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
} else {
|
|
497
|
+
// Non-integer scaling
|
|
498
|
+
double x_ratio = static_cast<double>(qr_size) / final_size;
|
|
499
|
+
double y_ratio = static_cast<double>(qr_size) / final_size;
|
|
500
|
+
|
|
501
|
+
for (int y = 0; y < final_size; y++) {
|
|
502
|
+
int src_y = static_cast<int>(y * y_ratio);
|
|
503
|
+
int src_row_idx = src_y * qr_size;
|
|
504
|
+
for (int x = 0; x < final_size; x++) {
|
|
505
|
+
int src_x = static_cast<int>(x * x_ratio);
|
|
506
|
+
unsigned char val = (qr_data[src_row_idx + src_x] & 1) ? 0 : 255;
|
|
507
|
+
int idx = (y * final_size + x) * 3;
|
|
508
|
+
final_image[idx] = final_image[idx + 1] = final_image[idx + 2] = val;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Add logo (RGB)
|
|
514
|
+
add_logo_to_image(final_image, final_size, 3, options.logo_path, options.logo_size_percent);
|
|
515
|
+
|
|
516
|
+
return write_rgb_png(output_path.c_str(), final_image, final_size, final_size);
|
|
517
|
+
} else {
|
|
518
|
+
// COLOR PATH: RGB output for custom colors
|
|
519
|
+
bool is_grayscale = (options.foreground.r == options.foreground.g &&
|
|
520
|
+
options.foreground.g == options.foreground.b &&
|
|
521
|
+
options.background.r == options.background.g &&
|
|
522
|
+
options.background.g == options.background.b);
|
|
523
|
+
|
|
524
|
+
if (is_grayscale) {
|
|
525
|
+
// Use grayscale PNG
|
|
526
|
+
std::vector<unsigned char> final_image(final_size * final_size);
|
|
527
|
+
unsigned char fg = options.foreground.r;
|
|
528
|
+
unsigned char bg = options.background.r;
|
|
529
|
+
|
|
530
|
+
int scale = final_size / qr_size;
|
|
531
|
+
if (scale * qr_size == final_size) {
|
|
532
|
+
// Integer scaling
|
|
533
|
+
std::vector<unsigned char> scaled_row(final_size);
|
|
534
|
+
|
|
535
|
+
for (int src_y = 0; src_y < qr_size; src_y++) {
|
|
536
|
+
for (int src_x = 0; src_x < qr_size; src_x++) {
|
|
537
|
+
unsigned char val = (qr_data[src_y * qr_size + src_x] & 1) ? fg : bg;
|
|
538
|
+
int dst_x_start = src_x * scale;
|
|
539
|
+
std::memset(&scaled_row[dst_x_start], val, scale);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
int dst_y_start = src_y * scale;
|
|
543
|
+
unsigned char* dst_ptr = &final_image[dst_y_start * final_size];
|
|
544
|
+
std::memcpy(dst_ptr, scaled_row.data(), final_size);
|
|
545
|
+
for (int dy = 1; dy < scale; dy++) {
|
|
546
|
+
std::memcpy(dst_ptr + dy * final_size, dst_ptr, final_size);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
// Non-integer scaling
|
|
551
|
+
double x_ratio = static_cast<double>(qr_size) / final_size;
|
|
552
|
+
double y_ratio = static_cast<double>(qr_size) / final_size;
|
|
553
|
+
|
|
554
|
+
for (int y = 0; y < final_size; y++) {
|
|
555
|
+
int src_y = static_cast<int>(y * y_ratio);
|
|
556
|
+
int src_row_idx = src_y * qr_size;
|
|
557
|
+
for (int x = 0; x < final_size; x++) {
|
|
558
|
+
int src_x = static_cast<int>(x * x_ratio);
|
|
559
|
+
final_image[y * final_size + x] = (qr_data[src_row_idx + src_x] & 1) ? fg : bg;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Add logo if specified
|
|
565
|
+
if (!options.logo_path.empty()) {
|
|
566
|
+
add_logo_to_image(final_image, final_size, 1, options.logo_path, options.logo_size_percent);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return write_grayscale_png(output_path.c_str(), final_image, final_size, final_size);
|
|
570
|
+
} else {
|
|
571
|
+
// Full RGB for non-grayscale colors
|
|
572
|
+
std::vector<unsigned char> final_image(final_size * final_size * 3);
|
|
573
|
+
|
|
574
|
+
int scale = final_size / qr_size;
|
|
575
|
+
if (scale * qr_size == final_size) {
|
|
576
|
+
// Integer scaling
|
|
577
|
+
std::vector<unsigned char> scaled_row(final_size * 3);
|
|
578
|
+
|
|
579
|
+
for (int src_y = 0; src_y < qr_size; src_y++) {
|
|
580
|
+
for (int src_x = 0; src_x < qr_size; src_x++) {
|
|
581
|
+
bool is_black = qr_data[src_y * qr_size + src_x] & 1;
|
|
582
|
+
unsigned char r = is_black ? options.foreground.r : options.background.r;
|
|
583
|
+
unsigned char g = is_black ? options.foreground.g : options.background.g;
|
|
584
|
+
unsigned char b = is_black ? options.foreground.b : options.background.b;
|
|
585
|
+
|
|
586
|
+
int dst_x_start = src_x * scale;
|
|
587
|
+
for (int dx = 0; dx < scale; dx++) {
|
|
588
|
+
int idx = (dst_x_start + dx) * 3;
|
|
589
|
+
scaled_row[idx] = r;
|
|
590
|
+
scaled_row[idx + 1] = g;
|
|
591
|
+
scaled_row[idx + 2] = b;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
int dst_y_start = src_y * scale;
|
|
596
|
+
unsigned char* dst_ptr = &final_image[dst_y_start * final_size * 3];
|
|
597
|
+
std::memcpy(dst_ptr, scaled_row.data(), final_size * 3);
|
|
598
|
+
for (int dy = 1; dy < scale; dy++) {
|
|
599
|
+
std::memcpy(dst_ptr + dy * final_size * 3, dst_ptr, final_size * 3);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
} else {
|
|
603
|
+
// Non-integer scaling
|
|
604
|
+
double x_ratio = static_cast<double>(qr_size) / final_size;
|
|
605
|
+
double y_ratio = static_cast<double>(qr_size) / final_size;
|
|
606
|
+
|
|
607
|
+
for (int y = 0; y < final_size; y++) {
|
|
608
|
+
int src_y = static_cast<int>(y * y_ratio);
|
|
609
|
+
int src_row_idx = src_y * qr_size;
|
|
610
|
+
for (int x = 0; x < final_size; x++) {
|
|
611
|
+
int src_x = static_cast<int>(x * x_ratio);
|
|
612
|
+
bool is_black = qr_data[src_row_idx + src_x] & 1;
|
|
613
|
+
int idx = (y * final_size + x) * 3;
|
|
614
|
+
final_image[idx] = is_black ? options.foreground.r : options.background.r;
|
|
615
|
+
final_image[idx + 1] = is_black ? options.foreground.g : options.background.g;
|
|
616
|
+
final_image[idx + 2] = is_black ? options.foreground.b : options.background.b;
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Add logo if specified
|
|
622
|
+
if (!options.logo_path.empty()) {
|
|
623
|
+
add_logo_to_image(final_image, final_size, 3, options.logo_path, options.logo_size_percent);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
return write_rgb_png(output_path.c_str(), final_image, final_size, final_size);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
int generate_to_buffer(const std::string& data, void* buffer, size_t buffer_size, const QROptions& options) {
|
|
632
|
+
// For buffer generation, temporarily write to a temp file then read it back
|
|
633
|
+
// This is a simplified implementation - could be optimized to write directly to memory
|
|
634
|
+
std::string temp_file = "/tmp/fastqr_temp.png";
|
|
635
|
+
|
|
636
|
+
if (!generate(data, temp_file, options)) {
|
|
637
|
+
return -1;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
FILE* fp = fopen(temp_file.c_str(), "rb");
|
|
641
|
+
if (!fp) return -1;
|
|
642
|
+
|
|
643
|
+
fseek(fp, 0, SEEK_END);
|
|
644
|
+
long file_size = ftell(fp);
|
|
645
|
+
fseek(fp, 0, SEEK_SET);
|
|
646
|
+
|
|
647
|
+
if (static_cast<size_t>(file_size) > buffer_size) {
|
|
648
|
+
fclose(fp);
|
|
649
|
+
std::remove(temp_file.c_str());
|
|
650
|
+
return -1;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
size_t read_size = fread(buffer, 1, file_size, fp);
|
|
654
|
+
fclose(fp);
|
|
655
|
+
std::remove(temp_file.c_str());
|
|
656
|
+
|
|
657
|
+
return static_cast<int>(read_size);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const char* version() {
|
|
661
|
+
return FASTQR_VERSION;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
} // namespace fastqr
|
|
665
|
+
|