9303 Part 4, positions 29 to 42: "Any special characters, including spaces,
in the personal identification number ... shall be replaced by the filler
character (<). The number shall be followed by the filler character (<)
repeated up to position 42."
So the field holds two different kinds of filler. The trailing run is
padding, which was already stripped, but a filler inside the number stands
for a space or special character substituted on the way in, and those were
printed as they came - a US passport showed its personal number with a '<'
in the middle of it.
Strip the padding, then put a space back for what remains. It is the
closest recovery available: the original could have been a hyphen, and the
MRZ does not keep which.
optional_data itself stays a faithful copy of the field. Nothing else
reads it, and it is the personal number that is being rendered here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ICAO spills a document number longer than nine characters into the
optional-data field: positions 1 to 9 hold the first nine, position 10 a
filler in place of the check digit, and the optional field opens with the
rest of the number and the check digit for the whole of it.
_extended_document_number() already puts that back together and returns
the optional field with the overflow removed. _parse_td3() bound that
fourth value and then ignored it, re-slicing l2[28:42] raw, so the tail of
the document number stayed in the optional field. From there it was
reported as the holder's personal number, which falls back to the MRZ
optional-data field when DG11 carries none - a 12-character number showed
its last three digits and a check digit as a personal number.
Use the value the function already returns. The check digit still answers
for the field as transmitted, overflow included, since that is what the
MRZ actually carries.
_parse_td1() has always used the return value, which is where the intended
shape comes from. _parse_td2() does not call the helper at all, so TD2
carries no support for long numbers; that is a gap rather than this bug and
is left alone.
Found while checking the decoder against ICAO 9303 Part 4. Nothing else
came out of that: the composite check digit covers lower-line positions
1-10, 14-20 and 22-43 as the spec requires, verified by mutating all 43 and
confirming that nationality and sex are ignored and every other position is
caught; the 7-3-1 digits agree with an independent implementation across
four real TD3 documents.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tab headers carried their file names as fixed text - "Additional
personal details · EF_DG11" - so a Polish passport, which ships DG13 and
no DG11, was told its details came from a file it does not carry. The
ISSUER header claimed EF_DG12 on the same document, and SECURITY listed
EF_DG15 whether or not it was there.
personal_files, document_files and security_files report what each tab drew
from, and the pages compose their header from those. They sit on the
record rather than in the widgets so they can be tested without a display,
which is how the rest of the suite is built.
PERSONAL names EF_DG1 when the personal number came from the MRZ, that
being the file which supplied it, rather than crediting DG11 for a value
DG11 did not provide. With nothing read the header stays the bare
description: the record is an empty PassportRecord at startup rather than
None, and reporting an absence on a chip nobody read reads as a fault.
"none present" rather than "nothing on this chip", since the app opens
offline dumps too.
The same document showed the other half of this: "Personal number (from
DG13)" renders 203px against a dp(190) caption column, so it wrapped and
left the source on a line of its own. Without the "from" it is 167px.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
"Personal number (from DG13)" renders 203px wide and the caption column is
dp(190), so it wrapped and left the source sitting on a line of its own.
Without the "from" it comes to 167px and fits, with room to spare over the
140px of the longest caption already in that tab.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Kivy's stock disabled button texture is pale at the edges and dark in the
middle. Stretched across a wide button the edges stay put and the middle
grows, so it reads as three vertical bands with the label sitting on the
dark one and disappearing - "Delete dump" is unreadable at startup, where
it is disabled because no dump is loaded yet.
Keep the normal background for the disabled state and let the dimmed label
carry it instead. The text goes to 45% rather than Kivy's 30%, which is
faint on that grey.
Applied as a Button rule because nine buttons across the app bind
disabled: Export, Open folder, the camera button, Samples, the bin button
and the rest all band the same way. The one button that sets its own
transparent background stays transparent, since background_color
multiplies the texture.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A Polish passport carries EF_DG13 and no EF_DG11, so the PERSONAL tab had
nothing to show and the data page fell back to "not on chip", even though
the document does carry a personal number.
DG13 is optional details and ICAO assigns it no meaning, so parse_dg13()
reads it as tagged values and names none of them. The 5C tag list is
skipped: it enumerates what follows rather than being a field itself.
Poland is the one case worth naming. It puts the PESEL, its national
identity number, in tag 5F70, and is_pesel() checks the length and the
check digit - DG13 being issuer-defined, the checksum is what separates a
PESEL from eleven digits that happen to share a tag. That is applied only
when the MRZ nationality is POL, so the same tag on another state's
document is carried but not claimed as a personal number.
It slots in behind the two sources personal_number already had, which its
docstring anticipated: DG11, then the MRZ optional-data field, then this.
personal_number_source reports DG13 so the PERSONAL tab and the data page
can say where the value came from rather than implying DG11.
Confirmed against a Polish passport: the 11 digits validate as a PESEL,
their first six match the MRZ date of birth under PESEL's century-offset
month encoding, and the gender digit agrees with the MRZ sex field. The
two US documents to hand are unaffected - they keep taking the number from
the MRZ - and a document with none of the three shows no row, as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>