@oh-my-pi/pi-coding-agent 16.0.8 → 16.0.10
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/CHANGELOG.md +33 -0
- package/dist/cli.js +3004 -2976
- package/dist/types/cli/args.d.ts +1 -0
- package/dist/types/collab/host.d.ts +2 -2
- package/dist/types/collab/protocol.d.ts +4 -5
- package/dist/types/commands/launch.d.ts +3 -0
- package/dist/types/config/model-resolver.d.ts +11 -2
- package/dist/types/config/settings-schema.d.ts +12 -2
- package/dist/types/goals/runtime.d.ts +4 -1
- package/dist/types/modes/print-mode.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +13 -0
- package/dist/types/slash-commands/builtin-registry.d.ts +1 -1
- package/dist/types/slash-commands/helpers/collab-qrcode.d.ts +13 -0
- package/dist/types/tools/index.d.ts +9 -1
- package/dist/types/utils/image-loading.d.ts +12 -0
- package/dist/types/utils/qrcode.d.ts +48 -0
- package/package.json +12 -12
- package/src/cli/args.ts +10 -1
- package/src/cli/flag-tables.ts +1 -0
- package/src/collab/host.ts +4 -4
- package/src/collab/protocol.ts +48 -15
- package/src/commands/launch.ts +3 -0
- package/src/config/config-file.ts +1 -1
- package/src/config/keybindings.ts +2 -2
- package/src/config/model-registry.ts +16 -4
- package/src/config/model-resolver.ts +193 -35
- package/src/config/settings-schema.ts +14 -2
- package/src/config/settings.ts +3 -3
- package/src/goals/runtime.ts +19 -7
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/main.ts +10 -2
- package/src/modes/components/oauth-selector.ts +31 -2
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/print-mode.ts +5 -1
- package/src/prompts/advisor/advise-tool.md +3 -1
- package/src/prompts/advisor/system.md +55 -12
- package/src/prompts/tools/inspect-image.md +1 -1
- package/src/sdk.ts +26 -7
- package/src/session/agent-session.ts +103 -16
- package/src/slash-commands/builtin-registry.ts +29 -11
- package/src/slash-commands/helpers/collab-qrcode.ts +28 -0
- package/src/thinking.ts +25 -5
- package/src/tools/index.ts +10 -1
- package/src/tools/inspect-image.ts +72 -9
- package/src/utils/file-mentions.ts +5 -2
- package/src/utils/image-loading.ts +58 -0
- package/src/utils/qrcode.ts +535 -0
|
@@ -38,6 +38,17 @@ export interface LoadImageInputOptions {
|
|
|
38
38
|
excludeWebP?: boolean;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Options for loading an in-memory chat image attachment as a vision-model input. */
|
|
42
|
+
export interface LoadImageAttachmentInputOptions {
|
|
43
|
+
image: ImageContent;
|
|
44
|
+
label: string;
|
|
45
|
+
uri: string;
|
|
46
|
+
autoResize: boolean;
|
|
47
|
+
maxBytes?: number;
|
|
48
|
+
/** Force non-WebP output (e.g. for Ollama). Leave unset to honor `OMP_NO_WEBP`. */
|
|
49
|
+
excludeWebP?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
41
52
|
export interface LoadedImageInput {
|
|
42
53
|
resolvedPath: string;
|
|
43
54
|
mimeType: string;
|
|
@@ -161,3 +172,50 @@ export async function loadImageInput(options: LoadImageInputOptions): Promise<Lo
|
|
|
161
172
|
bytes: outputBytes,
|
|
162
173
|
};
|
|
163
174
|
}
|
|
175
|
+
|
|
176
|
+
/** Loads a chat attachment image through the same size and encoder policy as file-backed image inputs. */
|
|
177
|
+
export async function loadImageAttachmentInput(
|
|
178
|
+
options: LoadImageAttachmentInputOptions,
|
|
179
|
+
): Promise<LoadedImageInput | null> {
|
|
180
|
+
const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
|
|
181
|
+
if (!SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(options.image.mimeType)) {
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const inputBytes = Buffer.byteLength(options.image.data, "base64");
|
|
186
|
+
if (inputBytes > maxBytes) {
|
|
187
|
+
throw new ImageInputTooLargeError(inputBytes, maxBytes);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let outputData = options.image.data;
|
|
191
|
+
let outputMimeType = options.image.mimeType;
|
|
192
|
+
let outputBytes = inputBytes;
|
|
193
|
+
let dimensionNote: string | undefined;
|
|
194
|
+
|
|
195
|
+
const shouldReencodeWebP = options.excludeWebP === true && options.image.mimeType === "image/webp";
|
|
196
|
+
if (options.autoResize || shouldReencodeWebP) {
|
|
197
|
+
try {
|
|
198
|
+
const resized = await resizeImage(options.image, { excludeWebP: options.excludeWebP });
|
|
199
|
+
outputData = resized.data;
|
|
200
|
+
outputMimeType = resized.mimeType;
|
|
201
|
+
outputBytes = resized.buffer.byteLength;
|
|
202
|
+
dimensionNote = formatDimensionNote(resized);
|
|
203
|
+
} catch {
|
|
204
|
+
// keep original image when resize fails
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
let textNote = `Read image attachment ${options.label} [${outputMimeType}]`;
|
|
209
|
+
if (dimensionNote) {
|
|
210
|
+
textNote += `\n${dimensionNote}`;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
resolvedPath: options.uri,
|
|
215
|
+
mimeType: outputMimeType,
|
|
216
|
+
data: outputData,
|
|
217
|
+
textNote,
|
|
218
|
+
dimensionNote,
|
|
219
|
+
bytes: outputBytes,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Self-contained QR Code generator (byte mode, versions 1-40, EC levels
|
|
3
|
+
* L/M/Q/H) with a half-block ANSI terminal renderer.
|
|
4
|
+
*
|
|
5
|
+
* Pure TypeScript, zero dependencies: the collab `/collab qrcode` command uses
|
|
6
|
+
* it to print scannable browser-join codes without pulling a runtime QR
|
|
7
|
+
* package into the bundle. The algorithm follows ISO/IEC 18004; the two
|
|
8
|
+
* error-correction tables below are direct transcriptions of that spec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type QrEcLevel = "L" | "M" | "Q" | "H";
|
|
12
|
+
|
|
13
|
+
/** Per-EC-level metadata: index into the spec tables and the 2-bit format code. */
|
|
14
|
+
const EC_LEVELS: Record<QrEcLevel, { table: number; formatBits: number }> = {
|
|
15
|
+
L: { table: 0, formatBits: 1 },
|
|
16
|
+
M: { table: 1, formatBits: 0 },
|
|
17
|
+
Q: { table: 2, formatBits: 3 },
|
|
18
|
+
H: { table: 3, formatBits: 2 },
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const MIN_VERSION = 1;
|
|
22
|
+
const MAX_VERSION = 40;
|
|
23
|
+
|
|
24
|
+
// ISO/IEC 18004 Table 9 — error-correction codewords per block, indexed
|
|
25
|
+
// [ecTable][version]. Index 0 of each row pads the 1-based version axis.
|
|
26
|
+
// biome-ignore format: spec table, one row per EC level
|
|
27
|
+
const ECC_CODEWORDS_PER_BLOCK: readonly (readonly number[])[] = [
|
|
28
|
+
[-1, 7, 10, 15, 20, 26, 18, 20, 24, 30, 18, 20, 24, 26, 30, 22, 24, 28, 30, 28, 28, 28, 28, 30, 30, 26, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
29
|
+
[-1, 10, 16, 26, 18, 24, 16, 18, 22, 22, 26, 30, 22, 22, 24, 24, 28, 28, 26, 26, 26, 26, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28, 28],
|
|
30
|
+
[-1, 13, 22, 18, 26, 18, 24, 18, 22, 20, 24, 28, 26, 24, 20, 30, 24, 28, 28, 26, 30, 28, 30, 30, 30, 30, 28, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
31
|
+
[-1, 17, 28, 22, 16, 22, 28, 26, 26, 24, 28, 24, 28, 22, 24, 24, 30, 28, 28, 26, 28, 30, 24, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30, 30],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// ISO/IEC 18004 Table 9 — number of error-correction blocks, indexed
|
|
35
|
+
// [ecTable][version].
|
|
36
|
+
// biome-ignore format: spec table, one row per EC level
|
|
37
|
+
const NUM_EC_BLOCKS: readonly (readonly number[])[] = [
|
|
38
|
+
[-1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 4, 4, 4, 4, 4, 6, 6, 6, 6, 7, 8, 8, 9, 9, 10, 12, 12, 12, 13, 14, 15, 16, 17, 18, 19, 19, 20, 21, 22, 24, 25],
|
|
39
|
+
[-1, 1, 1, 1, 2, 2, 4, 4, 4, 5, 5, 5, 8, 9, 9, 10, 10, 11, 13, 14, 16, 17, 17, 18, 20, 21, 23, 25, 26, 28, 29, 31, 33, 35, 37, 38, 40, 43, 45, 47, 49],
|
|
40
|
+
[-1, 1, 1, 2, 2, 4, 4, 6, 6, 8, 8, 8, 10, 12, 16, 12, 17, 16, 18, 21, 20, 23, 23, 25, 27, 29, 34, 34, 35, 38, 40, 43, 45, 48, 51, 53, 56, 59, 62, 65, 68],
|
|
41
|
+
[-1, 1, 1, 2, 4, 4, 4, 5, 6, 8, 8, 11, 11, 16, 16, 18, 16, 19, 21, 25, 25, 25, 34, 30, 32, 35, 37, 40, 42, 45, 48, 51, 54, 57, 60, 63, 66, 70, 74, 77, 81],
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const BYTE_MODE_INDICATOR = 0x4;
|
|
45
|
+
const PAD_BYTES = [0xec, 0x11] as const;
|
|
46
|
+
|
|
47
|
+
const PENALTY_N1 = 3;
|
|
48
|
+
const PENALTY_N2 = 3;
|
|
49
|
+
const PENALTY_N3 = 40;
|
|
50
|
+
const PENALTY_N4 = 10;
|
|
51
|
+
|
|
52
|
+
function getBit(value: number, index: number): boolean {
|
|
53
|
+
return ((value >>> index) & 1) !== 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Whether mask `m` flips the module at (x, y); the 8 data-mask conditions from the spec. */
|
|
57
|
+
function maskBit(m: number, x: number, y: number): boolean {
|
|
58
|
+
switch (m) {
|
|
59
|
+
case 0:
|
|
60
|
+
return (x + y) % 2 === 0;
|
|
61
|
+
case 1:
|
|
62
|
+
return y % 2 === 0;
|
|
63
|
+
case 2:
|
|
64
|
+
return x % 3 === 0;
|
|
65
|
+
case 3:
|
|
66
|
+
return (x + y) % 3 === 0;
|
|
67
|
+
case 4:
|
|
68
|
+
return (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0;
|
|
69
|
+
case 5:
|
|
70
|
+
return ((x * y) % 2) + ((x * y) % 3) === 0;
|
|
71
|
+
case 6:
|
|
72
|
+
return (((x * y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
73
|
+
default:
|
|
74
|
+
return (((x + y) % 2) + ((x * y) % 3)) % 2 === 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** GF(256) multiply under the QR primitive polynomial x^8 + x^4 + x^3 + x^2 + 1 (0x11D). */
|
|
79
|
+
function gfMultiply(x: number, y: number): number {
|
|
80
|
+
let z = 0;
|
|
81
|
+
for (let i = 7; i >= 0; i--) {
|
|
82
|
+
z = (z << 1) ^ ((z >>> 7) * 0x11d);
|
|
83
|
+
z ^= ((y >>> i) & 1) * x;
|
|
84
|
+
}
|
|
85
|
+
return z & 0xff;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Reed-Solomon generator polynomial coefficients for `degree` EC codewords. */
|
|
89
|
+
function rsDivisor(degree: number): Uint8Array {
|
|
90
|
+
const result = new Uint8Array(degree);
|
|
91
|
+
result[degree - 1] = 1;
|
|
92
|
+
let root = 1;
|
|
93
|
+
for (let i = 0; i < degree; i++) {
|
|
94
|
+
for (let j = 0; j < result.length; j++) {
|
|
95
|
+
result[j] = gfMultiply(result[j]!, root);
|
|
96
|
+
if (j + 1 < result.length) result[j] ^= result[j + 1]!;
|
|
97
|
+
}
|
|
98
|
+
root = gfMultiply(root, 0x02);
|
|
99
|
+
}
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Reed-Solomon remainder (the EC codewords) of `data` divided by `divisor`. */
|
|
104
|
+
function rsRemainder(data: Uint8Array, divisor: Uint8Array): Uint8Array {
|
|
105
|
+
const result = new Uint8Array(divisor.length);
|
|
106
|
+
for (const b of data) {
|
|
107
|
+
const factor = b ^ result[0]!;
|
|
108
|
+
result.copyWithin(0, 1);
|
|
109
|
+
result[result.length - 1] = 0;
|
|
110
|
+
for (let i = 0; i < divisor.length; i++) result[i] ^= gfMultiply(divisor[i]!, factor);
|
|
111
|
+
}
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Total data modules (bits available before EC) for a version. */
|
|
116
|
+
function rawDataModules(version: number): number {
|
|
117
|
+
let result = (16 * version + 128) * version + 64;
|
|
118
|
+
if (version >= 2) {
|
|
119
|
+
const numAlign = Math.floor(version / 7) + 2;
|
|
120
|
+
result -= (25 * numAlign - 10) * numAlign - 55;
|
|
121
|
+
if (version >= 7) result -= 36;
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Number of usable data codewords (8-bit) at a given version + EC level. */
|
|
127
|
+
function dataCodewords(version: number, ecTable: number): number {
|
|
128
|
+
return (
|
|
129
|
+
Math.floor(rawDataModules(version) / 8) -
|
|
130
|
+
ECC_CODEWORDS_PER_BLOCK[ecTable]![version]! * NUM_EC_BLOCKS[ecTable]![version]!
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Byte-mode character-count indicator width in bits for a version. */
|
|
135
|
+
function charCountBits(version: number): number {
|
|
136
|
+
return version <= 9 ? 8 : 16;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export interface QrEncodeOptions {
|
|
140
|
+
/** Lowest version to consider (default 1). */
|
|
141
|
+
minVersion?: number;
|
|
142
|
+
/** Highest version to consider (default 40). */
|
|
143
|
+
maxVersion?: number;
|
|
144
|
+
/** Force a mask 0-7; -1 (default) auto-selects the lowest-penalty mask. */
|
|
145
|
+
mask?: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* A finished QR symbol: a square grid of dark/light modules plus the chosen
|
|
150
|
+
* version, EC level, and mask. `module(x, y)` is the only access path the
|
|
151
|
+
* renderers need.
|
|
152
|
+
*/
|
|
153
|
+
export class QrCode {
|
|
154
|
+
readonly size: number;
|
|
155
|
+
/** Selected mask pattern (0-7). */
|
|
156
|
+
readonly mask: number;
|
|
157
|
+
readonly #modules: boolean[][];
|
|
158
|
+
readonly #isFunction: boolean[][];
|
|
159
|
+
|
|
160
|
+
private constructor(
|
|
161
|
+
readonly version: number,
|
|
162
|
+
readonly ecLevel: QrEcLevel,
|
|
163
|
+
dataCodewordsInterleaved: Uint8Array,
|
|
164
|
+
mask: number,
|
|
165
|
+
) {
|
|
166
|
+
this.size = version * 4 + 17;
|
|
167
|
+
this.#modules = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
|
|
168
|
+
this.#isFunction = Array.from({ length: this.size }, () => new Array<boolean>(this.size).fill(false));
|
|
169
|
+
|
|
170
|
+
this.#drawFunctionPatterns();
|
|
171
|
+
this.#drawCodewords(dataCodewordsInterleaved);
|
|
172
|
+
this.mask = this.#selectMask(mask);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module(x: number, y: number): boolean {
|
|
176
|
+
return this.#modules[y]![x]!;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Encode a string in byte mode (UTF-8). Throws if it exceeds version 40. */
|
|
180
|
+
static encodeText(text: string, ecLevel: QrEcLevel = "M", options?: QrEncodeOptions): QrCode {
|
|
181
|
+
return QrCode.encodeBytes(new TextEncoder().encode(text), ecLevel, options);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Encode raw bytes in byte mode. Throws if they exceed version 40 at this EC level. */
|
|
185
|
+
static encodeBytes(data: Uint8Array, ecLevel: QrEcLevel = "M", options?: QrEncodeOptions): QrCode {
|
|
186
|
+
const ec = EC_LEVELS[ecLevel];
|
|
187
|
+
const minVersion = Math.max(MIN_VERSION, options?.minVersion ?? MIN_VERSION);
|
|
188
|
+
const maxVersion = Math.min(MAX_VERSION, options?.maxVersion ?? MAX_VERSION);
|
|
189
|
+
|
|
190
|
+
let version = minVersion;
|
|
191
|
+
for (; ; version++) {
|
|
192
|
+
const capacityBits = dataCodewords(version, ec.table) * 8;
|
|
193
|
+
const usedBits = 4 + charCountBits(version) + data.length * 8;
|
|
194
|
+
if (usedBits <= capacityBits) break;
|
|
195
|
+
if (version >= maxVersion) {
|
|
196
|
+
throw new Error(`data too long for a QR code (${data.length} bytes, EC ${ecLevel})`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const bits = new BitBuffer();
|
|
201
|
+
bits.append(BYTE_MODE_INDICATOR, 4);
|
|
202
|
+
bits.append(data.length, charCountBits(version));
|
|
203
|
+
for (const b of data) bits.append(b, 8);
|
|
204
|
+
|
|
205
|
+
const capacityBits = dataCodewords(version, ec.table) * 8;
|
|
206
|
+
bits.append(0, Math.min(4, capacityBits - bits.length)); // terminator
|
|
207
|
+
bits.append(0, (8 - (bits.length % 8)) % 8); // byte-align
|
|
208
|
+
for (let pad = 0; bits.length < capacityBits; pad ^= 1) bits.append(PAD_BYTES[pad]!, 8);
|
|
209
|
+
|
|
210
|
+
const codewords = QrCode.#interleave(bits.toBytes(), version, ec.table);
|
|
211
|
+
const mask = options?.mask ?? -1;
|
|
212
|
+
if (mask < -1 || mask > 7) throw new Error(`invalid mask ${mask}`);
|
|
213
|
+
return new QrCode(version, ecLevel, codewords, mask);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Split into blocks, append Reed-Solomon EC, and interleave per the spec. */
|
|
217
|
+
static #interleave(data: Uint8Array, version: number, ecTable: number): Uint8Array {
|
|
218
|
+
const numBlocks = NUM_EC_BLOCKS[ecTable]![version]!;
|
|
219
|
+
const eccLen = ECC_CODEWORDS_PER_BLOCK[ecTable]![version]!;
|
|
220
|
+
const rawCodewords = Math.floor(rawDataModules(version) / 8);
|
|
221
|
+
const numShort = numBlocks - (rawCodewords % numBlocks);
|
|
222
|
+
const shortLen = Math.floor(rawCodewords / numBlocks);
|
|
223
|
+
const divisor = rsDivisor(eccLen);
|
|
224
|
+
|
|
225
|
+
const blocks: Uint8Array[] = [];
|
|
226
|
+
const blockLen = shortLen + 1;
|
|
227
|
+
for (let i = 0, offset = 0; i < numBlocks; i++) {
|
|
228
|
+
const datLen = shortLen - eccLen + (i < numShort ? 0 : 1);
|
|
229
|
+
const dat = data.subarray(offset, offset + datLen);
|
|
230
|
+
offset += datLen;
|
|
231
|
+
// Every block is padded to the longest block's length so interleaving
|
|
232
|
+
// stays column-aligned; short blocks leave a zero in the last data slot.
|
|
233
|
+
const block = new Uint8Array(blockLen);
|
|
234
|
+
block.set(dat, 0);
|
|
235
|
+
block.set(rsRemainder(dat, divisor), blockLen - eccLen);
|
|
236
|
+
blocks.push(block);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const result = new Uint8Array(rawCodewords);
|
|
240
|
+
let w = 0;
|
|
241
|
+
for (let i = 0; i < blockLen; i++) {
|
|
242
|
+
for (let b = 0; b < numBlocks; b++) {
|
|
243
|
+
// Skip the padding column at the data/EC boundary of short blocks.
|
|
244
|
+
if (i === shortLen - eccLen && b < numShort) continue;
|
|
245
|
+
result[w++] = blocks[b]![i]!;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Module placement ──────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
#setFunction(x: number, y: number, dark: boolean): void {
|
|
254
|
+
this.#modules[y]![x] = dark;
|
|
255
|
+
this.#isFunction[y]![x] = true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
#drawFunctionPatterns(): void {
|
|
259
|
+
for (let i = 0; i < this.size; i++) {
|
|
260
|
+
this.#setFunction(6, i, i % 2 === 0);
|
|
261
|
+
this.#setFunction(i, 6, i % 2 === 0);
|
|
262
|
+
}
|
|
263
|
+
this.#drawFinder(3, 3);
|
|
264
|
+
this.#drawFinder(this.size - 4, 3);
|
|
265
|
+
this.#drawFinder(3, this.size - 4);
|
|
266
|
+
|
|
267
|
+
const align = this.#alignmentPositions();
|
|
268
|
+
const last = align.length - 1;
|
|
269
|
+
for (let i = 0; i <= last; i++) {
|
|
270
|
+
for (let j = 0; j <= last; j++) {
|
|
271
|
+
// Skip the three finder corners.
|
|
272
|
+
if ((i === 0 && j === 0) || (i === 0 && j === last) || (i === last && j === 0)) continue;
|
|
273
|
+
this.#drawAlignment(align[i]!, align[j]!);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
this.#drawFormatBits(0); // placeholder until mask chosen
|
|
278
|
+
this.#drawVersion();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
#drawFinder(cx: number, cy: number): void {
|
|
282
|
+
for (let dy = -4; dy <= 4; dy++) {
|
|
283
|
+
for (let dx = -4; dx <= 4; dx++) {
|
|
284
|
+
const dist = Math.max(Math.abs(dx), Math.abs(dy));
|
|
285
|
+
const x = cx + dx;
|
|
286
|
+
const y = cy + dy;
|
|
287
|
+
if (x >= 0 && x < this.size && y >= 0 && y < this.size) {
|
|
288
|
+
this.#setFunction(x, y, dist !== 2 && dist !== 4);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
#drawAlignment(cx: number, cy: number): void {
|
|
295
|
+
for (let dy = -2; dy <= 2; dy++) {
|
|
296
|
+
for (let dx = -2; dx <= 2; dx++) {
|
|
297
|
+
this.#setFunction(cx + dx, cy + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#alignmentPositions(): number[] {
|
|
303
|
+
if (this.version === 1) return [];
|
|
304
|
+
const numAlign = Math.floor(this.version / 7) + 2;
|
|
305
|
+
const step = this.version === 32 ? 26 : Math.ceil((this.size - 13) / (numAlign * 2 - 2)) * 2;
|
|
306
|
+
const result = [6];
|
|
307
|
+
for (let pos = this.size - 7; result.length < numAlign; pos -= step) result.splice(1, 0, pos);
|
|
308
|
+
return result;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
#drawFormatBits(mask: number): void {
|
|
312
|
+
const data = (EC_LEVELS[this.ecLevel].formatBits << 3) | mask;
|
|
313
|
+
let rem = data;
|
|
314
|
+
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537);
|
|
315
|
+
const bits = ((data << 10) | rem) ^ 0x5412;
|
|
316
|
+
|
|
317
|
+
for (let i = 0; i <= 5; i++) this.#setFunction(8, i, getBit(bits, i));
|
|
318
|
+
this.#setFunction(8, 7, getBit(bits, 6));
|
|
319
|
+
this.#setFunction(8, 8, getBit(bits, 7));
|
|
320
|
+
this.#setFunction(7, 8, getBit(bits, 8));
|
|
321
|
+
for (let i = 9; i < 15; i++) this.#setFunction(14 - i, 8, getBit(bits, i));
|
|
322
|
+
|
|
323
|
+
for (let i = 0; i < 8; i++) this.#setFunction(this.size - 1 - i, 8, getBit(bits, i));
|
|
324
|
+
for (let i = 8; i < 15; i++) this.#setFunction(8, this.size - 15 + i, getBit(bits, i));
|
|
325
|
+
this.#setFunction(8, this.size - 8, true); // always-dark module
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#drawVersion(): void {
|
|
329
|
+
if (this.version < 7) return;
|
|
330
|
+
let rem = this.version;
|
|
331
|
+
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25);
|
|
332
|
+
const bits = (this.version << 12) | rem;
|
|
333
|
+
for (let i = 0; i < 18; i++) {
|
|
334
|
+
const bit = getBit(bits, i);
|
|
335
|
+
const a = this.size - 11 + (i % 3);
|
|
336
|
+
const b = Math.floor(i / 3);
|
|
337
|
+
this.#setFunction(a, b, bit);
|
|
338
|
+
this.#setFunction(b, a, bit);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
#drawCodewords(data: Uint8Array): void {
|
|
343
|
+
let i = 0;
|
|
344
|
+
const totalBits = data.length * 8;
|
|
345
|
+
for (let right = this.size - 1; right >= 1; right -= 2) {
|
|
346
|
+
if (right === 6) right = 5;
|
|
347
|
+
for (let vert = 0; vert < this.size; vert++) {
|
|
348
|
+
for (let j = 0; j < 2; j++) {
|
|
349
|
+
const x = right - j;
|
|
350
|
+
const upward = ((right + 1) & 2) === 0;
|
|
351
|
+
const y = upward ? this.size - 1 - vert : vert;
|
|
352
|
+
if (!this.#isFunction[y]![x] && i < totalBits) {
|
|
353
|
+
this.#modules[y]![x] = getBit(data[i >>> 3]!, 7 - (i & 7));
|
|
354
|
+
i++;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ── Masking ───────────────────────────────────────────────────────────
|
|
362
|
+
|
|
363
|
+
#applyMask(mask: number): void {
|
|
364
|
+
for (let y = 0; y < this.size; y++) {
|
|
365
|
+
for (let x = 0; x < this.size; x++) {
|
|
366
|
+
if (!this.#isFunction[y]![x] && maskBit(mask, x, y)) {
|
|
367
|
+
this.#modules[y]![x] = !this.#modules[y]![x];
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
#selectMask(forced: number): number {
|
|
374
|
+
let mask = forced;
|
|
375
|
+
if (mask === -1) {
|
|
376
|
+
let minPenalty = Infinity;
|
|
377
|
+
for (let m = 0; m < 8; m++) {
|
|
378
|
+
this.#applyMask(m);
|
|
379
|
+
this.#drawFormatBits(m);
|
|
380
|
+
const penalty = this.#penaltyScore();
|
|
381
|
+
if (penalty < minPenalty) {
|
|
382
|
+
mask = m;
|
|
383
|
+
minPenalty = penalty;
|
|
384
|
+
}
|
|
385
|
+
this.#applyMask(m); // undo (XOR mask is self-inverse)
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
this.#applyMask(mask);
|
|
389
|
+
this.#drawFormatBits(mask);
|
|
390
|
+
return mask;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
#penaltyScore(): number {
|
|
394
|
+
let result = 0;
|
|
395
|
+
const size = this.size;
|
|
396
|
+
const mods = this.#modules;
|
|
397
|
+
|
|
398
|
+
// Rule 1 + Rule 3 — adjacent same-color runs, finder-like patterns (rows).
|
|
399
|
+
for (let y = 0; y < size; y++) {
|
|
400
|
+
let runColor = false;
|
|
401
|
+
let runLen = 0;
|
|
402
|
+
const history = [0, 0, 0, 0, 0, 0, 0];
|
|
403
|
+
for (let x = 0; x < size; x++) {
|
|
404
|
+
if (mods[y]![x] === runColor) {
|
|
405
|
+
runLen++;
|
|
406
|
+
if (runLen === 5) result += PENALTY_N1;
|
|
407
|
+
else if (runLen > 5) result++;
|
|
408
|
+
} else {
|
|
409
|
+
this.#finderAddHistory(runLen, history);
|
|
410
|
+
if (!runColor) result += this.#finderCountPatterns(history) * PENALTY_N3;
|
|
411
|
+
runColor = mods[y]![x]!;
|
|
412
|
+
runLen = 1;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
result += this.#finderTerminate(runColor, runLen, history) * PENALTY_N3;
|
|
416
|
+
}
|
|
417
|
+
// Rule 1 + Rule 3 — columns.
|
|
418
|
+
for (let x = 0; x < size; x++) {
|
|
419
|
+
let runColor = false;
|
|
420
|
+
let runLen = 0;
|
|
421
|
+
const history = [0, 0, 0, 0, 0, 0, 0];
|
|
422
|
+
for (let y = 0; y < size; y++) {
|
|
423
|
+
if (mods[y]![x] === runColor) {
|
|
424
|
+
runLen++;
|
|
425
|
+
if (runLen === 5) result += PENALTY_N1;
|
|
426
|
+
else if (runLen > 5) result++;
|
|
427
|
+
} else {
|
|
428
|
+
this.#finderAddHistory(runLen, history);
|
|
429
|
+
if (!runColor) result += this.#finderCountPatterns(history) * PENALTY_N3;
|
|
430
|
+
runColor = mods[y]![x]!;
|
|
431
|
+
runLen = 1;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
result += this.#finderTerminate(runColor, runLen, history) * PENALTY_N3;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Rule 2 — 2x2 blocks of one color.
|
|
438
|
+
for (let y = 0; y < size - 1; y++) {
|
|
439
|
+
for (let x = 0; x < size - 1; x++) {
|
|
440
|
+
const c = mods[y]![x];
|
|
441
|
+
if (c === mods[y]![x + 1] && c === mods[y + 1]![x] && c === mods[y + 1]![x + 1]) {
|
|
442
|
+
result += PENALTY_N2;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Rule 4 — dark/light balance.
|
|
448
|
+
let dark = 0;
|
|
449
|
+
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (mods[y]![x]) dark++;
|
|
450
|
+
const total = size * size;
|
|
451
|
+
const k = Math.ceil(Math.abs(dark * 20 - total * 10) / total) - 1;
|
|
452
|
+
result += k * PENALTY_N4;
|
|
453
|
+
return result;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
#finderCountPatterns(history: readonly number[]): number {
|
|
457
|
+
const n = history[1]!;
|
|
458
|
+
const core = n > 0 && history[2] === n && history[3] === n * 3 && history[4] === n && history[5] === n;
|
|
459
|
+
return (
|
|
460
|
+
(core && history[0]! >= n * 4 && history[6]! >= n ? 1 : 0) +
|
|
461
|
+
(core && history[6]! >= n * 4 && history[0]! >= n ? 1 : 0)
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
#finderAddHistory(runLen: number, history: number[]): void {
|
|
466
|
+
if (history[0] === 0) runLen += this.size; // light border before the first run
|
|
467
|
+
history.pop();
|
|
468
|
+
history.unshift(runLen);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#finderTerminate(runColor: boolean, runLen: number, history: number[]): number {
|
|
472
|
+
if (runColor) {
|
|
473
|
+
this.#finderAddHistory(runLen, history);
|
|
474
|
+
runLen = 0;
|
|
475
|
+
}
|
|
476
|
+
runLen += this.size; // light border after the final run
|
|
477
|
+
this.#finderAddHistory(runLen, history);
|
|
478
|
+
return this.#finderCountPatterns(history);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Append-only MSB-first bit buffer. */
|
|
483
|
+
class BitBuffer {
|
|
484
|
+
#bits: number[] = [];
|
|
485
|
+
|
|
486
|
+
get length(): number {
|
|
487
|
+
return this.#bits.length;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
append(value: number, count: number): void {
|
|
491
|
+
for (let i = count - 1; i >= 0; i--) this.#bits.push((value >>> i) & 1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
toBytes(): Uint8Array {
|
|
495
|
+
const out = new Uint8Array(this.#bits.length >>> 3);
|
|
496
|
+
for (let i = 0; i < this.#bits.length; i++) out[i >>> 3] = (out[i >>> 3]! << 1) | this.#bits[i]!;
|
|
497
|
+
return out;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
export interface QrRenderOptions {
|
|
502
|
+
/** Quiet-zone width in modules on every side (default 4, per spec). */
|
|
503
|
+
margin?: number;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const ANSI_RESET = "\x1b[0m";
|
|
507
|
+
const ANSI_QR_ROW_PREFIX = "\x1b[47m\x1b[30m"; // white background, black foreground
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Render a QR symbol as ANSI half-block rows: each text row packs two module
|
|
511
|
+
* rows via `▀`/`▄`/`█`, drawn black-on-white so a phone camera reads dark
|
|
512
|
+
* modules as data and the quiet zone as the light margin. The leading margin
|
|
513
|
+
* makes the symbol scannable regardless of the terminal's own background.
|
|
514
|
+
*/
|
|
515
|
+
export function renderQrHalfBlocks(qr: QrCode, options?: QrRenderOptions): string[] {
|
|
516
|
+
const margin = Math.max(0, options?.margin ?? 4);
|
|
517
|
+
const dim = qr.size + margin * 2;
|
|
518
|
+
const dark = (gx: number, gy: number): boolean => {
|
|
519
|
+
const x = gx - margin;
|
|
520
|
+
const y = gy - margin;
|
|
521
|
+
return x >= 0 && x < qr.size && y >= 0 && y < qr.size && qr.module(x, y);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const lines: string[] = [];
|
|
525
|
+
for (let gy = 0; gy < dim; gy += 2) {
|
|
526
|
+
let row = ANSI_QR_ROW_PREFIX;
|
|
527
|
+
for (let gx = 0; gx < dim; gx++) {
|
|
528
|
+
const top = dark(gx, gy);
|
|
529
|
+
const bottom = gy + 1 < dim && dark(gx, gy + 1);
|
|
530
|
+
row += top ? (bottom ? "█" : "▀") : bottom ? "▄" : " ";
|
|
531
|
+
}
|
|
532
|
+
lines.push(row + ANSI_RESET);
|
|
533
|
+
}
|
|
534
|
+
return lines;
|
|
535
|
+
}
|