Skip to content

feat: implement comprehensive ANSI/NIST support (NIEM XML & Traditional)#145

Merged
TheMultii merged 4 commits into
BiometricsUBB:masterfrom
Drawcris:master
Jun 20, 2026
Merged

feat: implement comprehensive ANSI/NIST support (NIEM XML & Traditional)#145
TheMultii merged 4 commits into
BiometricsUBB:masterfrom
Drawcris:master

Conversation

@Drawcris

@Drawcris Drawcris commented May 5, 2026

Copy link
Copy Markdown
Contributor

Key Features Added

1. Modern XML (NIEM) Support

  • Import/Export: Full support for ANSI/NIST-ITL 1-2025 XML records.
  • NIEM Compatibility: Seamlessly parses and generates NIEM-compliant biometric data packages.

2. Legacy Traditional Binary Support (.an2 / .eft)

  • Robust Binary Parser: Implemented a native TypeScript parser based on Record Length headers to prevent data corruption from internal delimiter collisions.
  • Legacy Records: Added support for Record Types 1 (Header), 4 (High-Res Grayscale), 9 (Minutiae), 13 (Latent Image), and 14 (Tenprint Image).
  • Type-4 Support: Specialized handling for legacy fixed-binary header records.

3. Image Decoding & Biometric Overlay

  • WSQ Support: Integrated WSQ image decoding via @li0ard/wsq.
  • Raw Grayscale Conversion: Added on-the-fly conversion of raw grayscale pixel data to PNG for browser-native rendering.
  • Minutiae Extraction: Automatic parsing of Type-9 minutiae with coordinate and angle translation, overlaid directly onto the biometric image.

4. UI/UX Improvements

  • Integrated Menus: Added dedicated "Load ANSI/NIST (XML)" and "Load Traditional ANSI/NIST" options to the canvas header.
  • Localization: Full translation support (EN/PL) for all new features and tooltips.

Technical Changes

  • Added loadAnsiNistWithDialog.ts and loadTraditionalAnsiNistWithDialog.ts for specialized loading logic.
  • Implemented a binary-safe record extraction utility to handle non-textual image data safely.
  • Updated canvas-header.tsx and translation files.
  • Ensured strict TypeScript compliance and linting (SonarJS/Security rules).

Verification

  • Validated against the BioCTS conformance dataset (both pass/fail scenarios).
  • Tested with real-world .an2 files containing compressed WSQ and raw grayscale images.

Comment thread src/lib/utils/viewport/saveAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadTraditionalAnsiNistWithDialog.ts Outdated
Comment thread package.json Outdated
Comment thread src/lib/utils/viewport/saveAnsiNistWithDialog.ts Outdated
Comment thread src/lib/utils/viewport/loadAnsiNistWithDialog.ts
Comment thread src/lib/utils/viewport/saveAnsiNistWithDialog.ts Outdated

@Drawcris Drawcris left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All code review feedback has been addressed. Added i18n, normalized minutiae angles, optimized XML queries, and pinned the WSQ library version.

@DGrygierczyk DGrygierczyk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few comments from my side. Feel free to disagree or resolve if not applicable as im not really deep into this repository

const { source } = sprite.texture.baseTexture.resource;
if (source) {
ctx.drawImage(source, 0, 0);
return canvas.toDataURL("image/jpeg").split(",")[1] ?? null;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image is exported as lossy JPEG - this bakes compression artifacts into evidence-grade fingerprint data. Use lossless PNG (the load path already decodes PNG):

return canvas.toDataURL("image/png").split(",")[1] ?? null;

angleDeg = ((Math.round(angleDeg) % 360) + 360) % 360;

let categoryCode = "UNK";
if (m.typeId === "e6cbde52-5a18-4236-8287-7a1daf941ba9") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These two UUIDs are duplicated from autoMarkWithSourceafis.ts, where they already exist as TYPE_ID_RIDGE_ENDING / TYPE_ID_BIFURCATION (just not exported). Export them at the source and import here so the ids can't drift:

// autoMarkWithSourceafis.ts
export const TYPE_ID_RIDGE_ENDING = "e6cbde52-5a18-4236-8287-7a1daf941ba9";
export const TYPE_ID_BIFURCATION = "f47c4b97-2d62-4959-aa21-edebfa7a756a";


while (pos < buffer.length) {
const { len, rType, isTagValue } = getRecordInfo(buffer, pos);
if (len <= 0 || pos + len > buffer.length) break;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the length field is malformed, parseInt returns NaN. NaN <= 0 is false, so this guard passes, then pos += len makes pos NaN and the loop exits - silently dropping every remaining record. Add a finite check:

if (!Number.isFinite(len) || len <= 0 || pos + len > buffer.length) break;

}

if (minutiaeNodes.length > 0) {
const importedMarkings = minutiaeNodes.map((node, index) => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This minutiae→RayMarking mapping (coord parsing, the (angleDeg - 90) * π/180 conversion, type resolution + fallback id) is duplicated in loadTraditionalAnsiNistWithDialog.ts. Extract a shared helper so the angle/type logic lives in one place and can't diverge.

Comment thread src/lib/utils/viewport/loadImage.ts Outdated
const defaultMarkingsFilePath = `${filePath}.json`;
if (await exists(defaultMarkingsFilePath)) {
await loadMarkingsData(defaultMarkingsFilePath, canvasId);
if (typeof filePathOrData === "string") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches run the same reset trio when no markings file applies. Collapse to one path:

const markingsPath =
    typeof filePathOrData === "string" ? `${filePathOrData}.json` : null;
if (markingsPath && (await exists(markingsPath))) {
    await loadMarkingsData(markingsPath, canvasId);
} else {
    MarkingsStore(canvasId).actions.markings.reset();
    MarkingsStore(canvasId).actions.labelGenerator.reset();
    MarkingsStore(getOppositeCanvasId(canvasId)).actions.labelGenerator.reset();
}

const typeId =
resolveSourceafisTypeId(typeStr) ||
MarkingTypesStore.state.types[0]?.id ||
"unknown-type-id";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fallback id is "unknown-type-id" here but "unknown" in the traditional loader - both are invalid ids that yield markings the UI can't categorize. Pick one real default (e.g. first configured marking type) and use it consistently, or skip the minutia.

Comment on lines +23 to +25
/**
* Parses fields within a record buffer.
*/

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need all of those comments for those lines and all of the below ones? the method names and return values should be descriptive enough for all devs to quickly get a grasp what the method do (which for me they are). Not sure what is the convention in repo so @Drawcris and @TheMultii feel free to disagree on that one. Some of them such as the one in line 117 etc. are fine as they are describing not descriptive things but rest of them for me is just a bloat

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've got a fairly large branch, just waiting for the semester to be over. It removes all those unnecessary comments (emojis included - along with some other stuff). Adding new ones at this point is pointless for me, especially when they add zero value and the functions are self-explanatory anyway.

@Drawcris

Drawcris commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

All feedback from the code review has been addressed. I switched the image export to lossless PNG, deduplicated the UUID constants, extracted a shared minutiae mapping helper, added a safety check for malformed record lengths to prevent infinite loops, and consolidated the reset logic.

@Drawcris Drawcris requested a review from TheMultii June 18, 2026 14:51
@TheMultii

TheMultii commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

@Drawcris resolve conflicts

@Drawcris

Drawcris commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

@TheMultii Conflicts have been resolved.

@TheMultii TheMultii merged commit 380756d into BiometricsUBB:master Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants