What is the valid range for the TCI product?

tldr: what’s the valid range for values in the TCI product? Is it 0-255, with no value for nodata? Or 1-255, with 0 being used for nodata?

I’m investigating an issue reported by a user at Problems with COG images downloaded from the Planetary Computer API - Data - Pangeo. That’s comparing a Cloud Optimized GeoTIFF created from the sen2cor output to the original TCI image from Copernicus.

Those white pixels in the water are values where one or more of the bands in the TCI output by sen2cor have a value of zero. As part of our COG conversion, we call gdal_translate with -a_nodata 0. We set that based on the text from User Guides - Sentinel-2 MSI - Definitions - Sentinel Online - Sentinel Online

The TCI is an RGB image built from the B02 (Blue), B03 (Green), and B04 (Red) Bands. The reflectances are coded between 1 and 255, 0 being reserved for ‘No Data’.

That makes it sound like the valid output values should be between 1 and 255. I checked the SCL output, and all those pixels in the top-left are (correctly) classified as water. So sen2cor thinks they’re valid pixels, but outputs a value of 0 for the darkest pixels. This shows reading the output of sen2cor (so before I add the nodata metadata), and shows that the red band is 0 at this pixel.

In [39]: ds = rasterio.open("data/l2a/jp2000/S2B_MSIL2A_20230111T121649_N0400_R066_T27PTS_20230117T211543.SAFE/GRANULE/L2A_T27PTS_A030551_20230111T121647/IMG_DATA/R10m/T27PTS_20230111T121649_TCI_10m.jp2")

In [40]: ds.read(window=rasterio.windows.Window(0, 4, 1, 1))


       [[9]]], dtype=uint8)

This can be reproduced locally with:

gdal_translate data/l2a/jp2000/S2B_MSIL2A_20230111T121649_N0400_R066_T27PTS_20230117T211543.SAFE/GRANULE/L2A_T27PTS_A030551_20230111T121647/IMG_DATA/R10m/T27PTS_20230111T121649_TCI_10m.jp2 ../T27PTS_20230111T121649_TCI_10m.tif -of COG -a_nodata 0 -co COMPRESS=DEFLATE -co OVERVIEW_RESAMPLING=cubic