diff --git a/IccJSON/IccLibJSON/IccProfileJson.cpp b/IccJSON/IccLibJSON/IccProfileJson.cpp index 259afc863..045c6875c 100644 --- a/IccJSON/IccLibJSON/IccProfileJson.cpp +++ b/IccJSON/IccLibJSON/IccProfileJson.cpp @@ -82,8 +82,8 @@ static IccJson icJsonGetHeaderFlags(icUInt32Number flags) j["UseWithEmbeddedDataOnly"] = (bool)(flags & icUseWithEmbeddedDataOnly); if (flags & icExtendedRangePCS) j["ExtendedRangePCS"] = true; if (flags & icMCSNeedsSubsetTrue) j["MCSNeedsSubset"] = true; - icUInt32Number other = flags & ~(icEmbeddedProfileTrue | icUseWithEmbeddedDataOnly | - icExtendedRangePCS | icMCSNeedsSubsetTrue); + icUInt32Number other = flags & ~(icUInt32Number)(icEmbeddedProfileTrue | icUseWithEmbeddedDataOnly | + icExtendedRangePCS | icMCSNeedsSubsetTrue); if (other) { char buf[16]; snprintf(buf, sizeof(buf), "%08x", other); j["VendorFlags"] = buf; diff --git a/IccJSON/IccLibJSON/IccTagJson.cpp b/IccJSON/IccLibJSON/IccTagJson.cpp index 929ae2f44..076103b45 100644 --- a/IccJSON/IccLibJSON/IccTagJson.cpp +++ b/IccJSON/IccLibJSON/IccTagJson.cpp @@ -727,11 +727,17 @@ bool CIccTagJsonColorantOrder::ParseJson(const IccJson &j, std::string & /*parse bool CIccTagJsonColorantTable::ToJson(IccJson &j) { + j["pcsEncoding"] = "Lab"; IccJson arr = IccJson::array(); for (icUInt32Number i = 0; i < m_nCount; i++) { + icFloatNumber pcs[3]; + pcs[0] = icU16toF(m_pData[i].data[0]); + pcs[1] = icU16toF(m_pData[i].data[1]); + pcs[2] = icU16toF(m_pData[i].data[2]); + icLabFromPcs(pcs); IccJson c; c["name"] = m_pData[i].name; - c["pcs"] = IccJson::array({ m_pData[i].data[0], m_pData[i].data[1], m_pData[i].data[2] }); + c["pcs"] = IccJson::array({ pcs[0], pcs[1], pcs[2] }); arr.push_back(c); } j["colorantTable"] = arr; @@ -740,6 +746,9 @@ bool CIccTagJsonColorantTable::ToJson(IccJson &j) bool CIccTagJsonColorantTable::ParseJson(const IccJson &j, std::string & /*parseStr*/) { + std::string pcsEncoding = "Lab"; + jGetString(j, "pcsEncoding", pcsEncoding); + if (jsonExistsField(j, "colorantTable") && j["colorantTable"].is_array()) { const IccJson &arr = j["colorantTable"]; if (!SetSize((icUInt32Number)arr.size())) return false; @@ -748,8 +757,21 @@ bool CIccTagJsonColorantTable::ParseJson(const IccJson &j, std::string & /*parse std::string name; if (jGetString(c, "name", name)) strncpy(m_pData[i].name, name.c_str(), sizeof(m_pData[i].name)-1); - if (jsonExistsField(c, "pcs") && c["pcs"].is_array() && c["pcs"].size() >= 3) - jGetArray(c, "pcs", m_pData[i].data, 3); + if (jsonExistsField(c, "pcs") && c["pcs"].is_array() && c["pcs"].size() >= 3) { + if (pcsEncoding == "16bit") { + jGetArray(c, "pcs", m_pData[i].data, 3); + } else { + icFloatNumber pcs[3]; + jGetArray(c, "pcs", pcs, 3); + if (pcsEncoding == "XYZ") + icXyzToPcs(pcs); + else // "Lab" (default) + icLabToPcs(pcs); + m_pData[i].data[0] = icFtoU16(pcs[0]); + m_pData[i].data[1] = icFtoU16(pcs[1]); + m_pData[i].data[2] = icFtoU16(pcs[2]); + } + } } } return true; @@ -1386,24 +1408,38 @@ bool CIccTagJsonCurve::ToJson(IccJson &j, icConvertType nType) j["curveType"] = "gamma"; j["gamma"] = (double)m_Curve[0]; } else { - j["curveType"] = "table"; - IccJson arr = IccJson::array(); - for (icUInt32Number i = 0; i < m_nSize; i++) { - switch (nType) { - case icConvert8Bit: - arr.push_back((int)(m_Curve[i] * 255.0f + 0.5f)); break; - case icConvert16Bit: - case icConvertVariable: - arr.push_back((int)(m_Curve[i] * 65535.0f + 0.5f)); break; - default: - arr.push_back((double)m_Curve[i]); break; + // Check whether the table is a sampled identity (linear ramp 0..1). + // Tolerance of 0.5/65535 covers 16-bit quantisation rounding. + bool isIdentity = true; + const icFloatNumber tol = 0.5f / 65535.0f; + for (icUInt32Number i = 0; i < m_nSize && isIdentity; i++) { + icFloatNumber expected = (icFloatNumber)i / (icFloatNumber)(m_nSize - 1); + if (fabsf(m_Curve[i] - expected) > tol) + isIdentity = false; + } + if (isIdentity) { + j["curveType"] = "identity"; + j["size"] = (int)m_nSize; + } else { + j["curveType"] = "table"; + IccJson arr = IccJson::array(); + for (icUInt32Number i = 0; i < m_nSize; i++) { + switch (nType) { + case icConvert8Bit: + arr.push_back((int)(m_Curve[i] * 255.0f + 0.5f)); break; + case icConvert16Bit: + case icConvertVariable: + arr.push_back((int)(m_Curve[i] * 65535.0f + 0.5f)); break; + default: + arr.push_back((double)m_Curve[i]); break; + } } + if (nType == icConvert8Bit) + j["precision"] = 1; + else if (nType == icConvert16Bit || nType == icConvertVariable) + j["precision"] = 2; + j["table"] = arr; } - if (nType == icConvert8Bit) - j["precision"] = 1; - else if (nType == icConvert16Bit || nType == icConvertVariable) - j["precision"] = 2; - j["table"] = arr; } return true; } @@ -1418,7 +1454,15 @@ bool CIccTagJsonCurve::ParseJson(const IccJson &j, icConvertType /*nType*/, std: std::string curveType; if (!jGetString(j, "curveType", curveType)) return false; if (curveType == "identity") { - SetSize(0); + int size = 0; + jGetValue(j, "size", size); + if (size >= 2) { + if (!SetSize((icUInt32Number)size)) return false; + for (int i = 0; i < size; i++) + m_Curve[i] = (icFloatNumber)i / (icFloatNumber)(size - 1); + } else { + SetSize(0); + } } else if (curveType == "gamma") { SetSize(1); double gamma = 1.0; diff --git a/docs/icc-profile.schema.json b/docs/icc-profile.schema.json index 8c2465f16..7d02b4fb0 100644 --- a/docs/icc-profile.schema.json +++ b/docs/icc-profile.schema.json @@ -234,6 +234,7 @@ "required": ["curveType"], "properties": { "curveType": { "type": "string", "enum": ["identity", "gamma", "table"] }, + "size": { "type": "integer", "minimum": 2, "description": "Number of samples in a sampled-identity curve (when curveType is 'identity' and the curve has 2 or more entries). Omitted for the 0-entry ICC identity." }, "gamma": { "type": "number", "description": "Gamma exponent (when curveType is 'gamma')" }, "precision": { "type": "integer", "enum": [1, 2], "description": "Table encoding: 1 = 8-bit integers (0–255), 2 = 16-bit integers (0–65535). Omitted for floating-point." }, "table": { "type": "array", "items": { "type": "number" }, "description": "Curve samples (when curveType is 'table'). Integers when precision is present, floats otherwise." } @@ -343,6 +344,11 @@ "description": "colorantTableType", "required": ["colorantTable"], "properties": { + "pcsEncoding": { + "type": "string", + "enum": ["Lab", "XYZ", "16bit"], + "description": "Encoding used for the 'pcs' arrays. 'Lab': L* (0–100), a*, b* (−128–127). 'XYZ': X, Y, Z floats (0–2 range). '16bit': raw ICC U16 integers. ToJson always writes 'Lab'; ParseJson defaults to 'Lab' when absent." + }, "colorantTable": { "type": "array", "items": { @@ -350,7 +356,13 @@ "required": ["name", "pcs"], "properties": { "name": { "type": "string" }, - "pcs": { "$ref": "#/$defs/XYZTriplet" } + "pcs": { + "type": "array", + "description": "PCS colorant coordinates. Interpretation depends on pcsEncoding.", + "items": { "type": "number" }, + "minItems": 3, + "maxItems": 3 + } } } } @@ -567,6 +579,7 @@ "properties": { "type": { "type": "string", "enum": ["Curve", "ParametricCurve", "SegmentedCurve"] }, "curveType": { "type": "string", "enum": ["identity", "gamma", "table"] }, + "size": { "type": "integer", "minimum": 2, "description": "Number of samples for a sampled-identity curve (when curveType is 'identity' and size >= 2)." }, "gamma": { "type": "number" }, "precision": { "type": "integer", "enum": [1, 2], "description": "Table encoding: 1 = 8-bit (0–255), 2 = 16-bit (0–65535). Omitted for float curves." }, "table": { "type": "array", "items": { "type": "number" }, "description": "Integers when precision present, floats otherwise." }, diff --git a/docs/iccjson.md b/docs/iccjson.md index b37a1442b..be50d0e9e 100644 --- a/docs/iccjson.md +++ b/docs/iccjson.md @@ -261,7 +261,7 @@ Each XYZ value is a JSON array `[X, Y, Z]`. The `"XYZ"` field is an array of suc The `"curveType"` field selects one of three subtypes: `"identity"`, `"gamma"`, or `"table"`. -Identity (no-op): +Identity (0-entry ICC no-op): ```json { "redTRCTag": { @@ -270,6 +270,17 @@ Identity (no-op): } ``` +Sampled identity (linear ramp with a specific number of entries — emitted instead of a full table when all entries match the ramp `i/(n-1)` within 16-bit quantisation tolerance): +```json +{ + "redTRCTag": { + "data": { "type": "curveType", "curveType": "identity", "size": 256 } + } +} +``` + +When parsing, a `"size"` of 2 or more reconstructs the linear ramp `m_Curve[i] = i/(size-1)`. When `"size"` is absent the 0-entry ICC identity is used. + Gamma: ```json { @@ -354,6 +365,37 @@ Segment types: | `FormulaSegment` | `functionType`, `parameters` | | `SampledSegment` | `samples` | +### Colorant Tags + +#### `colorantTableType` — colorant names and PCS coordinates + +Because `ToJson`/`ParseJson` have no access to the profile header, the encoding of the `"pcs"` arrays is declared explicitly via a `"pcsEncoding"` field: + +| `pcsEncoding` | `pcs` array contents | +|---------------|----------------------| +| `"Lab"` (default) | L\* (0–100), a\*, b\* (−128–127) — human-readable CIE Lab | +| `"XYZ"` | X, Y, Z floats (0–2 range) | +| `"16bit"` | Raw ICC U16 integers (0–65535) | + +`ToJson` always writes `"pcsEncoding": "Lab"`. `ParseJson` defaults to `"Lab"` when the field is absent (backward compatible with files written before this field was added). + +```json +{ + "colorantTableTag": { + "data": { + "type": "colorantTableType", + "pcsEncoding": "Lab", + "colorantTable": [ + { "name": "Cyan", "pcs": [55.11, -37.40, -5.18] }, + { "name": "Magenta", "pcs": [48.24, 74.12, -49.52] }, + { "name": "Yellow", "pcs": [89.02, -5.68, 93.14] }, + { "name": "Black", "pcs": [ 0.00, 0.00, 0.00] } + ] + } + } +} +``` + ### Signature Tags #### `signatureType` @@ -428,7 +470,7 @@ MBB curve type values: | `type` | Subtype fields | |------------------|---------------------------------------------| -| `Curve` | `curveType` (`"identity"`, `"gamma"`, `"table"`), `gamma`, `table` | +| `Curve` | `curveType` (`"identity"`, `"gamma"`, `"table"`), `size` (identity with entries), `gamma`, `table` | | `ParametricCurve`| `functionType`, `params` | | `SegmentedCurve` | `segments` array |