qrdnicapacitor 1.0.8 → 1.0.9
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.
- package/Package.swift +1 -1
- package/android/build.gradle +1 -1
- package/android/libs/qrdnidroid-release.aar +0 -0
- package/android/src/main/java/com/cqesolutions/qrdnicapacitor/QrCodeScanner.java +121 -34
- package/ios/Sources/qrdniPlugin/QRSegmentDecoder.swift +102 -0
- package/ios/Sources/qrdniPlugin/ScannerViewController.swift +82 -110
- package/package.json +1 -1
package/Package.swift
CHANGED
|
@@ -11,7 +11,7 @@ let package = Package(
|
|
|
11
11
|
],
|
|
12
12
|
dependencies: [
|
|
13
13
|
.package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", from: "8.0.0"),
|
|
14
|
-
.package(url: "https://github.com/diegocidm4/iQRDNI.git", from: "1.0.
|
|
14
|
+
.package(url: "https://github.com/diegocidm4/iQRDNI.git", from: "1.0.4"),
|
|
15
15
|
.package(url: "https://github.com/krzyzanowskim/CryptoSwift.git", exact: "1.6.0")
|
|
16
16
|
],
|
|
17
17
|
targets: [
|
package/android/build.gradle
CHANGED
|
@@ -53,7 +53,7 @@ dependencies {
|
|
|
53
53
|
//implementation(name: 'qrdnidroid-release', ext: 'aar')
|
|
54
54
|
implementation files('libs/jpeg2000.jar')
|
|
55
55
|
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
|
|
56
|
-
implementation ('
|
|
56
|
+
implementation ('com.journeyapps:zxing-android-embedded:4.3.0'){
|
|
57
57
|
exclude group: 'com.android.support'
|
|
58
58
|
}
|
|
59
59
|
implementation project(':capacitor-android')
|
|
Binary file
|
|
@@ -7,78 +7,165 @@ import android.util.Log;
|
|
|
7
7
|
|
|
8
8
|
import androidx.appcompat.app.AppCompatActivity;
|
|
9
9
|
|
|
10
|
+
import com.google.zxing.BarcodeFormat;
|
|
11
|
+
import com.google.zxing.DecodeHintType;
|
|
10
12
|
import com.google.zxing.Result;
|
|
13
|
+
import com.google.zxing.ResultMetadataType;
|
|
14
|
+
import com.google.zxing.ResultPoint;
|
|
15
|
+
import com.journeyapps.barcodescanner.BarcodeCallback;
|
|
16
|
+
import com.journeyapps.barcodescanner.BarcodeResult;
|
|
17
|
+
import com.journeyapps.barcodescanner.BarcodeView;
|
|
18
|
+
import com.journeyapps.barcodescanner.DefaultDecoderFactory;
|
|
11
19
|
|
|
12
|
-
import
|
|
20
|
+
import java.io.ByteArrayOutputStream;
|
|
21
|
+
import java.nio.charset.StandardCharsets;
|
|
22
|
+
import java.util.Collections;
|
|
23
|
+
import java.util.HashMap;
|
|
24
|
+
import java.util.List;
|
|
25
|
+
import java.util.Map;
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
public class QrCodeScanner extends AppCompatActivity {
|
|
15
30
|
public static final String KEY_QR_CODE = "qr_code";
|
|
16
|
-
private
|
|
31
|
+
private BarcodeView mBarcodeView;
|
|
17
32
|
private Boolean returnString = true;
|
|
33
|
+
private boolean handled = false; // evita procesar el mismo QR dos veces
|
|
34
|
+
|
|
18
35
|
@Override
|
|
19
36
|
public void onCreate(Bundle state) {
|
|
20
37
|
super.onCreate(state);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
// Set the scanner view as the content view
|
|
24
|
-
if(getIntent().hasExtra("returnString")) {
|
|
38
|
+
|
|
39
|
+
if (getIntent().hasExtra("returnString")) {
|
|
25
40
|
returnString = (Boolean) getIntent().getExtras().get("returnString");
|
|
26
41
|
}
|
|
27
42
|
|
|
28
|
-
|
|
43
|
+
mBarcodeView = new BarcodeView(this);
|
|
44
|
+
|
|
45
|
+
// CONFIGURACIÓN CLAVE: forzar ISO-8859-1 para que los segmentos byte
|
|
46
|
+
// se decodifiquen 1:1 y getText() reconstruya el payload sin pérdida
|
|
47
|
+
Map<DecodeHintType, Object> hints = new HashMap<>();
|
|
48
|
+
hints.put(DecodeHintType.CHARACTER_SET, "ISO-8859-1");
|
|
49
|
+
hints.put(DecodeHintType.TRY_HARDER, Boolean.TRUE);
|
|
50
|
+
|
|
51
|
+
mBarcodeView.setDecoderFactory(new DefaultDecoderFactory(
|
|
52
|
+
Collections.singletonList(BarcodeFormat.QR_CODE), // solo QR
|
|
53
|
+
hints,
|
|
54
|
+
"ISO-8859-1",
|
|
55
|
+
0)); // 0 = scan normal
|
|
56
|
+
|
|
57
|
+
mBarcodeView.setFramingRectSize(new com.journeyapps.barcodescanner.Size(1200, 1200));
|
|
58
|
+
|
|
59
|
+
setContentView(mBarcodeView);
|
|
60
|
+
mBarcodeView.decodeContinuous(callback);
|
|
29
61
|
}
|
|
30
62
|
|
|
31
63
|
@Override
|
|
32
64
|
public void onResume() {
|
|
33
65
|
super.onResume();
|
|
34
|
-
|
|
35
|
-
mScannerView.setResultHandler(this);
|
|
36
|
-
// Start camera on resume
|
|
37
|
-
mScannerView.startCamera();
|
|
66
|
+
mBarcodeView.resume();
|
|
38
67
|
}
|
|
39
68
|
|
|
40
69
|
@Override
|
|
41
70
|
public void onPause() {
|
|
42
71
|
super.onPause();
|
|
43
|
-
|
|
44
|
-
mScannerView.stopCamera();
|
|
72
|
+
mBarcodeView.pause();
|
|
45
73
|
}
|
|
74
|
+
private final BarcodeCallback callback = new BarcodeCallback() {
|
|
75
|
+
@Override
|
|
76
|
+
public void barcodeResult(BarcodeResult barcodeResult) {
|
|
77
|
+
handleResult(barcodeResult.getResult()); // Result original de ZXing
|
|
78
|
+
}
|
|
46
79
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
@Override
|
|
81
|
+
public void possibleResultPoints(List<ResultPoint> resultPoints) {
|
|
82
|
+
// No necesario
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
private void handleResult(Result rawResult) {
|
|
87
|
+
if (handled) return;
|
|
88
|
+
|
|
89
|
+
if (returnString) {
|
|
90
|
+
handled = true;
|
|
51
91
|
Intent intent = new Intent();
|
|
52
|
-
intent.putExtra(KEY_QR_CODE, rawResult.getText());
|
|
92
|
+
intent.putExtra(Constantes.KEY_QR_CODE, normalizeText(rawResult.getText()));
|
|
53
93
|
setResult(RESULT_OK, intent);
|
|
54
94
|
finish();
|
|
55
|
-
}
|
|
56
|
-
else {
|
|
95
|
+
} else {
|
|
57
96
|
Log.i("QrCodeScanner", "QR detectado");
|
|
58
|
-
// 1. OBTENER BYTES CRUDOS (CRÍTICO PARA DNI 4.0)
|
|
59
|
-
// No uses getText() para datos binarios
|
|
60
|
-
byte[] rawBytes = rawResult.getRawBytes();
|
|
61
97
|
|
|
62
|
-
|
|
98
|
+
byte[] rawBytes = extractPayload(rawResult);
|
|
63
99
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
// aquí debemos hacer el Base64.encode.
|
|
100
|
+
if (rawBytes != null && rawBytes.length > 0) {
|
|
101
|
+
handled = true;
|
|
67
102
|
String base64Result = Base64.encodeToString(rawBytes, Base64.NO_WRAP);
|
|
68
|
-
|
|
69
|
-
// Debug: Ver longitud
|
|
70
103
|
Log.d("QrCodeScanner", "Bytes leídos: " + rawBytes.length);
|
|
71
104
|
|
|
72
|
-
// 3. RETORNAR EL RESULTADO
|
|
73
105
|
Intent intent = new Intent();
|
|
74
|
-
intent.putExtra(KEY_QR_CODE, base64Result);
|
|
106
|
+
intent.putExtra(Constantes.KEY_QR_CODE, base64Result);
|
|
75
107
|
setResult(RESULT_OK, intent);
|
|
76
108
|
finish();
|
|
77
109
|
} else {
|
|
78
|
-
//
|
|
79
|
-
|
|
110
|
+
// En modo continuo no hace falta reanudar nada:
|
|
111
|
+
// el escáner sigue intentándolo solo
|
|
112
|
+
Log.w("QrCodeScanner", "Payload vacío, reintentando...");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
private byte[] extractPayload(Result result) {
|
|
118
|
+
// 1. Reconstrucción vía texto (con ISO-8859-1 forzado es determinista)
|
|
119
|
+
String text = result.getText();
|
|
120
|
+
if (text != null && !text.isEmpty()) {
|
|
121
|
+
byte[] full = text.getBytes(StandardCharsets.ISO_8859_1);
|
|
122
|
+
if (full.length > 2 && (full[0] & 0xFF) == 0xDC && (full[1] & 0xFF) == 0x03) {
|
|
123
|
+
Log.d("QrCodeScanner", "Payload vía getText (completo)");
|
|
124
|
+
return full;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 2. Fallback: byte segments
|
|
129
|
+
Map<ResultMetadataType, Object> meta = result.getResultMetadata();
|
|
130
|
+
if (meta != null && meta.containsKey(ResultMetadataType.BYTE_SEGMENTS)) {
|
|
131
|
+
List<byte[]> segments = (List<byte[]>) meta.get(ResultMetadataType.BYTE_SEGMENTS);
|
|
132
|
+
if (segments != null && !segments.isEmpty()) {
|
|
133
|
+
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
|
134
|
+
for (byte[] seg : segments) {
|
|
135
|
+
bos.write(seg, 0, seg.length);
|
|
136
|
+
}
|
|
137
|
+
byte[] payload = bos.toByteArray();
|
|
138
|
+
if (payload.length > 0) {
|
|
139
|
+
Log.d("QrCodeScanner", "Payload vía BYTE_SEGMENTS (fallback)");
|
|
140
|
+
return payload;
|
|
141
|
+
}
|
|
80
142
|
}
|
|
81
143
|
}
|
|
144
|
+
return result.getRawBytes(); // último recurso
|
|
82
145
|
}
|
|
83
146
|
|
|
147
|
+
/**
|
|
148
|
+
* El decoder está forzado a ISO-8859-1 (necesario para QRs binarios del DNI).
|
|
149
|
+
* Para QRs de texto, eso convierte el UTF-8 real en mojibake (ñ, á...).
|
|
150
|
+
* Como ISO-8859-1 es 1:1 con los bytes, podemos recuperar los bytes originales
|
|
151
|
+
* y re-decodificarlos como UTF-8 si son UTF-8 válido.
|
|
152
|
+
*/
|
|
153
|
+
private String normalizeText(String text) {
|
|
154
|
+
if (text == null || text.isEmpty()) return text;
|
|
155
|
+
byte[] raw = text.getBytes(StandardCharsets.ISO_8859_1);
|
|
156
|
+
if (isValidUtf8(raw)) {
|
|
157
|
+
return new String(raw, StandardCharsets.UTF_8);
|
|
158
|
+
}
|
|
159
|
+
return text; // ASCII puro o Latin-1 real: se queda como está
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private boolean isValidUtf8(byte[] bytes) {
|
|
163
|
+
java.nio.charset.CharsetDecoder decoder = StandardCharsets.UTF_8.newDecoder();
|
|
164
|
+
try {
|
|
165
|
+
decoder.decode(java.nio.ByteBuffer.wrap(bytes));
|
|
166
|
+
return true;
|
|
167
|
+
} catch (java.nio.charset.CharacterCodingException e) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
84
171
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
//
|
|
2
|
+
// QRSegmentDecoder.swift
|
|
3
|
+
// TramiteFoto
|
|
4
|
+
//
|
|
5
|
+
// Created by CQeSolutions on 07/06/2026.
|
|
6
|
+
// Copyright © 2026 Diego. All rights reserved.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
// QRSegmentDecoder.swift
|
|
11
|
+
// Reensambla el payload lógico de un QR a partir del stream de bits
|
|
12
|
+
// del errorCorrectedPayload, segmento a segmento (byte, alfanumérico,
|
|
13
|
+
// numérico), igual que hacen los decodificadores completos.
|
|
14
|
+
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
enum QRSegmentDecoder {
|
|
18
|
+
|
|
19
|
+
static func reassemble(payload: Data, version: Int) -> Data? {
|
|
20
|
+
let bytes = [UInt8](payload)
|
|
21
|
+
var bitPos = 0
|
|
22
|
+
let totalBits = bytes.count * 8
|
|
23
|
+
|
|
24
|
+
func readBits(_ n: Int) -> Int? {
|
|
25
|
+
guard n > 0, bitPos + n <= totalBits else { return nil }
|
|
26
|
+
var value = 0
|
|
27
|
+
for _ in 0..<n {
|
|
28
|
+
let byteIndex = bitPos >> 3
|
|
29
|
+
let bitIndex = 7 - (bitPos & 7)
|
|
30
|
+
value = (value << 1) | Int((bytes[byteIndex] >> UInt8(bitIndex)) & 1)
|
|
31
|
+
bitPos += 1
|
|
32
|
+
}
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func countBits(forMode mode: Int) -> Int? {
|
|
37
|
+
switch mode {
|
|
38
|
+
case 0b0001: return version <= 9 ? 10 : (version <= 26 ? 12 : 14) // numérico
|
|
39
|
+
case 0b0010: return version <= 9 ? 9 : (version <= 26 ? 11 : 13) // alfanumérico
|
|
40
|
+
case 0b0100: return version <= 9 ? 8 : 16 // byte
|
|
41
|
+
default: return nil
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
let alphaTable = Array("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:".utf8)
|
|
46
|
+
var out = [UInt8]()
|
|
47
|
+
|
|
48
|
+
while bitPos + 4 <= totalBits {
|
|
49
|
+
guard let mode = readBits(4) else { break }
|
|
50
|
+
if mode == 0b0000 { break } // terminador
|
|
51
|
+
|
|
52
|
+
if mode == 0b0111 { // ECI: saltar el designador (1, 2 o 3 bytes)
|
|
53
|
+
guard let first = readBits(8) else { return nil }
|
|
54
|
+
if first & 0x80 != 0 {
|
|
55
|
+
if first & 0x40 == 0 { _ = readBits(8) } else { _ = readBits(16) }
|
|
56
|
+
}
|
|
57
|
+
continue
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
guard let cBits = countBits(forMode: mode),
|
|
61
|
+
let count = readBits(cBits) else { return nil }
|
|
62
|
+
|
|
63
|
+
switch mode {
|
|
64
|
+
case 0b0100: // BYTE: count bytes tal cual
|
|
65
|
+
for _ in 0..<count {
|
|
66
|
+
guard let b = readBits(8) else { return nil }
|
|
67
|
+
out.append(UInt8(b))
|
|
68
|
+
}
|
|
69
|
+
case 0b0010: // ALFANUMÉRICO: 11 bits por par de caracteres
|
|
70
|
+
var remaining = count
|
|
71
|
+
while remaining >= 2 {
|
|
72
|
+
guard let v = readBits(11), v < 45 * 45 else { return nil }
|
|
73
|
+
out.append(alphaTable[v / 45])
|
|
74
|
+
out.append(alphaTable[v % 45])
|
|
75
|
+
remaining -= 2
|
|
76
|
+
}
|
|
77
|
+
if remaining == 1 {
|
|
78
|
+
guard let v = readBits(6), v < 45 else { return nil }
|
|
79
|
+
out.append(alphaTable[v])
|
|
80
|
+
}
|
|
81
|
+
case 0b0001: // NUMÉRICO: 10 bits por trío de dígitos
|
|
82
|
+
var remaining = count
|
|
83
|
+
while remaining >= 3 {
|
|
84
|
+
guard let v = readBits(10) else { return nil }
|
|
85
|
+
out.append(contentsOf: String(format: "%03d", v).utf8)
|
|
86
|
+
remaining -= 3
|
|
87
|
+
}
|
|
88
|
+
if remaining == 2 {
|
|
89
|
+
guard let v = readBits(7) else { return nil }
|
|
90
|
+
out.append(contentsOf: String(format: "%02d", v).utf8)
|
|
91
|
+
} else if remaining == 1 {
|
|
92
|
+
guard let v = readBits(4) else { return nil }
|
|
93
|
+
out.append(contentsOf: String(v).utf8)
|
|
94
|
+
}
|
|
95
|
+
default:
|
|
96
|
+
return nil // modo no soportado (kanji...): abortar al fallback
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out.isEmpty ? nil : Data(out)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -8,65 +8,63 @@
|
|
|
8
8
|
import AVFoundation
|
|
9
9
|
import UIKit
|
|
10
10
|
|
|
11
|
+
import AVFoundation
|
|
12
|
+
import UIKit
|
|
13
|
+
|
|
11
14
|
class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDelegate {
|
|
12
15
|
var captureSession: AVCaptureSession!
|
|
13
16
|
var previewLayer: AVCaptureVideoPreviewLayer!
|
|
14
17
|
var returnString: Bool = true
|
|
15
|
-
|
|
16
|
-
|
|
18
|
+
|
|
17
19
|
public func inicializa(returnString: Bool) {
|
|
18
20
|
self.returnString = returnString
|
|
19
21
|
}
|
|
20
|
-
|
|
22
|
+
|
|
21
23
|
override func viewDidLoad() {
|
|
22
24
|
super.viewDidLoad()
|
|
23
|
-
view.backgroundColor = .black
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
view.backgroundColor = UIColor.black
|
|
26
27
|
captureSession = AVCaptureSession()
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
guard let self = self else { return }
|
|
31
|
-
|
|
32
|
-
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
|
|
33
|
-
let videoInput: AVCaptureDeviceInput
|
|
34
|
-
|
|
35
|
-
do {
|
|
36
|
-
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
|
|
37
|
-
} catch {
|
|
38
|
-
return
|
|
39
|
-
}
|
|
29
|
+
guard let videoCaptureDevice = AVCaptureDevice.default(for: .video) else { return }
|
|
30
|
+
let videoInput: AVCaptureDeviceInput
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
do {
|
|
33
|
+
videoInput = try AVCaptureDeviceInput(device: videoCaptureDevice)
|
|
34
|
+
} catch {
|
|
35
|
+
return
|
|
36
|
+
}
|
|
44
37
|
|
|
45
|
-
|
|
38
|
+
if captureSession.canAddInput(videoInput) {
|
|
39
|
+
captureSession.addInput(videoInput)
|
|
40
|
+
} else {
|
|
41
|
+
failed()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
46
44
|
|
|
47
|
-
|
|
48
|
-
self.captureSession.addOutput(metadataOutput)
|
|
49
|
-
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
50
|
-
metadataOutput.metadataObjectTypes = [.qr]
|
|
51
|
-
}
|
|
45
|
+
let metadataOutput = AVCaptureMetadataOutput()
|
|
52
46
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
// Iniciamos la sesión fuera del hilo principal de nuevo
|
|
61
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
62
|
-
self.captureSession.startRunning()
|
|
63
|
-
}
|
|
64
|
-
}
|
|
47
|
+
if captureSession.canAddOutput(metadataOutput) {
|
|
48
|
+
captureSession.addOutput(metadataOutput)
|
|
49
|
+
metadataOutput.setMetadataObjectsDelegate(self, queue: DispatchQueue.main)
|
|
50
|
+
metadataOutput.metadataObjectTypes = [.qr]
|
|
51
|
+
} else {
|
|
52
|
+
failed()
|
|
53
|
+
return
|
|
65
54
|
}
|
|
55
|
+
|
|
56
|
+
previewLayer = AVCaptureVideoPreviewLayer(session: captureSession)
|
|
57
|
+
previewLayer.frame = view.layer.bounds
|
|
58
|
+
previewLayer.videoGravity = .resizeAspectFill
|
|
59
|
+
view.layer.addSublayer(previewLayer)
|
|
60
|
+
|
|
61
|
+
captureSession.startRunning()
|
|
66
62
|
}
|
|
67
|
-
|
|
63
|
+
|
|
68
64
|
func failed() {
|
|
69
|
-
let ac = UIAlertController(title: "Scanning not supported",
|
|
65
|
+
let ac = UIAlertController(title: "Scanning not supported",
|
|
66
|
+
message: "Your device does not support scanning a code from an item. Please use a device with a camera.",
|
|
67
|
+
preferredStyle: .alert)
|
|
70
68
|
ac.addAction(UIAlertAction(title: "OK", style: .default))
|
|
71
69
|
present(ac, animated: true)
|
|
72
70
|
captureSession = nil
|
|
@@ -74,90 +72,64 @@ class ScannerViewController: UIViewController, AVCaptureMetadataOutputObjectsDel
|
|
|
74
72
|
|
|
75
73
|
override func viewWillAppear(_ animated: Bool) {
|
|
76
74
|
super.viewWillAppear(animated)
|
|
77
|
-
|
|
78
|
-
if (captureSession?.isRunning == false) {
|
|
75
|
+
if captureSession?.isRunning == false {
|
|
79
76
|
captureSession.startRunning()
|
|
80
77
|
}
|
|
81
78
|
}
|
|
82
79
|
|
|
83
80
|
override func viewWillDisappear(_ animated: Bool) {
|
|
84
81
|
super.viewWillDisappear(animated)
|
|
85
|
-
|
|
86
|
-
if (captureSession?.isRunning == true) {
|
|
82
|
+
if captureSession?.isRunning == true {
|
|
87
83
|
captureSession.stopRunning()
|
|
88
84
|
}
|
|
89
85
|
}
|
|
90
|
-
|
|
91
|
-
func metadataOutput(_ output: AVCaptureMetadataOutput,
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
86
|
+
|
|
87
|
+
func metadataOutput(_ output: AVCaptureMetadataOutput,
|
|
88
|
+
didOutput metadataObjects: [AVMetadataObject],
|
|
89
|
+
from connection: AVCaptureConnection) {
|
|
90
|
+
|
|
91
|
+
guard let readableObject = metadataObjects.first as? AVMetadataMachineReadableCodeObject else { return }
|
|
92
|
+
|
|
93
|
+
var parametros: [String: Any] = [:]
|
|
94
|
+
|
|
95
|
+
if returnString {
|
|
96
|
+
guard let stringValue = readableObject.stringValue else { return }
|
|
97
|
+
captureSession.stopRunning()
|
|
98
|
+
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
|
|
99
|
+
parametros["qrcode"] = stringValue
|
|
100
|
+
} else {
|
|
101
|
+
// Le pasamos el OBJETO completo, no el payload:
|
|
102
|
+
// dentro decide entre stringValue (vía buena) y descriptor (fallback)
|
|
103
|
+
guard let payload = extractPayload(from: readableObject) else {
|
|
104
|
+
// Lectura inservible: no paramos la sesión, seguimos escaneando
|
|
105
|
+
return
|
|
102
106
|
}
|
|
107
|
+
captureSession.stopRunning()
|
|
108
|
+
AudioServicesPlaySystemSound(SystemSoundID(kSystemSoundID_Vibrate))
|
|
109
|
+
parametros["qrcodeData"] = payload
|
|
103
110
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
// El descriptor contiene los bytes crudos (RAW) del QR.
|
|
109
|
-
if let descriptor = metadataObject.descriptor as? CIQRCodeDescriptor {
|
|
110
|
-
|
|
111
|
-
let rawPayload = descriptor.errorCorrectedPayload
|
|
112
|
-
//print("Datos brutos: \(rawPayload.toPrettyHexString())")
|
|
113
|
-
parametros["qrcodeData"] = rawPayload
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "lecturaQR"), object: nil, userInfo: parametros)
|
|
111
|
+
|
|
112
|
+
NotificationCenter.default.post(name: NSNotification.Name(rawValue: "lecturaQR"),
|
|
113
|
+
object: nil,
|
|
114
|
+
userInfo: parametros)
|
|
119
115
|
}
|
|
120
|
-
*/
|
|
121
|
-
|
|
122
|
-
func metadataOutput(_ output: AVCaptureMetadataOutput, didOutput metadataObjects: [AVMetadataObject], from connection: AVCaptureConnection) {
|
|
123
|
-
|
|
124
|
-
// 1. Detenemos la sesión inmediatamente para evitar múltiples lecturas
|
|
125
|
-
// startRunning/stopRunning siempre fuera del hilo principal si es posible
|
|
126
|
-
DispatchQueue.global(qos: .userInitiated).async {
|
|
127
|
-
if self.captureSession.isRunning {
|
|
128
|
-
self.captureSession.stopRunning()
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
116
|
|
|
132
|
-
|
|
133
|
-
guard let
|
|
134
|
-
let descriptor = metadataObject.descriptor as? CIQRCodeDescriptor else {
|
|
135
|
-
// Si falla, reiniciamos sesión
|
|
136
|
-
DispatchQueue.global(qos: .userInitiated).async { self.captureSession.startRunning() }
|
|
137
|
-
return
|
|
138
|
-
}
|
|
117
|
+
func extractPayload(from object: AVMetadataMachineReadableCodeObject) -> Data? {
|
|
118
|
+
guard let descriptor = object.descriptor as? CIQRCodeDescriptor else { return nil }
|
|
139
119
|
|
|
140
|
-
let
|
|
141
|
-
let
|
|
142
|
-
|
|
143
|
-
//
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
name: NSNotification.Name(rawValue: "lecturaQR"),
|
|
154
|
-
object: nil,
|
|
155
|
-
userInfo: parametros
|
|
156
|
-
)
|
|
157
|
-
|
|
158
|
-
// Cerramos la cámara
|
|
159
|
-
self.dismiss(animated: true, completion: nil)
|
|
160
|
-
}
|
|
120
|
+
let raw = descriptor.errorCorrectedPayload
|
|
121
|
+
let version = descriptor.symbolVersion
|
|
122
|
+
|
|
123
|
+
// 1. Reensamblado de segmentos a nivel de bits (la vía correcta)
|
|
124
|
+
if let reassembled = QRSegmentDecoder.reassemble(payload: raw, version: version),
|
|
125
|
+
reassembled.count > 2,
|
|
126
|
+
reassembled[reassembled.startIndex] == 0xDC,
|
|
127
|
+
reassembled[reassembled.startIndex + 1] == 0x03 {
|
|
128
|
+
return reassembled
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// 2. Fallback: payload crudo; alignToMagicByte del parser lo intentará
|
|
132
|
+
return raw
|
|
161
133
|
}
|
|
162
134
|
|
|
163
135
|
override var prefersStatusBarHidden: Bool {
|