nbis-wrapper-js 0.1.0 → 0.3.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/README.md CHANGED
@@ -52,6 +52,26 @@ Stream inputs also accept `inputName`/`inputExtension` (for tools that infer for
52
52
  from filenames). For `Bozorth3`, you can pass `{ input, label }` to keep stable
53
53
  `probe`/`gallery` identifiers in results.
54
54
 
55
+ ## Image Auto-Convert (default on)
56
+
57
+ `Mindtct` and `Nfiq` can auto-convert PNG/JPEG inputs into WSQ before invoking NBIS.
58
+ This is enabled by default and can be disabled via the constructor.
59
+
60
+ ```ts
61
+ import { Mindtct } from 'nbis-wrapper-js';
62
+
63
+ const mindtct = new Mindtct({ autoConvert: false });
64
+
65
+ // Or tune WSQ conversion:
66
+ const tuned = new Mindtct({
67
+ autoConvert: {
68
+ wsqBitrate: 0.75,
69
+ ppi: 500,
70
+ upscale: { enabled: true, minSize: 256 }
71
+ }
72
+ });
73
+ ```
74
+
55
75
  ```ts
56
76
  import fs from 'fs';
57
77
  import { Cwsq } from 'nbis-wrapper-js';
@@ -0,0 +1,26 @@
1
+ export type AutoConvertConfig = {
2
+ enabled?: boolean;
3
+ wsqBitrate?: number;
4
+ ppi?: number;
5
+ upscale?: boolean | AutoConvertUpscaleConfig;
6
+ };
7
+ export type AutoConvertUpscaleConfig = {
8
+ enabled?: boolean;
9
+ minSize?: number;
10
+ };
11
+ export type NormalizedAutoConvertConfig = {
12
+ enabled: boolean;
13
+ wsqBitrate: number;
14
+ ppi?: number;
15
+ upscale: {
16
+ enabled: boolean;
17
+ minSize: number;
18
+ };
19
+ };
20
+ export type PreparedImage = {
21
+ path: string;
22
+ cleanup?: () => void;
23
+ converted?: boolean;
24
+ };
25
+ export declare function prepareFingerprintImageSync(inputPath: string, options?: boolean | AutoConvertConfig, tempDir?: string): PreparedImage;
26
+ export declare function normalizeAutoConvertConfig(options?: boolean | AutoConvertConfig): NormalizedAutoConvertConfig;
@@ -0,0 +1,153 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.prepareFingerprintImageSync = prepareFingerprintImageSync;
7
+ exports.normalizeAutoConvertConfig = normalizeAutoConvertConfig;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const os_1 = __importDefault(require("os"));
10
+ const path_1 = __importDefault(require("path"));
11
+ const pngjs_1 = require("pngjs");
12
+ const jpeg_js_1 = __importDefault(require("jpeg-js"));
13
+ const nbis_runtime_1 = require("./nbis-runtime");
14
+ const SUPPORTED_ANSI_NIST_EXTENSIONS = new Set(['.an2', '.an2k', '.an2k7', '.eft']);
15
+ function normalizeAutoConvert(config) {
16
+ const defaultUpscale = { enabled: false, minSize: 256 };
17
+ if (config == null) {
18
+ return { enabled: true, wsqBitrate: 0.75, upscale: defaultUpscale };
19
+ }
20
+ if (typeof config === 'boolean') {
21
+ return { enabled: config, wsqBitrate: 0.75, upscale: defaultUpscale };
22
+ }
23
+ const upscale = config.upscale == null
24
+ ? defaultUpscale
25
+ : typeof config.upscale === 'boolean'
26
+ ? { enabled: config.upscale, minSize: defaultUpscale.minSize }
27
+ : {
28
+ enabled: config.upscale.enabled ?? defaultUpscale.enabled,
29
+ minSize: config.upscale.minSize ?? defaultUpscale.minSize
30
+ };
31
+ return {
32
+ enabled: config.enabled ?? true,
33
+ wsqBitrate: config.wsqBitrate ?? 0.75,
34
+ ppi: config.ppi,
35
+ upscale
36
+ };
37
+ }
38
+ function detectFormat(inputPath) {
39
+ const ext = path_1.default.extname(inputPath).toLowerCase();
40
+ if (ext === '.wsq') {
41
+ return 'wsq';
42
+ }
43
+ if (SUPPORTED_ANSI_NIST_EXTENSIONS.has(ext)) {
44
+ return 'ansi-nist';
45
+ }
46
+ const fd = fs_1.default.openSync(inputPath, 'r');
47
+ try {
48
+ const header = Buffer.alloc(8);
49
+ const bytes = fs_1.default.readSync(fd, header, 0, header.length, 0);
50
+ if (bytes >= 8 && header.slice(0, 8).equals(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]))) {
51
+ return 'png';
52
+ }
53
+ if (bytes >= 3 && header[0] === 0xff && header[1] === 0xd8 && header[2] === 0xff) {
54
+ return 'jpeg';
55
+ }
56
+ }
57
+ finally {
58
+ fs_1.default.closeSync(fd);
59
+ }
60
+ return 'unknown';
61
+ }
62
+ function toGrayscale(rgba) {
63
+ const pixelCount = Math.floor(rgba.length / 4);
64
+ const gray = Buffer.alloc(pixelCount);
65
+ for (let i = 0; i < pixelCount; i += 1) {
66
+ const idx = i * 4;
67
+ const r = rgba[idx];
68
+ const g = rgba[idx + 1];
69
+ const b = rgba[idx + 2];
70
+ gray[i] = Math.round(0.299 * r + 0.587 * g + 0.114 * b);
71
+ }
72
+ return gray;
73
+ }
74
+ function upscaleNearest(pixels, width, height, newWidth, newHeight) {
75
+ const scaled = Buffer.alloc(newWidth * newHeight);
76
+ for (let y = 0; y < newHeight; y += 1) {
77
+ const srcY = Math.min(height - 1, Math.floor((y * height) / newHeight));
78
+ for (let x = 0; x < newWidth; x += 1) {
79
+ const srcX = Math.min(width - 1, Math.floor((x * width) / newWidth));
80
+ scaled[y * newWidth + x] = pixels[srcY * width + srcX];
81
+ }
82
+ }
83
+ return scaled;
84
+ }
85
+ function decodeImage(inputPath, format) {
86
+ const data = fs_1.default.readFileSync(inputPath);
87
+ if (format === 'png') {
88
+ const decoded = pngjs_1.PNG.sync.read(data);
89
+ return { width: decoded.width, height: decoded.height, pixels: toGrayscale(decoded.data) };
90
+ }
91
+ const decoded = jpeg_js_1.default.decode(data, { useTArray: true });
92
+ if (!decoded || !decoded.data) {
93
+ throw new Error('jpeg decode failed.');
94
+ }
95
+ return { width: decoded.width, height: decoded.height, pixels: toGrayscale(Buffer.from(decoded.data)) };
96
+ }
97
+ function replaceExtension(inputPath, outputExtension) {
98
+ const parsed = path_1.default.parse(inputPath);
99
+ const ext = outputExtension.startsWith('.') ? outputExtension : `.${outputExtension}`;
100
+ return path_1.default.join(parsed.dir, `${parsed.name}${ext}`);
101
+ }
102
+ function convertToWsq(inputPath, tempDir, config) {
103
+ const format = detectFormat(inputPath);
104
+ if (format !== 'png' && format !== 'jpeg') {
105
+ return inputPath;
106
+ }
107
+ const decoded = decodeImage(inputPath, format);
108
+ let width = decoded.width;
109
+ let height = decoded.height;
110
+ let pixels = decoded.pixels;
111
+ if (config.upscale.enabled) {
112
+ const minSize = config.upscale.minSize;
113
+ if (width < minSize || height < minSize) {
114
+ const scale = Math.max(minSize / width, minSize / height);
115
+ const newWidth = Math.ceil(width * scale);
116
+ const newHeight = Math.ceil(height * scale);
117
+ pixels = upscaleNearest(pixels, width, height, newWidth, newHeight);
118
+ width = newWidth;
119
+ height = newHeight;
120
+ }
121
+ }
122
+ const rawPath = path_1.default.join(tempDir, 'input.raw');
123
+ fs_1.default.writeFileSync(rawPath, pixels);
124
+ const rawAttrs = [width, height, 8];
125
+ if (config.ppi != null) {
126
+ rawAttrs.push(config.ppi);
127
+ }
128
+ const args = [String(config.wsqBitrate), 'wsq', rawPath, '-raw_in', rawAttrs.join(',')];
129
+ const result = (0, nbis_runtime_1.runNbis)('cwsq', args);
130
+ (0, nbis_runtime_1.assertSuccess)(result, 'cwsq (auto-convert)');
131
+ return replaceExtension(rawPath, 'wsq');
132
+ }
133
+ function prepareFingerprintImageSync(inputPath, options, tempDir) {
134
+ const config = normalizeAutoConvert(options);
135
+ if (!config.enabled) {
136
+ return { path: inputPath };
137
+ }
138
+ const format = detectFormat(inputPath);
139
+ if (format === 'wsq' || format === 'ansi-nist' || format === 'unknown') {
140
+ return { path: inputPath };
141
+ }
142
+ const ownedTempDir = tempDir ?? fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'nbis-img-'));
143
+ const convertedPath = convertToWsq(inputPath, ownedTempDir, config);
144
+ const cleanup = tempDir
145
+ ? undefined
146
+ : () => {
147
+ fs_1.default.rmSync(ownedTempDir, { recursive: true, force: true });
148
+ };
149
+ return { path: convertedPath, cleanup, converted: convertedPath !== inputPath };
150
+ }
151
+ function normalizeAutoConvertConfig(options) {
152
+ return normalizeAutoConvert(options);
153
+ }
@@ -1,3 +1,4 @@
1
+ import type { AutoConvertConfig } from './image-convert';
1
2
  import type { StreamInput, StreamInputOptions, StreamOutput } from './temp-io';
2
3
  export type MindtctDetectOptions = {
3
4
  enhanceLowContrast?: boolean;
@@ -19,6 +20,9 @@ export type MindtctDetectResult = {
19
20
  stdout: string;
20
21
  stderr: string;
21
22
  };
23
+ export type MindtctConstructorOptions = {
24
+ autoConvert?: boolean | AutoConvertConfig;
25
+ };
22
26
  export type MindtctStreamOutputs = Partial<Record<keyof MindtctDetectResult['outputs'], StreamOutput>>;
23
27
  export type MindtctStreamOptions = MindtctDetectOptions & StreamInputOptions & {
24
28
  outputs?: MindtctStreamOutputs;
@@ -30,6 +34,8 @@ export type MindtctStreamResult = {
30
34
  stderr: string;
31
35
  };
32
36
  export declare class Mindtct {
37
+ private autoConvert;
38
+ constructor(options?: MindtctConstructorOptions);
33
39
  version(): string;
34
40
  detect(fingerImagePath: string, outputRoot: string, options?: MindtctDetectOptions): MindtctDetectResult;
35
41
  detectStream(input: StreamInput, options?: MindtctStreamOptions): Promise<MindtctStreamResult>;
@@ -6,45 +6,56 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Mindtct = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const nbis_runtime_1 = require("./nbis-runtime");
9
+ const image_convert_1 = require("./image-convert");
9
10
  const temp_io_1 = require("./temp-io");
10
11
  class Mindtct {
12
+ constructor(options = {}) {
13
+ this.autoConvert = (0, image_convert_1.normalizeAutoConvertConfig)(options.autoConvert);
14
+ }
11
15
  version() {
12
16
  const result = (0, nbis_runtime_1.runNbis)('mindtct', ['-version']);
13
17
  (0, nbis_runtime_1.assertSuccess)(result, 'mindtct -version');
14
18
  return result.stdout + result.stderr;
15
19
  }
16
20
  detect(fingerImagePath, outputRoot, options = {}) {
17
- const args = [];
18
- if (options.enhanceLowContrast) {
19
- args.push('-b');
21
+ const prepared = (0, image_convert_1.prepareFingerprintImageSync)(fingerImagePath, this.autoConvert);
22
+ try {
23
+ const args = [];
24
+ if (options.enhanceLowContrast) {
25
+ args.push('-b');
26
+ }
27
+ if (options.useM1) {
28
+ args.push('-m1');
29
+ }
30
+ args.push(prepared.path, outputRoot);
31
+ const result = (0, nbis_runtime_1.runNbis)('mindtct', args);
32
+ (0, nbis_runtime_1.assertSuccess)(result, 'mindtct');
33
+ const outputs = {
34
+ mdt: `${outputRoot}.mdt`,
35
+ xyt: `${outputRoot}.xyt`,
36
+ brw: `${outputRoot}.brw`,
37
+ dm: `${outputRoot}.dm`,
38
+ hcm: `${outputRoot}.hcm`,
39
+ lcm: `${outputRoot}.lcm`,
40
+ lfm: `${outputRoot}.lfm`,
41
+ qm: `${outputRoot}.qm`,
42
+ min: `${outputRoot}.min`
43
+ };
44
+ return {
45
+ outputs,
46
+ outputRoot: path_1.default.resolve(outputRoot),
47
+ stdout: result.stdout,
48
+ stderr: result.stderr
49
+ };
20
50
  }
21
- if (options.useM1) {
22
- args.push('-m1');
51
+ finally {
52
+ prepared.cleanup?.();
23
53
  }
24
- args.push(fingerImagePath, outputRoot);
25
- const result = (0, nbis_runtime_1.runNbis)('mindtct', args);
26
- (0, nbis_runtime_1.assertSuccess)(result, 'mindtct');
27
- const outputs = {
28
- mdt: `${outputRoot}.mdt`,
29
- xyt: `${outputRoot}.xyt`,
30
- brw: `${outputRoot}.brw`,
31
- dm: `${outputRoot}.dm`,
32
- hcm: `${outputRoot}.hcm`,
33
- lcm: `${outputRoot}.lcm`,
34
- lfm: `${outputRoot}.lfm`,
35
- qm: `${outputRoot}.qm`,
36
- min: `${outputRoot}.min`
37
- };
38
- return {
39
- outputs,
40
- outputRoot: path_1.default.resolve(outputRoot),
41
- stdout: result.stdout,
42
- stderr: result.stderr
43
- };
44
54
  }
45
55
  async detectStream(input, options = {}) {
46
56
  return (0, temp_io_1.withTempDir)(async (tempDir) => {
47
57
  const fingerImagePath = await (0, temp_io_1.writeTempInput)(tempDir, input, options);
58
+ const prepared = (0, image_convert_1.prepareFingerprintImageSync)(fingerImagePath, this.autoConvert, tempDir);
48
59
  const outputRoot = path_1.default.join(tempDir, 'output');
49
60
  const outputToBuffer = options.outputToBuffer ?? false;
50
61
  const args = [];
@@ -54,7 +65,7 @@ class Mindtct {
54
65
  if (options.useM1) {
55
66
  args.push('-m1');
56
67
  }
57
- args.push(fingerImagePath, outputRoot);
68
+ args.push(prepared.path, outputRoot);
58
69
  const result = (0, nbis_runtime_1.runNbis)('mindtct', args);
59
70
  (0, nbis_runtime_1.assertSuccess)(result, 'mindtct');
60
71
  const outputs = {
@@ -1,3 +1,4 @@
1
+ import type { AutoConvertConfig } from './image-convert';
1
2
  import type { StreamInput, StreamInputOptions, StreamOutputOptions } from './temp-io';
2
3
  export type NfiqRecordSelectors = {
3
4
  fingerPosition?: string[];
@@ -31,6 +32,9 @@ export type NfiqResult = {
31
32
  stdout: string;
32
33
  stderr: string;
33
34
  };
35
+ export type NfiqConstructorOptions = {
36
+ autoConvert?: boolean | AutoConvertConfig;
37
+ };
34
38
  export type NfiqStreamOptions = Omit<NfiqOptions, 'ansiNistOutputPath'> & StreamInputOptions & {
35
39
  ansiNistOutput?: StreamOutputOptions;
36
40
  };
@@ -38,6 +42,8 @@ export type NfiqStreamResult = NfiqResult & {
38
42
  ansiNistBuffer?: Buffer;
39
43
  };
40
44
  export declare class Nfiq {
45
+ private autoConvert;
46
+ constructor(options?: NfiqConstructorOptions);
41
47
  version(): string;
42
48
  score(imagePath: string, options?: NfiqOptions): NfiqResult;
43
49
  scoreStream(input: StreamInput, options?: NfiqStreamOptions): Promise<NfiqStreamResult>;
package/dist/src/nfiq.js CHANGED
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Nfiq = void 0;
7
7
  const path_1 = __importDefault(require("path"));
8
8
  const nbis_runtime_1 = require("./nbis-runtime");
9
+ const image_convert_1 = require("./image-convert");
9
10
  const temp_io_1 = require("./temp-io");
10
11
  function buildRawAttr(raw) {
11
12
  const attrs = [raw.width, raw.height, raw.depth];
@@ -64,39 +65,49 @@ function parseScores(output) {
64
65
  });
65
66
  }
66
67
  class Nfiq {
68
+ constructor(options = {}) {
69
+ this.autoConvert = (0, image_convert_1.normalizeAutoConvertConfig)(options.autoConvert);
70
+ }
67
71
  version() {
68
72
  const result = (0, nbis_runtime_1.runNbis)('nfiq', ['-version']);
69
73
  (0, nbis_runtime_1.assertSuccess)(result, 'nfiq -version');
70
74
  return result.stdout + result.stderr;
71
75
  }
72
76
  score(imagePath, options = {}) {
73
- const args = [];
74
- if (options.mode === 'verbose') {
75
- args.push('-v');
76
- }
77
- if (options.oldBehavior) {
78
- args.push('-o');
79
- }
80
- if (options.raw) {
81
- args.push('-raw', buildRawAttr(options.raw));
77
+ const prepared = (0, image_convert_1.prepareFingerprintImageSync)(imagePath, this.autoConvert);
78
+ try {
79
+ const args = [];
80
+ if (options.mode === 'verbose') {
81
+ args.push('-v');
82
+ }
83
+ if (options.oldBehavior) {
84
+ args.push('-o');
85
+ }
86
+ if (options.raw) {
87
+ args.push('-raw', buildRawAttr(options.raw));
88
+ }
89
+ addSelectors(args, options.selectors);
90
+ args.push(prepared.path);
91
+ if (options.ansiNistOutputPath) {
92
+ args.push(options.ansiNistOutputPath);
93
+ }
94
+ const result = (0, nbis_runtime_1.runNbis)('nfiq', args);
95
+ (0, nbis_runtime_1.assertSuccess)(result, 'nfiq');
96
+ const output = result.stdout + result.stderr;
97
+ return {
98
+ scores: options.mode === 'verbose' ? [] : parseScores(output),
99
+ stdout: result.stdout,
100
+ stderr: result.stderr
101
+ };
82
102
  }
83
- addSelectors(args, options.selectors);
84
- args.push(imagePath);
85
- if (options.ansiNistOutputPath) {
86
- args.push(options.ansiNistOutputPath);
103
+ finally {
104
+ prepared.cleanup?.();
87
105
  }
88
- const result = (0, nbis_runtime_1.runNbis)('nfiq', args);
89
- (0, nbis_runtime_1.assertSuccess)(result, 'nfiq');
90
- const output = result.stdout + result.stderr;
91
- return {
92
- scores: options.mode === 'verbose' ? [] : parseScores(output),
93
- stdout: result.stdout,
94
- stderr: result.stderr
95
- };
96
106
  }
97
107
  async scoreStream(input, options = {}) {
98
108
  return (0, temp_io_1.withTempDir)(async (tempDir) => {
99
109
  const imagePath = await (0, temp_io_1.writeTempInput)(tempDir, input, options);
110
+ const prepared = (0, image_convert_1.prepareFingerprintImageSync)(imagePath, this.autoConvert, tempDir);
100
111
  const args = [];
101
112
  if (options.mode === 'verbose') {
102
113
  args.push('-v');
@@ -108,7 +119,7 @@ class Nfiq {
108
119
  args.push('-raw', buildRawAttr(options.raw));
109
120
  }
110
121
  addSelectors(args, options.selectors);
111
- args.push(imagePath);
122
+ args.push(prepared.path);
112
123
  let ansiNistPath;
113
124
  if (options.ansiNistOutput) {
114
125
  ansiNistPath = path_1.default.join(tempDir, 'output.an2k');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nbis-wrapper-js",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Node.js wrappers for NBIS 5.0.0 binaries",
5
5
  "main": "dist/src/index.js",
6
6
  "types": "dist/src/index.d.ts",
@@ -44,6 +44,10 @@
44
44
  "test:nbis-versions": "npm run build && node dist/scripts/test-nbis-versions.js",
45
45
  "test": "npm run build && node dist/test/nbis-versions.test.js"
46
46
  },
47
+ "dependencies": {
48
+ "jpeg-js": "^0.4.4",
49
+ "pngjs": "^7.0.0"
50
+ },
47
51
  "devDependencies": {
48
52
  "@types/node": "^20.14.10",
49
53
  "typescript": "^5.4.5"