Skip to content

The DX barcode subsystem

35mm film carries a DX barcode along its edge, encoding the film product and speed. The scanners read it with a dedicated infrared sensor and use it to pick film-specific processing. This subsystem is independent of the visible image path.

The scanners also take the frame numbers from that barcode, and those numbers become the frame labels and the default filenames when a roll is saved. So when no code is read, every frame is labelled DX_Error and the default filenames are all identical, which collapses a saved roll into one file. Two ordinary situations produce that: the DX sensor fails, which is common on these long out of support machines, and the film carries no edge code, which is true of respooled motion-picture stock and much else.

That is why this page documents not only how the host reads the code but what the engine requires to accept one. Those requirements were established by replaying the sensor replies: a bridge between the OEM software and the scanner substitutes a decoded code for what the faulty sensor returned, and the engine's response says which conditions it is testing. Facts established that way are marked as such below. The same technique is what lets an implementation supply a film code and frame numbers by hand.

From reverse engineering of the OEM DX handling and the product-code table, March 2026; product-code decoding cross-checked May 2026; host-read protocol and frame numbering established live August 2026.

What the barcode encodes

The edge barcode follows the PIMA/ISO DX standard and encodes two numbers:

  • DX Part 1: a 7-bit "product code" (called ProdCode internally): manufacturer + product line.
  • DX Part 2: a "generation code": the specific emulsion/speed within that product line.

Together they identify the film so the correct ISO speed and processing can be selected. [DOCUMENTED] from the OEM standard and the product-code table.

The product-code table

The OEM ships a lookup table mapping (product code, generation code) → film identity and ISO, at F-X35 COM SERVER\anselinstalldir\dataPathItems\common\common-ProdCodeTable.dpi. It was last revised 6 June 2006, so any film launched after 2006 has no complete entry and falls back to a default ISO. [DOCUMENTED] from the table file header. Every assigned entry, and the original file, are in Resources: the OEM's DX product-code table.

