diff --git a/data/main_fields.yaml b/data/main_fields.yaml index 00b535e..cdc33cc 100644 --- a/data/main_fields.yaml +++ b/data/main_fields.yaml @@ -195,6 +195,28 @@ - The alpha channel can be left out, in which case the data should have 3 bytes instead of 4 and the color will be considered fully opaque. - If a material doesn't have a single primary color (for example rainbow or coextruded filaments), this field can be null. +- key: 59 + name: primary_color_lab + type: color_lab + unit: '[L*, a*, b*]' + example: '[53.24, 111.12, -27.3]' + description: + - "Color of a material in the device-independent CIE L*a*b* (CIELAB 1976) color space with reference white D65/2\xB0." + - If present, the value MUST be obtained by physical spectrometry measurement; it MUST NOT be approximated (for example from RGB). + - "`L*` is bound to [0, 100], `a*` and `b*` values are dimensionless and are typically between \xB1127, but can theoretically get in the \xB1150 range." + +- key: 60 + name: primary_color_ral + type: string + unit: RAL code + max_length: 16 + example: 270 30 20 + description: + - RAL color identifier, without the "RAL" prefix. + - The value MUST correspond exactly to an official identifier (see https://www.ral-farben.de/en/all-ral-colours). + - If present, the physical material MUST match the referenced RAL swatch; it MUST NOT be approximated (for example from RGB/LAB). + - 'Examples of valid values: `3020`, `9005`, `1023`, `7016`, `270 30 20`, `190 50 35`, `530-1`, `850-M`, `P1 3020`.' + - key: 20 name: secondary_color_0 type: color_rgba @@ -485,4 +507,4 @@ description: - Recommended drying time (at `drying_temperature`). -# First unused key: 59 +# First unused key: 61 diff --git a/docs_src/nfc_data_format.md b/docs_src/nfc_data_format.md index 3a4cefe..64f631f 100644 --- a/docs_src/nfc_data_format.md +++ b/docs_src/nfc_data_format.md @@ -85,11 +85,12 @@ 1. `enum` fields are encoded as an integer, according to the enum field mapping 1. `enum_array` fields are encoded as CBOR arrays of integers, according to the field mapping 1. `timestamp` fields are encoded as UNIX timestamp integers -1. `bytes` and `uuid` types are encoded as CBOR byte string (type 2) -1. `color_rgba` fields are encoded as a CBOR byte string (type 2) with 3 to 4 bytes representing `[R, G, B]` or `[R, G, B, A]` values 1. `number` types can be encoded as either unsigned integers (type 0), signed integers (type 1), half floats or floats +1. `bytes` and `uuid` types are encoded as CBOR byte string (type 2) 1. `string` types are encoded as CBOR text string (type 3, UTF-8 is enforced by the CBOR specification) 1. The `X` in the `string:X` or `bytes:X` notation defines maximum permissible length of the data in bytes. +1. `color_rgba` fields are encoded as a CBOR byte string (type 2) with 3 to 4 bytes representing `[R, G, B]` or `[R, G, B, A]` values +1. `color_lab` fields are encoded as a CBOR array of 3 `number` type elements ### 3.2 UUIDs Some entities referenced in the data (see [Terminology](terminology.md)) can be identified by a [UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier). The UUID MAY be explicitly specified through a `XX_uuid` field, however that might not be desirable due to space constraints. As an alternative, the following algorithm defines a way to derive UUIDs from other fields. diff --git a/tests/encode_decode/01_data.bin b/tests/encode_decode/01_data.bin index 1442939..8f53757 100644 Binary files a/tests/encode_decode/01_data.bin and b/tests/encode_decode/01_data.bin differ diff --git a/tests/encode_decode/01_info.yaml b/tests/encode_decode/01_info.yaml index c622cea..0c7251d 100644 --- a/tests/encode_decode/01_info.yaml +++ b/tests/encode_decode/01_info.yaml @@ -8,7 +8,7 @@ regions: payload_offset: 4 absolute_offset: 70 size: 206 - used_size: 149 + used_size: 176 aux: payload_offset: 210 absolute_offset: 276 @@ -21,8 +21,8 @@ root: data_size: 312 payload_size: 245 overhead: 67 - payload_used_size: 154 - total_used_size: 221 + payload_used_size: 181 + total_used_size: 248 data: meta: aux_region_offset: 210 @@ -56,10 +56,15 @@ data: certifications: - ul_2818 - ul_94_v0 + primary_color_lab: + - 50 + - 11.3 + - 129.3 + primary_color_ral: 270 30 20 aux: {} raw_data: meta: a10218d2 - main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ffff000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 + main: bf041b000007d0fcab45f9056a33333463353466303838080009000a76504c412050727573612047616c61787920426c61636b0b6950727573616d656e740e1a68d3c7d7101903e8111903f41219011813443d3e3dff181c9f17ff181df93cf6182218cd182318e1182418aa182518281826183c18271218281828182914182a1840182b18c8182c1864182d183418389f0001ff183b831832fa4134cccdfa43014ccd183c69323730203330203230ff000000000000000000000000000000000000000000000000000000000000 aux: a000000000000000000000000000000000000000000000000000000000000000000000 uri: https://3dtag.org/s/334c54f088 validate: diff --git a/tests/encode_decode/01_input.yaml b/tests/encode_decode/01_input.yaml index 6e5c38f..520d041 100644 --- a/tests/encode_decode/01_input.yaml +++ b/tests/encode_decode/01_input.yaml @@ -9,6 +9,8 @@ data: brand_name: Prusament material_name: PLA Prusa Galaxy Black primary_color: "#3d3e3dff" + primary_color_lab: [50, 11.3, 129.3] + primary_color_ral: 270 30 20 tags: [glitter] certifications: [ul_2818, ul_94_v0] density: 1.24 diff --git a/utils/fields.py b/utils/fields.py index 9107af1..72c2c1f 100644 --- a/utils/fields.py +++ b/utils/fields.py @@ -32,15 +32,25 @@ def __init__(self, num: float): if num.is_integer(): self.value = int(num) - elif abs(num - numpy.float16(num)) < CompactFloat.required_precision: + elif abs(num - float(numpy.float16(num))) < CompactFloat.required_precision: self.value = float(numpy.float16(num)) - elif abs(num - numpy.float32(num)) < CompactFloat.required_precision: + elif abs(num - float(numpy.float32(num))) < CompactFloat.required_precision: self.value = float(numpy.float32(num)) else: self.value = num + @staticmethod + def decode(data): + num = float(data) + return int(num) if num.is_integer() else round(num, CompactFloat.decimal_precision) + + def encode_cbor(self, encoder: cbor2.CBOREncoder): + # Always encode floats canonically + # Noncanonically, floats would always be encoded in 8 B, which is a lot of wasted space + cbor2.CBOREncoder(encoder.fp, canonical=True).encode(self.value) + # Represent a raw CBOR data that are to be encoded verbatim class RawCBORData: @@ -49,6 +59,9 @@ class RawCBORData: def __init__(self, data: bytes): self.data = data + def encode_cbor(self, encoder: cbor2.CBOREncoder): + encoder.fp.write(self.data) + class Field: key: int @@ -81,8 +94,7 @@ def encode(self, data): class NumberField(Field): def decode(self, data): - num = float(data) - return int(num) if num.is_integer() else round(num, CompactFloat.decimal_precision) + return CompactFloat.decode(data) def encode(self, data): return CompactFloat(data) @@ -185,6 +197,29 @@ def encode(self, data): return bytes.fromhex(m.group(1)) +class LABCborData: + data: list + + def __init__(self, data: list): + self.data = data + + def encode_cbor(self, encoder: cbor2.CBOREncoder): + # Encode with definite container, it's smaller and the record is fixed size + cbor2.CBOREncoder(encoder.fp, canonical=True, indefinite_containers=False, default=encoder.default).encode(self.data) + + +class ColorLABField(Field): + def decode(self, data): + assert type(data) is list + assert len(data) == 3 + return [CompactFloat.decode(x) for x in data] + + def encode(self, data): + assert type(data) is list + assert len(data) == 3 + return LABCborData([CompactFloat(x) for x in data]) + + class UUIDField(Field): def decode(self, data): return str(uuid.UUID(bytes=data)) @@ -202,6 +237,7 @@ def encode(self, data): "enum_array": EnumArrayField, "timestamp": IntField, "color_rgba": ColorRGBAField, + "color_lab": ColorLABField, "uuid": UUIDField, } @@ -304,24 +340,12 @@ def update( for key, value in update_unknown_fields.items(): result[RawCBORData(bytes.fromhex(key))] = RawCBORData(bytes.fromhex(value)) - def default_enc(enc: cbor2.CBOREncoder, data: typing.Any): - if isinstance(data, CompactFloat): - # Always encode floats canonically - # Noncanonically, floats would always be encoded in 8 B, which is a lot of wasted space - cbor2.CBOREncoder(enc.fp, canonical=True).encode(data.value) - - elif isinstance(data, RawCBORData): - enc.fp.write(data.data) - - else: - raise RuntimeError(f"Unsupported type {type(data)} to encode") - data_io = io.BytesIO() encoder = cbor2.CBOREncoder( data_io, canonical=config.canonical, indefinite_containers=config.indefinite_containers, - default=default_enc, + default=lambda enc, data: data.encode_cbor(enc), ) encoder.encode(result) diff --git a/utils/schema/field_types.schema.json b/utils/schema/field_types.schema.json index f6912b1..d1d201d 100644 --- a/utils/schema/field_types.schema.json +++ b/utils/schema/field_types.schema.json @@ -42,6 +42,29 @@ "type": "string", "description": "RGB(A) color in a standard hex notation '#RRGGBB(AA)'", "pattern": "^#[0-9a-f]{6}([0-9a-f]{2})?$" + }, + "color_lab": { + "type": "array", + "prefixItems": [ + { + "type": "number", + "minimum": 0, + "maximum": 100 + }, + { + "type": "number", + "minimum": -150, + "maximum": 150 + }, + { + "type": "number", + "minimum": -150, + "maximum": 150 + } + ], + "items": false, + "minItems": 3, + "maxItems": 3 } } } diff --git a/utils/schema/fields.schema.json b/utils/schema/fields.schema.json index 7c74980..0c78f47 100644 --- a/utils/schema/fields.schema.json +++ b/utils/schema/fields.schema.json @@ -93,6 +93,12 @@ "primary_color": { "$ref": "field_types.schema.json#/definitions/color_rgba" }, + "primary_color_lab": { + "$ref": "field_types.schema.json#/definitions/color_lab" + }, + "primary_color_ral": { + "$ref": "field_types.schema.json#/definitions/string" + }, "secondary_color_0": { "$ref": "field_types.schema.json#/definitions/color_rgba" },