Title: [286628] trunk
Revision
286628
Author
[email protected]
Date
2021-12-07 16:22:32 -0800 (Tue, 07 Dec 2021)

Log Message

Support Animated JPEG-XL images
https://bugs.webkit.org/show_bug.cgi?id=233545
<rdar://problem/86077143>

Reviewed by Don Olmstead.

Source/WebCore:

Implement relevant API, such as frameCount() and support a positive
frame index for frameBufferAtIndex().

Introduce a query to count frames because JPEG XL code stream itself doesn't have frame
count metadata and we need to scan the code stream to the last frame.

Also introduce the Size query instead of the onlySize parameter of the decode() function.

With the DecodeImage query, the decoder decodes a single frame. To
specify which frame to decode, we make use of the JxlDecoderRewind()
and JxlDecoderSkipFrames() functions of libjxl.

To omit rewind and skip as much as possible, we record the query to
m_lastQuery and use it in shouldRewind().

Test: fast/images/animated-jpegxl-loop-count.html

* platform/image-decoders/jpegxl/JPEGXLImageDecoder.cpp:
(WebCore::JPEGXLImageDecoder::~JPEGXLImageDecoder):
(WebCore::JPEGXLImageDecoder::frameCount const):
(WebCore::JPEGXLImageDecoder::repetitionCount const):
(WebCore::JPEGXLImageDecoder::frameBufferAtIndex):
(WebCore::JPEGXLImageDecoder::clearFrameBufferCache):
(WebCore::JPEGXLImageDecoder::setFailed):
(WebCore::JPEGXLImageDecoder::tryDecodeSize):
(WebCore::JPEGXLImageDecoder::hasAlpha const):
(WebCore::JPEGXLImageDecoder::hasAnimation const):
(WebCore::JPEGXLImageDecoder::ensureDecoderInitialized):
(WebCore::JPEGXLImageDecoder::shouldRewind const):
(WebCore::JPEGXLImageDecoder::rewind):
(WebCore::JPEGXLImageDecoder::updateFrameCount):
(WebCore::JPEGXLImageDecoder::decode):
(WebCore::JPEGXLImageDecoder::processInput):
(WebCore::JPEGXLImageDecoder::imageOut):
(WebCore::JPEGXLImageDecoder::clear): Deleted.
* platform/image-decoders/jpegxl/JPEGXLImageDecoder.h:

LayoutTests:

 Add test for animated JPEG XL, based on animated-gif-loop-count.

* TestExpectations:
* fast/images/animated-jpegxl-loop-count-expected.html: Added.
* fast/images/animated-jpegxl-loop-count.html: Added.
* fast/images/resources/animated-red-green-blue-repeat-1.jxl: Added.
* fast/images/resources/animated-red-green-blue-repeat-2.jxl: Added.
* fast/images/resources/animated-red-green-blue-repeat-infinite.jxl: Added.
* platform/glib/TestExpectations:
* platform/wincairo/TestExpectations:

Modified Paths

Added Paths

Diff

Modified: trunk/LayoutTests/ChangeLog (286627 => 286628)


--- trunk/LayoutTests/ChangeLog	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/LayoutTests/ChangeLog	2021-12-08 00:22:32 UTC (rev 286628)
@@ -1,3 +1,22 @@
+2021-12-07  Yoshiaki Jitsukawa  <[email protected]>
+
+        Support Animated JPEG-XL images
+        https://bugs.webkit.org/show_bug.cgi?id=233545
+        <rdar://problem/86077143>
+
+        Reviewed by Don Olmstead.
+
+         Add test for animated JPEG XL, based on animated-gif-loop-count.
+
+        * TestExpectations:
+        * fast/images/animated-jpegxl-loop-count-expected.html: Added.
+        * fast/images/animated-jpegxl-loop-count.html: Added.
+        * fast/images/resources/animated-red-green-blue-repeat-1.jxl: Added.
+        * fast/images/resources/animated-red-green-blue-repeat-2.jxl: Added.
+        * fast/images/resources/animated-red-green-blue-repeat-infinite.jxl: Added.
+        * platform/glib/TestExpectations:
+        * platform/wincairo/TestExpectations:
+
 2021-12-07  Robert Jenner  <[email protected]>
 
         REGRESSION(iOS 14): svg/W3C-I18N/tspan-direction-rtl.svg is a constant failure 

