A .flt file is a plain-text recipe the CampSnap camera reads to reshape every photo's color as it's taken. It has no headers, no keys in most of its lines, and no metadata: just rows of comma-separated numbers in a fixed order. The camera trusts that order completely: it does not read labels, it reads position. That's why a value in the wrong place, or a row with the wrong number of entries, produces a broken or rejected filter rather than a partial one.
Everything in this reference reflects the two formats supported by the dimjelly CampSnap filter builder: the original CS1 / CS2 format (curves and a color matrix) and the newer, simpler CS Pro format (seven flat values).
↑ back to contentsBoth file types use the .flt extension, but they are structured completely differently and are not interchangeable:
A tool that imports or generates filters usually needs to detect which format it's looking at before parsing. The reliable signal: CS Pro lines look like label:value (for example lum:0); CS1/CS2 lines are just numbers separated by commas, with the 5th line onward being very long (a 256-value curve).
This is the original, full-control format. It has exactly seven lines, and each one has a fixed number of comma-separated values. A camera reading this file has no way to tell a short row from a shifted one, since it just counts commas, so the value count per row is not a suggestion, it's the entire contract.
| Row | Contents | Value count |
|---|---|---|
| 1 | Global adjustments | 7 values |
| 2–4 | 3×3 color matrix (one row per output channel) | 3 rows × 3 values |
| 5–7 | Per-channel tone curves (R, G, B) | 3 rows × 256 values |
That's 784 individual numbers in total (7 + 9 + 256×3). Any file claiming to be a CS1/CS2 filter should have exactly that many, no more and no fewer.
↑ back to contentsSeven values, always in this order:
brightness, contrast, saturation, hue, gammaR, gammaG, gammaB
| Field | Meaning | Range | Neutral |
|---|---|---|---|
| brightness | Added to every channel before anything else. Positive lightens, negative darkens. | −255 to 255 | 0 |
| contrast | Multiplies each channel's distance from mid-gray (128). Above 1 punches up the difference between lights and darks; below 1 flattens it. | 0 to 2 | 1 |
| saturation | Multiplies each channel's distance from that pixel's own luma (brightness). 0 is black & white; higher is more vivid. | 0 to 4 | 1 |
| hue | Rotates every color around the color wheel by this many degrees. | 0 to 360 | 0 |
| gammaR / gammaG / gammaB | Per-channel gamma exponent. Values below 1 brighten that channel's midtones; above 1 darken them. This is a baseline that the curve rows below can override point by point. | 0.01 to 2 | 1 |
An important quirk: saturation, hue, and contrast are all defined relative to a pixel's own brightness or to mid-gray. That means a perfectly neutral gray pixel (where R, G, and B are exactly equal) passes through brightness, contrast, saturation, and hue completely unchanged. Those four settings have no effect on true neutral pixels. Any color introduced into a neutral gray has to come from the matrix or the curves below, not from row 1.
↑ back to contentsThree rows of three numbers. Each row corresponds to one output channel (row 2 = output red, row 3 = output green, row 4 = output blue), and each of the three numbers in that row says how much of input red, input green, and input blue gets mixed into it.
R_out row: wR→R, wG→R, wB→R G_out row: wR→G, wG→G, wB→G B_out row: wR→B, wG→B, wB→B
The numbers are integers scaled by 1024 rather than plain fractions (fixed-point math is cheap for the camera's processor to compute; dividing by 1024 is a fast bit-shift instead of a slow floating-point division). A weight of 1024 means "100% of this input channel," and 512 means 50%.
Identity (no color mixing at all):
1024, 0, 0, 0, 1024, 0, 0, 0, 1024
A well-behaved row's three weights typically sum to around 1024, which keeps overall exposure neutral. If a row sums to less than 1024, that output channel dims; more than 1024, and it brightens (and rows with identical weights across all three collapse the image toward black & white, since every output channel would read the same luma-like blend).
Example: a pure red tint on the red channel with a whisper of blue bled in (a cool-shadow style tint).
980, 0, 44, ← red output leans slightly toward magenta 0, 1024, 0, ← green untouched 0, 0, 1024 ← blue untouched
The matrix applies to every pixel at every brightness level equally. Unlike the curves below, it can't distinguish shadows from highlights. If you want a tint that only shows up in bright areas while shadows stay neutral, that has to be built into the curves instead, not the matrix.
↑ back to contentsThree rows, each with exactly 256 values, one row per color channel, in R, G, B order. Each row is a lookup table: position i (from 0 to 255) holds the output value that input brightness i should become, for that channel only.
Identity curve (input equals output, no change at all) is simply the numbers 0 through 255 in order:
0, 1, 2, 3, 4, 5, … 253, 254, 255
Because each of the 256 positions can be set independently, a curve can do things brightness/contrast sliders can't: crush shadows to pure black while leaving highlights untouched, lift midtones without moving the black point, or make a channel diverge from the others only above a certain brightness (which is exactly how a highlight-only or shadow-only color tint gets built: make R, G, and B identical through the low end of the curve, then let them diverge higher up).
Example: a curve that keeps the bottom ~30% of the range pinned at 0 (crushed, detail-free shadows), then rises:
0, 0, 0, 0, 0, … 0, 0, 1, 3, 8, 19, 37, 63, …
The tool that generates these curves builds each one from a gamma baseline (row 1's gammaR/G/B) and then lets any of the 256 points be manually overridden on top of that baseline. A camera reading the file doesn't know or care which points were "manual". It just reads 256 numbers per channel and uses them as-is.
↑ back to contentsThe camera doesn't apply these three pieces independently. It runs every pixel through them in a fixed order. Understanding the order matters, because it explains some non-obvious behavior (like row 1 being a no-op on neutral grays):
Row 1's global adjustments run first, on the raw pixel. The per-channel curves run after that, so they reshape whatever brightness/contrast/saturation/hue already produced. The color matrix runs last, mixing the already-curved R, G, and B values together.
One practical consequence: because the curves run after saturation and hue, they are the only part of the format that can introduce a color split between shadows and highlights. The matrix, running last, then applies its fixed tint uniformly across whatever tonal range the curves already shaped.
↑ back to contentsCS Pro is a newer, much simpler format built for the Pro model. Instead of a matrix and curves, it's just seven lines, each written as label:value. There's no ambiguity about position since every line names its own field, but the values are still plain integers, and a camera parsing this format is just as strict about needing exactly seven recognizable lines.
lum:0 contrast:0 rgain:0 ggain:0 bgain:0 hue:0 sat:0
All seven fields default to 0 when neutral, unlike CS1/CS2, where the neutral value differs field to field (1 for contrast/saturation, 0 for brightness/hue). CS Pro's scale is simply centered on zero across the board.
| Field | Meaning | Range | Neutral |
|---|---|---|---|
| lum | Brightness. Added directly to every channel. | −150 to 150 | 0 |
| contrast | Contrast strength. Internally becomes a multiplier from 0.5× to 2× around mid-gray. | −50 to 100 | 0 |
| rgain | Red channel gain: a straight multiplier on the red channel after brightness/contrast. | −100 to 100 | 0 |
| ggain | Green channel gain, same idea as rgain. | −100 to 100 | 0 |
| bgain | Blue channel gain, same idea as rgain. | −100 to 100 | 0 |
| hue | Hue rotation in degrees. A much narrower range than CS1/CS2's full 0–360 wheel. | −60 to 60 | 0 |
| sat | Saturation. Internally becomes a multiplier from 0× to 1.5×. | −100 to 50 | 0 |
CS Pro cannot represent everything CS1/CS2 can. There are no per-channel curves and no matrix, so effects that depend on tonal range, like shadows tinted one way while highlights go another, or a crushed black point that leaves midtones untouched, simply aren't expressible in this format. The three channel gains are flat multipliers applied everywhere, all at once, regardless of how bright or dark a pixel already is.
↑ back to contentsNotice the channel gains sit between contrast and saturation. They're a flat per-channel multiply, not a curve, so they shift a channel's intensity uniformly rather than reshaping its tone curve. There is no separate curve or matrix stage afterward; whatever these five steps produce is the final pixel.
↑ back to contentsIf you're hand-editing a file, generating one programmatically, or writing an importer that needs to fall back to "no change" for a missing or invalid value, these are the exact numbers that mean do nothing for each field:
| Format | Field | Neutral value |
|---|---|---|
| CS1/CS2 | brightness | 0 |
| CS1/CS2 | contrast | 1 |
| CS1/CS2 | saturation | 1 |
| CS1/CS2 | hue | 0 |
| CS1/CS2 | gammaR / gammaG / gammaB | 1 (each) |
| CS1/CS2 | color matrix | identity: 1024,0,0, 0,1024,0, 0,0,1024 |
| CS1/CS2 | each curve | identity ramp: value at position i is i |
| CS Pro | all seven fields | 0 (each) |
These are the same defaults a well-behaved importer should fall back to, field by field, if a file has a missing or non-numeric value in an otherwise correctly-sized file, rather than rejecting the whole file or defaulting everything to the same number regardless of what that field actually represents.
↑ back to contentsBecause CS1/CS2 rows are just comma-separated numbers, a single missing or extra value anywhere in the file shifts every value after it by one position. A parser that drops invalid tokens instead of preserving their position will silently corrupt the rest of the file rather than just that one field.
Windows text editors often save with CRLF line endings and sometimes a UTF-8 byte-order mark (BOM) at the very start of the file. Depending on how strict the camera's own parser is, either of these can cause the file to be misread or rejected outright, even though the visible numbers look completely correct.
A value like contrast being written as 1.00 instead of 1 is numerically identical but not necessarily byte-identical to what a stricter parser was written against. When in doubt, match the plain, unpadded formatting of a known-working file.
Some filter-generating tools write a hardcoded placeholder into row 1 (or even the literal, non-numeric string N) because they only ever computed the matrix and curves from their source material and never touched row 1 at all. If you're importing files from an unfamiliar source, don't assume row 1 is meaningful just because it parses. Check whether the tool that produced it actually calculated those seven values.
| CS1 / CS2 | CS Pro | |
|---|---|---|
| File shape | 7 rows of numbers | 7 labeled lines |
| Total values | 784 numbers | 7 numbers |
| Global adjustments | brightness, contrast, saturation, hue, 3× gamma | lum, contrast, hue, sat |
| Per-channel control | full 256-point tone curve, per channel | flat gain multiplier, per channel |
| Color mixing | 3×3 matrix (cross-channel bleed possible) | none |
| Tone-dependent tinting (e.g. tinted shadows, neutral highlights) | possible, via the curves | not possible |
| Neutral/default scale | varies by field (0 or 1) | 0 across every field |