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
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FastQR - Fast QR Code Generator
|
|
5
|
+
* PHP Binding using FFI
|
|
6
|
+
*
|
|
7
|
+
* Copyright (C) 2025 FastQR Project
|
|
8
|
+
* Licensed under LGPL-2.1
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
namespace FastQR;
|
|
12
|
+
|
|
13
|
+
use FFI;
|
|
14
|
+
use RuntimeException;
|
|
15
|
+
|
|
16
|
+
class FastQR
|
|
17
|
+
{
|
|
18
|
+
private static ?FFI $ffi = null;
|
|
19
|
+
private static bool $initialized = false;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize FFI
|
|
23
|
+
*/
|
|
24
|
+
private static function init(): void
|
|
25
|
+
{
|
|
26
|
+
if (self::$initialized) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Detect platform
|
|
31
|
+
$os = PHP_OS_FAMILY === 'Darwin' ? 'macos' : (PHP_OS_FAMILY === 'Linux' ? 'linux' : 'unknown');
|
|
32
|
+
$arch = php_uname('m');
|
|
33
|
+
if ($arch === 'x86_64' || $arch === 'amd64') {
|
|
34
|
+
$arch = 'x86_64';
|
|
35
|
+
} elseif ($arch === 'aarch64' || $arch === 'arm64') {
|
|
36
|
+
$arch = 'arm64';
|
|
37
|
+
}
|
|
38
|
+
$platform = "$os-$arch";
|
|
39
|
+
$ext = $os === 'macos' ? 'dylib' : 'so';
|
|
40
|
+
|
|
41
|
+
// Try to find the library (pre-built first, then system)
|
|
42
|
+
$libPaths = [
|
|
43
|
+
__DIR__ . "/../../prebuilt/$platform/lib/libfastqr.$ext", // Pre-built binary
|
|
44
|
+
'/usr/local/lib/libfastqr.' . $ext, // System install
|
|
45
|
+
'/usr/lib/libfastqr.' . $ext,
|
|
46
|
+
__DIR__ . '/../../../build/libfastqr.' . $ext, // Local build
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
$libPath = null;
|
|
50
|
+
foreach ($libPaths as $path) {
|
|
51
|
+
if (file_exists($path)) {
|
|
52
|
+
$libPath = $path;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if ($libPath === null) {
|
|
58
|
+
throw new RuntimeException(
|
|
59
|
+
"FastQR library not found for platform: $platform\n" .
|
|
60
|
+
"Please install fastqr or build from source."
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Define C interface
|
|
65
|
+
$header = '
|
|
66
|
+
typedef struct {
|
|
67
|
+
int size;
|
|
68
|
+
int optimize_size;
|
|
69
|
+
unsigned char foreground_r;
|
|
70
|
+
unsigned char foreground_g;
|
|
71
|
+
unsigned char foreground_b;
|
|
72
|
+
unsigned char background_r;
|
|
73
|
+
unsigned char background_g;
|
|
74
|
+
unsigned char background_b;
|
|
75
|
+
int ec_level;
|
|
76
|
+
const char* logo_path;
|
|
77
|
+
int logo_size_percent;
|
|
78
|
+
const char* format;
|
|
79
|
+
int quality;
|
|
80
|
+
} QROptions;
|
|
81
|
+
|
|
82
|
+
bool fastqr_generate(const char* data, const char* output_path, QROptions* options);
|
|
83
|
+
const char* fastqr_version(void);
|
|
84
|
+
';
|
|
85
|
+
|
|
86
|
+
self::$ffi = FFI::cdef($header, $libPath);
|
|
87
|
+
self::$initialized = true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generate QR code
|
|
92
|
+
*
|
|
93
|
+
* @param string $data Data to encode (UTF-8 supported)
|
|
94
|
+
* @param string $outputPath Path to save the QR code image
|
|
95
|
+
* @param array $options Generation options
|
|
96
|
+
* @return bool true if successful
|
|
97
|
+
*
|
|
98
|
+
* @throws RuntimeException if generation fails
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```php
|
|
102
|
+
* use FastQR\FastQR;
|
|
103
|
+
*
|
|
104
|
+
* // Basic usage
|
|
105
|
+
* FastQR::generate('Hello World', 'qr.png');
|
|
106
|
+
*
|
|
107
|
+
* // With options
|
|
108
|
+
* FastQR::generate('Hello', 'qr.png', [
|
|
109
|
+
* 'size' => 500,
|
|
110
|
+
* 'optimizeSize' => true,
|
|
111
|
+
* 'foreground' => [255, 0, 0],
|
|
112
|
+
* 'background' => [255, 255, 200],
|
|
113
|
+
* 'errorLevel' => 'H'
|
|
114
|
+
* ]);
|
|
115
|
+
*
|
|
116
|
+
* // With logo
|
|
117
|
+
* FastQR::generate('Company', 'qr.png', [
|
|
118
|
+
* 'size' => 600,
|
|
119
|
+
* 'logo' => 'logo.png',
|
|
120
|
+
* 'logoSize' => 25
|
|
121
|
+
* ]);
|
|
122
|
+
*
|
|
123
|
+
* // UTF-8 support
|
|
124
|
+
* FastQR::generate('Xin chào Việt Nam! 🇻🇳', 'vietnamese.png');
|
|
125
|
+
* FastQR::generate('こんにちは日本', 'japanese.png');
|
|
126
|
+
* ```
|
|
127
|
+
*/
|
|
128
|
+
public static function generate(string $data, string $outputPath, array $options = []): bool
|
|
129
|
+
{
|
|
130
|
+
self::init();
|
|
131
|
+
|
|
132
|
+
if (empty($data)) {
|
|
133
|
+
throw new RuntimeException('Data cannot be empty');
|
|
134
|
+
}
|
|
135
|
+
if (empty($outputPath)) {
|
|
136
|
+
throw new RuntimeException('Output path cannot be empty');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Create options struct
|
|
140
|
+
$opts = self::$ffi->new('QROptions');
|
|
141
|
+
|
|
142
|
+
// Size (preferred) or width/height (backward compatibility)
|
|
143
|
+
if (isset($options['size'])) {
|
|
144
|
+
$opts->size = $options['size'];
|
|
145
|
+
} elseif (isset($options['width']) || isset($options['height'])) {
|
|
146
|
+
$opts->size = $options['width'] ?? $options['height'] ?? 300;
|
|
147
|
+
} else {
|
|
148
|
+
$opts->size = 300;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Optimize size
|
|
152
|
+
$opts->optimize_size = $options['optimizeSize'] ?? false ? 1 : 0;
|
|
153
|
+
|
|
154
|
+
// Foreground color
|
|
155
|
+
$fg = $options['foreground'] ?? [0, 0, 0];
|
|
156
|
+
$opts->foreground_r = $fg[0] ?? 0;
|
|
157
|
+
$opts->foreground_g = $fg[1] ?? 0;
|
|
158
|
+
$opts->foreground_b = $fg[2] ?? 0;
|
|
159
|
+
|
|
160
|
+
// Background color
|
|
161
|
+
$bg = $options['background'] ?? [255, 255, 255];
|
|
162
|
+
$opts->background_r = $bg[0] ?? 255;
|
|
163
|
+
$opts->background_g = $bg[1] ?? 255;
|
|
164
|
+
$opts->background_b = $bg[2] ?? 255;
|
|
165
|
+
|
|
166
|
+
// Error correction level
|
|
167
|
+
$ecLevel = $options['errorLevel'] ?? 'M';
|
|
168
|
+
$opts->ec_level = match($ecLevel) {
|
|
169
|
+
'L' => 0,
|
|
170
|
+
'M' => 1,
|
|
171
|
+
'Q' => 2,
|
|
172
|
+
'H' => 3,
|
|
173
|
+
default => 1
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// Logo
|
|
177
|
+
$opts->logo_path = $options['logo'] ?? null;
|
|
178
|
+
$opts->logo_size_percent = $options['logoSize'] ?? 20;
|
|
179
|
+
|
|
180
|
+
// Format and quality
|
|
181
|
+
$opts->format = $options['format'] ?? 'png';
|
|
182
|
+
$opts->quality = $options['quality'] ?? 95;
|
|
183
|
+
|
|
184
|
+
// Call C function
|
|
185
|
+
$result = self::$ffi->fastqr_generate($data, $outputPath, FFI::addr($opts));
|
|
186
|
+
|
|
187
|
+
if (!$result) {
|
|
188
|
+
throw new RuntimeException('Failed to generate QR code');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get library version
|
|
196
|
+
*
|
|
197
|
+
* @return string Version string
|
|
198
|
+
*/
|
|
199
|
+
public static function version(): string
|
|
200
|
+
{
|
|
201
|
+
self::init();
|
|
202
|
+
return self::$ffi->fastqr_version();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Generate multiple QR codes in batch mode (7x faster!)
|
|
207
|
+
*
|
|
208
|
+
* @param array $dataArray Array of strings to encode
|
|
209
|
+
* @param string $outputDir Directory to save QR codes (will be created if it doesn't exist)
|
|
210
|
+
* @param array $options Generation options (same as generate)
|
|
211
|
+
* @return array Result with 'success' and 'failed' counts
|
|
212
|
+
*
|
|
213
|
+
* @throws RuntimeException if generation fails
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```php
|
|
217
|
+
* use FastQR\FastQR;
|
|
218
|
+
*
|
|
219
|
+
* // Batch generation
|
|
220
|
+
* $data = ['QR 1', 'QR 2', 'QR 3'];
|
|
221
|
+
* FastQR::generateBatch($data, 'output_dir/', ['size' => 500]);
|
|
222
|
+
* // Creates: output_dir/1.png, output_dir/2.png, output_dir/3.png
|
|
223
|
+
* ```
|
|
224
|
+
*/
|
|
225
|
+
public static function generateBatch(array $dataArray, string $outputDir, array $options = []): array
|
|
226
|
+
{
|
|
227
|
+
if (empty($dataArray)) {
|
|
228
|
+
throw new RuntimeException('Data array cannot be empty');
|
|
229
|
+
}
|
|
230
|
+
if (empty($outputDir)) {
|
|
231
|
+
throw new RuntimeException('Output directory cannot be empty');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Create output directory
|
|
235
|
+
if (!is_dir($outputDir)) {
|
|
236
|
+
mkdir($outputDir, 0755, true);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Create temporary batch file
|
|
240
|
+
$tempFile = tempnam(sys_get_temp_dir(), 'fastqr_batch_') . '.txt';
|
|
241
|
+
try {
|
|
242
|
+
file_put_contents($tempFile, implode("\n", $dataArray));
|
|
243
|
+
|
|
244
|
+
// Find CLI binary
|
|
245
|
+
$os = PHP_OS_FAMILY === 'Darwin' ? 'macos' : (PHP_OS_FAMILY === 'Linux' ? 'linux' : 'unknown');
|
|
246
|
+
$arch = php_uname('m');
|
|
247
|
+
if ($arch === 'x86_64' || $arch === 'amd64') {
|
|
248
|
+
$arch = 'x86_64';
|
|
249
|
+
} elseif ($arch === 'aarch64' || $arch === 'arm64') {
|
|
250
|
+
$arch = 'arm64';
|
|
251
|
+
}
|
|
252
|
+
$platform = "$os-$arch";
|
|
253
|
+
|
|
254
|
+
$cliPaths = [
|
|
255
|
+
__DIR__ . "/../../prebuilt/$platform/bin/fastqr",
|
|
256
|
+
'/usr/local/bin/fastqr',
|
|
257
|
+
__DIR__ . '/../../../build/fastqr',
|
|
258
|
+
];
|
|
259
|
+
|
|
260
|
+
$cliPath = null;
|
|
261
|
+
foreach ($cliPaths as $path) {
|
|
262
|
+
if (file_exists($path)) {
|
|
263
|
+
$cliPath = $path;
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if ($cliPath === null) {
|
|
269
|
+
throw new RuntimeException("FastQR CLI not found for platform: $platform");
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Build command
|
|
273
|
+
$cmd = escapeshellarg($cliPath) . ' -F ' . escapeshellarg($tempFile) . ' ' . escapeshellarg($outputDir);
|
|
274
|
+
|
|
275
|
+
if (isset($options['size'])) {
|
|
276
|
+
$cmd .= ' -s ' . (int)$options['size'];
|
|
277
|
+
}
|
|
278
|
+
if (!empty($options['optimizeSize'])) {
|
|
279
|
+
$cmd .= ' -o';
|
|
280
|
+
}
|
|
281
|
+
if (isset($options['foreground'])) {
|
|
282
|
+
$fg = $options['foreground'];
|
|
283
|
+
$cmd .= ' -f ' . implode(',', $fg);
|
|
284
|
+
}
|
|
285
|
+
if (isset($options['background'])) {
|
|
286
|
+
$bg = $options['background'];
|
|
287
|
+
$cmd .= ' -b ' . implode(',', $bg);
|
|
288
|
+
}
|
|
289
|
+
if (isset($options['errorLevel'])) {
|
|
290
|
+
$cmd .= ' -e ' . escapeshellarg($options['errorLevel']);
|
|
291
|
+
}
|
|
292
|
+
if (isset($options['logo'])) {
|
|
293
|
+
$cmd .= ' -l ' . escapeshellarg($options['logo']);
|
|
294
|
+
}
|
|
295
|
+
if (isset($options['logoSize'])) {
|
|
296
|
+
$cmd .= ' -p ' . (int)$options['logoSize'];
|
|
297
|
+
}
|
|
298
|
+
if (isset($options['quality'])) {
|
|
299
|
+
$cmd .= ' -q ' . (int)$options['quality'];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
exec($cmd . ' 2>&1', $output, $returnCode);
|
|
303
|
+
|
|
304
|
+
if ($returnCode !== 0) {
|
|
305
|
+
throw new RuntimeException('Batch generation failed: ' . implode("\n", $output));
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return ['success' => count($dataArray), 'failed' => 0];
|
|
309
|
+
} finally {
|
|
310
|
+
if (file_exists($tempFile)) {
|
|
311
|
+
unlink($tempFile);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* FastQR PHP Tests
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
namespace FastQR\Tests;
|
|
8
|
+
|
|
9
|
+
use FastQR\FastQR;
|
|
10
|
+
use PHPUnit\Framework\TestCase;
|
|
11
|
+
|
|
12
|
+
class FastQRTest extends TestCase
|
|
13
|
+
{
|
|
14
|
+
private string $outputDir;
|
|
15
|
+
|
|
16
|
+
protected function setUp(): void
|
|
17
|
+
{
|
|
18
|
+
$this->outputDir = __DIR__ . '/output';
|
|
19
|
+
if (!is_dir($this->outputDir)) {
|
|
20
|
+
mkdir($this->outputDir, 0755, true);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public function testVersion(): void
|
|
25
|
+
{
|
|
26
|
+
$version = FastQR::version();
|
|
27
|
+
$this->assertNotEmpty($version);
|
|
28
|
+
$this->assertMatchesRegularExpression('/^\d+\.\d+\.\d+$/', $version);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
public function testBasicGeneration(): void
|
|
32
|
+
{
|
|
33
|
+
$output = $this->outputDir . '/basic.png';
|
|
34
|
+
$result = FastQR::generate('Hello, FastQR from PHP!', $output);
|
|
35
|
+
|
|
36
|
+
$this->assertTrue($result);
|
|
37
|
+
$this->assertFileExists($output);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public function testColoredQR(): void
|
|
41
|
+
{
|
|
42
|
+
$output = $this->outputDir . '/colored.png';
|
|
43
|
+
$result = FastQR::generate('Colored QR', $output, [
|
|
44
|
+
'width' => 500,
|
|
45
|
+
'height' => 500,
|
|
46
|
+
'foreground' => [255, 0, 0],
|
|
47
|
+
'background' => [255, 255, 200]
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
$this->assertTrue($result);
|
|
51
|
+
$this->assertFileExists($output);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public function testVietnameseText(): void
|
|
55
|
+
{
|
|
56
|
+
$output = $this->outputDir . '/vietnamese.png';
|
|
57
|
+
$result = FastQR::generate('Xin chào Việt Nam! 🇻🇳', $output);
|
|
58
|
+
|
|
59
|
+
$this->assertTrue($result);
|
|
60
|
+
$this->assertFileExists($output);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public function testJapaneseText(): void
|
|
64
|
+
{
|
|
65
|
+
$output = $this->outputDir . '/japanese.png';
|
|
66
|
+
$result = FastQR::generate('こんにちは日本', $output, [
|
|
67
|
+
'foreground' => [0, 0, 255]
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
$this->assertTrue($result);
|
|
71
|
+
$this->assertFileExists($output);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
public function testHighErrorCorrection(): void
|
|
75
|
+
{
|
|
76
|
+
$output = $this->outputDir . '/high_ec.png';
|
|
77
|
+
$result = FastQR::generate('High Error Correction', $output, [
|
|
78
|
+
'errorLevel' => 'H'
|
|
79
|
+
]);
|
|
80
|
+
|
|
81
|
+
$this->assertTrue($result);
|
|
82
|
+
$this->assertFileExists($output);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public function testEmptyDataThrowsException(): void
|
|
86
|
+
{
|
|
87
|
+
$this->expectException(\RuntimeException::class);
|
|
88
|
+
FastQR::generate('', 'output.png');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public function testEmptyOutputPathThrowsException(): void
|
|
92
|
+
{
|
|
93
|
+
$this->expectException(\RuntimeException::class);
|
|
94
|
+
FastQR::generate('Hello', '');
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'mkmf'
|
|
2
|
+
|
|
3
|
+
# Check for required libraries
|
|
4
|
+
unless have_library('qrencode')
|
|
5
|
+
abort "ERROR: libqrencode is required. Install it first."
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
unless have_library('vips')
|
|
9
|
+
abort "ERROR: libvips is required. Install it first."
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# Check for headers
|
|
13
|
+
unless have_header('qrencode.h')
|
|
14
|
+
abort "ERROR: qrencode.h not found"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
unless have_header('vips/vips.h')
|
|
18
|
+
abort "ERROR: vips/vips.h not found"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Add C++14 support
|
|
22
|
+
$CXXFLAGS << " -std=c++14"
|
|
23
|
+
|
|
24
|
+
# Set source directory
|
|
25
|
+
$srcs = ['fastqr_ruby.cpp', '../../src/fastqr.cpp']
|
|
26
|
+
$INCFLAGS << " -I$(srcdir)/../../include"
|
|
27
|
+
|
|
28
|
+
create_makefile('fastqr/fastqr')
|
|
29
|
+
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* FastQR Ruby Binding
|
|
3
|
+
* Copyright (C) 2025 FastQR Project
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
#include "fastqr.h"
|
|
7
|
+
#include <ruby.h>
|
|
8
|
+
|
|
9
|
+
static VALUE rb_mFastQR;
|
|
10
|
+
static VALUE rb_cQROptions;
|
|
11
|
+
|
|
12
|
+
// Convert Ruby hash to QROptions
|
|
13
|
+
static fastqr::QROptions hash_to_options(VALUE opts) {
|
|
14
|
+
fastqr::QROptions options;
|
|
15
|
+
|
|
16
|
+
if (NIL_P(opts)) {
|
|
17
|
+
return options;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
VALUE val;
|
|
21
|
+
|
|
22
|
+
// Size (preferred) or width/height (backward compatibility)
|
|
23
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("size")));
|
|
24
|
+
if (!NIL_P(val)) {
|
|
25
|
+
options.size = NUM2INT(val);
|
|
26
|
+
} else {
|
|
27
|
+
// Backward compatibility: if width or height specified, use them
|
|
28
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("width")));
|
|
29
|
+
if (!NIL_P(val)) options.size = NUM2INT(val);
|
|
30
|
+
|
|
31
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("height")));
|
|
32
|
+
if (!NIL_P(val)) options.size = NUM2INT(val);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Optimize size
|
|
36
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("optimize_size")));
|
|
37
|
+
if (!NIL_P(val)) options.optimize_size = RTEST(val);
|
|
38
|
+
|
|
39
|
+
// Foreground color
|
|
40
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("foreground")));
|
|
41
|
+
if (!NIL_P(val) && TYPE(val) == T_ARRAY) {
|
|
42
|
+
options.foreground.r = NUM2INT(rb_ary_entry(val, 0));
|
|
43
|
+
options.foreground.g = NUM2INT(rb_ary_entry(val, 1));
|
|
44
|
+
options.foreground.b = NUM2INT(rb_ary_entry(val, 2));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Background color
|
|
48
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("background")));
|
|
49
|
+
if (!NIL_P(val) && TYPE(val) == T_ARRAY) {
|
|
50
|
+
options.background.r = NUM2INT(rb_ary_entry(val, 0));
|
|
51
|
+
options.background.g = NUM2INT(rb_ary_entry(val, 1));
|
|
52
|
+
options.background.b = NUM2INT(rb_ary_entry(val, 2));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Error correction level
|
|
56
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("error_level")));
|
|
57
|
+
if (!NIL_P(val)) {
|
|
58
|
+
const char* level = StringValueCStr(val);
|
|
59
|
+
if (strcmp(level, "L") == 0) options.ec_level = fastqr::ErrorCorrectionLevel::LOW;
|
|
60
|
+
else if (strcmp(level, "M") == 0) options.ec_level = fastqr::ErrorCorrectionLevel::MEDIUM;
|
|
61
|
+
else if (strcmp(level, "Q") == 0) options.ec_level = fastqr::ErrorCorrectionLevel::QUARTILE;
|
|
62
|
+
else if (strcmp(level, "H") == 0) options.ec_level = fastqr::ErrorCorrectionLevel::HIGH;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Logo path
|
|
66
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("logo")));
|
|
67
|
+
if (!NIL_P(val)) {
|
|
68
|
+
options.logo_path = StringValueCStr(val);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Logo size
|
|
72
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("logo_size")));
|
|
73
|
+
if (!NIL_P(val)) {
|
|
74
|
+
options.logo_size_percent = NUM2INT(val);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Quality
|
|
78
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("quality")));
|
|
79
|
+
if (!NIL_P(val)) {
|
|
80
|
+
options.quality = NUM2INT(val);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Format
|
|
84
|
+
val = rb_hash_aref(opts, ID2SYM(rb_intern("format")));
|
|
85
|
+
if (!NIL_P(val)) {
|
|
86
|
+
options.format = StringValueCStr(val);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return options;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// FastQR.generate(data, output_path, options = {})
|
|
93
|
+
static VALUE rb_fastqr_generate(int argc, VALUE *argv, VALUE self) {
|
|
94
|
+
VALUE data, output_path, opts;
|
|
95
|
+
rb_scan_args(argc, argv, "21", &data, &output_path, &opts);
|
|
96
|
+
|
|
97
|
+
std::string data_str = StringValueCStr(data);
|
|
98
|
+
std::string output_str = StringValueCStr(output_path);
|
|
99
|
+
fastqr::QROptions options = hash_to_options(opts);
|
|
100
|
+
|
|
101
|
+
bool result = fastqr::generate(data_str, output_str, options);
|
|
102
|
+
|
|
103
|
+
return result ? Qtrue : Qfalse;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// FastQR.version
|
|
107
|
+
static VALUE rb_fastqr_version(VALUE self) {
|
|
108
|
+
return rb_str_new_cstr(fastqr::version());
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
extern "C" void Init_fastqr() {
|
|
112
|
+
rb_mFastQR = rb_define_module("FastQR");
|
|
113
|
+
|
|
114
|
+
rb_define_module_function(rb_mFastQR, "generate",
|
|
115
|
+
RUBY_METHOD_FUNC(rb_fastqr_generate), -1);
|
|
116
|
+
rb_define_module_function(rb_mFastQR, "version",
|
|
117
|
+
RUBY_METHOD_FUNC(rb_fastqr_version), 0);
|
|
118
|
+
|
|
119
|
+
// Constants
|
|
120
|
+
rb_define_const(rb_mFastQR, "VERSION", rb_str_new_cstr(fastqr::version()));
|
|
121
|
+
}
|
|
122
|
+
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FastQR
|
|
4
|
+
# Platform detection and pre-built binary management
|
|
5
|
+
module Platform
|
|
6
|
+
class << self
|
|
7
|
+
# Detects the current platform
|
|
8
|
+
# @return [String] Platform identifier (e.g., 'macos-arm64', 'linux-x86_64')
|
|
9
|
+
def detect
|
|
10
|
+
os = RbConfig::CONFIG['host_os']
|
|
11
|
+
arch = RbConfig::CONFIG['host_cpu']
|
|
12
|
+
|
|
13
|
+
case os
|
|
14
|
+
when /darwin/
|
|
15
|
+
case arch
|
|
16
|
+
when /arm64|aarch64/
|
|
17
|
+
'macos-arm64'
|
|
18
|
+
when /x86_64|x64/
|
|
19
|
+
'macos-x86_64'
|
|
20
|
+
else
|
|
21
|
+
raise "Unsupported macOS architecture: #{arch}"
|
|
22
|
+
end
|
|
23
|
+
when /linux/
|
|
24
|
+
case arch
|
|
25
|
+
when /x86_64|x64/
|
|
26
|
+
'linux-x86_64'
|
|
27
|
+
when /arm64|aarch64/
|
|
28
|
+
'linux-arm64'
|
|
29
|
+
else
|
|
30
|
+
raise "Unsupported Linux architecture: #{arch}"
|
|
31
|
+
end
|
|
32
|
+
else
|
|
33
|
+
raise "Unsupported platform: #{os}"
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extracts pre-built binary from tarball
|
|
38
|
+
# @param tarball_path [String] Path to the tarball
|
|
39
|
+
# @param dest_dir [String] Destination directory
|
|
40
|
+
def extract_binary(tarball_path, dest_dir)
|
|
41
|
+
FileUtils.mkdir_p(dest_dir)
|
|
42
|
+
system("tar -xzf '#{tarball_path}' -C '#{dest_dir}'") or raise "Failed to extract #{tarball_path}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Finds the fastqr binary
|
|
46
|
+
# @return [String] Path to fastqr binary
|
|
47
|
+
def find_binary
|
|
48
|
+
platform = detect
|
|
49
|
+
prebuilt_dir = File.expand_path("../../prebuilt/#{platform}", __dir__)
|
|
50
|
+
binary_path = File.join(prebuilt_dir, 'fastqr')
|
|
51
|
+
|
|
52
|
+
return binary_path if File.exist?(binary_path)
|
|
53
|
+
|
|
54
|
+
# Try to extract from tarball
|
|
55
|
+
tarball_path = File.expand_path("../../prebuilt/#{platform}.tar.gz", __dir__)
|
|
56
|
+
if File.exist?(tarball_path)
|
|
57
|
+
puts "Extracting pre-built binary from #{tarball_path}..."
|
|
58
|
+
extract_binary(tarball_path, prebuilt_dir)
|
|
59
|
+
|
|
60
|
+
if File.exist?(binary_path)
|
|
61
|
+
File.chmod(0755, binary_path)
|
|
62
|
+
return binary_path
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
raise "Pre-built binary not found for #{platform}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|