Modified: trunk/LayoutTests/TestExpectations (286627 => 286628)


--- trunk/LayoutTests/TestExpectations	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/LayoutTests/TestExpectations	2021-12-08 00:22:32 UTC (rev 286628)
@@ -135,6 +135,7 @@
 # These tests don't have to be platform-specific, but they are only implemented on WinCairo now.
 fast/images/jpegxl-image-decoding.html [ Skip ]
 fast/images/jpegxl-as-image.html [ Skip ]
+fast/images/animated-jpegxl-loop-count.html [ Skip ]
 
 # Only applicable on platforms with dark mode support
 css-dark-mode [ Skip ]

Added: trunk/LayoutTests/fast/images/animated-jpegxl-loop-count-expected.html (0 => 286628)


--- trunk/LayoutTests/fast/images/animated-jpegxl-loop-count-expected.html	                        (rev 0)
+++ trunk/LayoutTests/fast/images/animated-jpegxl-loop-count-expected.html	2021-12-08 00:22:32 UTC (rev 286628)
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html>
+<style>
+    .box {
+        width: 100px;
+        height: 100px;
+        display: inline-block;
+    }
+</style>
+<body>
+    <div>
+        <p>Frames of a 3-frame animated image with missing loopCount, (repetitionCount = 1):</p>
+        <div class="box" style="background-color: red;"></div>
+        <div class="box" style="background-color: green;"></div>
+        <div class="box" style="background-color: blue;"></div>
+        <div class="box" style="background-color: blue;"></div>
+    </div>
+    <div>
+        <p>Frames of a 3-frame animated image with loopCount = 1, (repetitionCount = 2):</p>
+        <div class="box" style="background-color: red;"></div>
+        <div class="box" style="background-color: green;"></div>
+        <div class="box" style="background-color: blue;"></div>
+        <div class="box" style="background-color: red;"></div>
+        <div class="box" style="background-color: green;"></div>
+        <div class="box" style="background-color: blue;"></div>
+        <div class="box" style="background-color: blue;"></div>
+    </div>
+    <div>
+        <p>Frames of a 3-frame animated image with loopCount = 0, (repetitionCount = infinite):</p>
+        <div class="box" style="background-color: red;"></div>
+        <div class="box" style="background-color: green;"></div>
+        <div class="box" style="background-color: blue;"></div>
+        <div class="box" style="background-color: red;"></div>
+        <div class="box" style="background-color: green;"></div>
+        <div class="box" style="background-color: blue;"></div>
+        <div class="box" style="background-color: red;"></div>
+    </div>
+</body>
+</html>

Added: trunk/LayoutTests/fast/images/animated-jpegxl-loop-count.html (0 => 286628)


