captcha-ocr 1.0.0

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/dist/index.js ADDED
@@ -0,0 +1,423 @@
1
+ // src/recognizer.ts
2
+ import sharp2 from "sharp";
3
+
4
+ // src/input.ts
5
+ var ImageInputError = class extends Error {
6
+ code = "INVALID_IMAGE_INPUT";
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = "ImageInputError";
10
+ }
11
+ };
12
+ function toImageBuffer(input) {
13
+ if (Buffer.isBuffer(input)) {
14
+ return input;
15
+ }
16
+ if (input instanceof Uint8Array) {
17
+ return Buffer.from(input);
18
+ }
19
+ if (typeof input !== "string") {
20
+ throw new ImageInputError(
21
+ "\u56FE\u7247\u8F93\u5165\u5FC5\u987B\u662F Buffer\u3001Uint8Array\u3001Data URL \u6216 Base64 \u5B57\u7B26\u4E32"
22
+ );
23
+ }
24
+ const value = input.trim();
25
+ const dataUrlMatch = value.match(
26
+ /^data:image\/(?:png|jpeg|webp);base64,([A-Za-z0-9+/=\s]+)$/i
27
+ );
28
+ const payload = (dataUrlMatch?.[1] ?? value).replace(/\s+/g, "");
29
+ if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 === 1) {
30
+ throw new ImageInputError("\u56FE\u7247\u5B57\u7B26\u4E32\u4E0D\u662F\u6709\u6548\u7684 Base64 \u6216 Data URL");
31
+ }
32
+ const buffer = Buffer.from(payload, "base64");
33
+ if (buffer.length === 0) {
34
+ throw new ImageInputError("\u56FE\u7247\u8F93\u5165\u4E3A\u7A7A");
35
+ }
36
+ return buffer;
37
+ }
38
+
39
+ // src/ocr.ts
40
+ import { createWorker } from "tesseract.js";
41
+ var TesseractOcr = class {
42
+ constructor(profile, logger) {
43
+ this.profile = profile;
44
+ this.logger = logger;
45
+ }
46
+ profile;
47
+ logger;
48
+ workerPromise;
49
+ getWorker() {
50
+ if (!this.workerPromise) {
51
+ const workerOptions = this.logger ? {
52
+ logger: (entry) => {
53
+ this.logger?.(entry);
54
+ }
55
+ } : {};
56
+ this.workerPromise = createWorker(
57
+ this.profile.language,
58
+ 1,
59
+ workerOptions
60
+ ).then(async (worker) => {
61
+ const typedWorker = worker;
62
+ await typedWorker.setParameters({
63
+ tessedit_char_whitelist: this.profile.whitelist,
64
+ tessedit_pageseg_mode: String(this.profile.pageSegMode),
65
+ preserve_interword_spaces: this.profile.preserveInterwordSpaces ? "1" : "0"
66
+ });
67
+ return typedWorker;
68
+ }).catch((error) => {
69
+ this.workerPromise = void 0;
70
+ throw error;
71
+ });
72
+ }
73
+ return this.workerPromise;
74
+ }
75
+ async recognize(image) {
76
+ const worker = await this.getWorker();
77
+ const result = await worker.recognize(image);
78
+ return result.data?.text ?? "";
79
+ }
80
+ async close() {
81
+ if (!this.workerPromise) {
82
+ return;
83
+ }
84
+ const worker = await this.workerPromise;
85
+ await worker.terminate();
86
+ this.workerPromise = void 0;
87
+ }
88
+ };
89
+
90
+ // src/preprocess.ts
91
+ import sharp from "sharp";
92
+ var DEFAULT_SCALE = 4;
93
+ var DEFAULT_THRESHOLD = 160;
94
+ async function preprocessImage(input, options = {}) {
95
+ const scale = options.scale ?? DEFAULT_SCALE;
96
+ const threshold = options.threshold ?? DEFAULT_THRESHOLD;
97
+ if (scale < 1 || scale > 10) {
98
+ throw new RangeError("scale must be between 1 and 10");
99
+ }
100
+ if (threshold < 1 || threshold > 254) {
101
+ throw new RangeError("threshold must be between 1 and 254");
102
+ }
103
+ const source = sharp(input, { failOn: "error" });
104
+ const metadata = await source.metadata();
105
+ const width = metadata.width ? Math.round(metadata.width * scale) : void 0;
106
+ const height = metadata.height ? Math.round(metadata.height * scale) : void 0;
107
+ const buffer = await source.rotate().resize({
108
+ width,
109
+ height,
110
+ fit: "fill",
111
+ kernel: sharp.kernel.lanczos3
112
+ }).flatten({ background: "#ffffff" }).grayscale().threshold(threshold).png().toBuffer();
113
+ return {
114
+ buffer,
115
+ source: getImageSource(metadata),
116
+ options: {
117
+ mode: "threshold",
118
+ scale,
119
+ threshold
120
+ }
121
+ };
122
+ }
123
+ function getImageSource(metadata) {
124
+ return {
125
+ width: metadata.width ?? null,
126
+ height: metadata.height ?? null,
127
+ format: metadata.format ?? null
128
+ };
129
+ }
130
+ function getPreprocessProfile(profile) {
131
+ return {
132
+ scale: profile?.scale ?? DEFAULT_SCALE,
133
+ thresholds: profile?.thresholds ?? [DEFAULT_THRESHOLD]
134
+ };
135
+ }
136
+
137
+ // src/recognizer.ts
138
+ var DEFAULT_OCR = {
139
+ language: "eng",
140
+ whitelist: "0123456789+-*/=?xX\xD7\xF7",
141
+ pageSegMode: 7,
142
+ preserveInterwordSpaces: false
143
+ };
144
+ var CaptchaRecognizer = class {
145
+ constructor(profile, logger) {
146
+ this.profile = profile;
147
+ this.ocr = new TesseractOcr(
148
+ {
149
+ ...DEFAULT_OCR,
150
+ ...profile.ocr
151
+ },
152
+ logger
153
+ );
154
+ }
155
+ profile;
156
+ ocr;
157
+ async recognize(input, options = {}) {
158
+ const startedAt = Date.now();
159
+ const image = toImageBuffer(input);
160
+ const original = await loadOriginal(image);
161
+ const originalAttempt = await this.runAttempt(original);
162
+ if (originalAttempt.ok) {
163
+ return this.formatResult(originalAttempt, startedAt, options);
164
+ }
165
+ const preprocessProfile = getPreprocessProfile(this.profile.preprocess);
166
+ const thresholds = options.thresholds ?? preprocessProfile.thresholds;
167
+ const scale = options.scale ?? preprocessProfile.scale;
168
+ let lastAttempt = originalAttempt;
169
+ for (const threshold of thresholds) {
170
+ const prepared = await preprocessImage(image, { scale, threshold });
171
+ const attempt = await this.runAttempt(prepared);
172
+ lastAttempt = attempt;
173
+ if (attempt.ok) {
174
+ break;
175
+ }
176
+ }
177
+ return this.formatResult(lastAttempt, startedAt, options);
178
+ }
179
+ async close() {
180
+ await this.ocr.close();
181
+ }
182
+ async runAttempt(prepared) {
183
+ const ocrText = await this.ocr.recognize(prepared.buffer);
184
+ const parsed = this.profile.rule.parse(ocrText);
185
+ if (!parsed.ok) {
186
+ return {
187
+ prepared,
188
+ ocrText: ocrText.trim(),
189
+ normalizedText: parsed.normalized,
190
+ expression: null,
191
+ answer: null,
192
+ error: parsed.error,
193
+ ok: false
194
+ };
195
+ }
196
+ try {
197
+ return {
198
+ prepared,
199
+ ocrText: ocrText.trim(),
200
+ normalizedText: parsed.normalized,
201
+ expression: parsed.expression,
202
+ answer: this.profile.rule.evaluate(parsed.parsed),
203
+ error: null,
204
+ ok: true
205
+ };
206
+ } catch (error) {
207
+ return {
208
+ prepared,
209
+ ocrText: ocrText.trim(),
210
+ normalizedText: parsed.normalized,
211
+ expression: parsed.expression,
212
+ answer: null,
213
+ error: {
214
+ code: "EVALUATION_FAILED",
215
+ message: error instanceof Error ? error.message : "\u7B54\u6848\u8BA1\u7B97\u5931\u8D25"
216
+ },
217
+ ok: false
218
+ };
219
+ }
220
+ }
221
+ formatResult(attempt, startedAt, options) {
222
+ const result = {
223
+ ok: attempt.ok,
224
+ profileId: this.profile.id,
225
+ ocrText: attempt.ocrText,
226
+ normalizedText: attempt.normalizedText,
227
+ expression: attempt.expression,
228
+ answer: attempt.answer,
229
+ error: attempt.error,
230
+ ocrVariant: attempt.prepared.options.mode,
231
+ durationMs: Date.now() - startedAt,
232
+ source: attempt.prepared.source,
233
+ preprocess: attempt.prepared.options
234
+ };
235
+ if (options.includeProcessedImage) {
236
+ result.processedImage = attempt.prepared.buffer;
237
+ }
238
+ return result;
239
+ }
240
+ };
241
+ function createRecognizer(options) {
242
+ return new CaptchaRecognizer(options.profile, options.logger);
243
+ }
244
+ async function loadOriginal(input) {
245
+ const metadata = await sharp2(input, { failOn: "error" }).metadata();
246
+ return {
247
+ buffer: input,
248
+ source: getImageSource(metadata),
249
+ options: {
250
+ mode: "original",
251
+ scale: 1,
252
+ threshold: null
253
+ }
254
+ };
255
+ }
256
+
257
+ // src/rules/arithmetic.ts
258
+ var DEFAULT_OPERATORS = {
259
+ "+": {
260
+ evaluate: (left, right) => left + right
261
+ },
262
+ "-": {
263
+ evaluate: (left, right) => left - right
264
+ },
265
+ "*": {
266
+ aliases: ["x", "X", "\xD7", "\xB7"],
267
+ evaluate: (left, right) => left * right
268
+ },
269
+ "/": {
270
+ aliases: ["\xF7"],
271
+ evaluate: (left, right) => left / right,
272
+ validate: (left, right) => right === 0 ? {
273
+ code: "DIVISION_BY_ZERO",
274
+ message: "\u9664\u6570\u4E0D\u80FD\u4E3A 0"
275
+ } : null
276
+ }
277
+ };
278
+ function createArithmeticRule(options = {}) {
279
+ const operators = options.operators ?? DEFAULT_OPERATORS;
280
+ const aliasToOperator = /* @__PURE__ */ new Map();
281
+ for (const [symbol, definition] of Object.entries(operators)) {
282
+ aliasToOperator.set(symbol, symbol);
283
+ for (const alias of definition.aliases ?? []) {
284
+ aliasToOperator.set(alias, symbol);
285
+ }
286
+ }
287
+ const operatorPattern = Object.keys(operators).sort((left, right) => right.length - left.length).map(escapeRegExp).join("|");
288
+ const expressionPattern = new RegExp(
289
+ `^(\\d{1,6})(${operatorPattern})(\\d{1,6})(?:=)?\\??$`
290
+ );
291
+ const normalize = (text) => {
292
+ let normalized = text.normalize("NFKC").replace(/\s+/g, "");
293
+ for (const [alias, symbol] of [...aliasToOperator.entries()].sort(
294
+ ([left], [right]) => right.length - left.length
295
+ )) {
296
+ normalized = normalized.split(alias).join(symbol);
297
+ }
298
+ normalized = normalized.replace(/[Oo]/g, "0").replace(/[Il|]/g, "1");
299
+ return normalized;
300
+ };
301
+ const parse = (text) => {
302
+ const normalized = normalize(text);
303
+ const match = normalized.match(expressionPattern);
304
+ if (!match) {
305
+ return {
306
+ ok: false,
307
+ normalized,
308
+ error: {
309
+ code: "EXPRESSION_NOT_MATCHED",
310
+ message: "\u65E0\u6CD5\u8BC6\u522B\u4E3A\u201C\u6570\u5B57 \u8FD0\u7B97\u7B26 \u6570\u5B57 =?\u201D\u683C\u5F0F"
311
+ }
312
+ };
313
+ }
314
+ const parsed = {
315
+ left: Number(match[1]),
316
+ operator: match[2],
317
+ right: Number(match[3])
318
+ };
319
+ const validationError = operators[parsed.operator].validate?.(parsed.left, parsed.right) ?? null;
320
+ if (validationError) {
321
+ return {
322
+ ok: false,
323
+ normalized,
324
+ error: validationError
325
+ };
326
+ }
327
+ return {
328
+ ok: true,
329
+ normalized,
330
+ expression: `${parsed.left}${parsed.operator}${parsed.right}`,
331
+ parsed
332
+ };
333
+ };
334
+ return {
335
+ id: options.id ?? "arithmetic-v1",
336
+ normalize,
337
+ parse,
338
+ evaluate: (parsed) => operators[parsed.operator].evaluate(parsed.left, parsed.right)
339
+ };
340
+ }
341
+ var arithmeticRule = createArithmeticRule();
342
+ function normalizeOcrText(text) {
343
+ return arithmeticRule.normalize(text);
344
+ }
345
+ function parseExpression(text) {
346
+ const parsed = arithmeticRule.parse(text);
347
+ if (!parsed.ok) {
348
+ return {
349
+ ok: false,
350
+ normalized: parsed.normalized,
351
+ expression: null,
352
+ answer: null,
353
+ error: parsed.error
354
+ };
355
+ }
356
+ return {
357
+ ok: true,
358
+ normalized: parsed.normalized,
359
+ expression: parsed.expression,
360
+ answer: arithmeticRule.evaluate(parsed.parsed),
361
+ error: null
362
+ };
363
+ }
364
+ function calculate(left, operator, right) {
365
+ const definition = DEFAULT_OPERATORS[operator];
366
+ if (!definition) {
367
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u8FD0\u7B97\u7B26: ${operator}`);
368
+ }
369
+ const validationError = definition.validate?.(left, right);
370
+ if (validationError) {
371
+ throw new Error(validationError.message);
372
+ }
373
+ return definition.evaluate(left, right);
374
+ }
375
+ var arithmeticProfile = {
376
+ id: "arithmetic-v1",
377
+ ocr: {
378
+ language: "eng",
379
+ whitelist: "0123456789+-*/=?xX\xD7\xF7",
380
+ pageSegMode: 7,
381
+ preserveInterwordSpaces: false
382
+ },
383
+ preprocess: {
384
+ scale: 4,
385
+ thresholds: [160, 120, 180, 200]
386
+ },
387
+ rule: arithmeticRule
388
+ };
389
+ function escapeRegExp(value) {
390
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
391
+ }
392
+
393
+ // src/index.ts
394
+ var defaultRecognizer;
395
+ function recognizeCaptcha(input, options = {}) {
396
+ if (!defaultRecognizer) {
397
+ defaultRecognizer = createRecognizer({ profile: arithmeticProfile });
398
+ }
399
+ return defaultRecognizer.recognize(input, options);
400
+ }
401
+ async function closeDefaultRecognizer() {
402
+ await defaultRecognizer?.close();
403
+ defaultRecognizer = void 0;
404
+ }
405
+ export {
406
+ CaptchaRecognizer,
407
+ DEFAULT_OPERATORS,
408
+ ImageInputError,
409
+ arithmeticProfile,
410
+ arithmeticRule,
411
+ calculate,
412
+ closeDefaultRecognizer,
413
+ createArithmeticRule,
414
+ createRecognizer,
415
+ getImageSource,
416
+ getPreprocessProfile,
417
+ normalizeOcrText,
418
+ parseExpression,
419
+ preprocessImage,
420
+ recognizeCaptcha,
421
+ toImageBuffer
422
+ };
423
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/recognizer.ts","../src/input.ts","../src/ocr.ts","../src/preprocess.ts","../src/rules/arithmetic.ts","../src/index.ts"],"sourcesContent":["import sharp from 'sharp';\nimport { toImageBuffer } from './input.js';\nimport { TesseractOcr } from './ocr.js';\nimport {\n getImageSource,\n getPreprocessProfile,\n preprocessImage,\n type PreparedImage\n} from './preprocess.js';\nimport type {\n CaptchaProfile,\n ImageInput,\n OcrLogEntry,\n OcrProfile,\n RecognitionError,\n RecognitionResult,\n RecognizeOptions,\n RuleParseResult\n} from './types.js';\n\nconst DEFAULT_OCR: OcrProfile = {\n language: 'eng',\n whitelist: '0123456789+-*/=?xX×÷',\n pageSegMode: 7,\n preserveInterwordSpaces: false\n};\n\ninterface Attempt<TAnswer> {\n prepared: PreparedImage;\n ocrText: string;\n normalizedText: string;\n expression: string | null;\n answer: TAnswer | null;\n error: RecognitionError | null;\n ok: boolean;\n}\n\nexport class CaptchaRecognizer<TParsed, TAnswer> {\n private readonly ocr: TesseractOcr;\n\n constructor(\n public readonly profile: CaptchaProfile<TParsed, TAnswer>,\n logger?: (entry: OcrLogEntry) => void\n ) {\n this.ocr = new TesseractOcr(\n {\n ...DEFAULT_OCR,\n ...profile.ocr\n },\n logger\n );\n }\n\n async recognize(\n input: ImageInput,\n options: RecognizeOptions = {}\n ): Promise<RecognitionResult<TAnswer>> {\n const startedAt = Date.now();\n const image = toImageBuffer(input);\n const original = await loadOriginal(image);\n const originalAttempt = await this.runAttempt(original);\n\n if (originalAttempt.ok) {\n return this.formatResult(originalAttempt, startedAt, options);\n }\n\n const preprocessProfile = getPreprocessProfile(this.profile.preprocess);\n const thresholds = options.thresholds ?? preprocessProfile.thresholds;\n const scale = options.scale ?? preprocessProfile.scale;\n let lastAttempt = originalAttempt;\n\n for (const threshold of thresholds) {\n const prepared = await preprocessImage(image, { scale, threshold });\n const attempt = await this.runAttempt(prepared);\n lastAttempt = attempt;\n if (attempt.ok) {\n break;\n }\n }\n\n return this.formatResult(lastAttempt, startedAt, options);\n }\n\n async close(): Promise<void> {\n await this.ocr.close();\n }\n\n private async runAttempt(prepared: PreparedImage): Promise<Attempt<TAnswer>> {\n const ocrText = await this.ocr.recognize(prepared.buffer);\n const parsed = this.profile.rule.parse(ocrText);\n\n if (!parsed.ok) {\n return {\n prepared,\n ocrText: ocrText.trim(),\n normalizedText: parsed.normalized,\n expression: null,\n answer: null,\n error: parsed.error,\n ok: false\n };\n }\n\n try {\n return {\n prepared,\n ocrText: ocrText.trim(),\n normalizedText: parsed.normalized,\n expression: parsed.expression,\n answer: this.profile.rule.evaluate(parsed.parsed),\n error: null,\n ok: true\n };\n } catch (error) {\n return {\n prepared,\n ocrText: ocrText.trim(),\n normalizedText: parsed.normalized,\n expression: parsed.expression,\n answer: null,\n error: {\n code: 'EVALUATION_FAILED',\n message: error instanceof Error ? error.message : '答案计算失败'\n },\n ok: false\n };\n }\n }\n\n private formatResult(\n attempt: Attempt<TAnswer>,\n startedAt: number,\n options: RecognizeOptions\n ): RecognitionResult<TAnswer> {\n const result: RecognitionResult<TAnswer> = {\n ok: attempt.ok,\n profileId: this.profile.id,\n ocrText: attempt.ocrText,\n normalizedText: attempt.normalizedText,\n expression: attempt.expression,\n answer: attempt.answer,\n error: attempt.error,\n ocrVariant: attempt.prepared.options.mode,\n durationMs: Date.now() - startedAt,\n source: attempt.prepared.source,\n preprocess: attempt.prepared.options\n };\n\n if (options.includeProcessedImage) {\n result.processedImage = attempt.prepared.buffer;\n }\n\n return result;\n }\n}\n\nexport function createRecognizer<TParsed, TAnswer>(options: {\n profile: CaptchaProfile<TParsed, TAnswer>;\n logger?: (entry: OcrLogEntry) => void;\n}): CaptchaRecognizer<TParsed, TAnswer> {\n return new CaptchaRecognizer(options.profile, options.logger);\n}\n\nasync function loadOriginal(input: Buffer): Promise<PreparedImage> {\n const metadata = await sharp(input, { failOn: 'error' }).metadata();\n return {\n buffer: input,\n source: getImageSource(metadata),\n options: {\n mode: 'original',\n scale: 1,\n threshold: null\n }\n };\n}\n","import type { ImageInput } from './types.js';\n\nexport class ImageInputError extends Error {\n readonly code = 'INVALID_IMAGE_INPUT';\n\n constructor(message: string) {\n super(message);\n this.name = 'ImageInputError';\n }\n}\n\nexport function toImageBuffer(input: ImageInput): Buffer {\n if (Buffer.isBuffer(input)) {\n return input;\n }\n\n if (input instanceof Uint8Array) {\n return Buffer.from(input);\n }\n\n if (typeof input !== 'string') {\n throw new ImageInputError(\n '图片输入必须是 Buffer、Uint8Array、Data URL 或 Base64 字符串'\n );\n }\n\n const value = input.trim();\n const dataUrlMatch = value.match(\n /^data:image\\/(?:png|jpeg|webp);base64,([A-Za-z0-9+/=\\s]+)$/i\n );\n const payload = (dataUrlMatch?.[1] ?? value).replace(/\\s+/g, '');\n\n if (!/^[A-Za-z0-9+/]+={0,2}$/.test(payload) || payload.length % 4 === 1) {\n throw new ImageInputError('图片字符串不是有效的 Base64 或 Data URL');\n }\n\n const buffer = Buffer.from(payload, 'base64');\n if (buffer.length === 0) {\n throw new ImageInputError('图片输入为空');\n }\n\n return buffer;\n}\n","import { createWorker } from 'tesseract.js';\nimport type { OcrLogEntry, OcrProfile } from './types.js';\n\ninterface OcrWorker {\n setParameters(parameters: Record<string, string>): Promise<unknown>;\n recognize(image: Buffer): Promise<{ data?: { text?: string } }>;\n terminate(): Promise<unknown>;\n}\n\nexport class TesseractOcr {\n private workerPromise?: Promise<OcrWorker>;\n\n constructor(\n private readonly profile: OcrProfile,\n private readonly logger?: (entry: OcrLogEntry) => void\n ) {}\n\n private getWorker(): Promise<OcrWorker> {\n if (!this.workerPromise) {\n const workerOptions = this.logger\n ? {\n logger: (entry: unknown) => {\n this.logger?.(entry as OcrLogEntry);\n }\n }\n : {};\n\n this.workerPromise = createWorker(\n this.profile.language,\n 1,\n workerOptions\n )\n .then(async (worker) => {\n const typedWorker = worker as unknown as OcrWorker;\n await typedWorker.setParameters({\n tessedit_char_whitelist: this.profile.whitelist,\n tessedit_pageseg_mode: String(this.profile.pageSegMode),\n preserve_interword_spaces: this.profile.preserveInterwordSpaces\n ? '1'\n : '0'\n });\n return typedWorker;\n })\n .catch((error) => {\n this.workerPromise = undefined;\n throw error;\n });\n }\n\n return this.workerPromise;\n }\n\n async recognize(image: Buffer): Promise<string> {\n const worker = await this.getWorker();\n const result = await worker.recognize(image);\n return result.data?.text ?? '';\n }\n\n async close(): Promise<void> {\n if (!this.workerPromise) {\n return;\n }\n\n const worker = await this.workerPromise;\n await worker.terminate();\n this.workerPromise = undefined;\n }\n}\n","import sharp, { type Metadata } from 'sharp';\nimport type {\n ImageSource,\n PreprocessInfo,\n PreprocessProfile\n} from './types.js';\n\nconst DEFAULT_SCALE = 4;\nconst DEFAULT_THRESHOLD = 160;\n\nexport interface PreparedImage {\n buffer: Buffer;\n source: ImageSource;\n options: PreprocessInfo;\n}\n\nexport async function preprocessImage(\n input: Buffer,\n options: { scale?: number; threshold?: number } = {}\n): Promise<PreparedImage> {\n const scale = options.scale ?? DEFAULT_SCALE;\n const threshold = options.threshold ?? DEFAULT_THRESHOLD;\n\n if (scale < 1 || scale > 10) {\n throw new RangeError('scale must be between 1 and 10');\n }\n if (threshold < 1 || threshold > 254) {\n throw new RangeError('threshold must be between 1 and 254');\n }\n\n const source = sharp(input, { failOn: 'error' });\n const metadata = await source.metadata();\n const width = metadata.width ? Math.round(metadata.width * scale) : undefined;\n const height = metadata.height\n ? Math.round(metadata.height * scale)\n : undefined;\n\n const buffer = await source\n .rotate()\n .resize({\n width,\n height,\n fit: 'fill',\n kernel: sharp.kernel.lanczos3\n })\n .flatten({ background: '#ffffff' })\n .grayscale()\n .threshold(threshold)\n .png()\n .toBuffer();\n\n return {\n buffer,\n source: getImageSource(metadata),\n options: {\n mode: 'threshold',\n scale,\n threshold\n }\n };\n}\n\nexport function getImageSource(\n metadata: Metadata\n): ImageSource {\n return {\n width: metadata.width ?? null,\n height: metadata.height ?? null,\n format: metadata.format ?? null\n };\n}\n\nexport function getPreprocessProfile(\n profile: PreprocessProfile | undefined\n): Required<PreprocessProfile> {\n return {\n scale: profile?.scale ?? DEFAULT_SCALE,\n thresholds: profile?.thresholds ?? [DEFAULT_THRESHOLD]\n };\n}\n","import type {\n CaptchaRule,\n CaptchaProfile,\n RecognitionError,\n RuleParseResult\n} from '../types.js';\n\nexport interface ArithmeticExpression {\n left: number;\n operator: string;\n right: number;\n}\n\nexport interface ArithmeticOperator {\n aliases?: string[];\n evaluate(left: number, right: number): number;\n validate?(left: number, right: number): RecognitionError | null;\n}\n\nexport const DEFAULT_OPERATORS: Record<string, ArithmeticOperator> = {\n '+': {\n evaluate: (left, right) => left + right\n },\n '-': {\n evaluate: (left, right) => left - right\n },\n '*': {\n aliases: ['x', 'X', '×', '·'],\n evaluate: (left, right) => left * right\n },\n '/': {\n aliases: ['÷'],\n evaluate: (left, right) => left / right,\n validate: (left, right) =>\n right === 0\n ? {\n code: 'DIVISION_BY_ZERO',\n message: '除数不能为 0'\n }\n : null\n }\n};\n\nexport interface ArithmeticRuleOptions {\n id?: string;\n whitelist?: string;\n operators?: Record<string, ArithmeticOperator>;\n}\n\nexport function createArithmeticRule(\n options: ArithmeticRuleOptions = {}\n): CaptchaRule<ArithmeticExpression, number> {\n const operators = options.operators ?? DEFAULT_OPERATORS;\n const aliasToOperator = new Map<string, string>();\n\n for (const [symbol, definition] of Object.entries(operators)) {\n aliasToOperator.set(symbol, symbol);\n for (const alias of definition.aliases ?? []) {\n aliasToOperator.set(alias, symbol);\n }\n }\n\n const operatorPattern = Object.keys(operators)\n .sort((left, right) => right.length - left.length)\n .map(escapeRegExp)\n .join('|');\n const expressionPattern = new RegExp(\n `^(\\\\d{1,6})(${operatorPattern})(\\\\d{1,6})(?:=)?\\\\??$`\n );\n\n const normalize = (text: string): string => {\n let normalized = text.normalize('NFKC').replace(/\\s+/g, '');\n for (const [alias, symbol] of [...aliasToOperator.entries()].sort(\n ([left], [right]) => right.length - left.length\n )) {\n normalized = normalized.split(alias).join(symbol);\n }\n normalized = normalized.replace(/[Oo]/g, '0').replace(/[Il|]/g, '1');\n return normalized;\n };\n\n const parse = (text: string): RuleParseResult<ArithmeticExpression> => {\n const normalized = normalize(text);\n const match = normalized.match(expressionPattern);\n\n if (!match) {\n return {\n ok: false,\n normalized,\n error: {\n code: 'EXPRESSION_NOT_MATCHED',\n message: '无法识别为“数字 运算符 数字 =?”格式'\n }\n };\n }\n\n const parsed: ArithmeticExpression = {\n left: Number(match[1]),\n operator: match[2],\n right: Number(match[3])\n };\n const validationError =\n operators[parsed.operator].validate?.(parsed.left, parsed.right) ?? null;\n\n if (validationError) {\n return {\n ok: false,\n normalized,\n error: validationError\n };\n }\n\n return {\n ok: true,\n normalized,\n expression: `${parsed.left}${parsed.operator}${parsed.right}`,\n parsed\n };\n };\n\n return {\n id: options.id ?? 'arithmetic-v1',\n normalize,\n parse,\n evaluate: (parsed) =>\n operators[parsed.operator].evaluate(parsed.left, parsed.right)\n };\n}\n\nexport const arithmeticRule = createArithmeticRule();\n\nexport interface LegacyExpressionSuccess {\n ok: true;\n normalized: string;\n expression: string;\n answer: number;\n error: null;\n}\n\nexport interface LegacyExpressionFailure {\n ok: false;\n normalized: string;\n expression: null;\n answer: null;\n error: RecognitionError;\n}\n\nexport type LegacyExpressionResult =\n | LegacyExpressionSuccess\n | LegacyExpressionFailure;\n\nexport function normalizeOcrText(text: string): string {\n return arithmeticRule.normalize(text);\n}\n\nexport function parseExpression(text: string): LegacyExpressionResult {\n const parsed = arithmeticRule.parse(text);\n if (!parsed.ok) {\n return {\n ok: false,\n normalized: parsed.normalized,\n expression: null,\n answer: null,\n error: parsed.error\n };\n }\n\n return {\n ok: true,\n normalized: parsed.normalized,\n expression: parsed.expression,\n answer: arithmeticRule.evaluate(parsed.parsed),\n error: null\n };\n}\n\nexport function calculate(\n left: number,\n operator: string,\n right: number\n): number {\n const definition = DEFAULT_OPERATORS[operator];\n if (!definition) {\n throw new Error(`不支持的运算符: ${operator}`);\n }\n const validationError = definition.validate?.(left, right);\n if (validationError) {\n throw new Error(validationError.message);\n }\n return definition.evaluate(left, right);\n}\n\nexport const arithmeticProfile: CaptchaProfile<\n ArithmeticExpression,\n number\n> = {\n id: 'arithmetic-v1',\n ocr: {\n language: 'eng',\n whitelist: '0123456789+-*/=?xX×÷',\n pageSegMode: 7,\n preserveInterwordSpaces: false\n },\n preprocess: {\n scale: 4,\n thresholds: [160, 120, 180, 200]\n },\n rule: arithmeticRule\n};\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n","import {\n CaptchaRecognizer,\n createRecognizer\n} from './recognizer.js';\nimport {\n arithmeticProfile,\n type ArithmeticExpression\n} from './rules/arithmetic.js';\nimport type {\n ImageInput,\n RecognitionResult,\n RecognizeOptions\n} from './types.js';\n\nexport * from './types.js';\nexport * from './input.js';\nexport * from './preprocess.js';\nexport * from './recognizer.js';\nexport * from './rules/arithmetic.js';\n\nexport { arithmeticProfile, createRecognizer, CaptchaRecognizer };\n\nlet defaultRecognizer:\n | CaptchaRecognizer<ArithmeticExpression, number>\n | undefined;\n\nexport function recognizeCaptcha(\n input: ImageInput,\n options: RecognizeOptions = {}\n): Promise<RecognitionResult<number>> {\n if (!defaultRecognizer) {\n defaultRecognizer = createRecognizer({ profile: arithmeticProfile });\n }\n\n return defaultRecognizer.recognize(input, options);\n}\n\nexport async function closeDefaultRecognizer(): Promise<void> {\n await defaultRecognizer?.close();\n defaultRecognizer = undefined;\n}\n"],"mappings":"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","names":["sharp","sharp"]}
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/rules/arithmetic.ts
21
+ var arithmetic_exports = {};
22
+ __export(arithmetic_exports, {
23
+ DEFAULT_OPERATORS: () => DEFAULT_OPERATORS,
24
+ arithmeticProfile: () => arithmeticProfile,
25
+ arithmeticRule: () => arithmeticRule,
26
+ calculate: () => calculate,
27
+ createArithmeticRule: () => createArithmeticRule,
28
+ normalizeOcrText: () => normalizeOcrText,
29
+ parseExpression: () => parseExpression
30
+ });
31
+ module.exports = __toCommonJS(arithmetic_exports);
32
+ var DEFAULT_OPERATORS = {
33
+ "+": {
34
+ evaluate: (left, right) => left + right
35
+ },
36
+ "-": {
37
+ evaluate: (left, right) => left - right
38
+ },
39
+ "*": {
40
+ aliases: ["x", "X", "\xD7", "\xB7"],
41
+ evaluate: (left, right) => left * right
42
+ },
43
+ "/": {
44
+ aliases: ["\xF7"],
45
+ evaluate: (left, right) => left / right,
46
+ validate: (left, right) => right === 0 ? {
47
+ code: "DIVISION_BY_ZERO",
48
+ message: "\u9664\u6570\u4E0D\u80FD\u4E3A 0"
49
+ } : null
50
+ }
51
+ };
52
+ function createArithmeticRule(options = {}) {
53
+ const operators = options.operators ?? DEFAULT_OPERATORS;
54
+ const aliasToOperator = /* @__PURE__ */ new Map();
55
+ for (const [symbol, definition] of Object.entries(operators)) {
56
+ aliasToOperator.set(symbol, symbol);
57
+ for (const alias of definition.aliases ?? []) {
58
+ aliasToOperator.set(alias, symbol);
59
+ }
60
+ }
61
+ const operatorPattern = Object.keys(operators).sort((left, right) => right.length - left.length).map(escapeRegExp).join("|");
62
+ const expressionPattern = new RegExp(
63
+ `^(\\d{1,6})(${operatorPattern})(\\d{1,6})(?:=)?\\??$`
64
+ );
65
+ const normalize = (text) => {
66
+ let normalized = text.normalize("NFKC").replace(/\s+/g, "");
67
+ for (const [alias, symbol] of [...aliasToOperator.entries()].sort(
68
+ ([left], [right]) => right.length - left.length
69
+ )) {
70
+ normalized = normalized.split(alias).join(symbol);
71
+ }
72
+ normalized = normalized.replace(/[Oo]/g, "0").replace(/[Il|]/g, "1");
73
+ return normalized;
74
+ };
75
+ const parse = (text) => {
76
+ const normalized = normalize(text);
77
+ const match = normalized.match(expressionPattern);
78
+ if (!match) {
79
+ return {
80
+ ok: false,
81
+ normalized,
82
+ error: {
83
+ code: "EXPRESSION_NOT_MATCHED",
84
+ message: "\u65E0\u6CD5\u8BC6\u522B\u4E3A\u201C\u6570\u5B57 \u8FD0\u7B97\u7B26 \u6570\u5B57 =?\u201D\u683C\u5F0F"
85
+ }
86
+ };
87
+ }
88
+ const parsed = {
89
+ left: Number(match[1]),
90
+ operator: match[2],
91
+ right: Number(match[3])
92
+ };
93
+ const validationError = operators[parsed.operator].validate?.(parsed.left, parsed.right) ?? null;
94
+ if (validationError) {
95
+ return {
96
+ ok: false,
97
+ normalized,
98
+ error: validationError
99
+ };
100
+ }
101
+ return {
102
+ ok: true,
103
+ normalized,
104
+ expression: `${parsed.left}${parsed.operator}${parsed.right}`,
105
+ parsed
106
+ };
107
+ };
108
+ return {
109
+ id: options.id ?? "arithmetic-v1",
110
+ normalize,
111
+ parse,
112
+ evaluate: (parsed) => operators[parsed.operator].evaluate(parsed.left, parsed.right)
113
+ };
114
+ }
115
+ var arithmeticRule = createArithmeticRule();
116
+ function normalizeOcrText(text) {
117
+ return arithmeticRule.normalize(text);
118
+ }
119
+ function parseExpression(text) {
120
+ const parsed = arithmeticRule.parse(text);
121
+ if (!parsed.ok) {
122
+ return {
123
+ ok: false,
124
+ normalized: parsed.normalized,
125
+ expression: null,
126
+ answer: null,
127
+ error: parsed.error
128
+ };
129
+ }
130
+ return {
131
+ ok: true,
132
+ normalized: parsed.normalized,
133
+ expression: parsed.expression,
134
+ answer: arithmeticRule.evaluate(parsed.parsed),
135
+ error: null
136
+ };
137
+ }
138
+ function calculate(left, operator, right) {
139
+ const definition = DEFAULT_OPERATORS[operator];
140
+ if (!definition) {
141
+ throw new Error(`\u4E0D\u652F\u6301\u7684\u8FD0\u7B97\u7B26: ${operator}`);
142
+ }
143
+ const validationError = definition.validate?.(left, right);
144
+ if (validationError) {
145
+ throw new Error(validationError.message);
146
+ }
147
+ return definition.evaluate(left, right);
148
+ }
149
+ var arithmeticProfile = {
150
+ id: "arithmetic-v1",
151
+ ocr: {
152
+ language: "eng",
153
+ whitelist: "0123456789+-*/=?xX\xD7\xF7",
154
+ pageSegMode: 7,
155
+ preserveInterwordSpaces: false
156
+ },
157
+ preprocess: {
158
+ scale: 4,
159
+ thresholds: [160, 120, 180, 200]
160
+ },
161
+ rule: arithmeticRule
162
+ };
163
+ function escapeRegExp(value) {
164
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
165
+ }
166
+ // Annotate the CommonJS export names for ESM import in node:
167
+ 0 && (module.exports = {
168
+ DEFAULT_OPERATORS,
169
+ arithmeticProfile,
170
+ arithmeticRule,
171
+ calculate,
172
+ createArithmeticRule,
173
+ normalizeOcrText,
174
+ parseExpression
175
+ });
176
+ //# sourceMappingURL=arithmetic.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/rules/arithmetic.ts"],"sourcesContent":["import type {\n CaptchaRule,\n CaptchaProfile,\n RecognitionError,\n RuleParseResult\n} from '../types.js';\n\nexport interface ArithmeticExpression {\n left: number;\n operator: string;\n right: number;\n}\n\nexport interface ArithmeticOperator {\n aliases?: string[];\n evaluate(left: number, right: number): number;\n validate?(left: number, right: number): RecognitionError | null;\n}\n\nexport const DEFAULT_OPERATORS: Record<string, ArithmeticOperator> = {\n '+': {\n evaluate: (left, right) => left + right\n },\n '-': {\n evaluate: (left, right) => left - right\n },\n '*': {\n aliases: ['x', 'X', '×', '·'],\n evaluate: (left, right) => left * right\n },\n '/': {\n aliases: ['÷'],\n evaluate: (left, right) => left / right,\n validate: (left, right) =>\n right === 0\n ? {\n code: 'DIVISION_BY_ZERO',\n message: '除数不能为 0'\n }\n : null\n }\n};\n\nexport interface ArithmeticRuleOptions {\n id?: string;\n whitelist?: string;\n operators?: Record<string, ArithmeticOperator>;\n}\n\nexport function createArithmeticRule(\n options: ArithmeticRuleOptions = {}\n): CaptchaRule<ArithmeticExpression, number> {\n const operators = options.operators ?? DEFAULT_OPERATORS;\n const aliasToOperator = new Map<string, string>();\n\n for (const [symbol, definition] of Object.entries(operators)) {\n aliasToOperator.set(symbol, symbol);\n for (const alias of definition.aliases ?? []) {\n aliasToOperator.set(alias, symbol);\n }\n }\n\n const operatorPattern = Object.keys(operators)\n .sort((left, right) => right.length - left.length)\n .map(escapeRegExp)\n .join('|');\n const expressionPattern = new RegExp(\n `^(\\\\d{1,6})(${operatorPattern})(\\\\d{1,6})(?:=)?\\\\??$`\n );\n\n const normalize = (text: string): string => {\n let normalized = text.normalize('NFKC').replace(/\\s+/g, '');\n for (const [alias, symbol] of [...aliasToOperator.entries()].sort(\n ([left], [right]) => right.length - left.length\n )) {\n normalized = normalized.split(alias).join(symbol);\n }\n normalized = normalized.replace(/[Oo]/g, '0').replace(/[Il|]/g, '1');\n return normalized;\n };\n\n const parse = (text: string): RuleParseResult<ArithmeticExpression> => {\n const normalized = normalize(text);\n const match = normalized.match(expressionPattern);\n\n if (!match) {\n return {\n ok: false,\n normalized,\n error: {\n code: 'EXPRESSION_NOT_MATCHED',\n message: '无法识别为“数字 运算符 数字 =?”格式'\n }\n };\n }\n\n const parsed: ArithmeticExpression = {\n left: Number(match[1]),\n operator: match[2],\n right: Number(match[3])\n };\n const validationError =\n operators[parsed.operator].validate?.(parsed.left, parsed.right) ?? null;\n\n if (validationError) {\n return {\n ok: false,\n normalized,\n error: validationError\n };\n }\n\n return {\n ok: true,\n normalized,\n expression: `${parsed.left}${parsed.operator}${parsed.right}`,\n parsed\n };\n };\n\n return {\n id: options.id ?? 'arithmetic-v1',\n normalize,\n parse,\n evaluate: (parsed) =>\n operators[parsed.operator].evaluate(parsed.left, parsed.right)\n };\n}\n\nexport const arithmeticRule = createArithmeticRule();\n\nexport interface LegacyExpressionSuccess {\n ok: true;\n normalized: string;\n expression: string;\n answer: number;\n error: null;\n}\n\nexport interface LegacyExpressionFailure {\n ok: false;\n normalized: string;\n expression: null;\n answer: null;\n error: RecognitionError;\n}\n\nexport type LegacyExpressionResult =\n | LegacyExpressionSuccess\n | LegacyExpressionFailure;\n\nexport function normalizeOcrText(text: string): string {\n return arithmeticRule.normalize(text);\n}\n\nexport function parseExpression(text: string): LegacyExpressionResult {\n const parsed = arithmeticRule.parse(text);\n if (!parsed.ok) {\n return {\n ok: false,\n normalized: parsed.normalized,\n expression: null,\n answer: null,\n error: parsed.error\n };\n }\n\n return {\n ok: true,\n normalized: parsed.normalized,\n expression: parsed.expression,\n answer: arithmeticRule.evaluate(parsed.parsed),\n error: null\n };\n}\n\nexport function calculate(\n left: number,\n operator: string,\n right: number\n): number {\n const definition = DEFAULT_OPERATORS[operator];\n if (!definition) {\n throw new Error(`不支持的运算符: ${operator}`);\n }\n const validationError = definition.validate?.(left, right);\n if (validationError) {\n throw new Error(validationError.message);\n }\n return definition.evaluate(left, right);\n}\n\nexport const arithmeticProfile: CaptchaProfile<\n ArithmeticExpression,\n number\n> = {\n id: 'arithmetic-v1',\n ocr: {\n language: 'eng',\n whitelist: '0123456789+-*/=?xX×÷',\n pageSegMode: 7,\n preserveInterwordSpaces: false\n },\n preprocess: {\n scale: 4,\n thresholds: [160, 120, 180, 200]\n },\n rule: arithmeticRule\n};\n\nfunction escapeRegExp(value: string): string {\n return value.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1 @@
1
+ export { A as ArithmeticExpression, d as ArithmeticOperator, e as ArithmeticRuleOptions, D as DEFAULT_OPERATORS, L as LegacyExpressionFailure, g as LegacyExpressionResult, h as LegacyExpressionSuccess, n as arithmeticProfile, o as arithmeticRule, p as calculate, q as createArithmeticRule, r as normalizeOcrText, s as parseExpression } from '../arithmetic-CUs9apP8.cjs';
@@ -0,0 +1 @@
1
+ export { A as ArithmeticExpression, d as ArithmeticOperator, e as ArithmeticRuleOptions, D as DEFAULT_OPERATORS, L as LegacyExpressionFailure, g as LegacyExpressionResult, h as LegacyExpressionSuccess, n as arithmeticProfile, o as arithmeticRule, p as calculate, q as createArithmeticRule, r as normalizeOcrText, s as parseExpression } from '../arithmetic-CUs9apP8.js';