Skip to content

Commit 9c0f01e

Browse files
fschleichpalemieux
andauthored
Add App2E HTJ2K constraints validation (#375)
Validate the CPL essence descriptors against the APP2.HT.REV and APP2.HT.IRV constraint sets specified in ST 2067-21, Annex I. Co-authored-by: Pierre-Anthony Lemieux <[email protected]>
1 parent beda248 commit 9c0f01e

File tree

7 files changed

+643
-5
lines changed

7 files changed

+643
-5
lines changed

README.md

+12-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11

22
# Photon
33

4-
Photon is a Java implementation of the [Interoperable Master Format (IMF)](https://www.imfug.com/explainer/imf-explainer-en/#what-is-imf) standard. IMF core constraints are defined by [SMPTE](https://www.smpte.org/who-we-are) specification [st2067-2:2013](https://ieeexplore.ieee.org/document/7291584) (paywall). Photon offers tools for parsing, interpreting and validating constituent files that make an Interoperable Master Package (IMP). These include AssetMap (st429-9:2014), PackingList (st429-8:2007), Composition Playlist (st2067-3:2013), and the essence containing IMF track file (st2067-5:2013) which follows the Material eXchange Format (MXF) format (st377-1:2011). Specifically, Photon parses and completely reads an MXF file containing a single audio or video essence as defined by the IMF Essence Component specification (st2067-5:2013) and serializes the metadata into the IMF Composition Playlist structure.
4+
Photon is a Java implementation of the [Interoperable Master Format (IMF)](https://www.smpte.org/standards/st2067) standard. Photon offers tools for parsing, interpreting and validating constituent files that make an Interoperable Master Package (IMP). These include:
5+
6+
- AssetMap (ST 429-9)
7+
- PackingList (ST 429-8)
8+
- Composition Playlist (ST 2067-3)
9+
- IMF track files (ST 2067-5)
10+
11+
Photon parses and reads IMF track files and serializes the metadata into the IMF Composition Playlist structure. Currently, Photon provides support for IMF Application #2E (ST 2067-21) and Application #5 ACES (ST 2067-50), and the Immersive Audio Bitstream (IAB) Plug-in (ST 2067-201).
512

613
The goal of the Photon is to provide a simple standardized interface to completely validate an IMP.
714

@@ -14,12 +21,16 @@ Photon can be built using JDK-8. Support for earlier jdk versions has not been t
1421
### Gradle
1522
Photon can be built very easily by using the included Gradle wrapper. Having downloaded the sources, simply invoke the following commands inside the folder containing the sources:
1623

24+
```
1725
$ ./gradlew clean
1826
$ ./gradlew build
27+
```
1928

2029
For Windows
30+
```
2131
$ gradlew.bat clean
2232
$ gradlew.bat build
33+
```
2334

2435
## Full Documentation
2536

build.gradle

+3-1
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,9 @@ dependencies {
104104
/*compile "com.sandflow:regxmllib:${revRegXMLSNAPSHOT}"*/
105105
testImplementation "org.mockito:mockito-core:3.3+"
106106
testImplementation "org.testng:testng:7.5+"
107-
testImplementation "org.slf4j:slf4j-simple:latest.release"
107+
implementation "org.slf4j:slf4j-simple:1.7.2"
108+
implementation "org.slf4j:slf4j-api:1.7.2"
109+
108110

109111
// JAX-B dependencies for JDK 9+
110112
implementation "jakarta.xml.bind:jakarta.xml.bind-api:2.3.2"

codequality/findbugs-excludeFilter-GeneratedCode.xml

+5
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,9 @@
3838
<Bug pattern="RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE" />
3939
</Match>
4040

41+
<!-- It should be possible to define a class that is tested but not fully used at this time -->
42+
<Match>
43+
<Bug pattern="URF_UNREAD_FIELD" />
44+
</Match>
45+
4146
</FindBugsFilter>

src/main/java/com/netflix/imflibrary/st2067_2/Application2E2021.java

+283-2
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
import com.netflix.imflibrary.Colorimetry.ColorModel;
55
import com.netflix.imflibrary.Colorimetry.Quantization;
66
import com.netflix.imflibrary.Colorimetry.Sampling;
7+
import com.netflix.imflibrary.st0377.header.GenericPictureEssenceDescriptor.FrameLayoutType;
78
import com.netflix.imflibrary.st0377.header.UL;
89
import com.netflix.imflibrary.IMFErrorLogger;
910
import com.netflix.imflibrary.st2067_2.ApplicationCompositionFactory.ApplicationCompositionType;
11+
import com.netflix.imflibrary.st2067_2.CompositionImageEssenceDescriptorModel.J2KHeaderParameters;
12+
import com.netflix.imflibrary.st2067_2.CompositionImageEssenceDescriptorModel.ProgressionOrder;
1013
import com.netflix.imflibrary.utils.Fraction;
1114
import com.netflix.imflibrary.JPEG2000;
1215

@@ -269,13 +272,291 @@ public Application2E2021(@Nonnull IMFCompositionPlaylistType imfCompositionPlayl
269272
}
270273
}
271274

275+
/* Validate codestream parameters against constraints listed in SMPTE ST 2067-21:2023 Annex I */
276+
277+
private static boolean validateHT(CompositionImageEssenceDescriptorModel imageDescriptor,
278+
IMFErrorLogger logger) {
279+
boolean isValid = true;
280+
281+
J2KHeaderParameters p = imageDescriptor.getJ2KHeaderParameters();
282+
283+
if (p == null) {
284+
logger.addError(
285+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
286+
IMFErrorLogger.IMFErrors.ErrorLevels.FATAL,
287+
"APP2.HT: Missing or incomplete JPEG 2000 Sub-descriptor");
288+
return false;
289+
}
290+
291+
if (p.xosiz != 0 || p.yosiz != 0 || p.xtosiz != 0 || p.ytosiz != 0) {
292+
logger.addError(
293+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
294+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
295+
"APP2.HT: Invalid XOsiz, YOsiz, XTOsiz or YTOsiz");
296+
isValid = false;
297+
}
298+
299+
if (p.xtsiz < p.xsiz || p.ytsiz < p.ysiz) {
300+
logger.addError(
301+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
302+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
303+
"APP2.HT: Invalid XTsiz or XYsiz");
304+
isValid = false;
305+
}
306+
307+
/* components constraints */
308+
309+
if (p.csiz.length <= 0 || p.csiz.length > 4) {
310+
logger.addError(
311+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
312+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
313+
String.format("APP2.HT: Invalid number (%d) of components", p.csiz.length));
314+
isValid = false;
315+
}
316+
317+
/* x sub-sampling */
318+
if (p.csiz[0].xrsiz != 1) {
319+
logger.addError(
320+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
321+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
322+
"APP2.HT: invalid horizontal sub-sampling for component 1");
323+
isValid = false;
324+
}
325+
if (p.csiz.length > 1 && p.csiz[1].xrsiz != 1 &&
326+
(p.csiz.length <= 2 || p.csiz[1].xrsiz != 2 || p.csiz[2].xrsiz != 2)) {
327+
logger.addError(
328+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
329+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
330+
"APP2.HT: invalid horizontal sub-sampling for component 2");
331+
isValid = false;
332+
}
333+
if (p.csiz.length > 2 && p.csiz[2].xrsiz != 1 && (p.csiz[1].xrsiz != 2 || p.csiz[2].xrsiz != 2)) {
334+
logger.addError(
335+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
336+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
337+
"APP2.HT: invalid horizontal sub-sampling for component 3");
338+
isValid = false;
339+
}
340+
if (p.csiz.length > 3 && p.csiz[3].xrsiz != 1) {
341+
logger.addError(
342+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
343+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
344+
"APP2.HT: invalid horizontal sub-sampling for component 4");
345+
isValid = false;
346+
}
347+
348+
/* y sub-sampling and sample width */
349+
if (p.csiz[0].ssiz > 15 || p.csiz[0].ssiz < 7) {
350+
logger.addError(
351+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
352+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
353+
String.format("APP2.HT: Invalid bit depth (%d)", p.csiz[0].ssiz + 1));
354+
isValid = false;
355+
}
356+
for (int i = 0; i < p.csiz.length; i++) {
357+
if (p.csiz[i].yrsiz != 1) {
358+
logger.addError(
359+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
360+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
361+
String.format("APP2.HT: invalid vertical sub-sampling for component %d", i));
362+
isValid = false;
363+
}
364+
if (p.csiz[i].ssiz != p.csiz[0].ssiz) {
365+
logger.addError(
366+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
367+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
368+
"APP2.HT: all components must have the same bit depth");
369+
isValid = false;
370+
}
371+
}
372+
/* CAP constraints */
373+
374+
/* Pcapi is 1 for i = 15, and 0 otherwise, per ST 2067-21 Annex I; therefore, pcap = 2^(32-15) = 131072 */
375+
if (p.cap == null || p.cap.pcap != 131072 || p.cap.ccap == null || p.cap.ccap.length != 1) {
376+
/* codestream shall require only Part 15 capabilities */
377+
logger.addError(
378+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
379+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
380+
"APP2.HT: missing or invalid CAP marker");
381+
return false;
382+
}
383+
384+
if ((p.cap.ccap[0] & 0b1111000000000000) != 0) {
385+
/* Bits 12-15 of Ccap15 shall be 0 */
386+
logger.addError(
387+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
388+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
389+
"APP2.HT: Bits 12-15 of Ccap15 shall be 0");
390+
isValid = false;
391+
}
392+
393+
boolean isHTREV = (p.cap.ccap[0] & 0b100000) == 0;
394+
395+
/* COD */
396+
397+
if (p.cod == null) {
398+
logger.addError(
399+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
400+
IMFErrorLogger.IMFErrors.ErrorLevels.FATAL,
401+
"APP2.HT: Missing COD marker");
402+
return false;
403+
}
404+
405+
/* no scod constraints */
406+
407+
/* code-block style */
408+
if (p.cod.cbStyle != 0b01000000) {
409+
/* bad code-block style */
410+
logger.addError(
411+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
412+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
413+
"APP2.HT: Invalid default code-block style");
414+
isValid = false;
415+
}
416+
417+
/* progression order - RPCL is not required, but ST 2067-21:2023 Annex I Note 3 implies a preference */
418+
if (p.cod.progressionOrder != ProgressionOrder.RPCL.value())
419+
logger.addError(
420+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
421+
IMFErrorLogger.IMFErrors.ErrorLevels.WARNING,
422+
"APP2.HT: JPEG 2000 progression order is not RPCL");
423+
424+
/* resolution layers */
425+
if (p.cod.numDecompLevels == 0) {
426+
logger.addError(
427+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
428+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
429+
"APP2.HT: Number of decomposition levels must be greater than 0");
430+
isValid = false;
431+
}
432+
433+
434+
long maxSz = Math.max(p.xsiz, p.ysiz);
435+
if ((maxSz <= 2048 && p.cod.numDecompLevels > 5) ||
436+
(maxSz <= 4096 && p.cod.numDecompLevels > 6) ||
437+
(maxSz <= 8192 && p.cod.numDecompLevels > 7)) {
438+
logger.addError(
439+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
440+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
441+
"APP2.HT: Invalid number of decomposition levels");
442+
isValid = false;
443+
}
444+
445+
/* number of layers */
446+
447+
if (p.cod.numLayers != 1) {
448+
logger.addError(
449+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
450+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
451+
String.format("APP2.HT: Number of layers (%d) is not 1", p.cod.numLayers));
452+
isValid = false;
453+
}
454+
455+
/* code-block sizes */
456+
457+
if (p.cod.ycb < 5 || p.cod.ycb > 6) {
458+
logger.addError(
459+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
460+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
461+
String.format("APP2.HT: Invalid vertical code-block size (ycb = %d)", p.cod.ycb));
462+
isValid = false;
463+
}
464+
465+
if (p.cod.xcb < 5 || p.cod.xcb > 7) {
466+
logger.addError(
467+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
468+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
469+
String.format("APP2.HT: Invalid horizontal code-block size (xcb = %d)", p.cod.xcb));
470+
isValid = false;
471+
}
472+
473+
474+
/* transformation */
475+
476+
boolean isReversibleFilter = (p.cod.transformation == 1);
477+
478+
if (isHTREV && !isReversibleFilter) {
479+
logger.addError(
480+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
481+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
482+
"APP2.HT: 9-7 irreversible filter is used but HTREV is signaled in CAP");
483+
isValid = false;
484+
}
485+
486+
/* precinct size */
487+
488+
if (p.cod.precinctSizes.length == 0 || p.cod.precinctSizes[0] != 0x77) {
489+
logger.addError(
490+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
491+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
492+
"APP2.HT: Invalid precinct sizes");
493+
isValid = false;
494+
}
495+
496+
for (int i = 1; i < p.cod.precinctSizes.length; i++)
497+
if (p.cod.precinctSizes[i] != 0x88) {
498+
logger.addError(
499+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
500+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
501+
"APP2.HT: Invalid precinct sizes");
502+
isValid = false;
503+
break;
504+
}
505+
506+
/* magbp - calculation according to ITU-T T.814 */
507+
508+
509+
int maxB = p.csiz[0].ssiz + 2;
510+
if (isReversibleFilter) {
511+
maxB += 2 + p.cod.multiComponentTransform;
512+
if (p.cod.numDecompLevels > 5)
513+
maxB += 1;
514+
} else if (p.cod.multiComponentTransform == 1 && p.csiz[0].ssiz > 9) {
515+
maxB += 1;
516+
}
517+
518+
int codestreamB = (p.cap.ccap[0] & 0b11111) + 8;
519+
520+
/*
521+
* NOTE: The Parameter B constraints in ST 2067-21:2023 are arguably too narrow, and existing implementations do violate them under certain circumstances.
522+
* Since practical issues are not expected from software decoders otherwise, an ERROR is currently returned only for values that exceed the max value (21)
523+
* allowed for any configuration by ST 2067-21:2023. A WARNING is provided for values that exceed the limit stated in ST 2067-21:2023, but not 21.
524+
*
525+
* TODO: This should be revisited as more implementations become available. Discussion for reference: https://github.com/SMPTE/st2067-21/issues/7
526+
*/
527+
528+
if (codestreamB > 21) {
529+
logger.addError(
530+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
531+
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,
532+
"APP2.HT: Parameter B has exceeded its limit to an extent that decoder issues are to be expected");
533+
isValid = false;
534+
} else if (codestreamB > maxB) {
535+
logger.addError(
536+
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
537+
IMFErrorLogger.IMFErrors.ErrorLevels.WARNING,
538+
"APP2.HT: Parameter B has exceeded its limits");
539+
}
540+
541+
return isValid;
542+
}
543+
544+
/**
545+
* @deprecated Instead use {@link #isValidJ2KProfile(CompositionImageEssenceDescriptorModel imageDescriptor, IMFErrorLogger logger)}
546+
*/
547+
@Deprecated
272548
public static boolean isValidJ2KProfile(CompositionImageEssenceDescriptorModel imageDescriptor) {
549+
return isValidJ2KProfile(imageDescriptor, new com.netflix.imflibrary.IMFErrorLoggerImpl());
550+
}
551+
552+
public static boolean isValidJ2KProfile(CompositionImageEssenceDescriptorModel imageDescriptor,
553+
IMFErrorLogger logger) {
273554
UL essenceCoding = imageDescriptor.getPictureEssenceCodingUL();
274555
Integer width = imageDescriptor.getStoredWidth();
275556
Integer height = imageDescriptor.getStoredHeight();
276557

277558
if (JPEG2000.isAPP2HT(essenceCoding))
278-
return true;
559+
return validateHT(imageDescriptor, logger);
279560

280561
if (JPEG2000.isIMF4KProfile(essenceCoding))
281562
return width > 2048 && width <= 4096 && height > 0 && height <= 3112;
@@ -293,7 +574,7 @@ public static void validateImageCharacteristics(CompositionImageEssenceDescripto
293574
IMFErrorLogger logger) {
294575

295576
// J2K profiles
296-
if (!isValidJ2KProfile(imageDescriptor)) {
577+
if (!isValidJ2KProfile(imageDescriptor, logger)) {
297578
logger.addError(
298579
IMFErrorLogger.IMFErrors.ErrorCodes.APPLICATION_COMPOSITION_ERROR,
299580
IMFErrorLogger.IMFErrors.ErrorLevels.NON_FATAL,

0 commit comments

Comments
 (0)