--- trunk/LayoutTests/fast/images/animated-jpegxl-loop-count.html	                        (rev 0)
+++ trunk/LayoutTests/fast/images/animated-jpegxl-loop-count.html	2021-12-08 00:22:32 UTC (rev 286628)
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html>
+<style>
+    canvas {
+        width: 100px;
+        height: 100px;
+    }
+</style>
+<script src=""
+<body>
+    <div>
+        <p>Frames of a 3-frame animated image with missing loopCount, (repetitionCount = 1):</p>
+        <canvas id="canvas-1"></canvas>
+        <canvas id="canvas-2"></canvas>
+        <canvas id="canvas-3"></canvas>
+        <canvas id="canvas-4"></canvas>
+    </div>
+    <div>
+        <p>Frames of a 3-frame animated image with loopCount = 1, (repetitionCount = 2):</p>
+        <canvas id="canvas-a"></canvas>
+        <canvas id="canvas-b"></canvas>
+        <canvas id="canvas-c"></canvas>
+        <canvas id="canvas-d"></canvas>
+        <canvas id="canvas-e"></canvas>
+        <canvas id="canvas-f"></canvas>
+        <canvas id="canvas-g"></canvas>
+    </div>
+    <div>
+        <p>Frames of a 3-frame animated image with loopCount = 0, (repetitionCount = infinite):</p>
+        <canvas id="canvas-A"></canvas>
+        <canvas id="canvas-B"></canvas>
+        <canvas id="canvas-C"></canvas>
+        <canvas id="canvas-D"></canvas>
+        <canvas id="canvas-E"></canvas>
+        <canvas id="canvas-F"></canvas>
+        <canvas id="canvas-G"></canvas>
+    </div>
+    <script>
+        (function() {
+            var images = [
+                { src: "resources/animated-red-green-blue-repeat-1.jxl", canvasId: '1', frameCount: 4 },
+                { src: "resources/animated-red-green-blue-repeat-2.jxl", canvasId: 'a', frameCount: 7 },
+                { src: "resources/animated-red-green-blue-repeat-infinite.jxl", canvasId: 'A', frameCount: 7 }
+            ];
+            runTest(images);
+        })();
+    </script>
+</body>
+</html>

Added: trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-1.jxl (0 => 286628)


--- trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-1.jxl	                        (rev 0)
+++ trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-1.jxl	2021-12-08 00:22:32 UTC (rev 286628)
@@ -0,0 +1,5 @@
+\xFF
+A(
+����d��K\x8B��\xD4c0v/�� R\xA7?��A_<
+����`��K\x8B��\xD4c0v/��ou����@_\xFC
+��L��K\x8B\x80]\xFE\xFF��ؘ6��(\xFA
\ No newline at end of file

Added: trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-2.jxl (0 => 286628)


--- trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-2.jxl	                        (rev 0)
+++ trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-2.jxl	2021-12-08 00:22:32 UTC (rev 286628)
@@ -0,0 +1,5 @@
+\xFF
+AH
+����d��K\x8B��\xD4c0v/�� R\xA7?��A_<
+����`��K\x8B��\xD4c0v/��ou����@_\xFC
+��L��K\x8B\x80]\xFE\xFF��ؘ6��(\xFA
\ No newline at end of file

Added: trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-infinite.jxl (0 => 286628)


--- trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-infinite.jxl	                        (rev 0)
+++ trunk/LayoutTests/fast/images/resources/animated-red-green-blue-repeat-infinite.jxl	2021-12-08 00:22:32 UTC (rev 286628)
@@ -0,0 +1,5 @@
+\xFF
+A��b
+����d��K\x8B��\xD4c0v/�� R\xA7?��A_<
+����`��K\x8B��\xD4c0v/��ou����@_\xFC
+��L��K\x8B\x80]\xFE\xFF��ؘ6��(\xFA
\ No newline at end of file

Modified: trunk/LayoutTests/platform/glib/TestExpectations (286627 => 286628)


--- trunk/LayoutTests/platform/glib/TestExpectations	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/LayoutTests/platform/glib/TestExpectations	2021-12-08 00:22:32 UTC (rev 286628)
@@ -107,6 +107,7 @@
 # JPEG-XL tests
 fast/images/jpegxl-as-image.html [ Pass ]
 fast/images/jpegxl-image-decoding.html [ Pass ]
+fast/images/animated-jpegxl-loop-count.html [ Pass ]
 
 # Some Apple ports don't support RTL scrollbars.
 fast/scrolling/rtl-scrollbars-elementFromPoint-static.html [ Pass ]

Modified: trunk/LayoutTests/platform/wincairo/TestExpectations (286627 => 286628)


--- trunk/LayoutTests/platform/wincairo/TestExpectations	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/LayoutTests/platform/wincairo/TestExpectations	2021-12-08 00:22:32 UTC (rev 286628)
@@ -272,9 +272,10 @@
 # PLATFORM(MAC), internals.rangeForDictionaryLookupAtLocation()
 fast/layers/prevent-hit-test-during-layout.html [ Skip ]
 
-# JPEG XL is disabled until WebKitRequirements is released with libjxl
-fast/images/jpegxl-as-image.html [ Skip ]
-fast/images/jpegxl-image-decoding.html [ Skip ]
+# JPEG-XL Tests
+fast/images/jpegxl-as-image.html [ Pass ]
+fast/images/jpegxl-image-decoding.html [ Pass ]
+fast/images/animated-jpegxl-loop-count.html [ Pass ]
 
 # DataTransferItems is not yet implemented
 fast/events/clipboard-dataTransferItemList.html [ Skip ]

Modified: trunk/Source/WebCore/ChangeLog (286627 => 286628)


--- trunk/Source/WebCore/ChangeLog	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/Source/WebCore/ChangeLog	2021-12-08 00:22:32 UTC (rev 286628)
@@ -1,3 +1,48 @@
+2021-12-07  Yoshiaki Jitsukawa  <[email protected]>
+
+        Support Animated JPEG-XL images
+        https://bugs.webkit.org/show_bug.cgi?id=233545
+        <rdar://problem/86077143>
+
+        Reviewed by Don Olmstead.
+
+        Implement relevant API, such as frameCount() and support a positive
+        frame index for frameBufferAtIndex().
+
+        Introduce a query to count frames because JPEG XL code stream itself doesn't have frame
+        count metadata and we need to scan the code stream to the last frame.
+
+        Also introduce the Size query instead of the onlySize parameter of the decode() function.
+
+        With the DecodeImage query, the decoder decodes a single frame. To
+        specify which frame to decode, we make use of the JxlDecoderRewind()
+        and JxlDecoderSkipFrames() functions of libjxl.
+
+        To omit rewind and skip as much as possible, we record the query to
+        m_lastQuery and use it in shouldRewind().
+
+        Test: fast/images/animated-jpegxl-loop-count.html
+
+        * platform/image-decoders/jpegxl/JPEGXLImageDecoder.cpp:
+        (WebCore::JPEGXLImageDecoder::~JPEGXLImageDecoder):
+        (WebCore::JPEGXLImageDecoder::frameCount const):
+        (WebCore::JPEGXLImageDecoder::repetitionCount const):
+        (WebCore::JPEGXLImageDecoder::frameBufferAtIndex):
+        (WebCore::JPEGXLImageDecoder::clearFrameBufferCache):
+        (WebCore::JPEGXLImageDecoder::setFailed):
+        (WebCore::JPEGXLImageDecoder::tryDecodeSize):
+        (WebCore::JPEGXLImageDecoder::hasAlpha const):
+        (WebCore::JPEGXLImageDecoder::hasAnimation const):
+        (WebCore::JPEGXLImageDecoder::ensureDecoderInitialized):
+        (WebCore::JPEGXLImageDecoder::shouldRewind const):
+        (WebCore::JPEGXLImageDecoder::rewind):
+        (WebCore::JPEGXLImageDecoder::updateFrameCount):
+        (WebCore::JPEGXLImageDecoder::decode):
+        (WebCore::JPEGXLImageDecoder::processInput):
+        (WebCore::JPEGXLImageDecoder::imageOut):
+        (WebCore::JPEGXLImageDecoder::clear): Deleted.
+        * platform/image-decoders/jpegxl/JPEGXLImageDecoder.h:
+
 2021-12-07  Cameron McCormack  <[email protected]>
 
         Move FontCache singleton and instance on WorkerGlobalScope to ThreadGlobalData

Modified: trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.cpp (286627 => 286628)


--- trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.cpp	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.cpp	2021-12-08 00:22:32 UTC (rev 286628)
@@ -28,6 +28,8 @@
 
 #if USE(JPEGXL)
 
+#include <optional>
+
 namespace WebCore {
 
 JPEGXLImageDecoder::JPEGXLImageDecoder(AlphaOption alphaOption, GammaAndColorProfileOption gammaAndColorProfileOption)
@@ -37,55 +39,178 @@
 
 JPEGXLImageDecoder::~JPEGXLImageDecoder()
 {
-    clear();
 }
 
+size_t JPEGXLImageDecoder::frameCount() const
+{
+    if (!hasAnimation())
+        return 1;
+
+    if (!m_isLastFrameHeaderReceived)
+        const_cast<JPEGXLImageDecoder*>(this)->updateFrameCount();
+
+    return m_frameCount;
+}
+
+RepetitionCount JPEGXLImageDecoder::repetitionCount() const
+{
+    if (hasAnimation()) {
+        if (!m_basicInfo->animation.num_loops) {
+            // If num_loops is zero, repeat infinitely.
+            return RepetitionCountInfinite;
+        }
+        return m_basicInfo->animation.num_loops;
+    }
+    return RepetitionCountNone;
+}
+
 ScalableImageDecoderFrame* JPEGXLImageDecoder::frameBufferAtIndex(size_t index)
 {
-    // TODO: To support animated JPEG XL in the future we need to handle second and subsequent frames.
-    if (index)
+    if (ScalableImageDecoder::encodedDataStatus() < EncodedDataStatus::SizeAvailable)
         return nullptr;
 
+    if (index >= frameCount())
+        index = frameCount() - 1;
+
     if (m_frameBufferCache.isEmpty())
         m_frameBufferCache.grow(1);
 
-    auto& frame = m_frameBufferCache[0];
+    auto& frame = m_frameBufferCache[index];
     if (!frame.isComplete())
-        decode(false, isAllDataReceived());
+        decode(Query::DecodedImage, index, isAllDataReceived());
     return &frame;
 }
 
-void JPEGXLImageDecoder::clear()
+void JPEGXLImageDecoder::clearFrameBufferCache(size_t clearBeforeFrame)
 {
-    m_decoder.reset();
-    m_readOffset = 0;
+    if (m_frameBufferCache.isEmpty())
+        return;
+
+    // Unlike the png and gif cases, we can always try to clear frames before "clearBeforeFrame" because
+    // the dependenciy to the previous frame is handled by libjxl.
+    const Vector<ScalableImageDecoderFrame>::iterator end(m_frameBufferCache.begin() + clearBeforeFrame);
+
+    for (Vector<ScalableImageDecoderFrame>::iterator i(m_frameBufferCache.begin()); i != end; ++i) {
+        // If the frame is partial, we're still on the way to decode the frame and it's likely
+        // we continue the decode, so we don't clear the frame.
+        if (i->isPartial())
+            continue;
+
+        i->clear();
+    }
 }
 
 bool JPEGXLImageDecoder::setFailed()
 {
-    clear();
+    m_decoder.reset();
     return ScalableImageDecoder::setFailed();
 }
 
-void JPEGXLImageDecoder::decode(bool onlySize, bool allDataReceived)
+void JPEGXLImageDecoder::tryDecodeSize(bool allDataReceived)
 {
+    if (m_basicInfo)
+        return;
+    decode(Query::Size, 0, allDataReceived);
+}
+
+bool JPEGXLImageDecoder::hasAlpha() const
+{
+    return m_basicInfo && m_basicInfo->alpha_bits > 0;
+}
+
+bool JPEGXLImageDecoder::hasAnimation() const
+{
+    return m_basicInfo && m_basicInfo->have_animation;
+}
+
+void JPEGXLImageDecoder::ensureDecoderInitialized()
+{
     if (failed())
         return;
 
+    if (m_decoder)
+        return;
+
+    m_decoder = JxlDecoderMake(nullptr);
     if (!m_decoder) {
-        clear();
-        m_decoder = JxlDecoderMake(nullptr);
-        if (!m_decoder) {
-            setFailed();
-            return;
-        }
+        setFailed();
+        return;
+    }
 
-        if (JxlDecoderSubscribeEvents(m_decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
-            setFailed();
-            return;
+    if (JxlDecoderSubscribeEvents(m_decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE) != JXL_DEC_SUCCESS) {
+        setFailed();
+        return;
+    }
+
+    m_readOffset = 0;
+    m_currentFrame = 0;
+    m_lastQuery = Query::Size;
+}
+
+bool JPEGXLImageDecoder::shouldRewind(Query query, size_t frameIndex) const
+{
+    if (m_lastQuery == Query::FrameCount) {
+        // If the current query is not FrameCount, we've completed the previous FrameCount query
+        // and the decoder has reached the EOF, so we need to rewind the decoder for the new query.
+        // Otherwise we continue the FrameCount query, so we must not rewind the decoder.
+        return query != Query::FrameCount;
+    }
+
+    if (m_lastQuery == Query::Size) {
+        // The decoder is at the stream header and doesn't need rewind.
+        return false;
+    }
+
+    // At this point we know m_lastQuery is Query::DecodedImage.
+
+    if (query != Query::DecodedImage)
+        return true;
+
+    // There's two cases where we don't need to rewind:
+    //   1. Previous decoding was interrupted with JXL_DEC_NEED_MORE_INPUT
+    //      and trying to continue decoding the same frame.
+    //   2. Previous decoding was completed (m_currentFrame was incremented) and starting a new frame.
+    // In both cases frameIndex is equal to m_currentFrame.
+    return frameIndex != m_currentFrame;
+}
+
+void JPEGXLImageDecoder::rewind()
+{
+    JxlDecoderRewind(m_decoder.get());
+    JxlDecoderSubscribeEvents(m_decoder.get(), JXL_DEC_BASIC_INFO | JXL_DEC_FRAME | JXL_DEC_FULL_IMAGE);
+    m_readOffset = 0;
+    m_currentFrame = 0;
+}
+
+void JPEGXLImageDecoder::updateFrameCount()
+{
+    if (failed())
+        return;
+
+    decode(Query::FrameCount, 0, isAllDataReceived());
+
+    if (m_frameCount != m_frameBufferCache.size())
+        m_frameBufferCache.resize(m_frameCount);
+}
+
+void JPEGXLImageDecoder::decode(Query query, size_t frameIndex, bool allDataReceived)
+{
+    ensureDecoderInitialized();
+
+    if (failed())
+        return;
+
+    if (shouldRewind(query, frameIndex)) {
+        rewind();
+        // We care about the frameIndex only if the query is Query::DecodedImage.
+        if (query == Query::DecodedImage && frameIndex) {
+            JxlDecoderSkipFrames(m_decoder.get(), frameIndex);
+            m_currentFrame = frameIndex;
         }
     }
 
+    m_lastQuery = query;
+
     m_data->data();
     size_t dataSize = m_data->size();
     if (JxlDecoderSetInput(m_decoder.get(), m_data->data() + m_readOffset, dataSize - m_readOffset) != JXL_DEC_SUCCESS) {
@@ -93,7 +218,7 @@
         return;
     }
 
-    JxlDecoderStatus status = processInput(onlySize);
+    JxlDecoderStatus status = processInput(query);
     // We set the status as failed if the decoder reports an error or requires more data while all data has been received.
     if (status == JXL_DEC_ERROR || (allDataReceived && status == JXL_DEC_NEED_MORE_INPUT)) {
         setFailed();
@@ -100,54 +225,98 @@
         return;
     }
 
-    // We release the decoder when we finish the decoding.
-    if (status == JXL_DEC_SUCCESS) {
-        clear();
+    if (query == Query::DecodedImage && status == JXL_DEC_FULL_IMAGE && m_isLastFrameHeaderReceived && m_currentFrame == m_frameCount) {
+        // We free the decoder when the last frame is decoded.
+        m_decoder.reset();
         return;
     }
 
-    // Otherwise we get the decoder ready for subsequent data.
     size_t remainingDataSize = JxlDecoderReleaseInput(m_decoder.get());
     m_readOffset = dataSize - remainingDataSize;
 }
 
-JxlDecoderStatus JPEGXLImageDecoder::processInput(bool onlySize)
+JxlDecoderStatus JPEGXLImageDecoder::processInput(Query query)
 {
     while (true) {
         auto status = JxlDecoderProcessInput(m_decoder.get());
 
+        // Return JXL_DEC_ERROR when we've reached EOF without receiving a frame marked as "is_last".
+        if (status == JXL_DEC_SUCCESS && !m_isLastFrameHeaderReceived)
+            return JXL_DEC_ERROR;
+
         // JXL_DEC_ERROR and JXL_DEC_SUCCESS are terminal states. We also exit from the loop if more data is needed.
         if (status == JXL_DEC_ERROR || status == JXL_DEC_SUCCESS || status == JXL_DEC_NEED_MORE_INPUT)
             return status;
 
         if (status == JXL_DEC_BASIC_INFO) {
-            JxlBasicInfo basicInfo;
-            if (JxlDecoderGetBasicInfo(m_decoder.get(), &basicInfo) != JXL_DEC_SUCCESS)
-                return JXL_DEC_ERROR;
+            if (!m_basicInfo) {
+                JxlBasicInfo basicInfo;
+                if (JxlDecoderGetBasicInfo(m_decoder.get(), &basicInfo) != JXL_DEC_SUCCESS)
+                    return JXL_DEC_ERROR;
 
-            setSize(IntSize(basicInfo.xsize, basicInfo.ysize));
-            if (onlySize)
+                m_basicInfo = basicInfo;
+            }
+
+            if (query == Query::Size) {
+                // setSize() must be called only if the query is Query::Size,
+                // otherwise this would roll back the encoded data status from completed.
+                setSize(IntSize(m_basicInfo->xsize, m_basicInfo->ysize));
                 return status;
+            }
+
             continue;
         }
 
         if (status == JXL_DEC_FRAME) {
             JxlPixelFormat format { 4, JXL_TYPE_UINT8, JXL_NATIVE_ENDIAN, 0 };
+            JxlFrameHeader frameHeader;
+            if (JxlDecoderGetFrameHeader(m_decoder.get(), &frameHeader) != JXL_DEC_SUCCESS)
+                return JXL_DEC_ERROR;
+
+            m_frameCount = std::max(m_frameCount, m_currentFrame + 1);
+
+            if (frameHeader.is_last)
+                m_isLastFrameHeaderReceived = true;
+
+            if (query != Query::DecodedImage) {
+                if (JxlDecoderSetImageOutCallback(m_decoder.get(), &format, [](void*, size_t, size_t, size_t, const void*) { }, nullptr) != JXL_DEC_SUCCESS)
+                    return JXL_DEC_ERROR;
+
+                continue;
+            }
+
+            if (m_currentFrame >= m_frameBufferCache.size())
+                m_frameBufferCache.resize(m_frameCount + 1);
+
+            auto& buffer = m_frameBufferCache[m_currentFrame];
+            if (buffer.isInvalid() && buffer.initialize(size(), m_premultiplyAlpha)) {
+                buffer.setDecodingStatus(DecodingStatus::Partial);
+                buffer.setHasAlpha(false);
+                if (m_basicInfo && m_basicInfo->have_animation) {
+                    buffer.setDuration(Seconds((double)frameHeader.duration * m_basicInfo->animation.tps_denominator / m_basicInfo->animation.tps_numerator));
+                    buffer.setDisposalMethod(ScalableImageDecoderFrame::DisposalMethod::DoNotDispose);
+                }
+            }
+
             if (JxlDecoderSetImageOutCallback(m_decoder.get(), &format, imageOutCallback, this) != JXL_DEC_SUCCESS)
                 return JXL_DEC_ERROR;
+
             continue;
         }
 
         if (status == JXL_DEC_FULL_IMAGE) {
-            if (m_frameBufferCache.isEmpty())
-                continue;
+            if (m_currentFrame < m_frameBufferCache.size()) {
+                auto& buffer = m_frameBufferCache[m_currentFrame];
+                if (!buffer.isInvalid())
+                    buffer.setDecodingStatus(DecodingStatus::Complete);
+            }
 
-            auto& buffer = m_frameBufferCache[0];
-            if (!buffer.isInvalid())
-                buffer.setDecodingStatus(DecodingStatus::Complete);
+            m_currentFrame++;
+            if (query == Query::DecodedImage)
+                return JXL_DEC_FULL_IMAGE;
 
-            // TODO: To support animated JPEG XL in the future we need to handle subsequent data.
-            return JXL_DEC_SUCCESS;
+            ASSERT(query == Query::FrameCount);
+            continue;
         }
     }
 }
@@ -159,18 +328,13 @@
 
 void JPEGXLImageDecoder::imageOut(size_t x, size_t y, size_t numPixels, const uint8_t* pixels)
 {
-    if (m_frameBufferCache.isEmpty())
+    if (m_currentFrame >= m_frameBufferCache.size())
         return;
 
-    auto& buffer = m_frameBufferCache[0];
-    if (buffer.isInvalid()) {
-        if (!buffer.initialize(size(), m_premultiplyAlpha))
-            return;
+    auto& buffer = m_frameBufferCache[m_currentFrame];
+    if (buffer.isInvalid())
+        return;
 
-        buffer.setDecodingStatus(DecodingStatus::Partial);
-        buffer.setHasAlpha(false);
-    }
-
     uint32_t* row = buffer.backingStore()->pixelAt(x, y);
     uint32_t* currentAddress = row;
     for (size_t i = 0; i < numPixels; i++) {

Modified: trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.h (286627 => 286628)


--- trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.h	2021-12-07 23:36:44 UTC (rev 286627)
+++ trunk/Source/WebCore/platform/image-decoders/jpegxl/JPEGXLImageDecoder.h	2021-12-08 00:22:32 UTC (rev 286628)
@@ -45,25 +45,50 @@
 
     // ScalableImageDecoder
     String filenameExtension() const override { return "jxl"_s; }
+    size_t frameCount() const override;
+    RepetitionCount repetitionCount() const override;
     ScalableImageDecoderFrame* frameBufferAtIndex(size_t index) override;
+    void clearFrameBufferCache(size_t clearBeforeFrame) override;
 
     bool setFailed() override;
 
 private:
+    enum class Query {
+        // This query is used for tryDecodeSize().
+        Size,
+        // We define a query for frame count because JPEG XL doesn't have frame count information in its code stream
+        // so we need to scan the code stream to get the frame count for animated JPEG XL.
+        // JPEG XL container can have frame count metadata but currently libjxl doesn't support it.
+        FrameCount,
+        // Query to decode a single frame.
+        DecodedImage,
+    };
+
     JPEGXLImageDecoder(AlphaOption, GammaAndColorProfileOption);
-    void tryDecodeSize(bool allDataReceived) override { decode(true, allDataReceived); }
+    void tryDecodeSize(bool allDataReceived) override;
 
-    void decode(bool onlySize, bool allDataReceived);
+    bool hasAlpha() const;
+    bool hasAnimation() const;
 
-    void clear();
+    void ensureDecoderInitialized();
+    bool shouldRewind(Query , size_t frameIndex) const;
+    void rewind();
+    void updateFrameCount();
 
-    JxlDecoderStatus processInput(bool onlySize);
-
-    static void imageOutCallback(void* that, size_t x, size_t y, size_t numPixels, const void* pixels);
+    void decode(Query, size_t frameIndex, bool allDataReceived);
+    JxlDecoderStatus processInput(Query);
+    static void imageOutCallback(void*, size_t x, size_t y, size_t numPixels, const void* pixels);
     void imageOut(size_t x, size_t y, size_t numPixels, const uint8_t* pixels);
 
     JxlDecoderPtr m_decoder;
-    size_t m_readOffset = 0;
+    size_t m_readOffset { 0 };
+    std::optional<JxlBasicInfo> m_basicInfo;
+
+    Query m_lastQuery { Query::Size };
+    size_t m_frameCount { 1 };
+    size_t m_currentFrame { 0 };
+
+    bool m_isLastFrameHeaderReceived { false }; // If this is true, we know we don't need to update m_frameCount.
 };
 
 } // namespace WebCore
_______________________________________________
webkit-changes mailing list
[email protected]
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to