A concrete consequence: modern Kodak Portra encodes as (product code 95, generation code 14). Product code 95 (Kodak's premium 35mm colour-negative class) is in the table, but generation code 14 was not allocated when the table was frozen, so a modern Portra strip hits a known-product / unknown-generation combination and the lookup yields the default ISO. [CONFIRMED 2026-05-01] the (95, 14) encoding was verified two independent ways: a computational decode of a scan, and an independent edge-scanner database. After the DX standards body stopped publishing assignments around 2006, manufacturers self-allocated within their existing product-code ranges, which is exactly what (95, 14) shows.

Hardware and firmware

DX reading uses a separate IR path, distinct from the imaging optics: illumination on one side of the film and a detector on the other, with analog gain set by adjustable pots ("DxPots"). [DOCUMENTED] from the service description.

There are two DX sensors, staggered along the film track, placed so that between them they can read the code on either side of the film, and the one on the exit side doubles as the scanner's film-presence sensor. [DOCUMENTED, F-135 service manual] Their levels are read as four bytes from PICL register 0x93. "DX sensor" below means the assembly as a whole; where a fault is attributed to the illumination or the detector, it says which.

The barcode is decoded inside the light-controller firmware (PICL), not on the host: the firmware interprets the analog signal and returns a pre-decoded result. The host sends commands to start/stop DX scanning, adjust the DxPots, and read the result; it never sees the raw analog waveform. [DOCUMENTED] from the OEM driver's DX command usage.

Per-model differences

DX handling is integrated into the light controller on the F-135 and F-135+, but the F-235 and F-335 carry a dedicated DX board with its own microcontroller (firmware prefixes DX on the F-235, DY on the F-335). The F-135/F-135+ have no separate DX firmware. [DOCUMENTED] from the firmware inventory; see scanner-family.md.

How the host reads it

Observed live on an F-135+ through a user-space bridge that logs every command and reply between the OEM engine and libusb (2026-08-18 to 20), and read from TLB.dll 3.1.0.28 decompilation for interoperability. Full command/reply captures of all six resolution and Digital ICE configurations are published at pakon-captures. Some facts below were established by replaying substituted replies through that bridge, as described above; that is noted where it applies, and no scanner firmware or OEM binary is modified.

The trigger names the resolution and IR state. [CONFIRMED live, six configurations] The scan-line trigger WRITE PICL 0x91 (SetScanLineParams) carries a 16-bit little-endian value identifying which of the six combinations of scan resolution and Digital ICE the host is about to run. It is issued twice per scan, once for the pre-scan calibration and once for the transport pass, with the same value both times. The values seen, each matched against an independent OEM Windows capture of the same unit:

resolution Digital ICE 0x91 value
Base 4 off 0x0107
Base 4 on 0x00c5
Base 8 off 0x0075
Base 8 on 0x004d
Base 16 off 0x003c
Base 16 on 0x0031

The motor rate written just before transport (WRITE PICM MOTOR RATE) is a second, independent identifier of the configuration: it is distinct for each of the six, and decreases as the resolution and IR load rise.

The read is event-driven, not polled. [CONFIRMED live, ~10 scans] The engine never issues ReadSensorData (PICL 0x90) on a timer during a scan. The sequence is: the HOST poll reply carries bit 0x80 ("a controller wants service"), the engine reads PICL register 0x02 and finds bit 0x02 set, writes the service acknowledge (0x06, payload 00 02), and only then issues the 30-byte 0x90 read. On the reference unit, whose sensor decodes nothing, this happens about once per scan (and once during the pre-scan calibration), so a scan sees one or two sensor reads. Asserting the same two bits in the bridge makes the engine acknowledge and read on demand, at any cadence tried (0.3–3 s), which is how the replayed replies below were delivered.

Reply layout. [CONFIRMED live; matches the engine's parser] The reply is 34 bytes: the usual 4-byte PPB header (01 20 <PICL address> 08), then a 30-byte payload:

payload bytes content
0–1 position counter, big-endian 16-bit
2 entry count
3… entries, 5 bytes each: type in the low nibble of byte 0, then 4 bytes

The engine sign-extends the 16-bit position with its own wrap counter. On the reference unit the idle reply is count 1, type 0 (a "no entry" entry); the position counter is live in every reply, and restarts at the 0x91 trigger.

Type-3 entry: the decoded barcode. [CONFIRMED by decompilation and by live acceptance of a substituted entry] Bytes after the type byte, for the two-slot mode the 135-line engine runs:

byte bits
1 bit 0 = "A" (half-frame) code, bit 1 = plain code (see type 5)
2 bits 7–2 = frame_raw[5:0]; bit 1 must be 0; bit 0 = parity spare
3 bits 7–6 = product[1:0]; bit 5 = invalid (must be 0); bits 4–1 = generation; bit 0 = frame_raw[6]
4 bits 4–0 = product[6:2]

frame_raw is the frame number in half frames (frame × 2 + half; a 24- exposure roll runs 2..49, exactly the range the F235 COM reference gives), 7-bit two's complement for the leader labels (−2 prints "00", −4 "X"). Parity is even over the 19 bits of bytes 2, 3 and 4[4:0], the same rule as the on-film code (18 data bits + parity). An entry carrying product 79 / generation 11, substituted this way, makes the engine report exactly that in the client and clear its "Product And Specifier" warning. [CONFIRMED live, 2026-08-18]

A type-3 entry has no position of its own. [CONFIRMED by decompilation] The engine takes the code's position from a slot filled by a preceding type-5 entry (byte 1 = flag, bytes 2–3 = 16-bit position; flag bit 0 selects the "A" slot, and the type-3's byte-1 bit must name the same slot). A code without a matching type-5 is dropped before the frame table; the product/generation vote happens earlier, which is why product can be accepted while frame numbering is not.

The engine moves the half-frame code. [CONFIRMED by decompilation and live, 2026-08-20] The type-3 handler does not use an "A"-slot code's position as given: it adds a fixed shift this+0x58 = width × 0x695F / 23700, which is 1138, 1707 and 2276 counts at Base 4, 8 and 16. A plain-slot code is not shifted. That is the same fraction of the frame pitch at every resolution, about 0.70 of a frame, so it represents a fixed distance along the film of roughly 27 mm rather than anything derived from the 19 mm code spacing; the gap between the DX sensor and the imaging line is the obvious candidate but is not established here. For an implementation what follows is that a substituted "A" code must be placed that many counts early to be recorded where intended, which was confirmed by pre-compensating: the code then appeared in the engine's own table at the position aimed at.

A film start must be recorded, but the engine mostly ignores its value. [CONFIRMED by decompilation and live] The numbering pass declares DX unusable before looking at any code unless a film-start position was recorded during the scan (a type-7 entry with byte-1 bit 1 set; a type-5 with bit 1 set satisfies the existence test but stores a sentinel that ruins placement). So the type-7 is required. Its position, however, is largely discarded: sub_10015520 derives a film-found offset (this+0x3c) from it, then bounds that against a nominal offset computed from the scan geometry alone, and substitutes the nominal when the derived value falls outside a window of width × 2000 / 23700 either side. On the reference unit the substitution fired on nearly every scan (FilmFoundOffset equalled uiOffsetCcdTest in the debug log), so the phase was set by the nominal, not by the injected film start. The nominal, per resolution, is width / divisor × 16042 / 23700, which is about 169 without IR at every resolution (width / divisor is 250) and about twice that with IR.

Event entries (types 7 and 8). [CONFIRMED live] The controller emits type-7 and type-8 entries of its own, each with a 16-bit position: sensor edge events (an 8/7 pair brackets a film edge; the engine keeps a list of the midpoints). These carry flag byte 0x01, not the 0x02 that marks a film start, so on this engine's two-slot path they establish neither the film start nor, once a film start exists, anything the numbering uses. Types 5–8 are the only entries this unit's controller has ever produced on its own; it has never produced a type 3.

Frame numbering. [CONFIRMED from decompilation and live with replayed replies, 2026-08-19/20] After the scan the engine walks the table of positioned codes and accepts numbering only if it finds at least three consecutive codes it did not have to interpolate or correct: consecutive positions one half pitch apart within ±1/8 of the expected pitch, frame_raw stepping by 1 per code (35 mm), the half-frame parity matching, and the run starting on an odd frame_raw (an "A" code). A code closer to its neighbour than seven-eighths of the expected half pitch is dropped; a gap wider than the tolerance is filled by interpolation. If no accepted run exists the scan carries the "DX Read" warning and every frame is labelled DX_Error, and because the default filenames are then identical, saving the roll writes a single file.

The expected pitch is a function of the scan width. [CONFIRMED by decompilation and live, all six configurations] sub_10015520 computes the expected half pitch as width × 19200 / 23700, so a frame pitch of width × 38400 / 23700: 1620, 2430 and 3240 counts at Base 4, 8 and 16. Digital ICE doubles it (the transport runs a second IR pass, so the position counter advances twice as far per frame): 3240, 4860 and 6480. Each value was confirmed by a run of replayed replies that numbered correctly; the OEM resolution table's slightly different figures (1603 / 2405 / 3206) also pass, both being inside the ±1/8 window.

The divisor, and Digital ICE. [CONFIRMED live, all six configurations] The engine reduces raw positions to normalised line units by dividing by this+0x14, its per-configuration divisor: 4, 6, 8 at Base 4, 8, 16 without IR, doubling to 8, 12, 16 with it. The divisor is legible in any scan with the debug log on: it is the film-start position divided by the FilmFoundDx line. Why IR doubles it is localised in the decompilation but not fully read out (the value is set in the scan-parameter marshalling, where the current decompiler output drops the argument); the OEM resolution table itself sets the divisor to 4/6/8 with no IR term, so the doubling is applied downstream. [the doubling is UNRESOLVED as to mechanism; the values are CONFIRMED]

Which picture a code labels. [from decompilation; the unit conversion CONFIRMED, the exact comparison INFERRED] Placement is not done in raw counts. A code's position is converted to a line coordinate (position − FilmFoundOffset) / divisor and compared against the picture coordinates the image framing found, which are in the same units (pictures come out about 405 line-units apart and 375 wide, at every resolution, which is what makes the framing normalised). Which code a given picture ends up with therefore depends on where the pictures fall, and on real film that is not fixed: the edge code is imprinted at the factory at a constant pitch while the frames are exposed later by the camera at whatever phase the loading produced, so a picture may sit over a whole-frame label, over its "A" half-frame label, or across both. That is the reason both labels exist.

So a strip whose pictures come out labelled 1A, 2A, 3A, 4A is a correct reading, not a fault: it says the frames were exposed nearer the half-frame labels than the whole-frame ones. Any phase is legitimate.

When the code positions are supplied rather than read from film, the phase is a setting rather than something the film imposes, so a whole-frame series can simply be chosen as the tidier result. Landing the configured number on the first picture that way needed the sequence started 1 half-frame early at Base 4 and Base 8 and 2 at Base 16. Those are measurements of one arrangement on one unit, not a property of the scanner. [CONFIRMED live for the configurations tested]

Everything the engine requires, in one place. [CONFIRMED live, all six configurations, 2026-08-20] Numbering was reproduced from replayed replies by: recording a film start (type 7, bit 1); placing one type-5 position event and one type-3 code per half frame on a grid of width × 19200 / 23700 counts (doubled for IR); sending "A"-slot codes early by width × 0x695F / 23700 to survive the engine's shift; and starting the sequence a frame ahead so the first picture takes the configured number. The client then showed the product and specifier, numbered the frames, and saved one file each. The relationship between the position counter and physical film travel is the one piece still not reconciled: the counter was measured at about 2 counts per image row during streaming, yet a frame's accepted pitch (about 1620 counts at Base 4) is close to its length in image rows (1603), which implies about 1 count per row. The two measurements have not been squared. [UNRESOLVED]

Registry switches: the engine's own DX log.

A note about the Pakon implementation. The settings below belong to the OEM Windows software, not to the scanner. They are recorded because the file one of them produces is the engine's own account of how it read a scan, which is how most of the behaviour above was checked.

[CONFIRMED live] Under …\Pakon\TLB\Scan\Test, DxCreateDebugFilesCommunication = 1 writes a PPB traffic log for the motor controller (Logs\PakonPpbDebugDx<serial>.txt), nothing DX-specific. DxCreateDebugFiles = 1 (read at engine start; on a 64-bit host it must land in the 32-bit registry view) makes the engine write Logs\DxCode.txt after each scan: the decoded code table (ID, frame, PAR, Line), Good Dx Count, the film-found position and offsets, and the picture-framing results. Reading it: Line = position / divisor − FilmFoundOffset, and the divisor is the film-start position over FilmFoundDx. It is the engine's own ground truth for DX debugging. [CONFIRMED live, 2026-08-19; an earlier failed attempt had not restarted the engine] Logs\PakonDxLog.txt stayed empty even on a successful pass; what would ever fill it is unknown. DxCalibrationFilmOffset is consumed only by the sensor pot calibration, not by numbering. [CONFIRMED by decompilation]

Note on the reference hardware

On the specific F-135+ used for much of this research (serial 16402), DX reading fails. In the captured sensor responses examined (three reads, including one during an active DX scan), the DX-related fields were exactly zero while other leading bytes were nonzero. [CONFIRMED] for those captured responses. A fault somewhere in the DX path on that unit is the suspected explanation, not a proven failure mode. [INFERRED]

Narrowed 2026-08-18: during the client's Film Track Test the engine polls PICL register 0x93 (4 bytes, one per sensor) every ~3 ms while running the transport. On this unit all four values sit near 248 with no film and each drops to roughly 110–135 while film covers it, in two pairs about four seconds apart.

These are the DX sensors, and they do double duty. [DOCUMENTED, F-135 service manual] The film track carries two DX sensors, staggered along it, positioned so that between them they can read the code on either side of the film. The manual is explicit that the second job is film presence: "the DX sensor on the film exit side of the scanner also acts as a film sensor, which will warn the operator that there is still film inside the scanner if a new scan is initiated".

That accounts for the four bytes and their timing: two staggered positions, each able to read either side of the film, gives four detector readings, and a strip advancing reaches one stagger about four seconds before the other. [INFERRED from the manual's arrangement and the observed pattern] The OEM's FilmTrackTest is consistent with it: the call takes a "DX pots adjust" flag, is documented for use "if the scanner is having trouble reading DX codes", and fails with EC_DXBadSwing, so the swing it measures is these sensors' swing.

So the DX detectors respond to light, respond to film, and are not dead, yet nothing has shown them seeing barcode modulation and the controller has never returned a decoded code. That does not show the DX illuminator is working: the imaging illuminant has its own IR channel for Digital ICE shining across the film path towards the CCD, so a detector near that path sees it as background regardless, and film attenuates the background exactly as it would the DX light. A dead or weak DX illuminator is a leading candidate, along with the optics, the gain, and the controller's decoder. [CONFIRMED readings; the cause INFERRED and open]

What would separate them: reading register 0x93 with the lamp off, so that any remaining signal is the DX illuminator's own; running a high contrast edge code past the sensors and watching for fast modulation on any of the four bytes; and letting a Film Track Test run to completion, since its own results report per-sensor swing.

Follow-up work

Product/generation, frame numbering, and the whole host-side read sequence are now established and reproduced from replayed replies across all six resolution and Digital ICE configurations. What remains open:

  • a capture from a unit whose DX sensor works. Everything above says what the engine requires; a real decoding controller would show what it actually emits (how a genuine type-3 arrives, event grouping, how "no film" and "decode failed" differ). The reference unit's sensor does not decode, so this cannot be answered here.
  • why Digital ICE doubles the divisor. The values are confirmed (4/6/8 to 8/12/16); the mechanism is localised to the scan-parameter marshalling, but the current decompiler output drops the argument at that call, so it needs a cleaner disassembly of that one code path.
  • the position counter's relation to film travel. It measured about 2 counts per image row during streaming, yet a frame's accepted pitch is close to its length in image rows, implying about 1. The two have not been reconciled.
  • why no barcode modulation reaches sensors that are otherwise healthy. The DX sensors respond to light and to film, so the remaining candidates are the DX illuminator, the optics, the analog gain, and the controller's decoder.
  • the distance along the film path from the DX detectors to the CCD line, measured on the machine. The engine carries two constants that are plainly distances, and converting them at a 38 mm frame pitch gives the shift it applies to a half-frame code as 26.7 mm and the nominal film-found offset as 15.9 mm. Both are attributed above to the sensor-to-imaging-line gap, on nothing better than plausibility. A tape measure inside the film path would say which, if either, is that distance, and a value matching neither would mean the constants are something else entirely.
  • what a cut through a code does. The barcode repeats along the film edge, so cutting a roll into strips leaves a partial code at each end of a strip. Whether the controller reports such a fragment, suppresses it, or returns it as a code that fails parity is not known, nor what the numbering pass then does with a strip's first and last pictures.

The reference unit's fault is a fault of the individual scanner, not a protocol fact, but it shaped which DX facts could be verified against live hardware versus derived from the OEM software. Full command/reply captures of all six configurations are at pakon-captures.

Further reading

This page covers how the scanner reads and uses the DX code. For the DX code system in general, see:

  • 35mm-dx-edge-code: a concise structural reference for the DX latent edge barcode itself: physical layout, bit encoding, field positions, and the PIMA/ISO 1007 decoding method.
  • The Big Film Database: an independent, community DX-code database (webcam edge-scanner decodes), for looking up specific films including modern emulsions absent from the OEM 2006 product-code table.